UNPKG

@aws-amplify/datastore

Version:

AppSyncLocal support for aws-amplify

1,139 lines (1,007 loc) • 31.9 kB
import { browserOrNode, ConsoleLogger as Logger, BackgroundProcessManager, Hub, } from '@aws-amplify/core'; import { CONTROL_MSG as PUBSUB_CONTROL_MSG, CONNECTION_STATE_CHANGE as PUBSUB_CONNECTION_STATE_CHANGE, ConnectionState, } from '@aws-amplify/pubsub'; import Observable, { ZenObservable } from 'zen-observable-ts'; import { ModelInstanceCreator } from '../datastore/datastore'; import { ModelPredicateCreator } from '../predicates'; import { ExclusiveStorage as Storage } from '../storage/storage'; import { ConflictHandler, ControlMessageType, ErrorHandler, InternalSchema, ModelInit, ModelInstanceMetadata, MutableModel, NamespaceResolver, OpType, PersistentModel, PersistentModelConstructor, SchemaModel, SchemaNamespace, TypeConstructorMap, ModelPredicate, AuthModeStrategy, ManagedIdentifier, OptionallyManagedIdentifier, AmplifyContext, } from '../types'; // tslint:disable:no-duplicate-imports import type { __modelMeta__ } from '../types'; import { getNow, SYNC, USER } from '../util'; import DataStoreConnectivity from './datastoreConnectivity'; import { ModelMerger } from './merger'; import { MutationEventOutbox } from './outbox'; import { MutationProcessor } from './processors/mutation'; import { CONTROL_MSG, SubscriptionProcessor } from './processors/subscription'; import { SyncProcessor } from './processors/sync'; import { createMutationInstanceFromModelOperation, getIdentifierValue, predicateToGraphQLCondition, TransformerMutationType, } from './utils'; const { isNode } = browserOrNode(); const logger = new Logger('DataStore'); const ownSymbol = Symbol('sync'); type StartParams = { fullSyncInterval: number; }; export declare class MutationEvent { readonly [__modelMeta__]: { identifier: OptionallyManagedIdentifier<MutationEvent, 'id'>; }; public readonly id: string; public readonly model: string; public readonly operation: TransformerMutationType; public readonly modelId: string; public readonly condition: string; public readonly data: string; constructor(init: ModelInit<MutationEvent>); static copyOf( src: MutationEvent, mutator: (draft: MutableModel<MutationEvent>) => void | MutationEvent ): MutationEvent; } export declare class ModelMetadata { readonly [__modelMeta__]: { identifier: ManagedIdentifier<ModelMetadata, 'id'>; }; public readonly id: string; public readonly namespace: string; public readonly model: string; public readonly fullSyncInterval: number; public readonly lastSync?: number; public readonly lastFullSync?: number; public readonly lastSyncPredicate?: null | string; constructor(init: ModelInit<ModelMetadata>); static copyOf( src: ModelMetadata, mutator: (draft: MutableModel<ModelMetadata>) => void | ModelMetadata ): ModelMetadata; } export enum ControlMessage { SYNC_ENGINE_STORAGE_SUBSCRIBED = 'storageSubscribed', SYNC_ENGINE_SUBSCRIPTIONS_ESTABLISHED = 'subscriptionsEstablished', SYNC_ENGINE_SYNC_QUERIES_STARTED = 'syncQueriesStarted', SYNC_ENGINE_SYNC_QUERIES_READY = 'syncQueriesReady', SYNC_ENGINE_MODEL_SYNCED = 'modelSynced', SYNC_ENGINE_OUTBOX_MUTATION_ENQUEUED = 'outboxMutationEnqueued', SYNC_ENGINE_OUTBOX_MUTATION_PROCESSED = 'outboxMutationProcessed', SYNC_ENGINE_OUTBOX_STATUS = 'outboxStatus', SYNC_ENGINE_NETWORK_STATUS = 'networkStatus', SYNC_ENGINE_READY = 'ready', } export class SyncEngine { private online = false; private readonly syncQueriesProcessor: SyncProcessor; private readonly subscriptionsProcessor: SubscriptionProcessor; private readonly mutationsProcessor: MutationProcessor; private readonly modelMerger: ModelMerger; private readonly outbox: MutationEventOutbox; private readonly datastoreConnectivity: DataStoreConnectivity; private readonly modelSyncedStatus: WeakMap< PersistentModelConstructor<any>, boolean > = new WeakMap(); private unsleepSyncQueriesObservable: (() => void) | null; private waitForSleepState: Promise<void>; private syncQueriesObservableStartSleeping: ( value?: void | PromiseLike<void> ) => void; private stopDisruptionListener: () => void; private connectionDisrupted = false; private runningProcesses: BackgroundProcessManager; public getModelSyncedStatus( modelConstructor: PersistentModelConstructor<any> ): boolean { return this.modelSyncedStatus.get(modelConstructor)!; } constructor( private readonly schema: InternalSchema, private readonly namespaceResolver: NamespaceResolver, private readonly modelClasses: TypeConstructorMap, private readonly userModelClasses: TypeConstructorMap, private readonly storage: Storage, private readonly modelInstanceCreator: ModelInstanceCreator, conflictHandler: ConflictHandler, errorHandler: ErrorHandler, private readonly syncPredicates: WeakMap< SchemaModel, ModelPredicate<any> | null >, private readonly amplifyConfig: Record<string, any> = {}, private readonly authModeStrategy: AuthModeStrategy, private readonly amplifyContext: AmplifyContext, private readonly connectivityMonitor?: DataStoreConnectivity ) { this.runningProcesses = new BackgroundProcessManager(); this.waitForSleepState = new Promise(resolve => { this.syncQueriesObservableStartSleeping = resolve; }); const MutationEvent = this.modelClasses[ 'MutationEvent' ] as PersistentModelConstructor<MutationEvent>; this.outbox = new MutationEventOutbox( this.schema, MutationEvent, modelInstanceCreator, ownSymbol ); this.modelMerger = new ModelMerger(this.outbox, ownSymbol); this.syncQueriesProcessor = new SyncProcessor( this.schema, this.syncPredicates, this.amplifyConfig, this.authModeStrategy, errorHandler, this.amplifyContext ); this.subscriptionsProcessor = new SubscriptionProcessor( this.schema, this.syncPredicates, this.amplifyConfig, this.authModeStrategy, errorHandler, this.amplifyContext ); this.mutationsProcessor = new MutationProcessor( this.schema, this.storage, this.userModelClasses, this.outbox, this.modelInstanceCreator, MutationEvent, this.amplifyConfig, this.authModeStrategy, errorHandler, conflictHandler, this.amplifyContext ); this.datastoreConnectivity = this.connectivityMonitor || new DataStoreConnectivity(); } start(params: StartParams) { return new Observable<ControlMessageType<ControlMessage>>(observer => { logger.log('starting sync engine...'); let subscriptions: ZenObservable.Subscription[] = []; this.runningProcesses.add(async () => { try { await this.setupModels(params); } catch (err) { observer.error(err); return; } // this is awaited at the bottom. so, we don't need to register // this explicitly with the context. it's already contained. const startPromise = new Promise<void>( (doneStarting, failedStarting) => { this.datastoreConnectivity.status().subscribe( async ({ online }) => this.runningProcesses.isOpen && this.runningProcesses.add(async onTerminate => { // From offline to online if (online && !this.online) { this.online = online; observer.next({ type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS, data: { active: this.online, }, }); let ctlSubsObservable: Observable<CONTROL_MSG>; let dataSubsObservable: Observable< [TransformerMutationType, SchemaModel, PersistentModel] >; // NOTE: need a way to override this conditional for testing. if (isNode) { logger.warn( 'Realtime disabled when in a server-side environment' ); } else { this.stopDisruptionListener = this.startDisruptionListener(); //#region GraphQL Subscriptions [ctlSubsObservable, dataSubsObservable] = this.subscriptionsProcessor.start(); try { await new Promise<void>((resolve, reject) => { onTerminate.then(reject); const ctlSubsSubscription = ctlSubsObservable.subscribe({ next: msg => { if (msg === CONTROL_MSG.CONNECTED) { resolve(); } }, error: err => { reject(err); const handleDisconnect = this.disconnectionHandler(); handleDisconnect(err); }, }); subscriptions.push(ctlSubsSubscription); }); } catch (err) { observer.error(err); failedStarting(); return; } logger.log('Realtime ready'); observer.next({ type: ControlMessage.SYNC_ENGINE_SUBSCRIPTIONS_ESTABLISHED, }); //#endregion } //#region Base & Sync queries try { await new Promise<void>((resolve, reject) => { const syncQuerySubscription = this.syncQueriesObservable().subscribe({ next: message => { const { type } = message; if ( type === ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY ) { resolve(); } observer.next(message); }, complete: () => { resolve(); }, error: error => { reject(error); }, }); if (syncQuerySubscription) { subscriptions.push(syncQuerySubscription); } }); } catch (error) { observer.error(error); failedStarting(); return; } //#endregion //#region process mutations (outbox) subscriptions.push( this.mutationsProcessor .start() .subscribe( ({ modelDefinition, model: item, hasMore }) => this.runningProcesses.add(async () => { const modelConstructor = this.userModelClasses[ modelDefinition.name ] as PersistentModelConstructor<any>; const model = this.modelInstanceCreator( modelConstructor, item ); await this.storage.runExclusive(storage => this.modelMerger.merge( storage, model, modelDefinition ) ); observer.next({ type: ControlMessage.SYNC_ENGINE_OUTBOX_MUTATION_PROCESSED, data: { model: modelConstructor, element: model, }, }); observer.next({ type: ControlMessage.SYNC_ENGINE_OUTBOX_STATUS, data: { isEmpty: !hasMore, }, }); }, 'mutation processor event') ) ); //#endregion //#region Merge subscriptions buffer // TODO: extract to function if (!isNode) { subscriptions.push( dataSubsObservable!.subscribe( ([_transformerMutationType, modelDefinition, item]) => this.runningProcesses.add(async () => { const modelConstructor = this.userModelClasses[ modelDefinition.name ] as PersistentModelConstructor<any>; const model = this.modelInstanceCreator( modelConstructor, item ); await this.storage.runExclusive(storage => this.modelMerger.merge( storage, model, modelDefinition ) ); }, 'subscription dataSubsObservable event') ) ); } //#endregion } else if (!online) { this.online = online; observer.next({ type: ControlMessage.SYNC_ENGINE_NETWORK_STATUS, data: { active: this.online, }, }); subscriptions.forEach(sub => sub.unsubscribe()); subscriptions = []; } doneStarting(); }, 'datastore connectivity event') ); } ); this.storage .observe(null, null, ownSymbol) .filter(({ model }) => { const modelDefinition = this.getModelDefinition(model); return modelDefinition.syncable === true; }) .subscribe({ next: async ({ opType, model, element, condition }) => this.runningProcesses.add(async () => { const namespace = this.schema.namespaces[this.namespaceResolver(model)]; const MutationEventConstructor = this.modelClasses[ 'MutationEvent' ] as PersistentModelConstructor<MutationEvent>; const modelDefinition = this.getModelDefinition(model); const graphQLCondition = predicateToGraphQLCondition( condition!, modelDefinition ); const mutationEvent = createMutationInstanceFromModelOperation( namespace.relationships!, this.getModelDefinition(model), opType, model, element, graphQLCondition, MutationEventConstructor, this.modelInstanceCreator ); await this.outbox.enqueue(this.storage, mutationEvent); observer.next({ type: ControlMessage.SYNC_ENGINE_OUTBOX_MUTATION_ENQUEUED, data: { model, element, }, }); observer.next({ type: ControlMessage.SYNC_ENGINE_OUTBOX_STATUS, data: { isEmpty: false, }, }); await startPromise; // Set by the this.datastoreConnectivity.status().subscribe() loop if (this.online) { this.mutationsProcessor.resume(); } }, 'storage event'), }); observer.next({ type: ControlMessage.SYNC_ENGINE_STORAGE_SUBSCRIBED, }); const hasMutationsInOutbox = (await this.outbox.peek(this.storage)) === undefined; observer.next({ type: ControlMessage.SYNC_ENGINE_OUTBOX_STATUS, data: { isEmpty: hasMutationsInOutbox, }, }); await startPromise; observer.next({ type: ControlMessage.SYNC_ENGINE_READY, }); }, 'sync start'); }); } private async getModelsMetadataWithNextFullSync( currentTimeStamp: number ): Promise<Map<SchemaModel, [string, number]>> { const modelLastSync: Map<SchemaModel, [string, number]> = new Map( ( await this.runningProcesses.add( () => this.getModelsMetadata(), 'sync/index getModelsMetadataWithNextFullSync' ) ).map( ({ namespace, model, lastSync, lastFullSync, fullSyncInterval, lastSyncPredicate, }) => { const nextFullSync = lastFullSync! + fullSyncInterval; const syncFrom = !lastFullSync || nextFullSync < currentTimeStamp ? 0 // perform full sync if expired : lastSync; // perform delta sync return [ this.schema.namespaces[namespace].models[model], [namespace, syncFrom!], ]; } ) ); return modelLastSync; } private syncQueriesObservable(): Observable< ControlMessageType<ControlMessage> > { if (!this.online) { return Observable.of<ControlMessageType<ControlMessage>>(); } return new Observable<ControlMessageType<ControlMessage>>(observer => { let syncQueriesSubscription: ZenObservable.Subscription; this.runningProcesses.isOpen && this.runningProcesses.add(async onTerminate => { let terminated = false; while (!observer.closed && !terminated) { const count: WeakMap< PersistentModelConstructor<any>, { new: number; updated: number; deleted: number; } > = new WeakMap(); const modelLastSync = await this.getModelsMetadataWithNextFullSync( Date.now() ); const paginatingModels = new Set(modelLastSync.keys()); let lastFullSyncStartedAt: number; let syncInterval: number; let start: number; let syncDuration: number; let lastStartedAt: number; await new Promise<void>((resolve, reject) => { if (!this.runningProcesses.isOpen) resolve(); onTerminate.then(() => resolve()); syncQueriesSubscription = this.syncQueriesProcessor .start(modelLastSync) .subscribe({ next: async ({ namespace, modelDefinition, items, done, startedAt, isFullSync, }) => { const modelConstructor = this.userModelClasses[ modelDefinition.name ] as PersistentModelConstructor<any>; if (!count.has(modelConstructor)) { count.set(modelConstructor, { new: 0, updated: 0, deleted: 0, }); start = getNow(); lastStartedAt = lastStartedAt === undefined ? startedAt : Math.max(lastStartedAt, startedAt); } /** * If there are mutations in the outbox for a given id, those need to be * merged individually. Otherwise, we can merge them in batches. */ await this.storage.runExclusive(async storage => { const idsInOutbox = await this.outbox.getModelIds( storage ); const oneByOne: ModelInstanceMetadata[] = []; const page = items.filter(item => { const itemId = getIdentifierValue( modelDefinition, item ); if (!idsInOutbox.has(itemId)) { return true; } oneByOne.push(item); return false; }); const opTypeCount: [any, OpType][] = []; for (const item of oneByOne) { const opType = await this.modelMerger.merge( storage, item, modelDefinition ); if (opType !== undefined) { opTypeCount.push([item, opType]); } } opTypeCount.push( ...(await this.modelMerger.mergePage( storage, modelConstructor, page, modelDefinition )) ); const counts = count.get(modelConstructor)!; opTypeCount.forEach(([, opType]) => { switch (opType) { case OpType.INSERT: counts.new++; break; case OpType.UPDATE: counts.updated++; break; case OpType.DELETE: counts.deleted++; break; default: throw new Error(`Invalid opType ${opType}`); } }); }); if (done) { const { name: modelName } = modelDefinition; //#region update last sync for type let modelMetadata = await this.getModelMetadata( namespace, modelName ); const { lastFullSync, fullSyncInterval } = modelMetadata; syncInterval = fullSyncInterval; lastFullSyncStartedAt = lastFullSyncStartedAt === undefined ? lastFullSync! : Math.max( lastFullSyncStartedAt, isFullSync ? startedAt : lastFullSync! ); modelMetadata = ( this.modelClasses .ModelMetadata as PersistentModelConstructor<ModelMetadata> ).copyOf(modelMetadata, draft => { draft.lastSync = startedAt; draft.lastFullSync = isFullSync ? startedAt : modelMetadata.lastFullSync; }); await this.storage.save( modelMetadata, undefined, ownSymbol ); //#endregion const counts = count.get(modelConstructor); this.modelSyncedStatus.set(modelConstructor, true); observer.next({ type: ControlMessage.SYNC_ENGINE_MODEL_SYNCED, data: { model: modelConstructor, isFullSync, isDeltaSync: !isFullSync, counts, }, }); paginatingModels.delete(modelDefinition); if (paginatingModels.size === 0) { syncDuration = getNow() - start; resolve(); observer.next({ type: ControlMessage.SYNC_ENGINE_SYNC_QUERIES_READY, }); syncQueriesSubscription.unsubscribe(); } } }, error: error => { observer.error(error); }, }); observer.next({ type: ControlMessage.SYNC_ENGINE_SYNC_QUERIES_STARTED, data: { models: Array.from(paginatingModels).map(({ name }) => name), }, }); }); // null is cast to 0 resulting in unexpected behavior. // undefined in arithmetic operations results in NaN also resulting in unexpected behavior. // If lastFullSyncStartedAt is null this is the first sync. // Assume lastStartedAt is is also newest full sync. let msNextFullSync; if (!lastFullSyncStartedAt!) { msNextFullSync = syncInterval! - syncDuration!; } else { msNextFullSync = lastFullSyncStartedAt! + syncInterval! - (lastStartedAt! + syncDuration!); } logger.debug( `Next fullSync in ${msNextFullSync / 1000} seconds. (${new Date( Date.now() + msNextFullSync )})` ); // TODO: create `BackgroundProcessManager.sleep()` ... but, need to put // a lot of thought into what that contract looks like to // support possible use-cases: // // 1. non-cancelable // 2. cancelable, unsleep on exit() // 3. cancelable, throw Error on exit() // 4. cancelable, callback first on exit()? // 5. ... etc. ? ... // // TLDR; this is a lot of complexity here for a sleep(), // but, it's not clear to me yet how to support an // extensible, centralized cancelable `sleep()` elegantly. await this.runningProcesses.add(async onTerminate => { let sleepTimer; let unsleep; const sleep = new Promise(_unsleep => { unsleep = _unsleep; sleepTimer = setTimeout(unsleep, msNextFullSync); }); onTerminate.then(() => { terminated = true; this.syncQueriesObservableStartSleeping(); unsleep(); }); this.unsleepSyncQueriesObservable = unsleep; this.syncQueriesObservableStartSleeping(); return sleep; }, 'syncQueriesObservable sleep'); this.unsleepSyncQueriesObservable = null; this.waitForSleepState = new Promise(resolve => { this.syncQueriesObservableStartSleeping = resolve; }); } }, 'syncQueriesObservable main'); }); } private disconnectionHandler(): (msg: string) => void { return (msg: string) => { // This implementation is tied to AWSAppSyncRealTimeProvider 'Connection closed', 'Timeout disconnect' msg if ( PUBSUB_CONTROL_MSG.CONNECTION_CLOSED === msg || PUBSUB_CONTROL_MSG.TIMEOUT_DISCONNECT === msg ) { this.datastoreConnectivity.socketDisconnected(); } }; } public unsubscribeConnectivity() { this.datastoreConnectivity.unsubscribe(); } /** * Stops all subscription activities and resolves when all activies report * that they're disconnected, done retrying, etc.. */ public async stop() { logger.debug('stopping sync engine'); /** * Gracefully disconnecting subscribers first just prevents *more* work * from entering the pipelines. */ this.unsubscribeConnectivity(); /** * Stop listening for websocket connection disruption */ this.stopDisruptionListener && this.stopDisruptionListener(); /** * aggressively shut down any lingering background processes. * some of this might be semi-redundant with unsubscribing. however, * unsubscribing doesn't allow us to wait for settling. * (Whereas `stop()` does.) */ await this.mutationsProcessor.stop(); await this.subscriptionsProcessor.stop(); await this.datastoreConnectivity.stop(); await this.syncQueriesProcessor.stop(); await this.runningProcesses.close(); await this.runningProcesses.open(); logger.debug('sync engine stopped and ready to restart'); } private async setupModels(params: StartParams) { const { fullSyncInterval } = params; const ModelMetadataConstructor = this.modelClasses .ModelMetadata as PersistentModelConstructor<ModelMetadata>; const models: [string, SchemaModel][] = []; let savedModel; Object.values(this.schema.namespaces).forEach(namespace => { Object.values(namespace.models) .filter(({ syncable }) => syncable) .forEach(model => { models.push([namespace.name, model]); if (namespace.name === USER) { const modelConstructor = this.userModelClasses[ model.name ] as PersistentModelConstructor<any>; this.modelSyncedStatus.set(modelConstructor, false); } }); }); const promises = models.map(async ([namespace, model]) => { const modelMetadata = await this.getModelMetadata(namespace, model.name); const syncPredicate = ModelPredicateCreator.getPredicates( this.syncPredicates.get(model)!, false ); const lastSyncPredicate = syncPredicate ? JSON.stringify(syncPredicate) : null; if (modelMetadata === undefined) { [[savedModel]] = await this.storage.save( this.modelInstanceCreator(ModelMetadataConstructor, { model: model.name, namespace, lastSync: null!, fullSyncInterval, lastFullSync: null!, lastSyncPredicate, }), undefined, ownSymbol ); } else { const prevSyncPredicate = modelMetadata.lastSyncPredicate ? modelMetadata.lastSyncPredicate : null; const syncPredicateUpdated = prevSyncPredicate !== lastSyncPredicate; [[savedModel]] = await this.storage.save( ModelMetadataConstructor.copyOf(modelMetadata, draft => { draft.fullSyncInterval = fullSyncInterval; // perform a base sync if the syncPredicate changed in between calls to DataStore.start // ensures that the local store contains all the data specified by the syncExpression if (syncPredicateUpdated) { draft.lastSync = null!; draft.lastFullSync = null!; (draft.lastSyncPredicate as any) = lastSyncPredicate; } }) ); } return savedModel; }); const result: Record<string, ModelMetadata> = {}; for (const modelMetadata of await Promise.all(promises)) { const { model: modelName } = modelMetadata; result[modelName] = modelMetadata; } return result; } private async getModelsMetadata(): Promise<ModelMetadata[]> { const ModelMetadata = this.modelClasses .ModelMetadata as PersistentModelConstructor<ModelMetadata>; const modelsMetadata = await this.storage.query(ModelMetadata); return modelsMetadata; } private async getModelMetadata( namespace: string, model: string ): Promise<ModelMetadata> { const ModelMetadata = this.modelClasses .ModelMetadata as PersistentModelConstructor<ModelMetadata>; const predicate = ModelPredicateCreator.createFromAST<ModelMetadata>( this.schema.namespaces[SYNC].models[ModelMetadata.name], { and: [{ namespace: { eq: namespace } }, { model: { eq: model } }] } ); const [modelMetadata] = await this.storage.query(ModelMetadata, predicate, { page: 0, limit: 1, }); return modelMetadata; } private getModelDefinition( modelConstructor: PersistentModelConstructor<any> ): SchemaModel { const namespaceName = this.namespaceResolver(modelConstructor); const modelDefinition = this.schema.namespaces[namespaceName].models[modelConstructor.name]; return modelDefinition; } static getNamespace() { const namespace: SchemaNamespace = { name: SYNC, relationships: {}, enums: { OperationType: { name: 'OperationType', values: ['CREATE', 'UPDATE', 'DELETE'], }, }, nonModels: {}, models: { MutationEvent: { name: 'MutationEvent', pluralName: 'MutationEvents', syncable: false, fields: { id: { name: 'id', type: 'ID', isRequired: true, isArray: false, }, model: { name: 'model', type: 'String', isRequired: true, isArray: false, }, data: { name: 'data', type: 'String', isRequired: true, isArray: false, }, modelId: { name: 'modelId', type: 'String', isRequired: true, isArray: false, }, operation: { name: 'operation', type: { enum: 'Operationtype', }, isArray: false, isRequired: true, }, condition: { name: 'condition', type: 'String', isArray: false, isRequired: true, }, }, }, ModelMetadata: { name: 'ModelMetadata', pluralName: 'ModelsMetadata', syncable: false, fields: { id: { name: 'id', type: 'ID', isRequired: true, isArray: false, }, namespace: { name: 'namespace', type: 'String', isRequired: true, isArray: false, }, model: { name: 'model', type: 'String', isRequired: true, isArray: false, }, lastSync: { name: 'lastSync', type: 'Int', isRequired: false, isArray: false, }, lastFullSync: { name: 'lastFullSync', type: 'Int', isRequired: false, isArray: false, }, fullSyncInterval: { name: 'fullSyncInterval', type: 'Int', isRequired: true, isArray: false, }, lastSyncPredicate: { name: 'lastSyncPredicate', type: 'String', isRequired: false, isArray: false, }, }, }, }, }; return namespace; } /** * listen for websocket connection disruption * * May indicate there was a period of time where messages * from AppSync were missed. A sync needs to be triggered to * retrieve the missed data. */ private startDisruptionListener() { return Hub.listen('api', (data: any) => { if ( data.source === 'PubSub' && data.payload.event === PUBSUB_CONNECTION_STATE_CHANGE ) { const connectionState = data.payload.data .connectionState as ConnectionState; switch (connectionState) { // Do not need to listen for ConnectionDisruptedPendingNetwork // Normal network reconnection logic will handle the sync case ConnectionState.ConnectionDisrupted: this.connectionDisrupted = true; break; case ConnectionState.Connected: if (this.connectionDisrupted) { this.scheduleSync(); } this.connectionDisrupted = false; break; } } }); } /* * Schedule a sync to start when syncQueriesObservable enters sleep state * Start sync immediately if syncQueriesObservable is already in sleep state */ private scheduleSync() { return ( this.runningProcesses.isOpen && this.runningProcesses.add(() => this.waitForSleepState.then(() => { // unsleepSyncQueriesObservable will be set if waitForSleepState has resolved this.unsleepSyncQueriesObservable!(); }) ) ); } }