From aa38a7683818b1713d29a753aee6517b4738fa0d Mon Sep 17 00:00:00 2001 From: dfahlander Date: Wed, 31 Jul 2024 22:02:29 +0200 Subject: [PATCH] Refactor: use table+prop instead of updatesTable because: * updatesTable is not meaningful outside of client * server need to know the parent table to verify access * parent table + yDoc prop combined forms the same uniqueness as updatesTable. --- addons/dexie-cloud/src/WSObservable.ts | 2 +- addons/dexie-cloud/src/yjs/applyYMessages.ts | 21 +++++++++++++------ addons/dexie-cloud/src/yjs/awareness.ts | 8 ++++--- .../src/yjs/createYClientUpdateObservable.ts | 20 +++++++++++------- .../src/yjs/listYClientMessages.ts | 5 +++-- libs/dexie-cloud-common/package.json | 2 +- libs/dexie-cloud-common/src/YMessage.ts | 15 ++++++++----- src/classes/table/table.ts | 2 +- src/public/index.d.ts | 9 ++------ src/public/types/yjs-related.ts | 18 ++++++++++------ src/yjs/createYDocProperty.ts | 4 +++- src/yjs/docCache.ts | 20 +++++++++--------- 12 files changed, 76 insertions(+), 50 deletions(-) diff --git a/addons/dexie-cloud/src/WSObservable.ts b/addons/dexie-cloud/src/WSObservable.ts index e69c4f5b2..e40baa1b1 100644 --- a/addons/dexie-cloud/src/WSObservable.ts +++ b/addons/dexie-cloud/src/WSObservable.ts @@ -305,7 +305,7 @@ export class WSConnection extends Subscription { this.rev = msg.rev; // No meaning but seems reasonable. } else if (msg.type === 'aware') { const docCache = DexieYProvider.getDocCache(this.db.dx); - const doc = docCache.find(msg.utbl, msg.k); + const doc = docCache.find(msg.table, msg.k, msg.prop); if (doc) { const awareness = getDocAwareness(doc); if (awareness) { diff --git a/addons/dexie-cloud/src/yjs/applyYMessages.ts b/addons/dexie-cloud/src/yjs/applyYMessages.ts index 68e4dfd80..7d98a93ff 100644 --- a/addons/dexie-cloud/src/yjs/applyYMessages.ts +++ b/addons/dexie-cloud/src/yjs/applyYMessages.ts @@ -1,6 +1,6 @@ import { InsertType, YSyncer, YUpdateRow } from 'dexie'; import { DexieCloudDB } from '../db/DexieCloudDB'; -import { YServerMessage } from 'dexie-cloud-common/src/YMessage'; +import { YServerMessage, YUpdateFromClientAck } from 'dexie-cloud-common/src/YMessage'; import { DEXIE_CLOUD_SYNCER_ID } from '../sync/DEXIE_CLOUD_SYNCER_ID'; export async function applyYServerMessages( @@ -10,18 +10,20 @@ export async function applyYServerMessages( for (const m of yMessages) { switch (m.type) { case 'u-s': { - await db.table(m.utbl).add({ + const utbl = getUpdatesTable(db, m.table, m.prop); + await db.table(utbl).add({ k: m.k, u: m.u, } satisfies InsertType); break; } case 'u-ack': { - await db.transaction('rw', m.utbl, async (tx) => { - let syncer = (await tx.table(m.utbl).get(DEXIE_CLOUD_SYNCER_ID)) as + const utbl = getUpdatesTable(db, m.table, m.prop); + await db.transaction('rw', utbl, async (tx) => { + let syncer = (await tx.table(utbl).get(DEXIE_CLOUD_SYNCER_ID)) as | YSyncer | undefined; - await tx.table(m.utbl).put(DEXIE_CLOUD_SYNCER_ID, { + await tx.table(utbl).put(DEXIE_CLOUD_SYNCER_ID, { ...(syncer || { i: DEXIE_CLOUD_SYNCER_ID }), unsentFrom: Math.max(syncer?.unsentFrom || 1, m.i + 1), } as YSyncer); @@ -36,9 +38,16 @@ export async function applyYServerMessages( // in a perfect world, we should send a reverse update to the open document to undo the change. // See my question in https://discuss.yjs.dev/t/generate-an-inverse-update/2765 console.debug(`Y update rejected. Deleting it.`); - await db.table(m.utbl).delete(m.i); + const utbl = getUpdatesTable(db, m.table, m.prop); + await db.table(utbl).delete(m.i); break; } } } } +function getUpdatesTable(db: DexieCloudDB, table: string, ydocProp: string) { + 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 utbl; +} + diff --git a/addons/dexie-cloud/src/yjs/awareness.ts b/addons/dexie-cloud/src/yjs/awareness.ts index c788442ff..cad593db4 100644 --- a/addons/dexie-cloud/src/yjs/awareness.ts +++ b/addons/dexie-cloud/src/yjs/awareness.ts @@ -16,7 +16,7 @@ export function createYHandler(db: DexieCloudDB) { const awap = getAwarenessLibrary(db); return (provider: DexieYProvider) => { const doc = provider.doc; - const { parentTable, parentId, updatesTable } = doc.meta as DexieYDocMeta; + const { parentTable, parentId, parentProp } = doc.meta as DexieYDocMeta; if (!db.cloud.schema?.[parentTable].markedForSync) { return; // The table that holds the doc is not marked for sync - leave it to dexie. No syncing, no awareness. } @@ -32,7 +32,8 @@ export function createYHandler(db: DexieCloudDB) { ); db.messageProducer.next({ type: 'aware', - utbl: updatesTable, + table: parentTable, + prop: parentProp, k: parentId, u: update, }); @@ -43,7 +44,8 @@ export function createYHandler(db: DexieCloudDB) { const update = awap.encodeAwarenessUpdate(awareness!, changedClients); db.messageProducer.next({ type: 'aware', - utbl: doc.meta.updatesTable!, + table: parentTable, + prop: parentProp, k: doc.meta.parentId, u: update, }); diff --git a/addons/dexie-cloud/src/yjs/createYClientUpdateObservable.ts b/addons/dexie-cloud/src/yjs/createYClientUpdateObservable.ts index 2ea02317f..cd169cf9a 100644 --- a/addons/dexie-cloud/src/yjs/createYClientUpdateObservable.ts +++ b/addons/dexie-cloud/src/yjs/createYClientUpdateObservable.ts @@ -1,5 +1,5 @@ import { Observable, Subject, Subscription, merge, mergeMap } from 'rxjs'; -import { YClientMessage } from 'dexie-cloud-common/src/YMessage'; +import { YClientMessage, YUpdateFromClientRequest } from 'dexie-cloud-common/src/YMessage'; import { DexieCloudDB } from '../db/DexieCloudDB'; import { flatten } from '../helpers/flatten'; import { liveQuery } from 'dexie'; @@ -7,16 +7,20 @@ import { DEXIE_CLOUD_SYNCER_ID } from '../sync/DEXIE_CLOUD_SYNCER_ID'; import { listUpdatesSince } from './listUpdatesSince'; export function createYClientUpdateObservable(db: DexieCloudDB): Observable { - const yTableNames = flatten( + const yTableRecords = flatten( db.tables .filter((table) => db.cloud.schema?.[table.name].markedForSync && table.schema.yProps) - .map((table) => table.schema.yProps!.map((prop) => prop.updatesTable)) + .map((table) => table.schema.yProps!.map((p) => ({ + table: table.name, + ydocProp: p.prop, + updatesTable: p.updatesTable + }))) ); return merge( - ...yTableNames.map((tblName) => { + ...yTableRecords.map(({table, ydocProp, updatesTable}) => { let currentUnsentFrom = 1; return liveQuery(async () => { - const yTbl = db.table(tblName); + const yTbl = db.table(updatesTable); const unsentFrom = await yTbl .where({ i: DEXIE_CLOUD_SYNCER_ID }) .first() @@ -33,10 +37,12 @@ export function createYClientUpdateObservable(db: DexieCloudDB): Observable { return { type: 'u-c', - utbl: tblName, + table, + prop: ydocProp, k: update.k, u: update.u, - } as YClientMessage; + i: update.i, + } satisfies YUpdateFromClientRequest; }); }); }) diff --git a/addons/dexie-cloud/src/yjs/listYClientMessages.ts b/addons/dexie-cloud/src/yjs/listYClientMessages.ts index 65974b2f8..3719026cd 100644 --- a/addons/dexie-cloud/src/yjs/listYClientMessages.ts +++ b/addons/dexie-cloud/src/yjs/listYClientMessages.ts @@ -21,10 +21,11 @@ export async function listYClientMessages( .map(({ i, k, u }: YUpdateRow) => { return { type: 'u-c', - utbl: yProp.updatesTable, - i, + table: table.name, + prop: yProp.prop, k, u, + i, } satisfies YClientMessage; }) ); diff --git a/libs/dexie-cloud-common/package.json b/libs/dexie-cloud-common/package.json index 33b357215..b564c1217 100644 --- a/libs/dexie-cloud-common/package.json +++ b/libs/dexie-cloud-common/package.json @@ -1,6 +1,6 @@ { "name": "dexie-cloud-common", - "version": "1.0.35", + "version": "1.0.36", "description": "Library for shared code between dexie-cloud-addon, dexie-cloud (CLI) and dexie-cloud-server", "type": "module", "module": "dist/index.js", diff --git a/libs/dexie-cloud-common/src/YMessage.ts b/libs/dexie-cloud-common/src/YMessage.ts index 3e5471774..efbc8784f 100644 --- a/libs/dexie-cloud-common/src/YMessage.ts +++ b/libs/dexie-cloud-common/src/YMessage.ts @@ -5,7 +5,8 @@ export type YServerMessage = YUpdateFromClientAck | YUpdateFromClientReject | YU export interface YUpdateFromClientRequest { type: 'u-c'; - utbl: string; + table: string; + prop: string; k: any; u: Uint8Array; i: number; @@ -13,20 +14,23 @@ export interface YUpdateFromClientRequest { export interface YUpdateFromClientAck { type: 'u-ack'; - utbl: string; + table: string; + prop: string; i: number; } export interface YUpdateFromClientReject { type: 'u-reject'; - utbl: string; + table: string; + prop: string; i: number; } export interface YUpdateFromServerMessage { type: 'u-s'; - utbl: string; + table: string; + prop: string; k: any; u: Uint8Array; realmSetHash: Uint8Array; @@ -35,7 +39,8 @@ export interface YUpdateFromServerMessage { export interface YAwarenessUpdate { type: 'aware'; - utbl: string; + table: string; + prop: string; k: any; u: Uint8Array; } diff --git a/src/classes/table/table.ts b/src/classes/table/table.ts index 7968c101c..712a36fea 100644 --- a/src/classes/table/table.ts +++ b/src/classes/table/table.ts @@ -281,7 +281,7 @@ export class Table implements ITable { const Y = getYLibrary(db); constructor = class extends (constructor as any) {}; this.schema.yProps.forEach(({prop, updatesTable}) => { - Object.defineProperty(constructor.prototype, prop, createYDocProperty(db, Y, this, updatesTable)); + Object.defineProperty(constructor.prototype, prop, createYDocProperty(db, Y, this, prop, updatesTable)); }); } // Collect all inherited property names (including method names) by diff --git a/src/public/index.d.ts b/src/public/index.d.ts index 69ec2b3ea..c451dd3f1 100644 --- a/src/public/index.d.ts +++ b/src/public/index.d.ts @@ -27,7 +27,7 @@ import { IntervalTree, RangeSetConstructor } from './types/rangeset'; import { Dexie, TableProp } from './types/dexie'; export type { TableProp }; import { PropModification, PropModSpec, PropModSymbol } from './types/prop-modification'; -import { DexieYProvider, DucktypedYDoc, YSyncer, YUpdateRow, YLastCompressed, DexieYDocMeta } from './types/yjs-related'; +import { DexieYProvider, DucktypedYDoc, YSyncer, YUpdateRow, YLastCompressed, DexieYDocMeta, YDocCache } from './types/yjs-related'; export { PropModification, PropModSpec, PropModSymbol }; export * from './types/entity'; export * from './types/entity-table'; @@ -72,12 +72,7 @@ export function remove(num: number | bigint | any[]): PropModification; declare var DexieYProvider: { (doc: DucktypedYDoc): DexieYProvider; new (doc: DucktypedYDoc): DexieYProvider; - getDocCache: (db: Dexie) => { - readonly size: number; - find: (updatesTable: string, parentId: any) => DucktypedYDoc | undefined; - add: (doc: DucktypedYDoc) => void; - delete: (doc: DucktypedYDoc) => void; - }; + getDocCache: (db: Dexie) => YDocCache; } export { DexieYProvider, RangeSet }; diff --git a/src/public/types/yjs-related.ts b/src/public/types/yjs-related.ts index 389fc32a6..e818eeff9 100644 --- a/src/public/types/yjs-related.ts +++ b/src/public/types/yjs-related.ts @@ -54,12 +54,11 @@ export interface DucktypedYDoc extends DucktypedYObservable { } export interface DexieYDocMeta { - db: Dexie, - updatesTable: string, - parentTable: string, - parentId: any - //prop: string, - //cacheKey: string + db: Dexie; + parentTable: string; + parentId: any; + parentProp: string; + updatesTable: string; } /** Docktyped Awareness */ @@ -137,3 +136,10 @@ export interface DexieYProvider { destroy(): void; readonly destroyed: boolean; } + +export interface YDocCache { + readonly size: number; + find: (table: string, primaryKey: any, ydocProp: string) => DucktypedYDoc | undefined + add: (doc: DucktypedYDoc) => void; + delete: (doc: DucktypedYDoc) => void; +} diff --git a/src/yjs/createYDocProperty.ts b/src/yjs/createYDocProperty.ts index c18412cd1..49de0859d 100644 --- a/src/yjs/createYDocProperty.ts +++ b/src/yjs/createYDocProperty.ts @@ -8,6 +8,7 @@ export function createYDocProperty( db: Dexie, Y: DucktypedY, table: Table, + prop: string, updatesTable: string ) { const pkKeyPath = table.schema.primKey.keyPath; @@ -16,13 +17,14 @@ export function createYDocProperty( get(this: object) { const id = getByKeyPath(this, pkKeyPath); - let doc = docCache.find(updatesTable, id); + let doc = docCache.find(table.name, id, prop); if (doc) return doc; doc = new Y.Doc({ meta: { db, updatesTable, + parentProp: prop, parentTable: table.name, parentId: id } satisfies DexieYDocMeta, diff --git a/src/yjs/docCache.ts b/src/yjs/docCache.ts index 62f8abeea..f2a1edf4b 100644 --- a/src/yjs/docCache.ts +++ b/src/yjs/docCache.ts @@ -1,30 +1,30 @@ import { Dexie } from '../public/types/dexie'; -import type { DucktypedYDoc } from '../public/types/yjs-related'; +import type { DexieYDocMeta, DucktypedYDoc, YDocCache } from '../public/types/yjs-related'; // The Y.Doc cache containing all active documents -export function getDocCache(db: Dexie) { +export function getDocCache(db: Dexie): YDocCache { return db._novip['_docCache'] ??= { cache: {} as { [key: string]: WeakRef; }, get size() { return Object.keys(this.cache).length; }, - find(updatesTable: string, parentId: any): DucktypedYDoc | undefined { - const cacheKey = getYDocCacheKey(updatesTable, parentId); + find(table: string, primaryKey: any, ydocProp: string): DucktypedYDoc | undefined { + const cacheKey = getYDocCacheKey(table, primaryKey, ydocProp); const docRef = this.cache[cacheKey]; return docRef ? docRef.deref() : undefined; }, add(doc: DucktypedYDoc): void { - const { updatesTable, parentId } = doc.meta; - if (!updatesTable || parentId == null) + const { parentTable, parentId, parentProp } = doc.meta as DexieYDocMeta; + if (!parentTable || !parentProp || parentId == null) throw new Error(`Missing Dexie-related metadata in Y.Doc`); - const cacheKey = getYDocCacheKey(updatesTable, parentId); + const cacheKey = getYDocCacheKey(parentTable, parentId, parentProp); this.cache[cacheKey] = new WeakRef(doc); docRegistry.register(doc, { cache: this.cache, key: cacheKey }); }, delete(doc: DucktypedYDoc): void { docRegistry.unregister(doc); delete this.cache[ - getYDocCacheKey(doc.meta.updatesTable, doc.meta.parentId) + getYDocCacheKey(doc.meta.parentTable, doc.meta.parentId, doc.meta.parentProp) ]; }, }; @@ -43,6 +43,6 @@ export function throwIfDestroyed(doc: object) { throw new Error('Y.Doc has been destroyed'); } -export function getYDocCacheKey(yTable: string, parentId: any): string { - return `${yTable}[${parentId}]`; +export function getYDocCacheKey(table: string, primaryKey: any, ydocProp: string): string { + return `${table}[${primaryKey}].${ydocProp}`; }