diff --git a/packages/datastore/__tests__/helpers.ts b/packages/datastore/__tests__/helpers.ts index 2162ed6df4d..ded42260b77 100644 --- a/packages/datastore/__tests__/helpers.ts +++ b/packages/datastore/__tests__/helpers.ts @@ -1,4 +1,4 @@ -import { ModelInit, MutableModel, Schema } from '../src/types'; +import { ModelInit, MutableModel, Schema, InternalSchema } from '../src/types'; export declare class Model { public readonly id: string; @@ -261,3 +261,303 @@ export function testSchema(): Schema { version: '1', }; } + +export function internalTestSchema(): InternalSchema { + return { + namespaces: { + datastore: { + name: 'datastore', + relationships: { + Setting: { + indexes: [], + relationTypes: [], + }, + }, + enums: {}, + nonModels: {}, + models: { + Setting: { + name: 'Setting', + pluralName: 'Settings', + syncable: false, + fields: { + id: { + name: 'id', + type: 'ID', + isRequired: true, + isArray: false, + }, + key: { + name: 'key', + type: 'String', + isRequired: true, + isArray: false, + }, + value: { + name: 'value', + type: 'String', + isRequired: true, + isArray: false, + }, + }, + }, + }, + }, + user: { + name: 'user', + enums: {}, + models: { + Model: { + name: 'Model', + pluralName: 'Models', + syncable: true, + fields: { + id: { + name: 'id', + isArray: false, + type: 'ID', + isRequired: true, + }, + field1: { + name: 'field1', + isArray: false, + type: 'String', + isRequired: true, + }, + optionalField1: { + name: 'optionalField1', + isArray: false, + type: 'String', + isRequired: false, + }, + dateCreated: { + name: 'dateCreated', + isArray: false, + type: 'AWSDateTime', + isRequired: true, + attributes: [], + }, + emails: { + name: 'emails', + isArray: true, + type: 'AWSEmail', + isRequired: true, + attributes: [], + isArrayNullable: true, + }, + ips: { + name: 'ips', + isArray: true, + type: 'AWSIPAddress', + isRequired: false, + attributes: [], + isArrayNullable: true, + }, + metadata: { + name: 'metadata', + isArray: false, + type: { + nonModel: 'Metadata', + }, + isRequired: false, + attributes: [], + }, + }, + }, + LocalModel: { + name: 'LocalModel', + pluralName: 'LocalModels', + syncable: false, + fields: { + id: { + name: 'id', + isArray: false, + type: 'ID', + isRequired: true, + }, + field1: { + name: 'field1', + isArray: false, + type: 'String', + isRequired: true, + }, + }, + }, + }, + nonModels: { + Metadata: { + name: 'Metadata', + fields: { + author: { + name: 'author', + isArray: false, + type: 'String', + isRequired: true, + attributes: [], + }, + tags: { + name: 'tags', + isArray: true, + type: 'String', + isRequired: false, + isArrayNullable: true, + attributes: [], + }, + rewards: { + name: 'rewards', + isArray: true, + type: 'String', + isRequired: true, + attributes: [], + }, + penNames: { + name: 'penNames', + isArray: true, + type: 'String', + isRequired: true, + isArrayNullable: true, + attributes: [], + }, + nominations: { + name: 'nominations', + isArray: true, + type: 'String', + isRequired: false, + attributes: [], + }, + misc: { + name: 'misc', + isArray: true, + type: 'String', + isRequired: false, + isArrayNullable: true, + attributes: [], + }, + }, + }, + }, + relationships: { + Model: { + indexes: [], + relationTypes: [], + }, + LocalModel: { + indexes: [], + relationTypes: [], + }, + }, + }, + sync: { + name: 'sync', + relationships: { + MutationEvent: { + indexes: [], + relationTypes: [], + }, + ModelMetadata: { + indexes: [], + relationTypes: [], + }, + }, + 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, + }, + }, + }, + }, + }, + }, + version: '1', + }; +} diff --git a/packages/datastore/__tests__/outbox.test.ts b/packages/datastore/__tests__/outbox.test.ts new file mode 100644 index 00000000000..06f21f7f1f5 --- /dev/null +++ b/packages/datastore/__tests__/outbox.test.ts @@ -0,0 +1,287 @@ +import 'fake-indexeddb/auto'; +import { + initSchema as initSchemaType, + syncClasses, + ModelInstanceCreator, +} from '../src/datastore/datastore'; +import { ExclusiveStorage as StorageType } from '../src/storage/storage'; +import { MutationEventOutbox } from '../src/sync/outbox'; +import { ModelMerger } from '../src/sync/merger'; +import { Model as ModelType, testSchema, internalTestSchema } from './helpers'; +import { + TransformerMutationType, + createMutationInstanceFromModelOperation, +} from '../src/sync/utils'; +import { PersistentModelConstructor, InternalSchema } from '../src/types'; +import { MutationEvent } from '../src/sync/'; + +let initSchema: typeof initSchemaType; +// using to access private members +let DataStore: any; +let Storage: StorageType; +let anyStorage: any; +let outbox: MutationEventOutbox; +let merger: ModelMerger; +let modelInstanceCreator: ModelInstanceCreator; +let Model: PersistentModelConstructor; + +const schema: InternalSchema = internalTestSchema(); + +describe('Outbox tests', () => { + let modelId: string; + + beforeAll(async () => { + jest.resetAllMocks(); + + await instantiateOutbox(); + + const newModel = new Model({ + field1: 'Some value', + dateCreated: new Date().toISOString(), + }); + + const mutationEvent = await createMutationEvent(newModel); + ({ modelId } = mutationEvent); + + await outbox.enqueue(Storage, mutationEvent); + }); + + it('Should return the create mutation from Outbox.peek', async () => { + await Storage.runExclusive(async s => { + let head = await outbox.peek(s); + const modelData: ModelType = JSON.parse(head.data); + + expect(head.modelId).toEqual(modelId); + expect(head.operation).toEqual(TransformerMutationType.CREATE); + expect(modelData.field1).toEqual('Some value'); + + const response = { + ...modelData, + _version: 1, + _lastChangedAt: Date.now(), + _deleted: false, + }; + + await processMutationResponse(s, response); + + head = await outbox.peek(s); + expect(head).toBeFalsy(); + }); + }); + + it('Should sync the _version from a mutation response to other items with the same `id` in the queue', async () => { + const last = await DataStore.query(Model, modelId); + + const updatedModel1 = Model.copyOf(last, updated => { + updated.field1 = 'another value'; + updated.dateCreated = new Date().toISOString(); + }); + + const mutationEvent = await createMutationEvent(updatedModel1); + await outbox.enqueue(Storage, mutationEvent); + + await Storage.runExclusive(async s => { + // this mutation is now "in progress" + const head = await outbox.peek(s); + const modelData: ModelType = JSON.parse(head.data); + + expect(head.modelId).toEqual(modelId); + expect(head.operation).toEqual(TransformerMutationType.UPDATE); + expect(modelData.field1).toEqual('another value'); + + const mutationsForModel = await outbox.getForModel(s, last); + expect(mutationsForModel.length).toEqual(1); + }); + + // add 2 update mutations to the queue: + const updatedModel2 = Model.copyOf(last, updated => { + updated.field1 = 'another value2'; + updated.dateCreated = new Date().toISOString(); + }); + + await outbox.enqueue(Storage, await createMutationEvent(updatedModel2)); + + const updatedModel3 = Model.copyOf(last, updated => { + updated.field1 = 'another value3'; + updated.dateCreated = new Date().toISOString(); + }); + + await outbox.enqueue(Storage, await createMutationEvent(updatedModel3)); + + // model2 should get deleted when model3 is enqueued, so we're expecting to see + // 2 items in the queue for this Model total (including the in progress record - updatedModel1) + const mutationsForModel = await outbox.getForModel(Storage, last); + expect(mutationsForModel.length).toEqual(2); + + const [_inProgress, nextMutation] = mutationsForModel; + const modelData: ModelType = JSON.parse(nextMutation.data); + + // and the next item in the queue should be updatedModel3 + expect(modelData.field1).toEqual('another value3'); + + // response from AppSync for the first update mutation - updatedModel1: + const response = { + ...updatedModel1, + _version: (updatedModel1 as any)._version + 1, // increment version like we would expect coming back from AppSync + _lastChangedAt: Date.now(), + _deleted: false, + }; + + await Storage.runExclusive(async s => { + // process mutation response, which dequeues updatedModel1 + // and syncs its version to the remaining item in the mutation queue + await processMutationResponse(s, response); + + const inProgress = await outbox.peek(s); + const inProgressData = JSON.parse(inProgress.data); + // updatedModel3 should now be in progress with the _version from the mutation response + + expect(inProgressData.field1).toEqual('another value3'); + expect(inProgressData._version).toEqual(2); + + // response from AppSync for the second update mutation - updatedModel3: + const response2 = { + ...updatedModel3, + _version: inProgressData._version + 1, // increment version like we would expect coming back from AppSync + _lastChangedAt: Date.now(), + _deleted: false, + }; + + await processMutationResponse(s, response2); + + const head = await outbox.peek(s); + expect(head).toBeFalsy(); + }); + }); + + it('Should NOT sync the _version from a handled conflict mutation response', async () => { + const last = await DataStore.query(Model, modelId); + + const updatedModel1 = Model.copyOf(last, updated => { + updated.field1 = 'another value'; + updated.dateCreated = new Date().toISOString(); + }); + + const mutationEvent = await createMutationEvent(updatedModel1); + await outbox.enqueue(Storage, mutationEvent); + + await Storage.runExclusive(async s => { + // this mutation is now "in progress" + const head = await outbox.peek(s); + const modelData: ModelType = JSON.parse(head.data); + + expect(head.modelId).toEqual(modelId); + expect(head.operation).toEqual(TransformerMutationType.UPDATE); + expect(modelData.field1).toEqual('another value'); + + const mutationsForModel = await outbox.getForModel(s, last); + expect(mutationsForModel.length).toEqual(1); + }); + + // add an update mutations to the queue: + const updatedModel2 = Model.copyOf(last, updated => { + updated.field1 = 'another value2'; + updated.dateCreated = new Date().toISOString(); + }); + + await outbox.enqueue(Storage, await createMutationEvent(updatedModel2)); + + // 2 items in the queue for this Model total (including the in progress record - updatedModel1) + const mutationsForModel = await outbox.getForModel(Storage, last); + expect(mutationsForModel.length).toEqual(2); + + const [_inProgress, nextMutation] = mutationsForModel; + const modelData: ModelType = JSON.parse(nextMutation.data); + + // and the next item in the queue should be updatedModel2 + expect(modelData.field1).toEqual('another value2'); + + // response from AppSync with a handled conflict: + const response = { + ...updatedModel1, + field1: 'a different value set by another client', + _version: (updatedModel1 as any)._version + 1, // increment version like we would expect coming back from AppSync + _lastChangedAt: Date.now(), + _deleted: false, + }; + + await Storage.runExclusive(async s => { + // process mutation response, which dequeues updatedModel1 + // but SHOULD NOT sync the _version, since the data in the response is different + await processMutationResponse(s, response); + + const inProgress = await outbox.peek(s); + const inProgressData = JSON.parse(inProgress.data); + + // updatedModel2 should now be in progress with the _version from the mutation response + expect(inProgressData.field1).toEqual('another value2'); + + const oldVersion = (modelData as any)._version; + + expect(inProgressData._version).toEqual(oldVersion); + + // same response as above, + await processMutationResponse(s, response); + + const head = await outbox.peek(s); + expect(head).toBeFalsy(); + }); + }); +}); + +// performs all the required dependency injection +// in order to have a functional Outbox without the Sync Engine +async function instantiateOutbox(): Promise { + ({ initSchema, DataStore } = require('../src/datastore/datastore')); + const classes = initSchema(testSchema()); + const ownSymbol = Symbol('sync'); + + ({ Model } = classes as { + Model: PersistentModelConstructor; + }); + + const MutationEvent = syncClasses[ + 'MutationEvent' + ] as PersistentModelConstructor; + + await DataStore.start(); + + Storage = DataStore.storage; + anyStorage = Storage; + + ({ modelInstanceCreator } = anyStorage.storage); + + outbox = new MutationEventOutbox(schema, null, MutationEvent, ownSymbol); + merger = new ModelMerger(outbox, ownSymbol); +} + +async function createMutationEvent(model): Promise { + const [[originalElement, opType]] = await anyStorage.storage.save(model); + + const MutationEventConstructor = syncClasses[ + 'MutationEvent' + ] as PersistentModelConstructor; + + const modelConstructor = (Object.getPrototypeOf(originalElement) as Object) + .constructor as PersistentModelConstructor; + + return createMutationInstanceFromModelOperation( + undefined, + undefined, + opType, + modelConstructor, + originalElement, + {}, + MutationEventConstructor, + modelInstanceCreator + ); +} + +async function processMutationResponse(storage, record): Promise { + await outbox.dequeue(storage, record); + + const modelConstructor = Model as PersistentModelConstructor; + const model = modelInstanceCreator(modelConstructor, record); + + await merger.merge(storage, model); +} diff --git a/packages/datastore/__tests__/util.test.ts b/packages/datastore/__tests__/util.test.ts index d3ef3109077..482730b92d6 100644 --- a/packages/datastore/__tests__/util.test.ts +++ b/packages/datastore/__tests__/util.test.ts @@ -1,4 +1,5 @@ import { + objectsEqual, isAWSDate, isAWSDateTime, isAWSEmail, @@ -11,6 +12,36 @@ import { } from '../src/util'; describe('datastore util', () => { + test('objectsEqual', () => { + expect(objectsEqual({}, {})).toEqual(true); + expect(objectsEqual([], [])).toEqual(true); + expect(objectsEqual([], {})).toEqual(false); + expect(objectsEqual([1, 2, 3], [1, 2, 3])).toEqual(true); + expect(objectsEqual([1, 2, 3], [1, 2, 3, 4])).toEqual(false); + expect(objectsEqual({ a: 1 }, { a: 1 })).toEqual(true); + expect(objectsEqual({ a: 1 }, { a: 2 })).toEqual(false); + expect( + objectsEqual({ a: [{ b: 2 }, { c: 3 }] }, { a: [{ b: 2 }, { c: 3 }] }) + ).toEqual(true); + expect( + objectsEqual({ a: [{ b: 2 }, { c: 3 }] }, { a: [{ b: 2 }, { c: 4 }] }) + ).toEqual(false); + expect(objectsEqual(new Set([1, 2, 3]), new Set([1, 2, 3]))).toEqual(true); + expect(objectsEqual(new Set([1, 2, 3]), new Set([1, 2, 3, 4]))).toEqual( + false + ); + + const map1 = new Map(); + map1.set('a', 1); + + const map2 = new Map(); + map2.set('a', 1); + + expect(objectsEqual(map1, map2)).toEqual(true); + map2.set('b', 2); + expect(objectsEqual(map1, map2)).toEqual(false); + }); + test('isAWSDate', () => { const valid = [ '2020-01-01', diff --git a/packages/datastore/package.json b/packages/datastore/package.json index 2be92821da5..71f3c67cc88 100644 --- a/packages/datastore/package.json +++ b/packages/datastore/package.json @@ -68,7 +68,7 @@ "es2015", "dom", "esnext.asynciterable", - "es2017.object" + "es2019.object" ], "allowJs": true, "esModuleInterop": true, diff --git a/packages/datastore/src/datastore/datastore.ts b/packages/datastore/src/datastore/datastore.ts index 52863320063..ad433eff31a 100644 --- a/packages/datastore/src/datastore/datastore.ts +++ b/packages/datastore/src/datastore/datastore.ts @@ -96,12 +96,10 @@ const isValidModelConstructor = ( const namespaceResolver: NamespaceResolver = modelConstructor => modelNamespaceMap.get(modelConstructor); +// exporting for testing purposes +export let syncClasses: TypeConstructorMap; let dataStoreClasses: TypeConstructorMap; - let userClasses: TypeConstructorMap; - -let syncClasses: TypeConstructorMap; - let storageClasses: TypeConstructorMap; const initSchema = (userSchema: Schema) => { diff --git a/packages/datastore/src/sync/merger.ts b/packages/datastore/src/sync/merger.ts index ca26bcd2d02..c90038d3cd7 100644 --- a/packages/datastore/src/sync/merger.ts +++ b/packages/datastore/src/sync/merger.ts @@ -5,7 +5,6 @@ import { PersistentModelConstructor, } from '../types'; import { MutationEventOutbox } from './outbox'; - class ModelMerger { constructor( private readonly outbox: MutationEventOutbox, diff --git a/packages/datastore/src/sync/outbox.ts b/packages/datastore/src/sync/outbox.ts index 10dad1b53be..1fc0f3e40ec 100644 --- a/packages/datastore/src/sync/outbox.ts +++ b/packages/datastore/src/sync/outbox.ts @@ -1,6 +1,10 @@ import { MutationEvent } from './index'; import { ModelPredicateCreator } from '../predicates'; -import { ExclusiveStorage as Storage, StorageFacade } from '../storage/storage'; +import { + ExclusiveStorage as Storage, + StorageFacade, + Storage as StorageClass, +} from '../storage/storage'; import { InternalSchema, NamespaceResolver, @@ -8,7 +12,7 @@ import { PersistentModelConstructor, QueryOne, } from '../types'; -import { SYNC } from '../util'; +import { SYNC, objectsEqual } from '../util'; import { TransformerMutationType } from './utils'; // TODO: Persist deleted ids @@ -27,7 +31,7 @@ class MutationEventOutbox { storage: Storage, mutationEvent: MutationEvent ): Promise { - storage.runExclusive(async s => { + return storage.runExclusive(async s => { const mutationEventModelDefinition = this.schema.namespaces[SYNC].models[ 'MutationEvent' ]; @@ -51,10 +55,9 @@ class MutationEventOutbox { if (first.operation === TransformerMutationType.CREATE) { if (incomingMutationType === TransformerMutationType.DELETE) { - // delete all for model await s.delete(this.MutationEvent, predicate); } else { - // first gets updated with incoming's data, condition intentionally skiped + // first gets updated with incoming's data, condition intentionally skipped await s.save( this.MutationEvent.copyOf(first, draft => { draft.data = mutationEvent.data; @@ -79,11 +82,17 @@ class MutationEventOutbox { }); } - public async dequeue(storage: StorageFacade): Promise { + public async dequeue( + storage: StorageClass, + record?: PersistentModel + ): Promise { const head = await this.peek(storage); - await storage.delete(head); + if (record) { + await this.syncOutboxVersionsOnDequeue(storage, record, head); + } + await storage.delete(head); this.inProgressMutationEventId = undefined; return head; @@ -129,6 +138,67 @@ class MutationEventOutbox { return result; } + + // applies _version from the AppSync mutation response to other items in the mutation queue with the same id + // see https://github.com/aws-amplify/amplify-js/pull/7354 for more details + private async syncOutboxVersionsOnDequeue( + storage: StorageClass, + record: PersistentModel, + head: PersistentModel + ): Promise { + const { _version, _lastChangedAt, ...incomingData } = record; + const { + _version: __version, + _lastChangedAt: __lastChangedAt, + ...outgoingData + } = JSON.parse(head.data); + + if (head.operation !== TransformerMutationType.UPDATE) { + return; + } + + // Don't sync the version when the data in the response does not match the data + // in the request, i.e., when there's a handled conflict + if (!objectsEqual(incomingData, outgoingData)) { + return; + } + + const mutationEventModelDefinition = this.schema.namespaces[SYNC].models[ + 'MutationEvent' + ]; + + const predicate = ModelPredicateCreator.createFromExisting( + mutationEventModelDefinition, + c => c.modelId('eq', record.id).id('ne', this.inProgressMutationEventId) + ); + + const outdatedMutations = await storage.query( + this.MutationEvent, + predicate + ); + + if (!outdatedMutations.length) { + return; + } + + const reconciledMutations = outdatedMutations.map(m => { + const oldData = JSON.parse(m.data); + + const newData = { ...oldData, _version, _lastChangedAt }; + + return this.MutationEvent.copyOf(m, draft => { + draft.data = JSON.stringify(newData); + }); + }); + + await storage.delete(this.MutationEvent, predicate); + + await Promise.all( + reconciledMutations.map( + async m => await storage.save(m, undefined, this.ownSymbol) + ) + ); + } } export { MutationEventOutbox }; diff --git a/packages/datastore/src/sync/processors/mutation.ts b/packages/datastore/src/sync/processors/mutation.ts index 92a100828e9..a0db083eb89 100644 --- a/packages/datastore/src/sync/processors/mutation.ts +++ b/packages/datastore/src/sync/processors/mutation.ts @@ -151,14 +151,21 @@ class MutationProcessor { if (result === undefined) { logger.debug('done retrying'); - await this.outbox.dequeue(this.storage); + await this.storage.runExclusive(async storage => { + await this.outbox.dequeue(storage); + }); continue; } const record = result.data[opName]; - await this.outbox.dequeue(this.storage); + let hasMore = false; - const hasMore = (await this.outbox.peek(this.storage)) !== undefined; + await this.storage.runExclusive(async storage => { + // using runExclusive to prevent possible race condition + // when another record gets enqueued between dequeue and peek + await this.outbox.dequeue(storage, record); + hasMore = (await this.outbox.peek(storage)) !== undefined; + }); this.observer.next({ operation, diff --git a/packages/datastore/src/util.ts b/packages/datastore/src/util.ts index 30cfbd71bb4..ca987bcb51d 100644 --- a/packages/datastore/src/util.ts +++ b/packages/datastore/src/util.ts @@ -462,6 +462,51 @@ export function getUpdateMutationInput( return mutationInput; } +// deep compare any 2 objects (including arrays, Sets, and Maps) +// returns true if equal +export function objectsEqual(objA: object, objB: object): boolean { + let a = objA; + let b = objB; + + if ( + (Array.isArray(a) && !Array.isArray(b)) || + (Array.isArray(b) && !Array.isArray(a)) + ) { + return false; + } + + if (a instanceof Set && b instanceof Set) { + a = [...a]; + b = [...b]; + } + + if (a instanceof Map && b instanceof Map) { + a = Object.fromEntries(a); + b = Object.fromEntries(b); + } + + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + + if (aKeys.length !== bKeys.length) { + return false; + } + + for (const key of aKeys) { + const aVal = a[key]; + const bVal = b[key]; + + if (aVal && typeof aVal === 'object') { + if (!objectsEqual(aVal, bVal)) { + return false; + } + } else if (aVal !== bVal) { + return false; + } + } + return true; +} + export const isAWSDate = (val: string): boolean => { return !!/^\d{4}-\d{2}-\d{2}(Z|[+-]\d{2}:\d{2}($|:\d{2}))?$/.exec(val); }; diff --git a/scripts/build.js b/scripts/build.js index 61b9097aea7..8b133b08cf6 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -157,7 +157,13 @@ async function buildES5(typeScriptCompiler) { let compilerOptions = { esModuleInterop: true, noImplicitAny: false, - lib: ['dom', 'es2017', 'esnext.asynciterable', 'es2018.asyncgenerator'], + lib: [ + 'dom', + 'es2017', + 'esnext.asynciterable', + 'es2018.asyncgenerator', + 'es2019.object', + ], downlevelIteration: true, jsx: jsx, sourceMap: true, @@ -202,7 +208,13 @@ function buildES6(typeScriptCompiler) { let compilerOptions = { esModuleInterop: true, noImplicitAny: false, - lib: ['dom', 'es2017', 'esnext.asynciterable', 'es2018.asyncgenerator'], + lib: [ + 'dom', + 'es2017', + 'esnext.asynciterable', + 'es2018.asyncgenerator', + 'es2019.object', + ], downlevelIteration: true, jsx: jsx, sourceMap: true,