-
-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Synchronizes database events between multiple clients
- Loading branch information
1 parent
e68a4f5
commit 3817687
Showing
6 changed files
with
1,210 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { Database, DatabaseEventsMap } from '../db/Database' | ||
|
||
interface DatabaseMessageEventData< | ||
OperationType extends keyof DatabaseEventsMap | ||
> { | ||
operationType: OperationType | ||
payload: DatabaseEventsMap[OperationType] | ||
} | ||
|
||
function removeListeners<Event extends keyof DatabaseEventsMap>( | ||
event: Event, | ||
db: Database<any>, | ||
) { | ||
const listeners = db.events.listeners(event) as DatabaseEventsMap[Event][] | ||
listeners.forEach((listener) => { | ||
db.events.removeListener(event, listener) | ||
}) | ||
|
||
return () => { | ||
listeners.forEach((listener) => { | ||
db.events.addListener(event, listener) | ||
}) | ||
} | ||
} | ||
|
||
/** | ||
* Synchronizes database operations across multiple clients. | ||
*/ | ||
export function sync(db: Database<any>) { | ||
const IS_BROWSER = typeof window !== 'undefined' | ||
const SUPPORTS_BROADCAST_CHANNEL = typeof BroadcastChannel !== 'undefined' | ||
|
||
if (!IS_BROWSER || !SUPPORTS_BROADCAST_CHANNEL) { | ||
return | ||
} | ||
|
||
const channel = new BroadcastChannel('mswjs/data/sync') | ||
|
||
channel.addEventListener( | ||
'message', | ||
(event: MessageEvent<DatabaseMessageEventData<any>>) => { | ||
const { operationType, payload } = event.data | ||
console.warn('[sync]', operationType, payload) | ||
|
||
// Remove database event listener for the signaled operation | ||
// to prevent an infinite loop when applying this operation. | ||
const restoreListeners = removeListeners(operationType, db) | ||
|
||
// Apply the database operation signaled from another client | ||
// to the current database instance. | ||
// @ts-ignore | ||
db[operationType](...payload) | ||
|
||
// Re-attach database event listeners. | ||
restoreListeners() | ||
}, | ||
) | ||
|
||
function broadcastDatabaseEvent<Event extends keyof DatabaseEventsMap>( | ||
operationType: Event, | ||
) { | ||
return (...args: Parameters<DatabaseEventsMap[Event]>) => { | ||
channel.postMessage({ | ||
operationType, | ||
payload: args, | ||
}) | ||
} | ||
} | ||
|
||
db.events.on('create', broadcastDatabaseEvent('create')) | ||
db.events.on('update', broadcastDatabaseEvent('update')) | ||
db.events.on('delete', broadcastDatabaseEvent('delete')) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { factory, primaryKey } from '@mswjs/data' | ||
|
||
const db = factory({ | ||
user: { | ||
id: primaryKey(String), | ||
firstName: String, | ||
}, | ||
}) | ||
|
||
window.db = db |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import * as path from 'path' | ||
import { createBrowser, CreateBrowserApi, pageWith } from 'page-with' | ||
import { FactoryAPI } from '../../src/glossary' | ||
|
||
declare namespace window { | ||
export const db: FactoryAPI<any> | ||
} | ||
|
||
let browser: CreateBrowserApi | ||
|
||
beforeAll(async () => { | ||
browser = await createBrowser({ | ||
serverOptions: { | ||
webpackConfig: { | ||
resolve: { | ||
alias: { | ||
'@mswjs/data': path.resolve(__dirname, '../..'), | ||
}, | ||
}, | ||
}, | ||
}, | ||
}) | ||
}) | ||
|
||
afterAll(async () => { | ||
await browser.cleanup() | ||
}) | ||
|
||
it('synchornizes entity create across multiple clients', async () => { | ||
const runtime = await pageWith({ | ||
example: path.resolve(__dirname, 'sync.runtime.js'), | ||
}) | ||
const secondPage = await runtime.context.newPage() | ||
await secondPage.goto(runtime.origin) | ||
await runtime.page.bringToFront() | ||
|
||
await runtime.page.evaluate(() => { | ||
window.db.user.create({ | ||
id: 'abc-123', | ||
firstName: 'John', | ||
}) | ||
}) | ||
|
||
const users = await secondPage.evaluate(() => { | ||
return window.db.user.getAll() | ||
}) | ||
expect(users).toEqual([ | ||
{ | ||
__type: 'user', | ||
__primaryKey: 'id', | ||
id: 'abc-123', | ||
firstName: 'John', | ||
}, | ||
]) | ||
}) | ||
|
||
it('synchornizes entity update across multiple clients', async () => { | ||
const runtime = await pageWith({ | ||
example: path.resolve(__dirname, 'sync.runtime.js'), | ||
}) | ||
const secondPage = await runtime.context.newPage() | ||
await secondPage.goto(runtime.origin) | ||
await runtime.page.bringToFront() | ||
|
||
await runtime.page.evaluate(() => { | ||
window.db.user.create({ | ||
id: 'abc-123', | ||
firstName: 'John', | ||
}) | ||
}) | ||
|
||
await secondPage.evaluate(() => { | ||
return window.db.user.update({ | ||
which: { | ||
id: { | ||
equals: 'abc-123', | ||
}, | ||
}, | ||
data: { | ||
firstName: 'Kate', | ||
}, | ||
}) | ||
}) | ||
|
||
const expectedUsers = [ | ||
{ | ||
__type: 'user', | ||
__primaryKey: 'id', | ||
id: 'abc-123', | ||
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) | ||
}) | ||
|
||
it('synchronizes entity delete across multiple clients', async () => { | ||
const runtime = await pageWith({ | ||
example: path.resolve(__dirname, 'sync.runtime.js'), | ||
}) | ||
const secondPage = await runtime.context.newPage() | ||
await secondPage.goto(runtime.origin) | ||
await runtime.page.bringToFront() | ||
|
||
await runtime.page.evaluate(() => { | ||
window.db.user.create({ | ||
id: 'abc-123', | ||
firstName: 'John', | ||
}) | ||
}) | ||
|
||
await secondPage.evaluate(() => { | ||
window.db.user.delete({ | ||
which: { | ||
id: { | ||
equals: 'abc-123', | ||
}, | ||
}, | ||
}) | ||
}) | ||
|
||
const users = await secondPage.evaluate(() => { | ||
return window.db.user.getAll() | ||
}) | ||
expect(users).toEqual([]) | ||
|
||
const extraneousUsers = await runtime.page.evaluate(() => { | ||
return window.db.user.getAll() | ||
}) | ||
expect(extraneousUsers).toEqual([]) | ||
}) |
Oops, something went wrong.