diff --git a/src/extensions/sync.ts b/src/extensions/sync.ts index 14079f26..651eaf40 100644 --- a/src/extensions/sync.ts +++ b/src/extensions/sync.ts @@ -40,7 +40,13 @@ export function sync(db: Database) { 'message', (event: MessageEvent>) => { const { operationType, payload } = event.data - console.warn('[sync]', operationType, payload) + const [sourceId, ...args] = payload + + // Ignore messages originating from unrelated databases. + // Useful in case of multiple databases on the same page. + if (db.id !== sourceId) { + return + } // Remove database event listener for the signaled operation // to prevent an infinite loop when applying this operation. @@ -49,7 +55,7 @@ export function sync(db: Database) { // Apply the database operation signaled from another client // to the current database instance. // @ts-ignore - db[operationType](...payload) + db[operationType](...args) // Re-attach database event listeners. restoreListeners() diff --git a/test/extensions/sync.multiple.runtime.js b/test/extensions/sync.multiple.runtime.js new file mode 100644 index 00000000..ddd96d4e --- /dev/null +++ b/test/extensions/sync.multiple.runtime.js @@ -0,0 +1,15 @@ +import { factory, primaryKey } from '@mswjs/data' + +window.db = factory({ + user: { + id: primaryKey(String), + firstName: String, + }, +}) + +window.secondDb = factory({ + user: { + id: primaryKey(String), + firstName: String, + }, +}) diff --git a/test/extensions/sync.test.ts b/test/extensions/sync.test.ts index 581e98dc..c59f2355 100644 --- a/test/extensions/sync.test.ts +++ b/test/extensions/sync.test.ts @@ -2,8 +2,14 @@ import * as path from 'path' import { createBrowser, CreateBrowserApi, pageWith } from 'page-with' import { FactoryAPI } from '../../src/glossary' +interface User { + id: string + firstName: string +} + declare namespace window { - export const db: FactoryAPI + export const db: FactoryAPI<{ user: User }> + export const secondDb: FactoryAPI<{ user: User }> } let browser: CreateBrowserApi @@ -41,10 +47,7 @@ test('synchornizes entity create across multiple clients', async () => { }) }) - const users = await secondPage.evaluate(() => { - return window.db.user.getAll() - }) - expect(users).toEqual([ + expect(await secondPage.evaluate(() => window.db.user.getAll())).toEqual([ { __type: 'user', __primaryKey: 'id', @@ -90,15 +93,12 @@ test('synchornizes entity update across multiple clients', async () => { firstName: 'Kate', }, ] - const users = await secondPage.evaluate(() => { - return window.db.user.getAll() - }) - expect(users).toEqual(expectedUsers) - - const extraneousUsers = await runtime.page.evaluate(() => { - return window.db.user.getAll() - }) - expect(extraneousUsers).toEqual(expectedUsers) + expect(await secondPage.evaluate(() => window.db.user.getAll())).toEqual( + expectedUsers, + ) + expect(await runtime.page.evaluate(() => window.db.user.getAll())).toEqual( + expectedUsers, + ) }) test('synchronizes entity delete across multiple clients', async () => { @@ -126,13 +126,92 @@ test('synchronizes entity delete across multiple clients', async () => { }) }) - const users = await secondPage.evaluate(() => { - return window.db.user.getAll() + expect(await secondPage.evaluate(() => window.db.user.getAll())).toEqual([]) + expect(await runtime.page.evaluate(() => window.db.user.getAll())).toEqual([]) +}) + +test('handles events from multiple database instances separately', async () => { + const runtime = await pageWith({ + example: path.resolve(__dirname, 'sync.multiple.runtime.js'), + }) + const secondPage = await runtime.context.newPage() + await secondPage.goto(runtime.origin) + await runtime.page.bringToFront() + + const john = { + __type: 'user', + __primaryKey: 'id', + id: 'abc-123', + firstName: 'John', + } + + const kate = { + __type: 'user', + __primaryKey: 'id', + id: 'def-456', + firstName: 'Kate', + } + + // Create a new user in the first database. + await runtime.page.evaluate(() => { + window.db.user.create({ id: 'abc-123', firstName: 'John' }) + }) + expect(await runtime.page.evaluate(() => window.db.user.getAll())).toEqual([ + john, + ]) + expect(await secondPage.evaluate(() => window.db.user.getAll())).toEqual([ + john, + ]) + + // No entities are created in the second, unrelated database. + expect( + await secondPage.evaluate(() => { + return window.secondDb.user.getAll() + }), + ).toEqual([]) + + await secondPage.evaluate(() => { + window.secondDb.user.create({ id: 'def-456', firstName: 'Kate' }) + }) + + // A new entity created in a different database is synchronized in another client. + expect( + await runtime.page.evaluate(() => window.secondDb.user.getAll()), + ).toEqual([kate]) + + // An unrelated database does not contain a newly created entity. + expect(await runtime.page.evaluate(() => window.db.user.getAll())).toEqual([ + john, + ]) +}) + +test('handles events from multiple databases on different hostnames', async () => { + const firstRuntime = await pageWith({ + example: path.resolve(__dirname, 'sync.runtime.js'), + }) + const secondRuntime = await pageWith({ + example: path.resolve(__dirname, 'sync.multiple.runtime.js'), }) - expect(users).toEqual([]) + expect(firstRuntime.origin).not.toEqual(secondRuntime.origin) - const extraneousUsers = await runtime.page.evaluate(() => { - return window.db.user.getAll() + await firstRuntime.page.evaluate(() => { + window.db.user.create({ id: 'abc-123', firstName: 'John' }) }) - expect(extraneousUsers).toEqual([]) + expect( + await secondRuntime.page.evaluate(() => window.db.user.getAll()), + ).toEqual([]) + + await secondRuntime.page.evaluate(() => { + window.db.user.create({ id: 'def-456', firstName: 'Kate' }) + }) + expect( + await firstRuntime.page.evaluate(() => window.db.user.getAll()), + ).toEqual([ + { + __type: 'user', + __primaryKey: 'id', + id: 'abc-123', + firstName: 'John', + }, + ]) })