-
-
Notifications
You must be signed in to change notification settings - Fork 642
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Update receivedUntil after getting server changes over syncWithServer() but not when getting chaotic server changes on a particular document over websocket. * Before syncWithServer, compute an Y.js state vector from the server changes that has arrived after receivedFrom so that the server can avoid sending the same changes back. * Update YSyncState after sync with the correct unsentFrom and receivedUntil values.
- Loading branch information
David Fahlander
committed
Sep 9, 2024
1 parent
50cd0a2
commit 0591312
Showing
10 changed files
with
231 additions
and
60 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,9 @@ | ||
import type * as Y from 'yjs'; | ||
import type { DexieCloudDB } from '../db/DexieCloudDB'; | ||
|
||
export function $Y(db: DexieCloudDB): typeof Y { | ||
const $Y = db.dx._options.Y; | ||
if (!$Y) throw new Error('Y library not supplied to Dexie constructor'); | ||
return $Y as typeof Y; | ||
} | ||
|
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,3 @@ | ||
import { EntityTable, YUpdateRow } from "dexie"; | ||
|
||
export type YTable = EntityTable<YUpdateRow, "i">; |
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,8 @@ | ||
import { DexieCloudDB } from "../db/DexieCloudDB"; | ||
import { YTable } from "./YTable"; | ||
|
||
export function getUpdatesTable(db: DexieCloudDB, table: string, ydocProp: string): YTable { | ||
const utbl = db.table(table)?.schema.yProps?.find(p => p.prop === ydocProp)?.updatesTable; | ||
if (!utbl) throw new Error(`No updatesTable found for ${table}.${ydocProp}`); | ||
return db.table(utbl); | ||
} |
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 |
---|---|---|
@@ -1,8 +1,8 @@ | ||
import type { Table, YUpdateRow } from 'dexie'; | ||
|
||
export function listUpdatesSince(yTable: Table, unsentFrom: number): Promise<YUpdateRow[]> { | ||
export function listUpdatesSince(yTable: Table, sinceIncluding: number): Promise<YUpdateRow[]> { | ||
return yTable | ||
.where('i') | ||
.between(unsentFrom, Infinity, true) | ||
.between(sinceIncluding, Infinity, true) | ||
.toArray(); | ||
} |
This file was deleted.
Oops, something went wrong.
112 changes: 112 additions & 0 deletions
112
addons/dexie-cloud/src/yjs/listYClientMessagesAndStateVector.ts
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,112 @@ | ||
import type { YSyncState, YUpdateRow } from 'dexie'; | ||
import type { YClientMessage } from 'dexie-cloud-common'; | ||
import { DexieCloudDB } from '../db/DexieCloudDB'; | ||
import { DEXIE_CLOUD_SYNCER_ID } from '../sync/DEXIE_CLOUD_SYNCER_ID'; | ||
import { listUpdatesSince } from './listUpdatesSince'; | ||
import { $Y } from './Y'; | ||
|
||
/** Queries the local database for YMessages to send to server. | ||
* | ||
* There are 2 messages that this function can provide: | ||
* YUpdateFromClientRequest ( for local updates ) | ||
* YStateVector ( for state vector of foreign updates so that server can reduce the number of udpates to send back ) | ||
* | ||
* Notice that we do not do a step 1 sync phase here to get a state vector from the server. Reason we can avoid | ||
* the 2-step sync is that we are client-server and not client-client here and we keep track of the client changes | ||
* sent to server by letting server acknowledge them. There is always a chance that some client update has already | ||
* been sent and that the client failed to receive the ack. However, if this happens it does not matter - the change | ||
* would be sent again and Yjs handles duplicate changes anyway. And it's rare so we earn the cost of roundtrips by | ||
* avoiding the step1 sync and instead keep track of this in the `unsentFrom` property of the SyncState. | ||
* | ||
* @param db | ||
* @returns | ||
*/ | ||
export async function listYClientMessagesAndStateVector( | ||
db: DexieCloudDB | ||
): Promise<{yMessages: YClientMessage[], lastUpdateIds: {[yTable: string]: number}}> { | ||
const result: YClientMessage[] = []; | ||
const lastUpdateIds: {[yTable: string]: number} = {}; | ||
for (const table of db.tables) { | ||
if (table.schema.yProps && db.cloud.schema?.[table.name].markedForSync) { | ||
for (const yProp of table.schema.yProps) { | ||
const Y = $Y(db); // This is how we retrieve the user-provided Y library | ||
const yTable = db.table(yProp.updatesTable); // the updates-table for this combo of table+propName | ||
const syncState = (await yTable.get(DEXIE_CLOUD_SYNCER_ID)) as | ||
| YSyncState | ||
| undefined; | ||
|
||
// unsentFrom = the `i` value of updates that aren't yet sent to server (or at least not acked by the server yet) | ||
const unsentFrom = syncState?.unsentFrom || 1; | ||
// receivedUntil = the `i` value of updates that both we and the server knows we already have (we know it by the outcome from last syncWithServer() because server keep track of its revision numbers | ||
const receivedUntil = syncState?.receivedUntil || 0; | ||
// Compute the least value of these two (but since receivedUntil is inclusive we need to add +1 to it) | ||
const unsyncedFrom = Math.min(unsentFrom, receivedUntil + 1); | ||
// Query all these updates for all docs of this table+prop combination | ||
const updates = await listUpdatesSince(yTable, unsyncedFrom); | ||
if (updates.length > 0) lastUpdateIds[yTable.name] = updates[updates.length -1].i; | ||
|
||
// Now sort them by document and whether they are local or not + ignore local updates already sent: | ||
const perDoc: { | ||
[docKey: string]: { | ||
i: number; | ||
k: any; | ||
isLocal: boolean; | ||
u: Uint8Array[]; | ||
}; | ||
} = {}; | ||
for (const update of updates) { | ||
// Sort updates into buckets of the doc primary key + the flag (whether it's local or foreign) | ||
const isLocal = ((update.f || 0) & 0x01) === 0x01; | ||
if (isLocal && update.i < unsentFrom) continue; // This local update has already been sent and acked. | ||
const docKey = JSON.stringify(update.k) + '/' + isLocal; | ||
let entry = perDoc[docKey]; | ||
if (!entry) { | ||
perDoc[docKey] = entry = { | ||
i: update.i, | ||
k: update.k, | ||
isLocal, | ||
u: [], | ||
}; | ||
entry.u.push(update.u); | ||
} else { | ||
entry.u.push(update.u); | ||
entry.i = Math.max(update.i, entry.i); | ||
} | ||
} | ||
|
||
// Now, go through all these and: | ||
// * For local updates, compute a merged update per document. | ||
// * For foreign updates, compute a state vector to pass to server, so that server can | ||
// avoid re-sending updates that we already have (they might have been sent of websocket | ||
// and when that happens, we do not mark them in any way nor do we update receivedUntil - | ||
// we only update receivedUntil after a "full sync" (syncWithServer())) | ||
for (const { k, isLocal, u, i } of Object.values(perDoc)) { | ||
const mergedUpdate = u.length === 1 ? u[0] : Y.mergeUpdatesV2(u); | ||
if (isLocal) { | ||
result.push({ | ||
type: 'u-c', | ||
table: table.name, | ||
prop: yProp.prop, | ||
k, | ||
u: mergedUpdate, | ||
i, | ||
}); | ||
} else { | ||
const stateVector = Y.encodeStateVectorFromUpdateV2(mergedUpdate); | ||
result.push({ | ||
type: 'sv', | ||
table: table.name, | ||
prop: yProp.prop, | ||
k, | ||
sv: stateVector, | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return { | ||
yMessages: result, | ||
lastUpdateIds | ||
}; | ||
} |
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,63 @@ | ||
import { DexieCloudDB } from '../db/DexieCloudDB'; | ||
import { DEXIE_CLOUD_SYNCER_ID } from '../sync/DEXIE_CLOUD_SYNCER_ID'; | ||
import { YSyncState } from 'dexie'; | ||
|
||
export async function updateYSyncStates( | ||
lastUpdateIdsBeforeSync: { [yTable: string]: number }, | ||
receivedUntilsAfterSync: { [yTable: string]: number }, | ||
db: DexieCloudDB | ||
) { | ||
// We want to update unsentFrom for each yTable to the value specified in first argument | ||
// because we got those values before we synced with server and here we are back from server | ||
// that has successfully received all those messages - no matter if the last update was a client or server update, | ||
// we can safely store unsentFrom to a value of the last update + 1 here. | ||
// We also want to update receivedUntil for each yTable to the value specified in the second argument, | ||
// because that contains the highest resulted id of each update from server after storing it. | ||
// We could do these two tasks separately, but that would require two update calls on the same YSyncState, so | ||
// to optimize the dexie calls, we merge these two maps into a single one so we can do a single update request | ||
// per yTable. | ||
const mergedSpec: { | ||
[yTable: string]: { unsentFrom?: number; receivedUntil?: number }; | ||
} = {}; | ||
for (const [yTable, lastUpdateId] of Object.entries( | ||
lastUpdateIdsBeforeSync | ||
)) { | ||
mergedSpec[yTable] ??= {}; | ||
mergedSpec[yTable].unsentFrom = lastUpdateId + 1; | ||
} | ||
for (const [yTable, lastUpdateId] of Object.entries(receivedUntilsAfterSync)) { | ||
mergedSpec[yTable] ??= {}; | ||
mergedSpec[yTable].receivedUntil = lastUpdateId; | ||
} | ||
|
||
// Now go through the merged map and update YSyncStates accordingly: | ||
for (const [yTable, { unsentFrom, receivedUntil }] of Object.entries( | ||
mergedSpec | ||
)) { | ||
// We're already in a transaction, but for the sake of | ||
// code readability and correctness, let's launch an atomic sub transaction: | ||
await db.transaction('rw', yTable, async () => { | ||
const state: YSyncState | undefined = await db.table(yTable).get( | ||
DEXIE_CLOUD_SYNCER_ID | ||
); | ||
if (!state) { | ||
await db.table(yTable).add({ | ||
i: DEXIE_CLOUD_SYNCER_ID, | ||
unsentFrom: unsentFrom || 1, | ||
receivedUntil: receivedUntil || 0 | ||
}); | ||
} else { | ||
if (unsentFrom) { | ||
state.unsentFrom = Math.max(unsentFrom, state.unsentFrom || 1); | ||
} | ||
if (receivedUntil) { | ||
state.receivedUntil = Math.max( | ||
receivedUntil, | ||
state.receivedUntil || 0 | ||
); | ||
} | ||
await db.table(yTable).put(state); | ||
} | ||
}); | ||
} | ||
} |
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