Open JohnAtFenestra opened 4 years ago
Adding a note that hopefully will be seen and answered by someone with internal knowledge. I attempted the brute force method of recreating the client when login occurs. However, that triggers a problem from keyPrefixesInUse
keyPrefix = keyPrefix || DEFAULT_KEY_PREFIX;
if (!disableOffline && keyPrefixesInUse.has(keyPrefix)) {
throw new Error(`The keyPrefix ${keyPrefix} is already in use. Multiple clients cannot share the same keyPrefix. Provide a different keyPrefix in the offlineConfig object.`);
}
I am beginning to think the only solution will be to take client.ts
and incorporate a modified version into my own code and comment out this check. My question is, will recreating the client cause any other issues? Is there any better solution?
@JohnAtFenestra can you share a code snippet on how you are initializing the client?
A snippet would be difficult. Here are the three main parts. Note that in aws.js I have a commented line:
// import AWSAppSyncClient, { createAppSyncLink } from 'aws-appsync'
and a replacement line:
import AWSAppSyncClient, { createAppSyncLink } from 'src/vendor/aws-appsync/lib'
The active one is a newly complied copy of AppSync where I made a change which fixes the issue for my use case:
export type OfflineConfig = Pick<
Partial<StoreOptions<any>>,
'storage' | 'callback' | 'keyPrefix'
> & {
storeCacheRootMutation?: boolean
allowKeyPrefixReuse?: boolean
}
...
keyPrefix = keyPrefix || DEFAULT_KEY_PREFIX
if (
!disableOffline &&
keyPrefixesInUse.has(keyPrefix) &&
!allowKeyPrefixReuse
) {
throw new Error(
`The keyPrefix ${keyPrefix} is already in use. Multiple clients cannot share the same keyPrefix. Provide a different keyPrefix in the offlineConfig object.`
)
}
In the boot section, I wrap the client in a proxy while exposes a method called $reinitialize
. This call re-initializes the client and is invoked whenever a user signs in, after the encrypted store is unlocked. This allows the cache to be rehydrated from the decrypted data.
I spent several hours combing through the appsync sdk code and this was the only option which seemed to work. I decided to add a new boolean flag allowKeyPrefixReuse
which would allow the change to be applied as a Pull Request if so desired.
src/boot/apolloClient.js
import { onlineListener } from 'src/lib/onlineManager'
import VueApollo from 'vue-apollo'
import AsyncComputed from 'vue-async-computed'
import { cryptoAdapterNew } from 'src/lib/cryptoAdapters'
import {
createEncryptedStore,
forageAdapterFactory
} from 'src/lib/localForageAdapter'
import { user } from 'src/lib/user'
import awsconfig from './config/local-awsconfiguration.json'
import apolloClientFactory from 'src/lib/apolloClient'
import localForage from 'localforage'
import { offlineManagerFactory } from 'src/lib/appSyncOfflineManagerFactory'
import axiosGraphQLFactory from 'src/lib/axiosGraphQL'
export default async ({ app, router, Vue }) => {
const offlineManager = offlineManagerFactory()
console.log({ offlineManager })
const mockStorage = {
signIn() {
return new Promise((resolve, reject) => {
resolve()
})
},
signOut() {
return new Promise((resolve, reject) => {
resolve()
})
}
}
const config = {}
const kind = 'offline-encrypted'
switch (kind) {
case 'offline-encrypted':
config.storage = createEncryptedStore(
cryptoAdapterNew,
forageAdapterFactory(),
{
dbName: 'VSFPersistance'
}
)
config.supportOffline = true
break
case 'default':
config.storage = undefined
config.supportOffline = true
break
case 'no-offline':
config.storage = mockStorage
config.supportOffline = false
break
}
function clientWrapperFactory() {
const clientReinitialize = () =>
apolloClientFactory(
awsconfig,
config.storage,
user,
true,
config.supportOffline,
offlineManager,
{ fetchPolicy: 'network-only' }
)
let client = clientReinitialize()
const result = new Proxy(
{},
{
get(target, p, receiver) {
if (p === '$reinitialize') {
console.log('Reinitializing ApolloClient')
client = clientReinitialize()
} else {
console.log('getting client property', p)
return client[p]
}
},
set(target, p, value, receiver) {
client[p] = value
console.log('setting client property', p)
return true
}
}
)
return result
}
const client = clientWrapperFactory()
const provider = new VueApollo({
defaultClient: client
})
// something to do
Vue.use(VueApollo)
Vue.use(AsyncComputed)
app.apolloProvider = provider
app.apolloClient = client
app.axiosGraphQL = axiosGraphQLFactory({
url: awsconfig.AppSync.Default.ApiUrl,
apiKey: awsconfig.AppSync.Default.ApiKey
})
Vue.prototype.$apolloClient = client
Vue.prototype.$offlineManager = offlineManager
app.storage = config.storage
app.$offlineManager = offlineManager
}
src/lib/apolloClient.js
export default function apolloClientFactory(
awsconfig,
storage,
user,
connectToDevTools,
supportOffline,
offlineManager,
opts = {}
) {
const clientFn = require('../lib/apollo-libs/aws.js').default
function getTokenFn() {
return new Promise(async (resolve, reject) => {
const userInstance = await user.getUser()
resolve(userInstance.token)
})
}
function getAlwaysTokenFn() {
return new Promise(async (resolve, reject) => {
resolve('always')
})
}
const localOpts = {
fetchPolicy: 'cache-and-network',
...opts
}
console.log({ offlineManager })
return clientFn(
awsconfig,
storage,
getTokenFn,
connectToDevTools,
supportOffline,
offlineManager,
localOpts
)
}
src/lib/apollo-libs/aws.js
// import AWSAppSyncClient, { createAppSyncLink } from 'aws-appsync'
import AWSAppSyncClient, { createAppSyncLink } from 'src/vendor/aws-appsync/lib'
import { setContext } from 'apollo-link-context'
export default function clientFn(
awsconfig,
storage,
getTokenFn,
connectToDevTools,
supportOffline,
offlineManager,
{ fetchPolicy }
) {
console.log('registering handler', offlineManager)
const primaryOfflineConfig = {
callback: offlineManager.$offlineHandler,
fetchPolicy: 'cache-and-network'
}
const offlineConfig = storage
? { storage, ...primaryOfflineConfig }
: primaryOfflineConfig
const primaryConfig = supportOffline
? {
offlineConfig,
disableOffline: false
}
: {
disableOffline: true
}
const customLink = createAppSyncLink({
url: awsconfig.AppSync.Default.ApiUrl,
region: awsconfig.AppSync.Default.Region,
auth: {
type: awsconfig.AppSync.Default.AuthMode,
apiKey: awsconfig.AppSync.Default.ApiKey
}
})
const contextSetterLink = setContext(async (operation, { headers }) => {
const token = await getTokenFn()
return {
headers: {
...headers,
'x-naep-token': token
}
}
})
const options = {
defaultOptions: {
watchQuery: {
fetchPolicy
},
connectToDevTools, // Remove this for production use
disableOffline: !supportOffline
},
link: contextSetterLink.concat(customLink)
}
const client = new AWSAppSyncClient(primaryConfig, options)
return client
}
I am using AppSync client with Vue JS as a PWA. Because we are working with PII, I have created a storage layer which encrypts the data before storing in IndexedDB.
The issue I am running into is as follows:
When the PWA is run in offline mode, the user logs in and the system decrypts the store using a derived key from the password.
The problem is that AppSync has already hydrated itself and has stored an empty cache in its internal cache. Hence all queries fail to return any of the cached results.
I tried creating the client, however, I got an error
Error: The keyPrefix reduxPersist: is already in use. Multiple clients cannot share the same keyPrefix. Provide a different keyPrefix in the offlineConfig object. at new AWSAppSyncClient (client.js?def7:186)
This is a critical problem which unfortunately I just noticed and will threaten the project as offline mode is essential. I appreciate any input from anyone with an idea of how to have the system reinitialize its internal store from the actual store.