Skip to content

Commit

Permalink
Refactor: use table+prop instead of updatesTable because:
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
dfahlander committed Jul 31, 2024
1 parent 659f7a1 commit aa38a76
Show file tree
Hide file tree
Showing 12 changed files with 76 additions and 50 deletions.
2 changes: 1 addition & 1 deletion addons/dexie-cloud/src/WSObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
21 changes: 15 additions & 6 deletions addons/dexie-cloud/src/yjs/applyYMessages.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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<YUpdateRow, 'i'>);
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);
Expand All @@ -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;
}

8 changes: 5 additions & 3 deletions addons/dexie-cloud/src/yjs/awareness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function createYHandler(db: DexieCloudDB) {
const awap = getAwarenessLibrary(db);
return (provider: DexieYProvider<import('yjs').Doc & {_awareness: any}>) => {
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.
}
Expand All @@ -32,7 +32,8 @@ export function createYHandler(db: DexieCloudDB) {
);
db.messageProducer.next({
type: 'aware',
utbl: updatesTable,
table: parentTable,
prop: parentProp,
k: parentId,
u: update,
});
Expand All @@ -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,
});
Expand Down
20 changes: 13 additions & 7 deletions addons/dexie-cloud/src/yjs/createYClientUpdateObservable.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
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';
import { DEXIE_CLOUD_SYNCER_ID } from '../sync/DEXIE_CLOUD_SYNCER_ID';
import { listUpdatesSince } from './listUpdatesSince';

export function createYClientUpdateObservable(db: DexieCloudDB): Observable<YClientMessage> {
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()
Expand All @@ -33,10 +37,12 @@ export function createYClientUpdateObservable(db: DexieCloudDB): Observable<YCli
.map((update) => {
return {
type: 'u-c',
utbl: tblName,
table,
prop: ydocProp,
k: update.k,
u: update.u,
} as YClientMessage;
i: update.i,
} satisfies YUpdateFromClientRequest;
});
});
})
Expand Down
5 changes: 3 additions & 2 deletions addons/dexie-cloud/src/yjs/listYClientMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
})
);
Expand Down
2 changes: 1 addition & 1 deletion libs/dexie-cloud-common/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
15 changes: 10 additions & 5 deletions libs/dexie-cloud-common/src/YMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,32 @@ export type YServerMessage = YUpdateFromClientAck | YUpdateFromClientReject | YU

export interface YUpdateFromClientRequest {
type: 'u-c';
utbl: string;
table: string;
prop: string;
k: any;
u: Uint8Array;
i: number;
}

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;
Expand All @@ -35,7 +39,8 @@ export interface YUpdateFromServerMessage {

export interface YAwarenessUpdate {
type: 'aware';
utbl: string;
table: string;
prop: string;
k: any;
u: Uint8Array;
}
Expand Down
2 changes: 1 addition & 1 deletion src/classes/table/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export class Table implements ITable<any, IndexableType> {
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
Expand Down
9 changes: 2 additions & 7 deletions src/public/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 };
Expand Down
18 changes: 12 additions & 6 deletions src/public/types/yjs-related.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -137,3 +136,10 @@ export interface DexieYProvider<YDoc=any> {
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;
}
4 changes: 3 additions & 1 deletion src/yjs/createYDocProperty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export function createYDocProperty(
db: Dexie,
Y: DucktypedY,
table: Table,
prop: string,
updatesTable: string
) {
const pkKeyPath = table.schema.primKey.keyPath;
Expand All @@ -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,
Expand Down
20 changes: 10 additions & 10 deletions src/yjs/docCache.ts
Original file line number Diff line number Diff line change
@@ -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<DucktypedYDoc>; },
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)
];
},
};
Expand All @@ -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}`;
}

0 comments on commit aa38a76

Please sign in to comment.