diff --git a/packages/analytics-core/src/core-client.ts b/packages/analytics-core/src/core-client.ts index 26a5496de..19769247b 100644 --- a/packages/analytics-core/src/core-client.ts +++ b/packages/analytics-core/src/core-client.ts @@ -30,8 +30,8 @@ export class AmplitudeCore implements CoreClient { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore timeline: Timeline; - protected q: CallableFunction[] = []; - protected dispatchQ: CallableFunction[] = []; + protected q: Array = []; + protected dispatchQ: Array = []; constructor(name = '$default') { this.timeline = new Timeline(this); @@ -48,7 +48,15 @@ export class AmplitudeCore implements CoreClient { const queuedFunctions = this[queueName]; this[queueName] = []; for (const queuedFunction of queuedFunctions) { - await queuedFunction(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const val: ReturnType | Promise = queuedFunction(); + if (val && 'promise' in val) { + await val.promise; + } else { + await val; + } } } diff --git a/packages/analytics-core/test/core-client.test.ts b/packages/analytics-core/test/core-client.test.ts index 38be132e3..1e960dccb 100644 --- a/packages/analytics-core/test/core-client.test.ts +++ b/packages/analytics-core/test/core-client.test.ts @@ -4,13 +4,25 @@ import { Event, Plugin, Status } from '@amplitude/analytics-types'; import { AmplitudeCore, Identify, Revenue } from '../src/index'; import { CLIENT_NOT_INITIALIZED, OPT_OUT_MESSAGE } from '../src/messages'; import { useDefaultConfig } from './helpers/default'; - +async function runScheduleTimers() { + // eslint-disable-next-line @typescript-eslint/unbound-method + await new Promise(process.nextTick); + jest.runAllTimers(); +} describe('core-client', () => { const success = { event: { event_type: 'sample' }, code: 200, message: Status.Success }; const badRequest = { event: { event_type: 'sample' }, code: 400, message: Status.Invalid }; const continueRequest = { event: { event_type: 'sample' }, code: 100, message: Status.Unknown }; const client = new AmplitudeCore(); + beforeEach(() => { + jest.useFakeTimers({ doNotFake: ['nextTick'] }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + describe('init', () => { test('should call init', async () => { expect(client.config).toBeUndefined(); @@ -106,6 +118,44 @@ describe('core-client', () => { expect(register).toHaveBeenCalledTimes(1); expect(deregister).toHaveBeenCalledTimes(1); }); + + test('should queue promises in correct order', async () => { + function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + const client = new AmplitudeCore(); + const register = jest.spyOn(client.timeline, 'register'); + const setupMockResolve = Promise.resolve(); + const setupMock = jest.fn().mockResolvedValue(setupMockResolve); + await client.add({ + name: 'firstPlugin', + type: 'before', + setup: async () => { + await sleep(10); + setupMock('firstPlugin'); + return; + }, + execute: jest.fn(), + }).promise; + client.add({ + name: 'secondPlugin', + type: 'before', + setup: async () => { + setupMock('secondPlugin'); + return; + }, + execute: jest.fn(), + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const initPromise = (client as any)._init(useDefaultConfig()); + await runScheduleTimers(); + await initPromise; + await setupMockResolve; + expect(register).toHaveBeenCalledTimes(2); + expect(setupMock).toHaveBeenCalledTimes(2); + expect(setupMock.mock.calls[0][0]).toEqual('firstPlugin'); + expect(setupMock.mock.calls[1][0]).toEqual('secondPlugin'); + }); }); describe('dispatchWithCallback', () => {