From b56cee36840d05e5479085ea22b9e9cd5b9be779 Mon Sep 17 00:00:00 2001 From: dfahlander Date: Mon, 10 Jun 2024 19:39:51 +0200 Subject: [PATCH 01/23] Using lib es2021 (for FinalizationRegistry) + installed yjs as devdep. --- package.json | 5 +++-- pnpm-lock.yaml | 22 ++++++++++++++++++++++ src/tsconfig.json | 2 +- test/tsconfig.json | 2 +- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 85fa4cd92..db15c14e7 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "# Build dist/dexie.js, dist/dexie.mjs and dist/dexie.d.ts", "cd src", "tsc [--watch 'Watching for file changes']", - "tsc --target es2020 --outdir ../tools/tmp/modern/src/", + "tsc --target es2021 --outdir ../tools/tmp/modern/src/", "rollup -c ../tools/build-configs/rollup.config.js", "rollup -c ../tools/build-configs/rollup.umd.config.js", "rollup -c ../tools/build-configs/rollup.modern.config.js", @@ -140,6 +140,7 @@ "terser": "^5.3.1", "tslib": "^2.1.0", "typescript": "^5.3.3", - "uglify-js": "^3.9.2" + "uglify-js": "^3.9.2", + "yjs": "^13.6.16" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e64070249..9a28f663f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: uglify-js: specifier: ^3.9.2 version: 3.14.2 + yjs: + specifier: ^13.6.16 + version: 13.6.16 addons/Dexie.Observable: devDependencies: @@ -7990,6 +7993,10 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + /isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + dev: true + /isstream@0.1.2: resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} dev: true @@ -9004,6 +9011,14 @@ packages: prelude-ls: 1.2.1 type-check: 0.4.0 + /lib0@0.2.94: + resolution: {integrity: sha512-hZ3p54jL4Wpu7IOg26uC7dnEWiMyNlUrb9KoG7+xYs45WkQwpVvKFndVq2+pqLYKe1u8Fp3+zAfZHVvTK34PvQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + isomorphic.js: 0.2.5 + dev: true + /lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -13873,6 +13888,13 @@ packages: y18n: 5.0.8 yargs-parser: 20.2.9 + /yjs@13.6.16: + resolution: {integrity: sha512-uEq+n/dFIecBElEdeQea8nDnltScBfuhCSyAxDw4CosveP9Ag0eW6iZi2mdpW7EgxSFT7VXK2MJl3tKaLTmhAQ==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + dependencies: + lib0: 0.2.94 + dev: true + /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} diff --git a/src/tsconfig.json b/src/tsconfig.json index 5ed50fa97..035e2d049 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -8,7 +8,7 @@ "noImplicitAny": false, "noImplicitReturns": false, "moduleResolution": "node", - "lib": ["es2020", "dom"], + "lib": ["es2021", "dom"], "forceConsistentCasingInFileNames": true, "outDir": "../tools/tmp/src/", "sourceMap": true diff --git a/test/tsconfig.json b/test/tsconfig.json index 419a578d3..a5559f4b2 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -8,7 +8,7 @@ "noImplicitAny": false, "noImplicitReturns": false, "moduleResolution": "node", - "lib": ["es2020", "dom"], + "lib": ["es2021", "dom"], "forceConsistentCasingInFileNames": true, "outDir": "../tools/tmp/", "sourceMap": true, From 24ac71f55ee550bbbabc17f6a5b992dc3f71f7a3 Mon Sep 17 00:00:00 2001 From: dfahlander Date: Wed, 19 Jun 2024 13:33:21 +0200 Subject: [PATCH 02/23] Support for Y.js complete (dry impl) --- import-wrapper-prod.mjs | 6 +- import-wrapper.mjs | 7 +- src/classes/collection/collection.ts | 31 +++-- src/classes/dexie/dexie.ts | 9 +- src/classes/table/table.ts | 9 ++ .../transaction/transaction-constructor.ts | 15 +++ src/classes/version/schema-helpers.ts | 8 +- src/classes/version/version.ts | 127 ++++++++++++++---- src/helpers/index-spec.ts | 6 +- src/helpers/table-schema.ts | 12 +- src/index.ts | 1 + src/public/index.d.ts | 5 + src/public/types/_insert-type.d.ts | 19 +++ src/public/types/db-events.d.ts | 9 ++ src/public/types/dexie-constructor.d.ts | 2 + src/public/types/dexie.d.ts | 4 +- src/public/types/index-spec.d.ts | 1 + src/public/types/insert-type.d.ts | 8 +- src/public/types/keypaths.d.ts | 6 +- src/public/types/table-schema.d.ts | 1 + src/public/types/yjs-related.ts | 93 +++++++++++++ src/yjs/DexieYProvider.ts | 40 ++++++ src/yjs/createYDocProperty.ts | 44 ++++++ src/yjs/docCache.ts | 16 +++ src/yjs/observeUpdates.ts | 92 +++++++++++++ test/typings-test/test-typings.ts | 31 +++++ 26 files changed, 547 insertions(+), 55 deletions(-) create mode 100644 src/public/types/_insert-type.d.ts create mode 100644 src/public/types/yjs-related.ts create mode 100644 src/yjs/DexieYProvider.ts create mode 100644 src/yjs/createYDocProperty.ts create mode 100644 src/yjs/docCache.ts create mode 100644 src/yjs/observeUpdates.ts diff --git a/import-wrapper-prod.mjs b/import-wrapper-prod.mjs index a6556c91a..69a9ca098 100644 --- a/import-wrapper-prod.mjs +++ b/import-wrapper-prod.mjs @@ -9,7 +9,9 @@ if (_Dexie.semVer !== Dexie.semVer) { } const { liveQuery, mergeRanges, rangesOverlap, RangeSet, cmp, Entity, - PropModSymbol, PropModification, replacePrefix, add, remove } = Dexie; + PropModSymbol, PropModification, replacePrefix, add, remove, + DexieYProvider } = Dexie; export { liveQuery, mergeRanges, rangesOverlap, RangeSet, cmp, Dexie, Entity, - PropModSymbol, PropModification, replacePrefix, add, remove }; + PropModSymbol, PropModification, replacePrefix, add, remove, + DexieYProvider}; export default Dexie; diff --git a/import-wrapper.mjs b/import-wrapper.mjs index c70c6b72a..24a30ee6e 100644 --- a/import-wrapper.mjs +++ b/import-wrapper.mjs @@ -8,7 +8,10 @@ if (_Dexie.semVer !== Dexie.semVer) { throw new Error(`Two different versions of Dexie loaded in the same app: ${_Dexie.semVer} and ${Dexie.semVer}`); } const { liveQuery, mergeRanges, rangesOverlap, RangeSet, cmp, Entity, - PropModSymbol, PropModification, replacePrefix, add, remove } = Dexie; + PropModSymbol, PropModification, replacePrefix, add, remove, + DexieYProvider } = Dexie; export { liveQuery, mergeRanges, rangesOverlap, RangeSet, cmp, Dexie, Entity, - PropModSymbol, PropModification, replacePrefix, add, remove }; + PropModSymbol, PropModification, replacePrefix, add, remove, + DexieYProvider}; + export default Dexie; diff --git a/src/classes/collection/collection.ts b/src/classes/collection/collection.ts index abca9c089..70b16a7f7 100644 --- a/src/classes/collection/collection.ts +++ b/src/classes/collection/collection.ts @@ -502,29 +502,31 @@ export class Collection implements ICollection { totalFailures.push(failures[pos]); } } + const isUnconditionalDelete = changes === deleteCallback; // Collection.delete() calls this. return this.clone().primaryKeys().then(keys => { const criteria = isPlainKeyRange(ctx) && ctx.limit === Infinity && - (typeof changes !== 'function' || changes === deleteCallback) && { + (typeof changes !== 'function' || isUnconditionalDelete) && { index: ctx.index, range: ctx.range }; const nextChunk = (offset: number) => { const count = Math.min(limit, keys.length - offset); - return coreTable.getMany({ + const keysInChunk = keys.slice(offset, offset + count); + return (isUnconditionalDelete ? Promise.resolve([]) : coreTable.getMany({ trans, - keys: keys.slice(offset, offset + count), + keys: keysInChunk, cache: "immutable" // Optimize for 2 things: // 1) observability-middleware can track changes better. // 2) hooks middleware don't have to query the existing values again when tracking changes. // We can use "immutable" because we promise to not touch the values we retrieve here! - }).then(values => { + })).then(values => { const addValues = []; const putValues = []; const putKeys = outbound ? [] : null; - const deleteKeys = []; - for (let i=0; i 0 }).then(res=>applyMutateResult(putValues.length, res)) - ).then(()=>(deleteKeys.length > 0 || (criteria && changes === deleteCallback)) && + ).then(()=>(deleteKeys.length > 0 || (criteria && isUnconditionalDelete)) && coreTable.mutate({ trans, type: 'delete', @@ -575,6 +577,17 @@ export class Collection implements ICollection { isAdditionalChunk: offset > 0 }).then(res=>applyMutateResult(deleteKeys.length, res)) ).then(()=>{ + if (ctx.table.schema.yProps) { + // Delete related document updates. Otherwise, if a row with same ID is created + // again, its document would not be empty. + // Document providers will get notified on the main table's row deletion and destroy + // document. Sync of this action is outside of the Y.js scope but will be handled + // by the dexie cloud sync layer or equivalent sync layer. + return Promise.all(ctx.table.schema.yProps.map(prop => { + return this.db.table(prop.updTable).where('k').anyOf(keysInChunk).delete(); + })); + } + }).then(()=>{ return keys.length > offset + count && nextChunk(offset + limit); }); }); @@ -602,6 +615,7 @@ export class Collection implements ICollection { //deletingHook = ctx.table.hook.deleting.fire, //hasDeleteHook = deletingHook !== nop; if (isPlainKeyRange(ctx) && + !ctx.table.schema.yProps && // No Y documents on this table (requires special care) (ctx.isPrimKey || range.type === DBCoreRangeType.Any)) // if no range, we'll use clear(). { // May use IDBObjectStore.delete(IDBKeyRange) in this case (Issue #208) @@ -612,9 +626,10 @@ export class Collection implements ICollection { // Our API contract is to return a count of deleted items, so we have to count() before delete(). const {primaryKey} = ctx.table.core.schema; const coreRange = range; + return ctx.table.core.count({trans, query: {index: primaryKey, range: coreRange}}).then(count => { return ctx.table.core.mutate({trans, type: 'deleteRange', range: coreRange}) - .then(({failures, lastResult, results, numFailures}) => { + .then(({failures, numFailures}) => { if (numFailures) throw new ModifyError("Could not delete some values", Object.keys(failures).map(pos => failures[pos]), count - numFailures); diff --git a/src/classes/dexie/dexie.ts b/src/classes/dexie/dexie.ts index dd393019e..4fd7b1697 100644 --- a/src/classes/dexie/dexie.ts +++ b/src/classes/dexie/dexie.ts @@ -138,7 +138,14 @@ export class Dexie implements IDexie { }); this._state = state; this.name = name; - this.on = Events(this, "populate", "blocked", "versionchange", "close", { ready: [promisableChain, nop] }) as DbEvents; + this.on = Events(this, + "populate", + "blocked", + "versionchange", + "close", + "y", + { ready: [promisableChain, nop] } + ) as DbEvents; this.on.ready.subscribe = override(this.on.ready.subscribe, subscribe => { return (subscriber, bSticky) => { (Dexie as any as DexieConstructor).vip(() => { diff --git a/src/classes/table/table.ts b/src/classes/table/table.ts index e081a4d98..2f9bdea6d 100644 --- a/src/classes/table/table.ts +++ b/src/classes/table/table.ts @@ -19,6 +19,7 @@ import { workaroundForUndefinedPrimKey } from '../../functions/workaround-undefi import { Entity } from '../entity/Entity'; import { UpdateSpec } from '../../public'; import { cmp } from '../../functions/cmp'; +import { createYDocProperty } from '../../yjs/createYDocProperty'; /** class Table * @@ -274,6 +275,14 @@ export class Table implements ITable { table() { return tableName; } } } + if (this.schema.yProps) { + const { Y } = db._options; + if (!Y) throw new exceptions.MissingAPI('Y library not supplied to Dexie constructor'); + constructor = class extends (constructor as any) {}; + this.schema.yProps.forEach(({prop, updTable}) => { + Object.defineProperty(constructor.prototype, prop, createYDocProperty(db, Y, this, prop, updTable)); + }); + } // Collect all inherited property names (including method names) by // walking the prototype chain. This is to avoid overwriting them from // database data - so application code can rely on inherited props never diff --git a/src/classes/transaction/transaction-constructor.ts b/src/classes/transaction/transaction-constructor.ts index 0472fb3dd..625805f24 100644 --- a/src/classes/transaction/transaction-constructor.ts +++ b/src/classes/transaction/transaction-constructor.ts @@ -32,6 +32,21 @@ export function createTransactionConstructor(db: Dexie) { chromeTransactionDurability: ChromeTransactionDurability, parent?: Transaction) { + let cloned = false; + if (mode !== 'readonly') storeNames.forEach(storeName => { + // Uncollapse storeName to include Y update tables in case deletion of a record - then we must also delete its Y updates. + const yProps = dbschema[storeName]?.yProps; + if (yProps) { + if (!cloned) { + storeNames = storeNames.slice(0); // Clone storeNames + cloned = true; + } + yProps.forEach(yProp => { + storeNames.push(`$${storeName}.${yProp}_updates`); + }); + } + }); + this.db = db; this.mode = mode; this.storeNames = storeNames; diff --git a/src/classes/version/schema-helpers.ts b/src/classes/version/schema-helpers.ts index 366fd4a0a..40fe5f753 100644 --- a/src/classes/version/schema-helpers.ts +++ b/src/classes/version/schema-helpers.ts @@ -463,7 +463,10 @@ export function adjustToExistingIndexNames(db: Dexie, schema: DbSchema, idbtrans export function parseIndexSyntax(primKeyAndIndexes: string): IndexSpec[] { return primKeyAndIndexes.split(',').map((index, indexNum) => { - index = index.trim(); + const typeSplit = index.split(':'); + const type = typeSplit[1]?.trim(); + index = typeSplit[0].trim(); + if (type && type !== 'Y') throw new exceptions.Schema(`Unsupported type '${type}'`); // Y is currently the only supported type. const name = index.replace(/([&*]|\+\+)/g, ""); // Remove "&", "++" and "*" // Let keyPath of "[a+b]" be ["a","b"]: const keyPath = /^\[/.test(name) ? name.match(/^\[(.*)\]$/)[1].split('+') : name; @@ -475,7 +478,8 @@ export function parseIndexSyntax(primKeyAndIndexes: string): IndexSpec[] { /\*/.test(index), /\+\+/.test(index), isArray(keyPath), - indexNum === 0 + indexNum === 0, + type ); }); } diff --git a/src/classes/version/version.ts b/src/classes/version/version.ts index 83f425458..25f7cf7d6 100644 --- a/src/classes/version/version.ts +++ b/src/classes/version/version.ts @@ -15,55 +15,126 @@ import { nop, promisableChain } from '../../functions/chaining-functions'; export class Version implements IVersion { db: Dexie; _cfg: { - version: number, - storesSource: { [tableName: string]: string | null }, - dbschema: DbSchema, - tables: {}, - contentUpgrade: Function | null - } + version: number; + storesSource: { [tableName: string]: string | null }; + dbschema: DbSchema; + tables: {}; + contentUpgrade: Function | null; + }; - _parseStoresSpec(stores: { [tableName: string]: string | null }, outSchema: DbSchema): any { - keys(stores).forEach(tableName => { + _parseStoresSpec( + stores: { [tableName: string]: string | null }, + outSchema: DbSchema + ): any { + keys(stores).forEach((tableName) => { if (stores[tableName] !== null) { - var indexes = parseIndexSyntax(stores[tableName]); - var primKey = indexes.shift(); - primKey.unique = true; - if (primKey.multi) throw new exceptions.Schema("Primary key cannot be multi-valued"); - indexes.forEach(idx => { - if (idx.auto) throw new exceptions.Schema("Only primary key can be marked as autoIncrement (++)"); - if (!idx.keyPath) throw new exceptions.Schema("Index must have a name and cannot be an empty string"); - }); - outSchema[tableName] = createTableSchema(tableName, primKey, indexes); + let indexes = parseIndexSyntax(stores[tableName]); + + // + // Support Y.js specific syntax + // + const yProps = indexes.filter((idx) => idx.type === 'Y').map((idx) => idx.name); + indexes = indexes.filter((idx) => idx.type !== 'Y'); // Y marks just the Y.Doc type and is not an index + const primKey = indexes.shift(); + if (!primKey) { + // {table: ':Y'} not supported. + throw new exceptions.Schema( + 'Invalid schema for table ' + tableName + ': ' + stores[tableName] + ); + } + + primKey.unique = true; + if (primKey.multi) + throw new exceptions.Schema('Primary key cannot be multiEntry*'); + indexes.forEach((idx) => { + if (idx.auto) + throw new exceptions.Schema( + 'Only primary key can be marked as autoIncrement (++)' + ); + if (!idx.keyPath) + throw new exceptions.Schema( + 'Index must have a name and cannot be an empty string' + ); + }); + const tblSchema = createTableSchema( + tableName, + primKey, + indexes, + yProps.length ? yProps : undefined + ); + outSchema[tableName] = tblSchema; + + // Generate update tables for Y.js properties + for (const yProp of tblSchema.yProps || []) { + this._parseStoresSpec( + // Add a table for each yProp containing document updates. + // See interface YUpdateRow { i: number, k: IndexableType, u: Uint8Array, f?: number} + // where + // i is the auto-incremented primary key of the update table, + // k is the primary key from the other table holding the document in a property. + // u is the update data from Y.js + // f is a flag indicating if the update comes from this client or another. + // Index use cases: + // * Load entire document: Use index k (part of [k+i] ) + // * After object load, observe updates on a certain document since a given revision: Use index [k+i] + // * After initial sync, observe flagged updates since a given revision: Use index [f+i]. Local updates are flagged + // while remote updates are not. + // + { [yProp.updTable]: '++i,[k+i],[f+i]' }, + outSchema + ); + } } }); } - stores(stores: { [key: string]: string | null; }): IVersion { + stores(stores: { [key: string]: string | null }): IVersion { const db = this.db; - this._cfg.storesSource = this._cfg.storesSource ? - extend(this._cfg.storesSource, stores) : - stores; + this._cfg.storesSource = this._cfg.storesSource + ? extend(this._cfg.storesSource, stores) + : stores; const versions = db._versions; // Derive stores from earlier versions if they are not explicitely specified as null or a new syntax. - const storesSpec: { [key: string]: string; } = {}; - let dbschema = {}; - versions.forEach(version => { // 'versions' is always sorted by lowest version first. + const storesSpec: { [key: string]: string } = {}; + let dbschema: DbSchema = {}; + versions.forEach((version) => { + // 'versions' is always sorted by lowest version first. extend(storesSpec, version._cfg.storesSource); - dbschema = (version._cfg.dbschema = {}); + dbschema = version._cfg.dbschema = {}; version._parseStoresSpec(storesSpec, dbschema); }); // Update the latest schema to this version db._dbSchema = dbschema; // Update APIs removeTablesApi(db, [db._allTables, db, db.Transaction.prototype]); - setApiOnPlace(db, [db._allTables, db, db.Transaction.prototype, this._cfg.tables], keys(dbschema), dbschema); + setApiOnPlace( + db, + [db._allTables, db, db.Transaction.prototype, this._cfg.tables], + keys(dbschema), + dbschema + ); db._storeNames = keys(dbschema); + db._storeNames.forEach((tableName) => { + if (dbschema[tableName].yProps) { + // If a table as yProps, make sure to derive a class with generated Y properties. + // This is done in the mapToClass method. In case user has called mapToClass already, respect mappedClass, + // otherwise use Object as default to create a top-level class with the generated y properties. + db.table(tableName).mapToClass( + dbschema[tableName].mappedClass || Object + ); + } + }); return this; } - upgrade(upgradeFunction: (trans: Transaction) => PromiseLike | void): Version { - this._cfg.contentUpgrade = promisableChain(this._cfg.contentUpgrade || nop, upgradeFunction); + upgrade( + upgradeFunction: (trans: Transaction) => PromiseLike | void + ): Version { + this._cfg.contentUpgrade = promisableChain( + this._cfg.contentUpgrade || nop, + upgradeFunction + ); return this; } } diff --git a/src/helpers/index-spec.ts b/src/helpers/index-spec.ts index d33f6ef9f..1b4b59af0 100644 --- a/src/helpers/index-spec.ts +++ b/src/helpers/index-spec.ts @@ -7,7 +7,8 @@ export function createIndexSpec( multi: boolean, auto: boolean, compound: boolean, - isPrimKey: boolean + isPrimKey: boolean, + type?: string ): IndexSpec { return { name, @@ -16,7 +17,8 @@ export function createIndexSpec( multi, auto, compound, - src: (unique && !isPrimKey ? '&' : '') + (multi ? '*' : '') + (auto ? "++" : "") + nameFromKeyPath(keyPath) + src: (unique && !isPrimKey ? '&' : '') + (multi ? '*' : '') + (auto ? "++" : "") + nameFromKeyPath(keyPath), + type } } diff --git a/src/helpers/table-schema.ts b/src/helpers/table-schema.ts index 2115b0cf0..6f70a7b6f 100644 --- a/src/helpers/table-schema.ts +++ b/src/helpers/table-schema.ts @@ -1,18 +1,22 @@ import { IndexSpec } from '../public/types/index-spec'; import { TableSchema } from '../public/types/table-schema'; -import { createIndexSpec } from './index-spec'; import { arrayToObject } from '../functions/utils'; -export function createTableSchema ( +export function createTableSchema( name: string, primKey: IndexSpec, - indexes: IndexSpec[] + indexes: IndexSpec[], + yProps?: string[] ): TableSchema { return { name, primKey, indexes, mappedClass: null, - idxByName: arrayToObject(indexes, index => [index.name, index]) + yProps: yProps?.map((prop) => ({ + prop, + updTable: `$${name}.${prop}_updates`, + })), + idxByName: arrayToObject(indexes, (index) => [index.name, index]), }; } diff --git a/src/index.ts b/src/index.ts index ba0aca2ca..40f9674d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,4 +31,5 @@ export { Dexie, liveQuery }; // Comply with public/index.d.ts. export { Entity }; export { cmp }; export { PropModSymbol, PropModification, replacePrefix, add, remove }; +export { DexieYProvider } from './yjs/DexieYProvider'; export default Dexie; diff --git a/src/public/index.d.ts b/src/public/index.d.ts index c22777a84..c0b12b0ec 100644 --- a/src/public/index.d.ts +++ b/src/public/index.d.ts @@ -27,6 +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 } from './types/yjs-related'; export { PropModification, PropModSpec, PropModSymbol }; export * from './types/entity'; export * from './types/entity-table'; @@ -67,6 +68,10 @@ export function cmp(a: any, b: any): number; export function replacePrefix(a: string, b: string): PropModification; export function add(num: number | bigint | any[]): PropModification; export function remove(num: number | bigint | any[]): PropModification; +declare var DexieYProvider: { + (doc: DucktypedYDoc): DexieYProvider; + new (doc: DucktypedYDoc): DexieYProvider; +} /** Exporting 'Dexie' as the default export. **/ diff --git a/src/public/types/_insert-type.d.ts b/src/public/types/_insert-type.d.ts new file mode 100644 index 000000000..1c9d09bd7 --- /dev/null +++ b/src/public/types/_insert-type.d.ts @@ -0,0 +1,19 @@ +import { IsStrictlyAny } from "./is-strictly-any"; + +/** Extract the union of literal method names in T + */ + type NonInsertProps = { + [P in keyof T]: IsStrictlyAny extends true + ? never // Plain property of type any (not method) + : T[P] extends (...args: any[]) => any + ? P // a function (method) + : T[P] extends {on:any,subdocs:any,gc:any,isSynced:any,isLoaded:any,shouldLoad:any} + ? P // an YDoc property - should be generated by dexie and not by user + : never; // Not a non-insert property (don't omit it from InsertType) +}[keyof T]; + +/** Default insert type of T is a subset of T where: + * * given optional props (such as an auto-generated primary key) are made optional + * * methods are omitted + */ + export type InsertType = Omit> & {[P in OptionalProps]?: T[P]}; diff --git a/src/public/types/db-events.d.ts b/src/public/types/db-events.d.ts index 720c3ad16..dc0e002d9 100644 --- a/src/public/types/db-events.d.ts +++ b/src/public/types/db-events.d.ts @@ -3,6 +3,7 @@ import { DexieEvent } from "./dexie-event"; import { Transaction } from "./transaction"; import { Dexie } from "./dexie"; import { IntervalTree } from "./rangeset"; +import { DexieYProvider } from "./yjs-related"; export interface DexieOnReadyEvent { subscribe(fn: (vipDb: Dexie) => any, bSticky: boolean): void; @@ -28,17 +29,25 @@ export interface DexieCloseEvent { fire(event: Event): any; } +export interface DexieYEvent { + subscribe(fn: (provider: DexieYProvider, Y: any) => void): void; + unsubscribe(fn: (provider: DexieYProvider, Y: any) => void): void; + fire(provider: DexieYProvider, Y: any): void; +} + export interface DbEvents extends DexieEventSet { (eventName: 'ready', subscriber: (vipDb: Dexie) => any, bSticky?: boolean): void; (eventName: 'populate', subscriber: (trans: Transaction) => any): void; (eventName: 'blocked', subscriber: (event: IDBVersionChangeEvent) => any): void; (eventName: 'versionchange', subscriber: (event: IDBVersionChangeEvent) => any): void; (eventName: 'close', subscriber: (event: Event) => any): void; + (eventName: 'y', subscriber: (provider: DexieYProvider, Y: any) => void): void; ready: DexieOnReadyEvent; populate: DexiePopulateEvent; blocked: DexieEvent; versionchange: DexieVersionChangeEvent; close: DexieCloseEvent; + y: DexieYEvent; } /** Set of mutated parts of the database diff --git a/src/public/types/dexie-constructor.d.ts b/src/public/types/dexie-constructor.d.ts index 29f2c45be..8a3d24432 100644 --- a/src/public/types/dexie-constructor.d.ts +++ b/src/public/types/dexie-constructor.d.ts @@ -10,6 +10,7 @@ import { DexieDOMDependencies } from "./dexie-dom-dependencies"; import { GlobalDexieEvents, ObservabilitySet } from "./db-events"; import { Observable } from "./observable"; import { GlobalQueryCache } from "./cache"; +import { DucktypedY } from "./yjs-related"; export type ChromeTransactionDurability = 'default' | 'strict' | 'relaxed' @@ -22,6 +23,7 @@ export interface DexieOptions { modifyChunkSize?: number; chromeTransactionDurability?: ChromeTransactionDurability; cache?: 'immutable' | 'cloned' | 'disabled'; + Y?: DucktypedY; // Caller supplies Y from the following: import * as Y from 'yjs'; } export interface DexieConstructor extends DexieExceptionClasses { diff --git a/src/public/types/dexie.d.ts b/src/public/types/dexie.d.ts index 67189f651..2b419c9df 100644 --- a/src/public/types/dexie.d.ts +++ b/src/public/types/dexie.d.ts @@ -6,8 +6,7 @@ import { Transaction } from './transaction'; import { WhereClause } from './where-clause'; import { Collection } from './collection'; import { DbSchema } from './db-schema'; -import { TableSchema } from './table-schema'; -import { DexieConstructor } from './dexie-constructor'; +import { DexieOptions } from './dexie-constructor'; import { PromiseExtended } from './promise-extended'; import { IndexableType } from './indexable-type'; import { DBCore } from './dbcore'; @@ -29,6 +28,7 @@ export interface Dexie { readonly vip: Dexie; readonly _allTables: { [name: string]: Table }; + readonly _options: DexieOptions; readonly core: DBCore; diff --git a/src/public/types/index-spec.d.ts b/src/public/types/index-spec.d.ts index 65b8e3fe9..404cb0778 100644 --- a/src/public/types/index-spec.d.ts +++ b/src/public/types/index-spec.d.ts @@ -6,4 +6,5 @@ export interface IndexSpec { auto: boolean | undefined; compound: boolean | undefined; src: string; + type?: string | undefined; } diff --git a/src/public/types/insert-type.d.ts b/src/public/types/insert-type.d.ts index 188dc0f84..1c9d09bd7 100644 --- a/src/public/types/insert-type.d.ts +++ b/src/public/types/insert-type.d.ts @@ -2,16 +2,18 @@ import { IsStrictlyAny } from "./is-strictly-any"; /** Extract the union of literal method names in T */ - export type MethodProps = { + type NonInsertProps = { [P in keyof T]: IsStrictlyAny extends true ? never // Plain property of type any (not method) : T[P] extends (...args: any[]) => any ? P // a function (method) - : never; // Not function (not method) + : T[P] extends {on:any,subdocs:any,gc:any,isSynced:any,isLoaded:any,shouldLoad:any} + ? P // an YDoc property - should be generated by dexie and not by user + : never; // Not a non-insert property (don't omit it from InsertType) }[keyof T]; /** Default insert type of T is a subset of T where: * * given optional props (such as an auto-generated primary key) are made optional * * methods are omitted */ - export type InsertType = Omit> & {[P in OptionalProps]?: T[P]}; + export type InsertType = Omit> & {[P in OptionalProps]?: T[P]}; diff --git a/src/public/types/keypaths.d.ts b/src/public/types/keypaths.d.ts index 4b13ed04b..e390c6913 100644 --- a/src/public/types/keypaths.d.ts +++ b/src/public/types/keypaths.d.ts @@ -1,3 +1,5 @@ +import { DucktypedYDoc } from "./yjs-related"; + export type KeyPaths = { [P in keyof T]: P extends string @@ -6,7 +8,9 @@ export type KeyPaths = { ? P | `${P}.${number}` | `${P}.${number}.${KeyPaths}` : P | `${P}.${number}` : T[P] extends (...args: any[]) => any // Method - ? never + ? never + : T[P] extends DucktypedYDoc // circular reference + not valid in update spec or where clause + ? never : T[P] extends object ? P | `${P}.${KeyPaths}` : P diff --git a/src/public/types/table-schema.d.ts b/src/public/types/table-schema.d.ts index 65aaea32b..a447af91a 100644 --- a/src/public/types/table-schema.d.ts +++ b/src/public/types/table-schema.d.ts @@ -4,6 +4,7 @@ export interface TableSchema { name: string; primKey: IndexSpec; indexes: IndexSpec[]; + yProps?: {prop: string, updTable: string}[]; mappedClass: Function; idxByName: {[name: string]: IndexSpec}; readHook?: (x:any) => any diff --git a/src/public/types/yjs-related.ts b/src/public/types/yjs-related.ts new file mode 100644 index 000000000..93425712c --- /dev/null +++ b/src/public/types/yjs-related.ts @@ -0,0 +1,93 @@ +/* Since dexie use "bring your own Y" approach, we provide a + * minimal interface that we require from Yjs document. + * + * We only list the interface that dexie or dexie-cloud might need to call on this + * library. + * + */ + +import { Dexie } from "./dexie"; +import { EntityTable } from "./entity-table"; +import { IndexableType } from "./indexable-type"; +import { Table } from "./table"; + +export interface DucktypedY { + Doc: new(options?: {guid?: string, collectionid?: string, gc?: boolean, gcFilter?: any, meta?: any, autoLoad?: boolean, shouldLoad?: boolean}) => DucktypedYDoc; + applyUpdate: Function; + applyUpdateV2: Function; + encodeStateAsUpdate: Function; + encodeStateAsUpdateV2: Function; + encodeStateVector: Function; + encodeStateVectorFromUpdate: Function; + encodeStateVectorFromUpdateV2: Function; + mergeUpdates: Function; + mergeUpdatesV2: Function; + diffUpdate: Function; + diffUpdateV2: Function; + transact: Function; +} + +export interface DucktypedYObservable { + on (name: string, f: (...args: any[]) => any): void; + off (name: string, f: (...args: any[]) => any): void; + once (name: string, f: (...args: any[]) => any): void; + emit (name: string, args: any[]): void; + destroy(): void; +} + +/** Dock-typed Y.Doc */ +export interface DucktypedYDoc extends DucktypedYObservable { + guid?: any; + collectionid?: any; + collection?: any; + whenLoaded: PromiseLike; + whenSynced: PromiseLike; + isLoaded: any; + isSynced: any; + transact: any; + toJSON: ()=>any; + destroy: ()=>void; + meta?: any; + share: Map; +} + +export interface DexieYDocMeta { + db: Dexie, + table: string, + updatesTable: string, + prop: string, + id: any, + cacheKey: string +} + +/** Docktyped Awareness */ +export interface DucktypedAwareness extends DucktypedYObservable { + doc: DucktypedYDoc; + destroy: () => void; + clientID: any; + states: any; + meta: any; + getLocalState: any; + setLocalState: any; + setLocalStateField: any; + getStates: any; +} + + +export interface YUpdateRow { + i: number; + k: IndexableType; + u: Uint8Array; + f?: number; +} + +export interface DexieYProvider { + doc: YDoc; + awareness?: any; + + on (name: string, f: (...args: any[]) => any): void; + off (name: string, f: (...args: any[]) => any): void; + once (name: string, f: (...args: any[]) => any): void; + emit (name: string, args: any[]): void; + destroy(): void; +} diff --git a/src/yjs/DexieYProvider.ts b/src/yjs/DexieYProvider.ts new file mode 100644 index 000000000..6c981f9c7 --- /dev/null +++ b/src/yjs/DexieYProvider.ts @@ -0,0 +1,40 @@ +import type { + DexieYProvider, + DucktypedYDoc, + DucktypedYObservable, +} from '../public/types/yjs-related'; +import { throwIfDestroyed } from './docCache'; +import { observeUpdates } from './observeUpdates'; + +export function DexieYProvider (doc: DucktypedYDoc): DexieYProvider { + const { guid, collectionid: updatesTable, meta: { db, table }} = + (doc as DucktypedYDoc) || {}; + if (!db || !table || !updatesTable) + throw new Error('Y.Doc not generated by Dexie'); + if (!db.table(table) || !db.table(updatesTable)) { + throw new Error(`Table ${table} or ${updatesTable} not found in db`); + } + throwIfDestroyed(doc); + const Y = db._options.Y; + if (!Y) throw new Error('Y library not supplied to Dexie constructor'); + + const provider: DexieYProvider = new class extends (Y.Observable as new()=>DucktypedYObservable) { + doc = doc; + whenLoaded = new Promise((resolve, reject) => { + this.once('load', resolve); + this.once('error', reject); + }); + whenSynced = new Promise((resolve, reject) => { + this.once('sync', resolve); + this.once('error', reject); + }); + destroy() { + stopObserving(); + super.destroy(); + } + } + const stopObserving = observeUpdates(provider, doc, db, table, updatesTable, guid, Y); + db.on.y.fire(provider, Y); // Allow for addons to invoke their sync- and awareness providers here. + + return provider; +} diff --git a/src/yjs/createYDocProperty.ts b/src/yjs/createYDocProperty.ts new file mode 100644 index 000000000..c9079efaf --- /dev/null +++ b/src/yjs/createYDocProperty.ts @@ -0,0 +1,44 @@ +import type { Table } from '../public/types/table'; +import type { Dexie } from '../public/types/dexie'; +import type { DexieYDocMeta, DucktypedY } from '../public/types/yjs-related'; +import { getByKeyPath } from '../functions/utils'; +import { docCache, destroyed, registry } from './docCache'; + +export function createYDocProperty( + db: Dexie, + Y: DucktypedY, + table: Table, + prop: string, + updatesTable: string +) { + const pkKeyPath = table.schema.primKey.keyPath; + return { + get(this: object) { + const id = getByKeyPath(this, pkKeyPath); + const cacheKey = `${table.name}[${id}].${prop}`; + let docRef = docCache[cacheKey]; + if (docRef) return docRef.deref(); + + const doc = new Y.Doc({ + collectionid: updatesTable, + guid: ''+id, + meta: { + db, + table: table.name, + cacheKey, + } as DexieYDocMeta, + }); + + docCache[cacheKey] = new WeakRef(doc); + registry.register(doc, cacheKey); + + doc.on('destroy', () => { + destroyed.add(doc); + registry.unregister(doc); + delete docCache[cacheKey]; + }); + + return doc; + }, + }; +} diff --git a/src/yjs/docCache.ts b/src/yjs/docCache.ts new file mode 100644 index 000000000..ca26eb8f2 --- /dev/null +++ b/src/yjs/docCache.ts @@ -0,0 +1,16 @@ +import type { DucktypedYDoc } from '../public/types/yjs-related'; + +// The cache +export let docCache: { [key: string]: WeakRef; } = {}; +// The finalization registry +export const registry = new FinalizationRegistry((heldValue) => { + delete docCache[heldValue]; +}); +// The weak map +//export const doc2ProviderWeakMap = new WeakMap>>(); +export const destroyed = new WeakSet(); + +export function throwIfDestroyed(doc: object) { + if (destroyed.has(doc)) + throw new Error('Y.Doc has been destroyed'); +} diff --git a/src/yjs/observeUpdates.ts b/src/yjs/observeUpdates.ts new file mode 100644 index 000000000..9dc346368 --- /dev/null +++ b/src/yjs/observeUpdates.ts @@ -0,0 +1,92 @@ +import type { Dexie } from '../public/types/dexie'; +import type { + DexieYProvider, + DucktypedY, + DucktypedYDoc, + YUpdateRow, +} from '../public/types/yjs-related'; +import type { EntityTable } from '../public/types/entity-table'; +import { throwIfDestroyed } from './docCache'; +import { liveQuery } from '../live-query'; + +export function observeUpdates( + provider: DexieYProvider, + doc: DucktypedYDoc, + db: Dexie, + parentTableName: string, + updatesTableName: string, + id: any, + Y: DucktypedY +): () => void { + let lastUpdateId = 0; + let initial = true; + const subscription = liveQuery(() => { + throwIfDestroyed(doc); + return Promise.all([(db.table(updatesTableName) as EntityTable) + .where('[k+i]') + .between([id, lastUpdateId], [id, Infinity], false) + .toArray() + .then((updates) => { + if (updates.length > 0) lastUpdateId = updates[updates.length - 1].i; + return updates; + }), db.table(parentTableName).where(':id').equals(id).count()]) + }).subscribe( + ([updates, parentRowExists]) => { + if (!parentRowExists) { + // Row deleted. Destroy Y.Doc. + doc.destroy(); + return; + } + throwIfDestroyed(doc); + Y.transact( + doc, + () => { + updates.forEach((update) => { + Y.applyUpdateV2(doc, update.u); + }); + }, + subscription, + false + ); + if (initial) { + initial = false; + provider.emit('load', [provider]); + doc.emit('load', [doc]); + } + }, + (error) => { + provider.emit('error', [error]); + } + ); + + const onUpdate = (update: Uint8Array, origin: any) => { + if (origin === subscription) return; // Already applied. + db.table(updatesTableName) + .add({ + k: id, + u: update, + f: 1, // Flag as local update (not yet synced) + }) + .then((i: number) => { + // Optimization (not critical): Don't query for this update to put it back into the doc. + // However, skip this optimization if the lastUpdateId is behind the current update. + // In that case, next liveQuery emission will include also this update and re-apply it into doc, + // but it will not be an issue because Y.Doc will ignore duplicate updates. + if (i === lastUpdateId - 1) ++lastUpdateId; + }) + .catch((error) => { + provider.emit('error', [error]); + }); + }; + + const stopObserving = () => { + subscription.unsubscribe(); + doc.off('updateV2', onUpdate); + doc.off('destroy', stopObserving); + }; + + doc.on('updateV2', onUpdate); + doc.on('destroy', stopObserving); + + return stopObserving; +} diff --git a/test/typings-test/test-typings.ts b/test/typings-test/test-typings.ts index cb05c9010..380f9c4db 100644 --- a/test/typings-test/test-typings.ts +++ b/test/typings-test/test-typings.ts @@ -6,6 +6,7 @@ import Dexie, { EntityTable, IndexableType, Table, replacePrefix } from '../../dist/dexie'; // Imports the source Dexie.d.ts file import './test-extend-dexie'; import './test-updatespec'; +import * as Y from 'yjs'; // constructor overloads: { @@ -295,3 +296,33 @@ import './test-updatespec'; tx.friends.add({name: "Bar", age: 33}); }) } + +{ + // Typings for Y.Doc + interface TodoItem { + id: string; + title: string; + done: number; + text: Y.Doc; + } + + const db = new Dexie('dbname', { Y }) as Dexie & { + todos: EntityTable; + }; + + db.version(1).stores({ + todos: 'id, title, done, text:Y' + }); + + db.todos.add({title: "Foo", done: 0}); // Verify that Y.Doc prop is not allowed here + + db.todos.get('some-id').then(item => { + if (!item) return; + // Verify that Y.Doc prop is retrieved here: + item.text.get('some-prop'); + + // Verify we can change things and put it back: + item.done = 1; + db.todos.put(item); // Yes, we could put it back despite having more props than in the InsertType. + }); +} From 4cd9ac10f4d51e7016165d1431015fcf9af43bec Mon Sep 17 00:00:00 2001 From: dfahlander Date: Wed, 19 Jun 2024 15:00:47 +0200 Subject: [PATCH 03/23] First yjs unit-test + bug fix --- .../transaction/transaction-constructor.ts | 11 +----- test/tests-all.js | 1 + test/tests-yjs.js | 38 +++++++++++++++++++ 3 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 test/tests-yjs.js diff --git a/src/classes/transaction/transaction-constructor.ts b/src/classes/transaction/transaction-constructor.ts index 625805f24..74bab3269 100644 --- a/src/classes/transaction/transaction-constructor.ts +++ b/src/classes/transaction/transaction-constructor.ts @@ -32,19 +32,10 @@ export function createTransactionConstructor(db: Dexie) { chromeTransactionDurability: ChromeTransactionDurability, parent?: Transaction) { - let cloned = false; if (mode !== 'readonly') storeNames.forEach(storeName => { // Uncollapse storeName to include Y update tables in case deletion of a record - then we must also delete its Y updates. const yProps = dbschema[storeName]?.yProps; - if (yProps) { - if (!cloned) { - storeNames = storeNames.slice(0); // Clone storeNames - cloned = true; - } - yProps.forEach(yProp => { - storeNames.push(`$${storeName}.${yProp}_updates`); - }); - } + if (yProps) storeNames = storeNames.concat(yProps.map(p => p.updTable)); }); this.db = db; diff --git a/test/tests-all.js b/test/tests-all.js index e587ae85e..046fdfe8f 100644 --- a/test/tests-all.js +++ b/test/tests-all.js @@ -1,5 +1,6 @@ import Dexie from 'dexie'; Dexie.test = true; // Improve code coverage +import "./tests-yjs.js"; import "./tests-cmp.js"; import "./tests-table.js"; import "./tests-chrome-transaction-durability.js"; diff --git a/test/tests-yjs.js b/test/tests-yjs.js new file mode 100644 index 000000000..dcd726705 --- /dev/null +++ b/test/tests-yjs.js @@ -0,0 +1,38 @@ +import Dexie, { liveQuery } from 'dexie'; +import { module, stop, start, asyncTest, equal, deepEqual, ok } from 'QUnit'; +import { + resetDatabase, + spawnedTest, + promisedTest, + supports, + isIE, + isEdge, +} from './dexie-unittest-utils'; +import * as Y from 'yjs'; + +const db = new Dexie('TestYjs', { Y }); +db.version(1).stores({ + docs: 'id, title, content:Y', +}); + +module('yjs', { + setup: () => { + stop(); + resetDatabase(db) + .catch((e) => { + ok(false, 'Error resetting database: ' + e.stack); + }) + .finally(start); + }, + teardown: () => {}, +}); + +promisedTest('Test Y.js basic support', async () => { + await db.docs.put({ + id: "doc1", + title: "Hello", + }); + const doc = await db.docs.get("doc1"); + equal(doc.title, "Hello", "Title is correct"); + ok(doc.content instanceof Y.Doc, "Content is a Y.Doc"); +}); From 0cce18f25200517746a9d5e3b29001756fdde34d Mon Sep 17 00:00:00 2001 From: dfahlander Date: Wed, 19 Jun 2024 16:38:46 +0200 Subject: [PATCH 04/23] Test DexieYProvider + bugfixes --- src/public/types/yjs-related.ts | 9 ++- src/yjs/DexieYProvider.ts | 40 +++++++------ ...bserveUpdates.ts => observeYDocUpdates.ts} | 8 +-- test/tests-yjs.js | 56 ++++++++++++++++--- 4 files changed, 80 insertions(+), 33 deletions(-) rename src/yjs/{observeUpdates.ts => observeYDocUpdates.ts} (93%) diff --git a/src/public/types/yjs-related.ts b/src/public/types/yjs-related.ts index 93425712c..3f79be4dc 100644 --- a/src/public/types/yjs-related.ts +++ b/src/public/types/yjs-related.ts @@ -7,6 +7,8 @@ */ import { Dexie } from "./dexie"; +import { DexieEvent } from "./dexie-event"; +import { DexieEventSet } from "./dexie-event-set"; import { EntityTable } from "./entity-table"; import { IndexableType } from "./indexable-type"; import { Table } from "./table"; @@ -85,9 +87,10 @@ export interface DexieYProvider { doc: YDoc; awareness?: any; - on (name: string, f: (...args: any[]) => any): void; + whenLoaded: Promise; + whenSynced: Promise; + + on: DexieEventSet & ((name: string, f: (...args: any[]) => any) => void); off (name: string, f: (...args: any[]) => any): void; - once (name: string, f: (...args: any[]) => any): void; - emit (name: string, args: any[]): void; destroy(): void; } diff --git a/src/yjs/DexieYProvider.ts b/src/yjs/DexieYProvider.ts index 6c981f9c7..5fedd4a78 100644 --- a/src/yjs/DexieYProvider.ts +++ b/src/yjs/DexieYProvider.ts @@ -1,10 +1,11 @@ +import Events from '../helpers/Events'; +import { DexieEventSet } from '../public/types/dexie-event-set'; import type { DexieYProvider, DucktypedYDoc, - DucktypedYObservable, } from '../public/types/yjs-related'; import { throwIfDestroyed } from './docCache'; -import { observeUpdates } from './observeUpdates'; +import { observeYDocUpdates } from './observeYDocUpdates'; export function DexieYProvider (doc: DucktypedYDoc): DexieYProvider { const { guid, collectionid: updatesTable, meta: { db, table }} = @@ -17,24 +18,29 @@ export function DexieYProvider (doc: DucktypedYDoc): DexieYProvider { throwIfDestroyed(doc); const Y = db._options.Y; if (!Y) throw new Error('Y library not supplied to Dexie constructor'); - - const provider: DexieYProvider = new class extends (Y.Observable as new()=>DucktypedYObservable) { - doc = doc; - whenLoaded = new Promise((resolve, reject) => { - this.once('load', resolve); - this.once('error', reject); - }); - whenSynced = new Promise((resolve, reject) => { - this.once('sync', resolve); - this.once('error', reject); - }); + function createEvents() { + return Events(null, "load", "sync", "error") as DexieYProvider["on"]; + } + let on = createEvents(); + const provider = { + doc, + on, + off (name: string, f: Function) { on[name]?.unsubscribe(f)}, + whenLoaded: new Promise((resolve, reject) => { + on('load', resolve); + on('error', reject); + }), + whenSynced: new Promise((resolve, reject) => { + on('sync', resolve); + on('error', reject); + }), destroy() { stopObserving(); - super.destroy(); + on = this.on = createEvents(); // Releases listeners for GC } - } - const stopObserving = observeUpdates(provider, doc, db, table, updatesTable, guid, Y); + }; + const stopObserving = observeYDocUpdates(provider, doc, db, table, updatesTable, guid, Y); db.on.y.fire(provider, Y); // Allow for addons to invoke their sync- and awareness providers here. return provider; -} +} diff --git a/src/yjs/observeUpdates.ts b/src/yjs/observeYDocUpdates.ts similarity index 93% rename from src/yjs/observeUpdates.ts rename to src/yjs/observeYDocUpdates.ts index 9dc346368..430d22523 100644 --- a/src/yjs/observeUpdates.ts +++ b/src/yjs/observeYDocUpdates.ts @@ -9,7 +9,7 @@ import type { EntityTable } from '../public/types/entity-table'; import { throwIfDestroyed } from './docCache'; import { liveQuery } from '../live-query'; -export function observeUpdates( +export function observeYDocUpdates( provider: DexieYProvider, doc: DucktypedYDoc, db: Dexie, @@ -50,12 +50,12 @@ export function observeUpdates( ); if (initial) { initial = false; - provider.emit('load', [provider]); + provider.on('load').fire(provider); doc.emit('load', [doc]); } }, (error) => { - provider.emit('error', [error]); + provider.on('error').fire(error); } ); @@ -75,7 +75,7 @@ export function observeUpdates( if (i === lastUpdateId - 1) ++lastUpdateId; }) .catch((error) => { - provider.emit('error', [error]); + provider.on('error').fire(error); }); }; diff --git a/test/tests-yjs.js b/test/tests-yjs.js index dcd726705..ca9f53896 100644 --- a/test/tests-yjs.js +++ b/test/tests-yjs.js @@ -1,12 +1,8 @@ -import Dexie, { liveQuery } from 'dexie'; -import { module, stop, start, asyncTest, equal, deepEqual, ok } from 'QUnit'; +import Dexie, { DexieYProvider } from 'dexie'; +import { module, stop, start, equal, deepEqual, ok } from 'QUnit'; import { resetDatabase, - spawnedTest, promisedTest, - supports, - isIE, - isEdge, } from './dexie-unittest-utils'; import * as Y from 'yjs'; @@ -32,7 +28,49 @@ promisedTest('Test Y.js basic support', async () => { id: "doc1", title: "Hello", }); - const doc = await db.docs.get("doc1"); - equal(doc.title, "Hello", "Title is correct"); - ok(doc.content instanceof Y.Doc, "Content is a Y.Doc"); + let row = await db.docs.get("doc1"); + equal(row.title, "Hello", "Title is correct"); + let doc = row.content; + ok(doc instanceof Y.Doc, "Content is a Y.Doc"); + + let row2 = await db.docs.get("doc1"); + let doc2 = row2.content; + equal(doc, doc2, "The two doc instances are the same"); + + let rows = await db.docs.toArray(); + equal(rows[0].content, doc, "The two doc instances are the same"); + + // Now destroy the doc: + doc.destroy(); + row2 = await db.docs.get("doc1"); + doc2 = row2.content; + ok(doc !== doc2, "After destroying doc, a new instance is retrieved"); + + // Delete document + await db.docs.delete("doc1"); +}); + +promisedTest('Test DexieYProvider', async () => { + await db.docs.put({ + id: "doc2", + title: "Hello2", + }); + let row = await db.docs.get("doc2"); + /* @type {Y.Doc} */ + let doc = row.content; + let provider = new DexieYProvider(doc); + doc.getArray('arr').insert(0, ['a', 'b', 'c']); + await provider.whenLoaded; + doc.destroy(); + db.close({disableAutoOpen: false}); + await db.open(); + row = await db.docs.get("doc2"); + doc = row.content; + provider = new DexieYProvider(doc); + await doc.whenLoaded; + // Verify that we got the same data: + deepEqual(doc.getArray('arr').toJSON(), ['a', 'b', 'c'], "Array is correct after reload"); + // Verify we have updates in the update table (this part can be deleted if implementation is changed) + const updates = await db.table('$docs.content_updates').toArray(); + ok(updates.length > 0, "Got updates in update table"); }); From b2e86b0d4bb7da69df6630cc2668d0a9f511cf11 Mon Sep 17 00:00:00 2001 From: dfahlander Date: Thu, 20 Jun 2024 00:28:06 +0200 Subject: [PATCH 05/23] trigger update deletion when main row is deleted --- src/classes/collection/collection.ts | 15 +++------------ src/classes/table/table-helpers.ts | 19 +++++++++++++++++++ src/classes/table/table.ts | 15 ++++++++++----- test/tests-yjs.js | 5 ++++- 4 files changed, 36 insertions(+), 18 deletions(-) create mode 100644 src/classes/table/table-helpers.ts diff --git a/src/classes/collection/collection.ts b/src/classes/collection/collection.ts index 70b16a7f7..bf34058ee 100644 --- a/src/classes/collection/collection.ts +++ b/src/classes/collection/collection.ts @@ -14,6 +14,7 @@ import { DBCoreCursor, DBCoreTransaction, DBCoreRangeType, DBCoreMutateResponse, import { cmp } from "../../functions/cmp"; import { PropModification } from "../../helpers/prop-modification"; import { UpdateSpec } from "../../public/types/update-spec"; +import { builtInDeletionTrigger } from "../table/table-helpers"; /** class Collection * @@ -575,19 +576,9 @@ export class Collection implements ICollection { keys: deleteKeys, criteria, isAdditionalChunk: offset > 0 - }).then(res=>applyMutateResult(deleteKeys.length, res)) + }).then(res => builtInDeletionTrigger(ctx.table, deleteKeys, res)) + .then(res => applyMutateResult(deleteKeys.length, res)) ).then(()=>{ - if (ctx.table.schema.yProps) { - // Delete related document updates. Otherwise, if a row with same ID is created - // again, its document would not be empty. - // Document providers will get notified on the main table's row deletion and destroy - // document. Sync of this action is outside of the Y.js scope but will be handled - // by the dexie cloud sync layer or equivalent sync layer. - return Promise.all(ctx.table.schema.yProps.map(prop => { - return this.db.table(prop.updTable).where('k').anyOf(keysInChunk).delete(); - })); - } - }).then(()=>{ return keys.length > offset + count && nextChunk(offset + limit); }); }); diff --git a/src/classes/table/table-helpers.ts b/src/classes/table/table-helpers.ts new file mode 100644 index 000000000..1b2bffa2b --- /dev/null +++ b/src/classes/table/table-helpers.ts @@ -0,0 +1,19 @@ +import { DBCoreMutateResponse } from "../../public/types/dbcore"; +import { Table } from "./table"; + + +export function builtInDeletionTrigger (table: Table, keys: null | readonly any[], res: DBCoreMutateResponse): DBCoreMutateResponse | Promise { + // Delete related document updates. Otherwise, if a row with same ID is created + // again, its document would not be empty. + // Document providers will get notified on the main table's row deletion and destroy + // document. Sync of this action is outside of the Y.js scope but will be handled + // by the dexie cloud sync layer or equivalent sync layer. + const { yProps } = table.schema; + if (!yProps) return res; + if (keys && res.numFailures > 0) keys = keys.filter((_, i) => !res.failures[i]); + return Promise.all(yProps.map(({updTable}) => + keys + ? table.db.table(updTable).where('k').anyOf(keys).delete() + : table.db.table(updTable).clear() + )).then(() => res); +} \ No newline at end of file diff --git a/src/classes/table/table.ts b/src/classes/table/table.ts index 2f9bdea6d..37450c4eb 100644 --- a/src/classes/table/table.ts +++ b/src/classes/table/table.ts @@ -20,6 +20,7 @@ import { Entity } from '../entity/Entity'; import { UpdateSpec } from '../../public'; import { cmp } from '../../functions/cmp'; import { createYDocProperty } from '../../yjs/createYDocProperty'; +import { builtInDeletionTrigger } from './table-helpers'; /** class Table * @@ -413,8 +414,10 @@ export class Table implements ITable { **/ delete(key: IndexableType): PromiseExtended { return this._trans('readwrite', - trans => this.core.mutate({trans, type: 'delete', keys: [key]})) - .then(res => res.numFailures ? Promise.reject(res.failures[0]) : undefined); + trans => this.core.mutate({trans, type: 'delete', keys: [key]}) + .then(res => builtInDeletionTrigger(this, [key], res)) + .then(res => res.numFailures ? Promise.reject(res.failures[0]) : undefined)); + ; } /** Table.clear() @@ -424,8 +427,9 @@ export class Table implements ITable { **/ clear() { return this._trans('readwrite', - trans => this.core.mutate({trans, type: 'deleteRange', range: AnyRange})) - .then(res => res.numFailures ? Promise.reject(res.failures[0]) : undefined); + trans => this.core.mutate({trans, type: 'deleteRange', range: AnyRange}) + .then(res => builtInDeletionTrigger(this, null, res))) + .then(res => res.numFailures ? Promise.reject(res.failures[0]) : undefined); } /** Table.bulkGet() @@ -594,7 +598,8 @@ export class Table implements ITable { bulkDelete(keys: ReadonlyArray): PromiseExtended { const numKeys = keys.length; return this._trans('readwrite', trans => { - return this.core.mutate({trans, type: 'delete', keys: keys as IndexableType[]}); + return this.core.mutate({trans, type: 'delete', keys: keys as IndexableType[]}) + .then(res => builtInDeletionTrigger(this, keys, res)); }).then(({numFailures, lastResult, failures}) => { if (numFailures === 0) return lastResult; throw new BulkError( diff --git a/test/tests-yjs.js b/test/tests-yjs.js index ca9f53896..3ba852ba6 100644 --- a/test/tests-yjs.js +++ b/test/tests-yjs.js @@ -71,6 +71,9 @@ promisedTest('Test DexieYProvider', async () => { // Verify that we got the same data: deepEqual(doc.getArray('arr').toJSON(), ['a', 'b', 'c'], "Array is correct after reload"); // Verify we have updates in the update table (this part can be deleted if implementation is changed) - const updates = await db.table('$docs.content_updates').toArray(); + let updates = await db.table('$docs.content_updates').toArray(); ok(updates.length > 0, "Got updates in update table"); + await db.docs.clear(); + updates = await db.table('$docs.content_updates').toArray(); + equal(updates.length, 0, "No updates in update table after deleting document"); }); From ad6c737676909aa36adf434f8e2dcaafce0585bf Mon Sep 17 00:00:00 2001 From: dfahlander Date: Thu, 20 Jun 2024 00:42:03 +0200 Subject: [PATCH 06/23] Manipulate Y.js doc within a dexie transaction to avoid race condition --- test/tests-yjs.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/tests-yjs.js b/test/tests-yjs.js index 3ba852ba6..3d13ca3d2 100644 --- a/test/tests-yjs.js +++ b/test/tests-yjs.js @@ -59,8 +59,12 @@ promisedTest('Test DexieYProvider', async () => { /* @type {Y.Doc} */ let doc = row.content; let provider = new DexieYProvider(doc); - doc.getArray('arr').insert(0, ['a', 'b', 'c']); - await provider.whenLoaded; + // Await the transaction of doc manipulation to not + // bring down the database before it has been stored. + await db.transaction('rw', db.docs, () => { + doc.getArray('arr').insert(0, ['a', 'b', 'c']); + }); + //await provider.whenLoaded; doc.destroy(); db.close({disableAutoOpen: false}); await db.open(); From 6742cfa062bdf39fd0f3b50a873b0e385ae58558 Mon Sep 17 00:00:00 2001 From: dfahlander Date: Thu, 20 Jun 2024 00:58:49 +0200 Subject: [PATCH 07/23] Upgrading tests to use Firefox version 120 --- test/karma.lambdatest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/karma.lambdatest.js b/test/karma.lambdatest.js index 57ddbb08d..54953882d 100644 --- a/test/karma.lambdatest.js +++ b/test/karma.lambdatest.js @@ -2,7 +2,7 @@ const ltBrowsers = { remote_firefox: { browserName: 'firefox', - browserVersion: '118', + browserVersion: '120', 'LT:Options': { platformName: 'Windows 10' } From 60e83bc9cd407badddc374adf5bd9f349b61d4cb Mon Sep 17 00:00:00 2001 From: dfahlander Date: Thu, 20 Jun 2024 01:02:36 +0200 Subject: [PATCH 08/23] Upgrading tests to FireFox 126 --- test/karma.lambdatest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/karma.lambdatest.js b/test/karma.lambdatest.js index 54953882d..1b2de512d 100644 --- a/test/karma.lambdatest.js +++ b/test/karma.lambdatest.js @@ -2,7 +2,7 @@ const ltBrowsers = { remote_firefox: { browserName: 'firefox', - browserVersion: '120', + browserVersion: '126', 'LT:Options': { platformName: 'Windows 10' } From 1c4b5b0d56ca21abe95a245cb1471fc59a728bed Mon Sep 17 00:00:00 2001 From: dfahlander Date: Thu, 20 Jun 2024 01:08:37 +0200 Subject: [PATCH 09/23] For curiosity, downgrade Firefox to 125 --- test/karma.lambdatest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/karma.lambdatest.js b/test/karma.lambdatest.js index 1b2de512d..015350a11 100644 --- a/test/karma.lambdatest.js +++ b/test/karma.lambdatest.js @@ -2,7 +2,7 @@ const ltBrowsers = { remote_firefox: { browserName: 'firefox', - browserVersion: '126', + browserVersion: '125', 'LT:Options': { platformName: 'Windows 10' } From 0e069f11ea379cd8a695c2fd45dc134d9fff218b Mon Sep 17 00:00:00 2001 From: dfahlander Date: Thu, 20 Jun 2024 01:52:04 +0200 Subject: [PATCH 10/23] Support Firefox < 126: Don't use compoun indexes with auto-incremented PK. --- src/classes/version/version.ts | 11 ++++---- src/yjs/observeYDocUpdates.ts | 51 ++++++++++++++++++++++------------ 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/classes/version/version.ts b/src/classes/version/version.ts index 25f7cf7d6..00038228e 100644 --- a/src/classes/version/version.ts +++ b/src/classes/version/version.ts @@ -75,12 +75,13 @@ export class Version implements IVersion { // u is the update data from Y.js // f is a flag indicating if the update comes from this client or another. // Index use cases: - // * Load entire document: Use index k (part of [k+i] ) - // * After object load, observe updates on a certain document since a given revision: Use index [k+i] - // * After initial sync, observe flagged updates since a given revision: Use index [f+i]. Local updates are flagged - // while remote updates are not. + // * Load entire document: Use index k + // * After object load, observe updates on a certain document since a given revision: Use index k or i since [k+i] is not supported before Firefox 126. + // * After initial sync, observe flagged updates since a given revision: Use index i and ignore unflagged. + // Could be using an index [f+i] but that wouldn't gain too much and Firefox before 126 doesnt support it. + // Local updates are flagged while remote updates are not. // - { [yProp.updTable]: '++i,[k+i],[f+i]' }, + { [yProp.updTable]: '++i,k' }, outSchema ); } diff --git a/src/yjs/observeYDocUpdates.ts b/src/yjs/observeYDocUpdates.ts index 430d22523..fe11fa4d8 100644 --- a/src/yjs/observeYDocUpdates.ts +++ b/src/yjs/observeYDocUpdates.ts @@ -8,6 +8,7 @@ import type { import type { EntityTable } from '../public/types/entity-table'; import { throwIfDestroyed } from './docCache'; import { liveQuery } from '../live-query'; +import { cmp } from '../functions/cmp'; export function observeYDocUpdates( provider: DexieYProvider, @@ -22,32 +23,46 @@ export function observeYDocUpdates( let initial = true; const subscription = liveQuery(() => { throwIfDestroyed(doc); - return Promise.all([(db.table(updatesTableName) as EntityTable) - .where('[k+i]') - .between([id, lastUpdateId], [id, Infinity], false) - .toArray() - .then((updates) => { + const updatesTable = db.table(updatesTableName) as EntityTable< + YUpdateRow, + 'i' + >; + return Promise.all([ + (lastUpdateId > 0 + ? updatesTable + .where('i') + .between(lastUpdateId, Infinity, false) + .toArray() + .then((updates) => + updates.filter((update) => cmp(update.k, id) === 0) + ) + : updatesTable.where({ k: id }).toArray() + ).then((updates) => { if (updates.length > 0) lastUpdateId = updates[updates.length - 1].i; return updates; - }), db.table(parentTableName).where(':id').equals(id).count()]) + }), + db.table(parentTableName).where(':id').equals(id).toArray(), // Why not just count() or get()? Because of cache only works with toArray() currently (optimization) + ]); }).subscribe( - ([updates, parentRowExists]) => { - if (!parentRowExists) { + ([updates, parentRow]) => { + if (parentRow.length === 0) { // Row deleted. Destroy Y.Doc. doc.destroy(); return; } throwIfDestroyed(doc); - Y.transact( - doc, - () => { - updates.forEach((update) => { - Y.applyUpdateV2(doc, update.u); - }); - }, - subscription, - false - ); + if (updates.length > 0) { + Y.transact( + doc, + () => { + updates.forEach((update) => { + Y.applyUpdateV2(doc, update.u); + }); + }, + subscription, + false + ); + } if (initial) { initial = false; provider.on('load').fire(provider); From 22d79b500eb6c6a1c0c770e2fe993d8f51d95240 Mon Sep 17 00:00:00 2001 From: dfahlander Date: Thu, 20 Jun 2024 01:55:47 +0200 Subject: [PATCH 11/23] Revert to using FF118 again --- test/karma.lambdatest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/karma.lambdatest.js b/test/karma.lambdatest.js index 015350a11..57ddbb08d 100644 --- a/test/karma.lambdatest.js +++ b/test/karma.lambdatest.js @@ -2,7 +2,7 @@ const ltBrowsers = { remote_firefox: { browserName: 'firefox', - browserVersion: '125', + browserVersion: '118', 'LT:Options': { platformName: 'Windows 10' } From 7742ebb14d6223c819ab426c3859e559cf695c1f Mon Sep 17 00:00:00 2001 From: dfahlander Date: Tue, 2 Jul 2024 00:19:02 +0200 Subject: [PATCH 12/23] Comment --- src/yjs/observeYDocUpdates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yjs/observeYDocUpdates.ts b/src/yjs/observeYDocUpdates.ts index fe11fa4d8..de1fb68ba 100644 --- a/src/yjs/observeYDocUpdates.ts +++ b/src/yjs/observeYDocUpdates.ts @@ -80,7 +80,7 @@ export function observeYDocUpdates( .add({ k: id, u: update, - f: 1, // Flag as local update (not yet synced) + f: 1, // Flag as local update (to be included when syncing) }) .then((i: number) => { // Optimization (not critical): Don't query for this update to put it back into the doc. From 27e13e4ab3f5d634356937fd700425f80d9f7547 Mon Sep 17 00:00:00 2001 From: dfahlander Date: Fri, 19 Jul 2024 16:38:50 +0200 Subject: [PATCH 13/23] Let db.on('close') fire also when db.close() is called. --- addons/dexie-cloud/src/helpers/dbOnClosed.ts | 6 +++--- src/classes/dexie/dexie.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/addons/dexie-cloud/src/helpers/dbOnClosed.ts b/addons/dexie-cloud/src/helpers/dbOnClosed.ts index 17d939117..709fc2ea8 100644 --- a/addons/dexie-cloud/src/helpers/dbOnClosed.ts +++ b/addons/dexie-cloud/src/helpers/dbOnClosed.ts @@ -4,16 +4,16 @@ import Dexie from "dexie"; */ export function dbOnClosed(db: Dexie, handler: () => void) { db.on.close.subscribe(handler); - // @ts-ignore + /*// @ts-ignore const origClose = db._close; // @ts-ignore db._close = function () { origClose.call(this); handler(); - }; + };*/ return () => { db.on.close.unsubscribe(handler); // @ts-ignore - db._close = origClose; + //db._close = origClose; }; } diff --git a/src/classes/dexie/dexie.ts b/src/classes/dexie/dexie.ts index 4fd7b1697..ac400ee98 100644 --- a/src/classes/dexie/dexie.ts +++ b/src/classes/dexie/dexie.ts @@ -317,6 +317,7 @@ export class Dexie implements IDexie { } _close(): void { + this.on.close.fire(new CustomEvent('close')); const state = this._state; const idx = connections.indexOf(this); if (idx >= 0) connections.splice(idx, 1); From a3ef4b1cddcc526143717e1b753e264c087d87af Mon Sep 17 00:00:00 2001 From: dfahlander Date: Mon, 22 Jul 2024 14:56:06 +0200 Subject: [PATCH 14/23] GC implemented and tested. Still need to think it through. Might need to find a better way than iterating all updates in entire DB periodically. --- src/classes/dexie/dexie.ts | 8 +++ src/classes/table/table.ts | 4 +- src/public/types/dexie.d.ts | 2 + src/public/types/yjs-related.ts | 8 ++- src/yjs/DexieYProvider.ts | 7 ++- src/yjs/compressYDocs.ts | 106 ++++++++++++++++++++++++++++++++ src/yjs/getYLibrary.ts | 9 +++ src/yjs/periodicGC.ts | 29 +++++++++ test/tests-yjs.js | 82 ++++++++++++++++++++++++ 9 files changed, 250 insertions(+), 5 deletions(-) create mode 100644 src/yjs/compressYDocs.ts create mode 100644 src/yjs/getYLibrary.ts create mode 100644 src/yjs/periodicGC.ts diff --git a/src/classes/dexie/dexie.ts b/src/classes/dexie/dexie.ts index ac400ee98..38caeb08c 100644 --- a/src/classes/dexie/dexie.ts +++ b/src/classes/dexie/dexie.ts @@ -46,6 +46,8 @@ import { observabilityMiddleware } from '../../live-query/observability-middlewa import { cacheExistingValuesMiddleware } from '../../dbcore/cache-existing-values-middleware'; import { cacheMiddleware } from "../../live-query/cache/cache-middleware"; import { vipify } from "../../helpers/vipify"; +import { periodicGC } from "../../yjs/periodicGC"; +import { compressYDocs } from "../../yjs/compressYDocs"; export interface DbReadyState { dbOpenError: any; @@ -246,6 +248,8 @@ export class Dexie implements IDexie { }); this.vip = vipDB; + if (options?.Y) periodicGC(this); + // Call each addon: addons.forEach(addon => addon(this)); } @@ -492,4 +496,8 @@ export class Dexie implements IDexie { throw new exceptions.InvalidTable(`Table ${tableName} does not exist`); } return this._allTables[tableName]; } + + gc() { + return compressYDocs(this); + } } diff --git a/src/classes/table/table.ts b/src/classes/table/table.ts index 37450c4eb..0c764a796 100644 --- a/src/classes/table/table.ts +++ b/src/classes/table/table.ts @@ -21,6 +21,7 @@ import { UpdateSpec } from '../../public'; import { cmp } from '../../functions/cmp'; import { createYDocProperty } from '../../yjs/createYDocProperty'; import { builtInDeletionTrigger } from './table-helpers'; +import { getYLibrary } from '../../yjs/getYLibrary'; /** class Table * @@ -277,8 +278,7 @@ export class Table implements ITable { } } if (this.schema.yProps) { - const { Y } = db._options; - if (!Y) throw new exceptions.MissingAPI('Y library not supplied to Dexie constructor'); + const Y = getYLibrary(db); constructor = class extends (constructor as any) {}; this.schema.yProps.forEach(({prop, updTable}) => { Object.defineProperty(constructor.prototype, prop, createYDocProperty(db, Y, this, prop, updTable)); diff --git a/src/public/types/dexie.d.ts b/src/public/types/dexie.d.ts index 2b419c9df..9e3cf6d34 100644 --- a/src/public/types/dexie.d.ts +++ b/src/public/types/dexie.d.ts @@ -114,6 +114,8 @@ export interface Dexie { unuse({ stack, create }: Middleware<{ stack: keyof DexieStacks }>): this; unuse({ stack, name }: { stack: keyof DexieStacks; name: string }): this; + gc(): Promise; + // Make it possible to touch physical class constructors where they reside - as properties on db instance. // For example, checking if (x instanceof db.Table). Can't do (x instanceof Dexie.Table because it's just a virtual interface) Table: { prototype: Table }; diff --git a/src/public/types/yjs-related.ts b/src/public/types/yjs-related.ts index 3f79be4dc..c3e800ec0 100644 --- a/src/public/types/yjs-related.ts +++ b/src/public/types/yjs-related.ts @@ -83,8 +83,13 @@ export interface YUpdateRow { f?: number; } +export interface YSyncer { + i: string; + unsentFrom: number; +} + export interface DexieYProvider { - doc: YDoc; + readonly doc: YDoc; awareness?: any; whenLoaded: Promise; @@ -93,4 +98,5 @@ export interface DexieYProvider { on: DexieEventSet & ((name: string, f: (...args: any[]) => any) => void); off (name: string, f: (...args: any[]) => any): void; destroy(): void; + readonly destroyed: boolean; } diff --git a/src/yjs/DexieYProvider.ts b/src/yjs/DexieYProvider.ts index 5fedd4a78..88f85935b 100644 --- a/src/yjs/DexieYProvider.ts +++ b/src/yjs/DexieYProvider.ts @@ -5,6 +5,7 @@ import type { DucktypedYDoc, } from '../public/types/yjs-related'; import { throwIfDestroyed } from './docCache'; +import { getYLibrary } from './getYLibrary'; import { observeYDocUpdates } from './observeYDocUpdates'; export function DexieYProvider (doc: DucktypedYDoc): DexieYProvider { @@ -16,13 +17,13 @@ export function DexieYProvider (doc: DucktypedYDoc): DexieYProvider { throw new Error(`Table ${table} or ${updatesTable} not found in db`); } throwIfDestroyed(doc); - const Y = db._options.Y; - if (!Y) throw new Error('Y library not supplied to Dexie constructor'); + const Y = getYLibrary(db); function createEvents() { return Events(null, "load", "sync", "error") as DexieYProvider["on"]; } let on = createEvents(); const provider = { + destroyed: false, doc, on, off (name: string, f: Function) { on[name]?.unsubscribe(f)}, @@ -35,11 +36,13 @@ export function DexieYProvider (doc: DucktypedYDoc): DexieYProvider { on('error', reject); }), destroy() { + this.destroyed = true; stopObserving(); on = this.on = createEvents(); // Releases listeners for GC } }; const stopObserving = observeYDocUpdates(provider, doc, db, table, updatesTable, guid, Y); + doc.on('destroy', provider.destroy.bind(provider)); db.on.y.fire(provider, Y); // Allow for addons to invoke their sync- and awareness providers here. return provider; diff --git a/src/yjs/compressYDocs.ts b/src/yjs/compressYDocs.ts new file mode 100644 index 000000000..38f8f1f68 --- /dev/null +++ b/src/yjs/compressYDocs.ts @@ -0,0 +1,106 @@ +import { Dexie } from '../public/types/dexie'; +import Promise from '../helpers/promise'; +import type { Table } from '../public/types/table'; +import type { YSyncer, YUpdateRow } from '../public/types/yjs-related'; +import { PromiseExtended } from '../public/types/promise-extended'; +import { getYLibrary } from './getYLibrary'; + +/** Go through all Y.Doc tables in the entire local db and compress updates + * + * @param db Dexie + * @returns + */ +export function compressYDocs(db: Dexie) { + return db.tables.reduce( + (promise, table) => + promise.then(() => + table.schema.yProps?.reduce( + (prom2, yProp) => prom2.then(() => compressYDocsTable(db, yProp)), + Promise.resolve() + ) + ), + Promise.resolve() + ); +} + +/** Compress an individual Y.Doc table */ +function compressYDocsTable( + db: Dexie, + { updTable }: { prop: string; updTable: string } +) { + return db + .table(updTable) + .where('i') + .startsWith('') + .toArray((syncers) => { + const unsentFrom = Math.min( + ...syncers.map((s) => s.unsentFrom || Infinity) + ); + return db + .table(updTable) + .orderBy('k') + .uniqueKeys((docIdsToCompress) => { + return docIdsToCompress.reduce((promise, docId) => { + return promise.then(() => + compressYDoc(db, updTable, docId, unsentFrom) + ); + }, Promise.resolve()); + }); + }); +} + +/** Compress an individual Y.Doc. + * + * Lists all updates for the Y.Doc and replaces them with a single compressed update if there are more than one updates. + * + * If there is a Syncer entry in the updates table (an entry where primary key `i` is a string, not a number), + * then the `unsentFrom` value is used to determine the last update that has not been sent to the server and therefore + * should not be compressed. Sync addons may store their syncers in the update table and name them in the primary key, + * with a string value instead of a number, to keep track of which updates have been sent to its server. This is a bit + * special that we reuse the same table that we have for updates also for syncers, but it's a way to keep track of + * which updates have been sent to the server without having to create a separate table for that. + * + * @param db Dexie instance + * @param updTable Name of the table where updates are stored + * @param k The primary key of the related table that holds the virtual Y.Doc property. + * @param dontCompressFrom Infinity if all updates can be compressed, otherwise id of the first update not to compress. + * @returns + */ +function compressYDoc( + db: Dexie, + updTable: string, + k: any, + dontCompressFrom: number +): PromiseExtended { + const Y = getYLibrary(db); + return db.transaction('rw', updTable, (tx) => { + const updTbl = tx.table(updTable); + return updTbl + .where('k') + .equals(k) // Could have been using where('[k+i]').between([k, 0], [k, dontCompressFrom], true, false) but that would not work in older FF browsers. + .until((s) => s.i >= dontCompressFrom, false) // It's naturally ordered by i, as it is the primary key of updates + .toArray((updates) => { + const doc = new Y.Doc({gc: true}); + if (updates.length > 1) { + // 1. compress updates where i is between these values + updates.forEach((update) => { + Y.applyUpdateV2(doc, update.u); + }); + const compressedUpdate = Y.encodeStateAsUpdateV2(doc); + // 2. replace the last update with the compressed update + const lastUpdate = updates[updates.length - 1]; + updTbl.put({ + i: lastUpdate.i, + k, + u: compressedUpdate, + f: 2, + }); + // 3. delete the compressed updates + updTbl + .where('i') + .between(updates[0].i, lastUpdate.i, true, false) + .delete(); + } + }); + }); +} diff --git a/src/yjs/getYLibrary.ts b/src/yjs/getYLibrary.ts new file mode 100644 index 000000000..ca2573c86 --- /dev/null +++ b/src/yjs/getYLibrary.ts @@ -0,0 +1,9 @@ +import { exceptions } from "../errors"; +import type { Dexie } from "../public/types/dexie"; +import type { DucktypedY } from "../public/types/yjs-related"; + +export function getYLibrary(db: Dexie): DucktypedY { + const Y = db._options.Y; + if (!Y) throw new exceptions.MissingAPI('Y library not supplied to Dexie constructor'); + return Y; +} \ No newline at end of file diff --git a/src/yjs/periodicGC.ts b/src/yjs/periodicGC.ts new file mode 100644 index 000000000..1afaa61b1 --- /dev/null +++ b/src/yjs/periodicGC.ts @@ -0,0 +1,29 @@ +import { Dexie } from '../classes/dexie/dexie'; +import { compressYDocs } from './compressYDocs'; + +const INTERVAL = 10_000; // Every 10 seconds + +export function periodicGC(db: Dexie) { + let timer = null; + db.on( + 'ready', + (db: Dexie) => { + if (db.tables.some(tbl => tbl.schema.yProps)) { + const gc = () => { + if (!db.isOpen()) return; + compressYDocs(db).catch(err => { + console.debug('Error during periodic GC', err); + }).then(() => { + timer = setTimeout(gc, INTERVAL); + }); + }; + timer = setTimeout(gc, INTERVAL); + } + }, + true + ); + db.on('close', () => { + if (timer) clearTimeout(timer); + timer = null; + }); +} diff --git a/test/tests-yjs.js b/test/tests-yjs.js index 3d13ca3d2..9261cd924 100644 --- a/test/tests-yjs.js +++ b/test/tests-yjs.js @@ -81,3 +81,85 @@ promisedTest('Test DexieYProvider', async () => { updates = await db.table('$docs.content_updates').toArray(); equal(updates.length, 0, "No updates in update table after deleting document"); }); + + +promisedTest('Test Y document compression', async () => { + await db.docs.put({ + id: 'doc1', + title: 'Hello', + }); + let row = await db.docs.get('doc1'); + let doc = row.content; + let provider = new DexieYProvider(doc); + + // Verify there are no updates in the updates table initially: + const updateTable = db.docs.schema.yProps.find( + (p) => p.prop === 'content' + ).updTable; + equal(await db.table(updateTable).count(), 0, 'No docs stored yet'); + + // Create three updates: + await db.transaction('rw', db.docs, () => { + doc.getArray('arr').insert(0, ['a', 'b', 'c']); + doc.getArray('arr').insert(0, ['1', '2', '3']); + doc.getArray('arr').insert(0, ['x', 'y', 'z']); + }); + // Verify we have 3 updates: + equal(await db.table(updateTable).count(), 3, 'Three updates stored'); + // Run the GC: + await db.gc(); + // Verify we have 1 (compressed) update: + equal(await db.table(updateTable).count(), 1, 'One update stored after gc'); + // Verify the provider is still alive: + ok(!provider.destroyed, "Provider is not destroyed"); + // Now clear the docs table, which should implicitly clear the updates as well as destroying connected providers: + await db.docs.clear(); + // Verify there are no updates now: + equal( + await db.table(updateTable).count(), + 0, + 'Zero update stored after clearing docs' + ); + // Verify the provider has been destroyed: + ok(provider.destroyed, "Provider was destroyed when document was deleted"); +}); + + +promisedTest('Test that syncers prohibit GC from compressing unsynced updates', async () => { + await db.docs.put({ + id: 'doc1', + title: 'Hello', + }); + let row = await db.docs.get('doc1'); + let doc = row.content; + let provider = new DexieYProvider(doc); + + // Verify there are no updates in the updates table initially: + const updateTable = db.docs.schema.yProps.find( + (p) => p.prop === 'content' + ).updTable; + equal(await db.table(updateTable).count(), 0, 'No docs stored yet'); + + // Create three updates: + await db.transaction('rw', db.docs, () => { + doc.getArray('arr').insert(0, ['a', 'b', 'c']); + doc.getArray('arr').insert(0, ['1', '2', '3']); + doc.getArray('arr').insert(0, ['x', 'y', 'z']); + }); + // Verify we have 3 updates: + equal(await db.table(updateTable).count(), 3, 'Three updates stored'); + + // Put a syncer in place that will not sync the updates: + await db.table(updateTable).put({ + i: "MySyncer", + unsentFrom: await db.table(updateTable).orderBy('i').lastKey(), // Keep the last update and updates after that from being compressed + }); + + await db.gc(); + // Verify we have 2 updates (the first 2 was compressed but the last one was not): + equal(await db.table(updateTable).where('i').between(1, Infinity).count(), 2, '2 updates stored'); + await db.docs.delete(row.id); + // Verify we have 0 updates after deleting the row holding our Y.Doc property: + equal(await db.table(updateTable).where('i').between(1, Infinity).count(), 0, '0 updates stored'); + ok(provider.destroyed, "Provider was destroyed when our document was deleted"); +}); \ No newline at end of file From 3e324136820ad14c4e6f482445b623b5f15a067c Mon Sep 17 00:00:00 2001 From: dfahlander Date: Tue, 23 Jul 2024 07:14:56 +0200 Subject: [PATCH 15/23] Finally a working flow --- src/public/types/yjs-related.ts | 24 ++++- src/yjs/compressYDocs.ts | 167 +++++++++++++++++++------------- test/tests-yjs.js | 38 +++++++- 3 files changed, 156 insertions(+), 73 deletions(-) diff --git a/src/public/types/yjs-related.ts b/src/public/types/yjs-related.ts index c3e800ec0..aaf5337ee 100644 --- a/src/public/types/yjs-related.ts +++ b/src/public/types/yjs-related.ts @@ -77,10 +77,27 @@ export interface DucktypedAwareness extends DucktypedYObservable { export interface YUpdateRow { + /** The primary key in the update-table + * + */ i: number; + + /** The primary key of the row in related table holding the document property. + * + */ k: IndexableType; + + /** The Y update + * + */ u: Uint8Array; - f?: number; + + /** Optional flag + * + * 1 = LOCAL_CHANGE_MAYBE_UNSYNCED + * + */ + f?: number; } export interface YSyncer { @@ -88,6 +105,11 @@ export interface YSyncer { unsentFrom: number; } +export interface YLastCompressed { + i: 0; + compressedUntil: number; +} + export interface DexieYProvider { readonly doc: YDoc; awareness?: any; diff --git a/src/yjs/compressYDocs.ts b/src/yjs/compressYDocs.ts index 38f8f1f68..5028cd895 100644 --- a/src/yjs/compressYDocs.ts +++ b/src/yjs/compressYDocs.ts @@ -1,9 +1,15 @@ import { Dexie } from '../public/types/dexie'; -import Promise from '../helpers/promise'; +//import Promise from '../helpers/promise'; import type { Table } from '../public/types/table'; -import type { YSyncer, YUpdateRow } from '../public/types/yjs-related'; +import type { + YLastCompressed, + YSyncer, + YUpdateRow, +} from '../public/types/yjs-related'; import { PromiseExtended } from '../public/types/promise-extended'; import { getYLibrary } from './getYLibrary'; +import { RangeSet, getRangeSetIterator } from '../helpers/rangeset'; +import { cmp } from '../functions/cmp'; /** Go through all Y.Doc tables in the entire local db and compress updates * @@ -20,7 +26,7 @@ export function compressYDocs(db: Dexie) { ) ), Promise.resolve() - ); + ) as PromiseExtended; } /** Compress an individual Y.Doc table */ @@ -28,79 +34,106 @@ function compressYDocsTable( db: Dexie, { updTable }: { prop: string; updTable: string } ) { - return db - .table(updTable) - .where('i') - .startsWith('') - .toArray((syncers) => { - const unsentFrom = Math.min( - ...syncers.map((s) => s.unsentFrom || Infinity) - ); - return db - .table(updTable) - .orderBy('k') - .uniqueKeys((docIdsToCompress) => { - return docIdsToCompress.reduce((promise, docId) => { - return promise.then(() => - compressYDoc(db, updTable, docId, unsentFrom) + const updTbl = db.table(updTable); + return Promise.all([ + // syncers (for example dexie-cloud-addon or other 3rd part syncers) They may have unsentFrom set. + updTbl + .where('i') + .startsWith('') // Syncers have string primary keys while updates have auto-incremented numbers. + .toArray(), + // lastCompressed (pointer to the last compressed update) + updTbl.get(0), + ]).then(([syncers, lastCompressed]: [YSyncer[], YLastCompressed]) => { + const unsentFrom = Math.min( + ...syncers.map((s) => s.unsentFrom || Infinity) + ); + const compressedUntil = lastCompressed?.compressedUntil || 0; + // Per updates-table: + // 1. Find all updates after lastCompressedId. Run toArray() on them. + // 2. IF there are any "mine" (flagged) updates AFTER unsentFrom, skip all from including this entry, else include all regardless of unsentFrom. + // 3. Now we know which keys have updates since last compression. We also know how far we're gonna go (max unsentFrom unless all additional updates are foreign). + // 4. For every key that had updates, load their main update (this is one single update per key before the lastCompressedId marker) + // 5. For every key that had updates: Compress main update along with additional updates until and including the number that was computed on step 2 (could be Infinity). + // 6. Update lastCompressedId to the i of the latest compressed entry. + return updTbl + .where('i') + .between(compressedUntil, Infinity, false, false) + .toArray() + .then((addedUpdates: YUpdateRow[]) => { + if (addedUpdates.length <= 1) return; // For sure no updates to compress if there would be only 1. + const docIdsToCompress = new RangeSet(); + let lastUpdateToCompress = compressedUntil + 1; + for (let j = 0; j < addedUpdates.length; ++j) { + const { i, f, k } = addedUpdates[j]; + if (i >= unsentFrom) if (f) break; // An update that need to be synced was found. Stop here and let dontCompressFrom stay. + docIdsToCompress.addKey(k); + lastUpdateToCompress = i; + } + let promise = Promise.resolve(); + let iter = getRangeSetIterator(docIdsToCompress); + for ( + let keyIterRes = iter.next(); + !keyIterRes.done; + keyIterRes = iter.next() + ) { + const key = keyIterRes.value.from; // or keyIterRes.to - they are same. + const addedUpdatesForDoc = addedUpdates.filter( + (update) => cmp(update.k, key) === 0 + ); + if (addedUpdatesForDoc.length > 0) { + promise = promise.then(() => + compressUpdatesForDoc(db, updTable, key, addedUpdatesForDoc) ); - }, Promise.resolve()); + } + } + return promise.then(() => { + // Update lastCompressed atomically to the value we computed. + // Do it with respect to the case when another job was done in parallel + // that maybe compressed one or more extra updates and updated lastCompressed + // before us. + return db.transaction('rw', updTbl, () => + updTbl.get(0).then( + (current) => + lastUpdateToCompress > current.compressedUntil && + updTbl.put({ + i: 0, + compressedUntil: lastUpdateToCompress, + }) + ) + ); }); - }); + }); + }); } -/** Compress an individual Y.Doc. - * - * Lists all updates for the Y.Doc and replaces them with a single compressed update if there are more than one updates. - * - * If there is a Syncer entry in the updates table (an entry where primary key `i` is a string, not a number), - * then the `unsentFrom` value is used to determine the last update that has not been sent to the server and therefore - * should not be compressed. Sync addons may store their syncers in the update table and name them in the primary key, - * with a string value instead of a number, to keep track of which updates have been sent to its server. This is a bit - * special that we reuse the same table that we have for updates also for syncers, but it's a way to keep track of - * which updates have been sent to the server without having to create a separate table for that. - * - * @param db Dexie instance - * @param updTable Name of the table where updates are stored - * @param k The primary key of the related table that holds the virtual Y.Doc property. - * @param dontCompressFrom Infinity if all updates can be compressed, otherwise id of the first update not to compress. - * @returns - */ -function compressYDoc( +export function compressUpdatesForDoc( db: Dexie, updTable: string, - k: any, - dontCompressFrom: number -): PromiseExtended { - const Y = getYLibrary(db); + docRowId: any, + addedUpdatesToCompress: YUpdateRow[] +) { + if (addedUpdatesToCompress.length < 1) throw new Error('Invalid input'); return db.transaction('rw', updTable, (tx) => { const updTbl = tx.table(updTable); - return updTbl - .where('k') - .equals(k) // Could have been using where('[k+i]').between([k, 0], [k, dontCompressFrom], true, false) but that would not work in older FF browsers. - .until((s) => s.i >= dontCompressFrom, false) // It's naturally ordered by i, as it is the primary key of updates - .toArray((updates) => { - const doc = new Y.Doc({gc: true}); - if (updates.length > 1) { - // 1. compress updates where i is between these values - updates.forEach((update) => { - Y.applyUpdateV2(doc, update.u); - }); - const compressedUpdate = Y.encodeStateAsUpdateV2(doc); - // 2. replace the last update with the compressed update - const lastUpdate = updates[updates.length - 1]; - updTbl.put({ - i: lastUpdate.i, - k, - u: compressedUpdate, - f: 2, - }); - // 3. delete the compressed updates - updTbl - .where('i') - .between(updates[0].i, lastUpdate.i, true, false) - .delete(); + return updTbl.where({ k: docRowId }).first((mainUpdate: YUpdateRow) => { + const updates = [mainUpdate].concat(addedUpdatesToCompress); // in some situations, mainUpdate will be included twice here. But Y.js doesn't care! + const Y = getYLibrary(db); + const doc = new Y.Doc({ gc: true }); + updates.forEach((update) => { + if (cmp(update.k, docRowId) !== 0) { + throw new Error('Invalid update'); } + Y.applyUpdateV2(doc, update.u); }); + const compressedUpdate = Y.encodeStateAsUpdateV2(doc); + const lastUpdate = updates.pop(); + return updTbl + .put({ + i: lastUpdate.i, + k: docRowId, + u: compressedUpdate + }) + .then(() => updTbl.bulkDelete(updates.map((update) => update.i))); + }); }); } diff --git a/test/tests-yjs.js b/test/tests-yjs.js index 9261cd924..71f7e63d0 100644 --- a/test/tests-yjs.js +++ b/test/tests-yjs.js @@ -105,18 +105,44 @@ promisedTest('Test Y document compression', async () => { doc.getArray('arr').insert(0, ['x', 'y', 'z']); }); // Verify we have 3 updates: - equal(await db.table(updateTable).count(), 3, 'Three updates stored'); + equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 3, 'Three updates stored'); // Run the GC: + console.debug('Running GC', await db.table(updateTable).toArray()); await db.gc(); + console.debug('After running GC', await db.table(updateTable).toArray()); // Verify we have 1 (compressed) update: - equal(await db.table(updateTable).count(), 1, 'One update stored after gc'); + equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 1, 'One update stored after gc'); // Verify the provider is still alive: ok(!provider.destroyed, "Provider is not destroyed"); + await db.transaction('rw', db.docs, () => { + doc.getArray('arr').insert(0, ['a', 'b', 'c']); + doc.getArray('arr').insert(0, ['1', '2', '3']); + doc.getArray('arr').insert(0, ['x', 'y', 'z']); + }); + equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 4, 'Four updates stored after additional inserts'); + await db.gc(); + equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 1, 'One update stored after gc'); + await db.docs.put({ + id: 'doc2', + title: 'Hello2', + }); + let row2 = await db.docs.get('doc2'); + let doc2 = row2.content; + await new DexieYProvider(doc2).whenLoaded; + await db.transaction('rw', db.docs, async () => { + doc2.getArray('arr2').insert(0, ['a', 'b', 'c']); + doc2.getArray('arr2').insert(0, ['1', '2', '3']); + doc2.getArray('arr2').insert(0, ['x', 'y', 'z']); + }); + equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 4, 'Four updates stored after additional inserts'); + await db.gc(); + equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 2, 'Two updates stored after gc (2 different docs)'); + // Now clear the docs table, which should implicitly clear the updates as well as destroying connected providers: await db.docs.clear(); // Verify there are no updates now: equal( - await db.table(updateTable).count(), + await db.table(updateTable).where('i').between(1,Infinity).count(), 0, 'Zero update stored after clearing docs' ); @@ -138,7 +164,7 @@ promisedTest('Test that syncers prohibit GC from compressing unsynced updates', const updateTable = db.docs.schema.yProps.find( (p) => p.prop === 'content' ).updTable; - equal(await db.table(updateTable).count(), 0, 'No docs stored yet'); + equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 0, 'No docs stored yet'); // Create three updates: await db.transaction('rw', db.docs, () => { @@ -147,7 +173,7 @@ promisedTest('Test that syncers prohibit GC from compressing unsynced updates', doc.getArray('arr').insert(0, ['x', 'y', 'z']); }); // Verify we have 3 updates: - equal(await db.table(updateTable).count(), 3, 'Three updates stored'); + equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 3, 'Three updates stored'); // Put a syncer in place that will not sync the updates: await db.table(updateTable).put({ @@ -155,7 +181,9 @@ promisedTest('Test that syncers prohibit GC from compressing unsynced updates', unsentFrom: await db.table(updateTable).orderBy('i').lastKey(), // Keep the last update and updates after that from being compressed }); + console.debug('Running GC'); await db.gc(); + console.debug('After running GC'); // Verify we have 2 updates (the first 2 was compressed but the last one was not): equal(await db.table(updateTable).where('i').between(1, Infinity).count(), 2, '2 updates stored'); await db.docs.delete(row.id); From 16a04d094656d73930893bc7c127213ae60d4015 Mon Sep 17 00:00:00 2001 From: dfahlander Date: Tue, 23 Jul 2024 11:12:33 +0200 Subject: [PATCH 16/23] Compression of updates complete and tested. --- src/public/types/yjs-related.ts | 17 ++++- src/yjs/compressYDocs.ts | 111 +++++++++++++++++--------------- src/yjs/periodicGC.ts | 2 +- test/tests-yjs.js | 7 +- 4 files changed, 82 insertions(+), 55 deletions(-) diff --git a/src/public/types/yjs-related.ts b/src/public/types/yjs-related.ts index aaf5337ee..79c1de264 100644 --- a/src/public/types/yjs-related.ts +++ b/src/public/types/yjs-related.ts @@ -76,6 +76,9 @@ export interface DucktypedAwareness extends DucktypedYObservable { } +/** Stored in the updates table with auto-incremented number as primary key + * + */ export interface YUpdateRow { /** The primary key in the update-table * @@ -100,14 +103,26 @@ export interface YUpdateRow { f?: number; } +/** Stored in update tables along with YUpdateRows but with a string representing the syncing enging, as primary key + * A syncing engine can create an YSyncer row with an unsentFrom value set to the a number representing primary key (i) + * of updates that has not been sent to server or peer yet. Dexie will spare all updates that occur after the least + * unsentFrom value in the updates table from being compressed and garbage collected into the main update. +*/ export interface YSyncer { i: string; unsentFrom: number; } +/** A stamp of the last compressed and garbage collected update in the update table. + * The garbage collection process will find out which documents have got new updates since the last compressed update + * and compress them into their corresponding main update. + * + * The id of this row is always 0 - which is a reserved id for this purpose. +*/ export interface YLastCompressed { i: 0; - compressedUntil: number; + lastCompressed: number; + lastRun?: Date; } export interface DexieYProvider { diff --git a/src/yjs/compressYDocs.ts b/src/yjs/compressYDocs.ts index 5028cd895..9fc753462 100644 --- a/src/yjs/compressYDocs.ts +++ b/src/yjs/compressYDocs.ts @@ -1,14 +1,10 @@ import { Dexie } from '../public/types/dexie'; -//import Promise from '../helpers/promise'; -import type { Table } from '../public/types/table'; import type { YLastCompressed, YSyncer, YUpdateRow, } from '../public/types/yjs-related'; -import { PromiseExtended } from '../public/types/promise-extended'; import { getYLibrary } from './getYLibrary'; -import { RangeSet, getRangeSetIterator } from '../helpers/rangeset'; import { cmp } from '../functions/cmp'; /** Go through all Y.Doc tables in the entire local db and compress updates @@ -16,23 +12,21 @@ import { cmp } from '../functions/cmp'; * @param db Dexie * @returns */ -export function compressYDocs(db: Dexie) { - return db.tables.reduce( - (promise, table) => - promise.then(() => - table.schema.yProps?.reduce( - (prom2, yProp) => prom2.then(() => compressYDocsTable(db, yProp)), - Promise.resolve() - ) - ), - Promise.resolve() - ) as PromiseExtended; +export function compressYDocs(db: Dexie, interval?: number) { + let p: Promise = Promise.resolve(); + for (const table of db.tables) { + for (const yProp of table.schema.yProps || []) { + p = p.then(() => compressYDocsTable(db, yProp, interval)); + } + } + return p; } /** Compress an individual Y.Doc table */ function compressYDocsTable( db: Dexie, - { updTable }: { prop: string; updTable: string } + { updTable }: { prop: string; updTable: string }, + skipIfRunnedSince?: number // milliseconds ) { const updTbl = db.table(updTable); return Promise.all([ @@ -41,13 +35,34 @@ function compressYDocsTable( .where('i') .startsWith('') // Syncers have string primary keys while updates have auto-incremented numbers. .toArray(), + // lastCompressed (pointer to the last compressed update) - updTbl.get(0), - ]).then(([syncers, lastCompressed]: [YSyncer[], YLastCompressed]) => { + db.transaction('rw', updTable, () => + updTbl.get(0).then((lastCompressed: YLastCompressed | undefined) => { + if ( + lastCompressed && + skipIfRunnedSince && + lastCompressed.lastRun.getTime() > Date.now() - skipIfRunnedSince + ) { + // Skip it. It's still running. + return null; + } + // isRunning might be true but we don't respect it if started before skipIfRunningSince. + lastCompressed = lastCompressed || { i: 0, lastCompressed: 0 }; + return updTbl + .put({ + ...lastCompressed, + lastRun: new Date(), + }) + .then(() => lastCompressed); + }) + ), + ]).then(([syncers, stamp]: [YSyncer[], YLastCompressed]) => { + if (!stamp) return; // Skip. Already running. + const lastCompressedUpdate = stamp.lastCompressed; const unsentFrom = Math.min( ...syncers.map((s) => s.unsentFrom || Infinity) ); - const compressedUntil = lastCompressed?.compressedUntil || 0; // Per updates-table: // 1. Find all updates after lastCompressedId. Run toArray() on them. // 2. IF there are any "mine" (flagged) updates AFTER unsentFrom, skip all from including this entry, else include all regardless of unsentFrom. @@ -57,48 +72,40 @@ function compressYDocsTable( // 6. Update lastCompressedId to the i of the latest compressed entry. return updTbl .where('i') - .between(compressedUntil, Infinity, false, false) - .toArray() - .then((addedUpdates: YUpdateRow[]) => { + .between(lastCompressedUpdate, Infinity, false, false) + .toArray((addedUpdates: YUpdateRow[]) => { if (addedUpdates.length <= 1) return; // For sure no updates to compress if there would be only 1. - const docIdsToCompress = new RangeSet(); - let lastUpdateToCompress = compressedUntil + 1; + const docsToCompress: { docId: any; updates: YUpdateRow[] }[] = []; + let lastUpdateToCompress = lastCompressedUpdate + 1; for (let j = 0; j < addedUpdates.length; ++j) { - const { i, f, k } = addedUpdates[j]; - if (i >= unsentFrom) if (f) break; // An update that need to be synced was found. Stop here and let dontCompressFrom stay. - docIdsToCompress.addKey(k); + const updateRow = addedUpdates[j]; + const { i, f, k } = updateRow; + if (i >= unsentFrom && f & 0x01) break; // An update that need to be synced was found. Stop here and let dontCompressFrom stay. + const entry = docsToCompress.find( + (entry) => cmp(entry.docId, k) === 0 + ); + if (entry) entry.updates.push(updateRow); + else docsToCompress.push({ docId: k, updates: [updateRow] }); lastUpdateToCompress = i; } - let promise = Promise.resolve(); - let iter = getRangeSetIterator(docIdsToCompress); - for ( - let keyIterRes = iter.next(); - !keyIterRes.done; - keyIterRes = iter.next() - ) { - const key = keyIterRes.value.from; // or keyIterRes.to - they are same. - const addedUpdatesForDoc = addedUpdates.filter( - (update) => cmp(update.k, key) === 0 - ); - if (addedUpdatesForDoc.length > 0) { - promise = promise.then(() => - compressUpdatesForDoc(db, updTable, key, addedUpdatesForDoc) - ); - } + let p = Promise.resolve(); + for (const { docId, updates } of docsToCompress) { + p = p.then(() => compressUpdatesForDoc(db, updTable, docId, updates)); } - return promise.then(() => { + return p.then(() => { // Update lastCompressed atomically to the value we computed. // Do it with respect to the case when another job was done in parallel // that maybe compressed one or more extra updates and updated lastCompressed // before us. return db.transaction('rw', updTbl, () => - updTbl.get(0).then( - (current) => - lastUpdateToCompress > current.compressedUntil && - updTbl.put({ - i: 0, - compressedUntil: lastUpdateToCompress, - }) + updTbl.get(0).then((current: YLastCompressed) => + updTbl.put({ + ...current, + lastCompressed: Math.max( + lastUpdateToCompress, + current?.lastCompressed || 0 + ), + }) ) ); }); @@ -131,7 +138,7 @@ export function compressUpdatesForDoc( .put({ i: lastUpdate.i, k: docRowId, - u: compressedUpdate + u: compressedUpdate, }) .then(() => updTbl.bulkDelete(updates.map((update) => update.i))); }); diff --git a/src/yjs/periodicGC.ts b/src/yjs/periodicGC.ts index 1afaa61b1..9a6b38921 100644 --- a/src/yjs/periodicGC.ts +++ b/src/yjs/periodicGC.ts @@ -11,7 +11,7 @@ export function periodicGC(db: Dexie) { if (db.tables.some(tbl => tbl.schema.yProps)) { const gc = () => { if (!db.isOpen()) return; - compressYDocs(db).catch(err => { + compressYDocs(db, INTERVAL).catch(err => { console.debug('Error during periodic GC', err); }).then(() => { timer = setTimeout(gc, INTERVAL); diff --git a/test/tests-yjs.js b/test/tests-yjs.js index 71f7e63d0..a083b1a72 100644 --- a/test/tests-yjs.js +++ b/test/tests-yjs.js @@ -120,7 +120,9 @@ promisedTest('Test Y document compression', async () => { doc.getArray('arr').insert(0, ['x', 'y', 'z']); }); equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 4, 'Four updates stored after additional inserts'); + console.debug('Running GC', await db.table(updateTable).toArray()); await db.gc(); + console.debug('After running GC', await db.table(updateTable).toArray()); equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 1, 'One update stored after gc'); await db.docs.put({ id: 'doc2', @@ -134,12 +136,15 @@ promisedTest('Test Y document compression', async () => { doc2.getArray('arr2').insert(0, ['1', '2', '3']); doc2.getArray('arr2').insert(0, ['x', 'y', 'z']); }); - equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 4, 'Four updates stored after additional inserts'); + console.debug('After adding thigns to other doc', await db.table(updateTable).toArray()); + equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 4, 'Four updates stored after additional inserts on other doc'); await db.gc(); + console.debug('After GC where we have 2 docs', await db.table(updateTable).toArray()); equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 2, 'Two updates stored after gc (2 different docs)'); // Now clear the docs table, which should implicitly clear the updates as well as destroying connected providers: await db.docs.clear(); + console.debug('After db.docs.clear()', await db.table(updateTable).toArray()); // Verify there are no updates now: equal( await db.table(updateTable).where('i').between(1,Infinity).count(), From 5cb596ab9f34705d67ff1425fb7d86f56d4085d6 Mon Sep 17 00:00:00 2001 From: dfahlander Date: Tue, 23 Jul 2024 13:55:23 +0200 Subject: [PATCH 17/23] Send and receive Y-updates in dexie-cloud-addon --- .../src/sync/DEXIE_CLOUD_SYNCER_ID.ts | 2 + addons/dexie-cloud/src/sync/applyYMessages.ts | 44 +++++++++++++++++++ .../src/sync/listYClientMessages.ts | 38 ++++++++++++++++ addons/dexie-cloud/src/sync/sync.ts | 17 +++++-- addons/dexie-cloud/src/sync/syncWithServer.ts | 3 ++ libs/dexie-cloud-common/src/SyncRequest.ts | 2 + libs/dexie-cloud-common/src/SyncResponse.ts | 2 + libs/dexie-cloud-common/src/YMessage.ts | 41 +++++++++++++++++ src/public/index.d.ts | 3 +- 9 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 addons/dexie-cloud/src/sync/DEXIE_CLOUD_SYNCER_ID.ts create mode 100644 addons/dexie-cloud/src/sync/applyYMessages.ts create mode 100644 addons/dexie-cloud/src/sync/listYClientMessages.ts create mode 100644 libs/dexie-cloud-common/src/YMessage.ts diff --git a/addons/dexie-cloud/src/sync/DEXIE_CLOUD_SYNCER_ID.ts b/addons/dexie-cloud/src/sync/DEXIE_CLOUD_SYNCER_ID.ts new file mode 100644 index 000000000..ff07fa377 --- /dev/null +++ b/addons/dexie-cloud/src/sync/DEXIE_CLOUD_SYNCER_ID.ts @@ -0,0 +1,2 @@ + +export const DEXIE_CLOUD_SYNCER_ID = 'dexie-cloud-syncer'; diff --git a/addons/dexie-cloud/src/sync/applyYMessages.ts b/addons/dexie-cloud/src/sync/applyYMessages.ts new file mode 100644 index 000000000..4aef78342 --- /dev/null +++ b/addons/dexie-cloud/src/sync/applyYMessages.ts @@ -0,0 +1,44 @@ +import { InsertType, YSyncer, YUpdateRow } from 'dexie'; +import { DexieCloudDB } from '../db/DexieCloudDB'; +import { YServerMessage } from 'dexie-cloud-common/src/YMessage'; +import { DEXIE_CLOUD_SYNCER_ID } from './DEXIE_CLOUD_SYNCER_ID'; + +export async function applyYServerMessages( + yMessages: YServerMessage[], + db: DexieCloudDB +): Promise { + for (const m of yMessages) { + switch (m.type) { + case 'u-s': { + await db.table(m.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 + | YSyncer + | undefined; + await tx.table(m.utbl).put(DEXIE_CLOUD_SYNCER_ID, { + ...(syncer || { i: DEXIE_CLOUD_SYNCER_ID }), + unsentFrom: Math.max(syncer?.unsentFrom || 0, m.i + 1), + } as YSyncer); + }); + break; + } + case 'u-reject': { + // Acces control or constraint rejected the update. + // We delete it. It's not going to be sent again. + // What's missing is a way to notify consumers, such as Tiptap editor, that the update was rejected. + // This is only an issue when the document is open. We could find the open document and + // 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); + break; + } + } + } +} diff --git a/addons/dexie-cloud/src/sync/listYClientMessages.ts b/addons/dexie-cloud/src/sync/listYClientMessages.ts new file mode 100644 index 000000000..14b66da50 --- /dev/null +++ b/addons/dexie-cloud/src/sync/listYClientMessages.ts @@ -0,0 +1,38 @@ +import { DexieYProvider, Table, YSyncer, YUpdateRow } from 'dexie'; +import { getTableFromMutationTable } from '../helpers/getTableFromMutationTable'; +import { DexieCloudDB } from '../db/DexieCloudDB'; +import { DBOperation, DBOperationsSet } from 'dexie-cloud-common'; +import { flatten } from '../helpers/flatten'; +import { YClientMessage } from 'dexie-cloud-common/src/YMessage'; +import { DEXIE_CLOUD_SYNCER_ID } from './DEXIE_CLOUD_SYNCER_ID'; + +export async function listYClientMessages( + db: DexieCloudDB +): Promise { + const result: YClientMessage[] = []; + for (const table of db.tables) { + for (const yProp of table.schema.yProps || []) { + const yTable = db.table(yProp.updTable); + const syncer = (await yTable.get(DEXIE_CLOUD_SYNCER_ID)) as YSyncer | undefined; + const unsentFrom = syncer?.unsentFrom || 0; + const updates = await yTable + .where('i') + .aboveOrEqual(unsentFrom) + .toArray(); + result.push( + ...updates + .filter((update) => update.f & 0x01) // Don't send back updates that we got from server or other clients. + .map(({ i, k, u }: YUpdateRow) => { + return { + type: 'u-c', + utbl: yProp.updTable, + i, + k, + u, + } satisfies YClientMessage; + }) + ); + } + } + return result; +} diff --git a/addons/dexie-cloud/src/sync/sync.ts b/addons/dexie-cloud/src/sync/sync.ts index 7b72fb004..25a171657 100644 --- a/addons/dexie-cloud/src/sync/sync.ts +++ b/addons/dexie-cloud/src/sync/sync.ts @@ -26,6 +26,8 @@ import { updateBaseRevs } from './updateBaseRevs'; import { getLatestRevisionsPerTable } from './getLatestRevisionsPerTable'; import { applyServerChanges } from './applyServerChanges'; import { checkSyncRateLimitDelay } from './ratelimit'; +import { listYClientMessages } from './listYClientMessages'; +import { applyYServerMessages } from './applyYMessages'; export const CURRENT_SYNC_WORKER = 'currentSyncWorker'; @@ -147,13 +149,14 @@ async function _sync( // // List changes to sync // - const [clientChangeSet, syncState, baseRevs] = await db.transaction( + const [clientChangeSet, syncState, baseRevs, yMessages] = await db.transaction( 'r', db.tables, async () => { const syncState = await db.getPersistedSyncState(); const baseRevs = await db.$baseRevs.toArray(); let clientChanges = await listClientChanges(mutationTables, db); + const yMessages = await listYClientMessages(db); throwIfCancelled(cancelToken); if (doSyncify) { const alreadySyncedRealms = [ @@ -168,15 +171,15 @@ async function _sync( ); throwIfCancelled(cancelToken); clientChanges = clientChanges.concat(syncificationInserts); - return [clientChanges, syncState, baseRevs]; + return [clientChanges, syncState, baseRevs, yMessages]; } - return [clientChanges, syncState, baseRevs]; + return [clientChanges, syncState, baseRevs, yMessages]; } ); const pushSyncIsNeeded = clientChangeSet.some((set) => set.muts.some((mut) => mut.keys.length > 0) - ); + ) || yMessages.length > 0; if (justCheckIfNeeded) { console.debug('Sync is needed:', pushSyncIsNeeded); return pushSyncIsNeeded; @@ -199,6 +202,7 @@ async function _sync( throwIfCancelled(cancelToken); const res = await syncWithServer( clientChangeSet, + yMessages, syncState, baseRevs, db, @@ -328,6 +332,11 @@ async function _sync( // await applyServerChanges(filteredChanges, db); + // + // apply yMessages + // + await applyYServerMessages(res.yMessages, db); + // // Update syncState // diff --git a/addons/dexie-cloud/src/sync/syncWithServer.ts b/addons/dexie-cloud/src/sync/syncWithServer.ts index 73ac39109..03a59bbdf 100644 --- a/addons/dexie-cloud/src/sync/syncWithServer.ts +++ b/addons/dexie-cloud/src/sync/syncWithServer.ts @@ -14,10 +14,12 @@ import { import { encodeIdsForServer } from './encodeIdsForServer'; import { UserLogin } from '../db/entities/UserLogin'; import { updateSyncRateLimitDelays } from './ratelimit'; +import { YClientMessage } from 'dexie-cloud-common/src/YMessage'; //import {BisonWebStreamReader} from "dreambase-library/dist/typeson-simplified/BisonWebStreamReader"; export async function syncWithServer( changes: DBOperationsSet, + y: YClientMessage[], syncState: PersistedSyncState | undefined, baseRevs: BaseRevisionMapEntry[], db: DexieCloudDB, @@ -63,6 +65,7 @@ export async function syncWithServer( : undefined, baseRevs, changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes), + y }; console.debug('Sync request', syncRequest); db.syncStateChangedEvent.next({ diff --git a/libs/dexie-cloud-common/src/SyncRequest.ts b/libs/dexie-cloud-common/src/SyncRequest.ts index 71c839805..283275d48 100644 --- a/libs/dexie-cloud-common/src/SyncRequest.ts +++ b/libs/dexie-cloud-common/src/SyncRequest.ts @@ -1,6 +1,7 @@ import { BaseRevisionMapEntry } from './BaseRevisionMapEntry.js'; import { DBOperationsSet } from './DBOperationsSet.js'; import { DexieCloudSchema } from './DexieCloudSchema.js'; +import { YClientMessage } from './YMessage.js'; export interface SyncRequest { v?: number; @@ -14,5 +15,6 @@ export interface SyncRequest { }; baseRevs: BaseRevisionMapEntry[]; changes: DBOperationsSet; + y?: YClientMessage[]; //invites: {[inviteId: string]: "accept" | "reject"} } diff --git a/libs/dexie-cloud-common/src/SyncResponse.ts b/libs/dexie-cloud-common/src/SyncResponse.ts index 7b34fec2f..075d507b3 100644 --- a/libs/dexie-cloud-common/src/SyncResponse.ts +++ b/libs/dexie-cloud-common/src/SyncResponse.ts @@ -1,5 +1,6 @@ import { DBOperationsSet } from './DBOperationsSet.js'; import { DexieCloudSchema } from './DexieCloudSchema.js'; +import { YServerMessage } from './YMessage.js'; export interface SyncResponse { serverRevision: string | bigint; // string "[1,\"2823\"]" in protocol version 2. bigint in version 1. @@ -9,5 +10,6 @@ export interface SyncResponse { schema: DexieCloudSchema; changes: DBOperationsSet; rejections: { name: string; message: string; txid: string }[]; + yMessages: YServerMessage[]; //invites: DBInvite[]; } diff --git a/libs/dexie-cloud-common/src/YMessage.ts b/libs/dexie-cloud-common/src/YMessage.ts new file mode 100644 index 000000000..8815433c9 --- /dev/null +++ b/libs/dexie-cloud-common/src/YMessage.ts @@ -0,0 +1,41 @@ + +export type YMessage = YClientMessage | YServerMessage; +export type YClientMessage = YUpdateFromClientRequest | YAwarenessUpdate; +export type YServerMessage = YUpdateFromClientAck | YUpdateFromClientReject | YUpdateFromServerMessage | YAwarenessUpdate; + +export interface YUpdateFromClientRequest { + type: 'u-c'; + utbl: string; + k: any; + u: Uint8Array; + i: number; +} + +export interface YUpdateFromClientAck { + type: 'u-ack'; + utbl: string; + i: number; +} + +export interface YUpdateFromClientReject { + type: 'u-reject'; + utbl: string; + i: number; +} + + +export interface YUpdateFromServerMessage { + type: 'u-s'; + utbl: string; + k: any; + u: Uint8Array; + realmSetHash: Uint8Array; + newRev: string; +} + +export interface YAwarenessUpdate { + type: 'awareness'; + utbl: string; + k: any; + u: Uint8Array; +} diff --git a/src/public/index.d.ts b/src/public/index.d.ts index c0b12b0ec..1b245d963 100644 --- a/src/public/index.d.ts +++ b/src/public/index.d.ts @@ -27,12 +27,13 @@ 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 } from './types/yjs-related'; +import { DexieYProvider, DucktypedYDoc, YSyncer, YUpdateRow, YLastCompressed, DexieYDocMeta } from './types/yjs-related'; export { PropModification, PropModSpec, PropModSymbol }; export * from './types/entity'; export * from './types/entity-table'; export { UpdateSpec } from './types/update-spec'; export * from './types/insert-type'; +export type { YSyncer, YUpdateRow, YLastCompressed, DexieYDocMeta }; // Alias of Table and Collection in order to be able to refer them from module below... interface _Table extends Table {} From 8995d85e5e4d5e1f8d430f586f6edd5c39d269b0 Mon Sep 17 00:00:00 2001 From: dfahlander Date: Sun, 28 Jul 2024 11:52:09 +0200 Subject: [PATCH 18/23] Added Dexie.once in addition to Dexie.on. Handy example: db.once('close', callback). --- src/classes/dexie/dexie.ts | 10 +++++++++- src/helpers/table-schema.ts | 2 +- src/public/types/db-events.d.ts | 6 ++++-- src/public/types/dexie.d.ts | 4 +++- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/classes/dexie/dexie.ts b/src/classes/dexie/dexie.ts index 38caeb08c..5bc5e39fe 100644 --- a/src/classes/dexie/dexie.ts +++ b/src/classes/dexie/dexie.ts @@ -1,7 +1,7 @@ // Import types from the public API import { Dexie as IDexie } from "../../public/types/dexie"; import { DexieOptions, DexieConstructor } from "../../public/types/dexie-constructor"; -import { DbEvents } from "../../public/types/db-events"; +import { DbEvents, DbEventFns } from "../../public/types/db-events"; //import { PromiseExtended, PromiseExtendedConstructor } from '../../public/types/promise-extended'; import { Table as ITable } from '../../public/types/table'; import { TableSchema } from "../../public/types/table-schema"; @@ -86,6 +86,7 @@ export class Dexie implements IDexie { idbdb: IDBDatabase | null; vip: Dexie; on: DbEvents; + once: DbEventFns; Table: TableConstructor; WhereClause: WhereClauseConstructor; @@ -148,6 +149,13 @@ export class Dexie implements IDexie { "y", { ready: [promisableChain, nop] } ) as DbEvents; + this.once = (event: any, callback: any) => { + const fn = (...args: any[]) => { + this.on(event).unsubscribe(fn); + callback.apply(this, args); + }; + return this.on(event as any, fn); + }; this.on.ready.subscribe = override(this.on.ready.subscribe, subscribe => { return (subscriber, bSticky) => { (Dexie as any as DexieConstructor).vip(() => { diff --git a/src/helpers/table-schema.ts b/src/helpers/table-schema.ts index 6f70a7b6f..5b4607889 100644 --- a/src/helpers/table-schema.ts +++ b/src/helpers/table-schema.ts @@ -15,7 +15,7 @@ export function createTableSchema( mappedClass: null, yProps: yProps?.map((prop) => ({ prop, - updTable: `$${name}.${prop}_updates`, + updatesTable: `$${name}.${prop}_updates`, })), idxByName: arrayToObject(indexes, (index) => [index.name, index]), }; diff --git a/src/public/types/db-events.d.ts b/src/public/types/db-events.d.ts index dc0e002d9..7ee5a7b2f 100644 --- a/src/public/types/db-events.d.ts +++ b/src/public/types/db-events.d.ts @@ -35,13 +35,15 @@ export interface DexieYEvent { fire(provider: DexieYProvider, Y: any): void; } -export interface DbEvents extends DexieEventSet { - (eventName: 'ready', subscriber: (vipDb: Dexie) => any, bSticky?: boolean): void; +export interface DbEventFns { (eventName: 'populate', subscriber: (trans: Transaction) => any): void; (eventName: 'blocked', subscriber: (event: IDBVersionChangeEvent) => any): void; (eventName: 'versionchange', subscriber: (event: IDBVersionChangeEvent) => any): void; (eventName: 'close', subscriber: (event: Event) => any): void; (eventName: 'y', subscriber: (provider: DexieYProvider, Y: any) => void): void; +} +export interface DbEvents extends DbEventFns, DexieEventSet { + (eventName: 'ready', subscriber: (vipDb: Dexie) => any, bSticky?: boolean): void; ready: DexieOnReadyEvent; populate: DexiePopulateEvent; blocked: DexieEvent; diff --git a/src/public/types/dexie.d.ts b/src/public/types/dexie.d.ts index 9e3cf6d34..ae30f340d 100644 --- a/src/public/types/dexie.d.ts +++ b/src/public/types/dexie.d.ts @@ -1,6 +1,6 @@ import { Table } from './table'; import { Version } from './version'; -import { DbEvents } from './db-events'; +import { DbEvents, DbEventFns } from './db-events'; import { TransactionMode } from './transaction-mode'; import { Transaction } from './transaction'; import { WhereClause } from './where-clause'; @@ -48,6 +48,8 @@ export interface Dexie { on: DbEvents; + once: DbEventFns; + open(): PromiseExtended; table(tableName: string): Table; From 31ac76e0c1ed722d08adc0313cf98e117116796b Mon Sep 17 00:00:00 2001 From: dfahlander Date: Sun, 28 Jul 2024 11:53:31 +0200 Subject: [PATCH 19/23] Update dts-bundle-generator. Could not take latest as it generates double types. Found out we needed version 9.3.1 exactly. --- package.json | 3 +- pnpm-lock.yaml | 611 +++++++++++++++++++++++++++++-------------------- 2 files changed, 363 insertions(+), 251 deletions(-) diff --git a/package.json b/package.json index d3ede9818..2a0fa52c1 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "devDependencies": { "@lambdatest/node-tunnel": "^4.0.7", "cross-env": "^7.0.3", - "dts-bundle-generator": "^5.9.0", + "dts-bundle-generator": "^9.3.1", "just-build": "^0.9.24", "karma": "^6.1.1", "karma-chrome-launcher": "^3.1.0", @@ -141,6 +141,7 @@ "tslib": "^2.1.0", "typescript": "^5.3.3", "uglify-js": "^3.9.2", + "y-protocols": "^1.0.6", "yjs": "^13.6.16" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2b120efe..76422a90e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^7.0.3 version: 7.0.3 dts-bundle-generator: - specifier: ^5.9.0 - version: 5.9.0 + specifier: ^9.3.1 + version: 9.3.1 just-build: specifier: ^0.9.24 version: 0.9.24 @@ -83,6 +83,9 @@ importers: uglify-js: specifier: ^3.9.2 version: 3.14.2 + y-protocols: + specifier: ^1.0.6 + version: 1.0.6(yjs@13.6.16) yjs: specifier: ^13.6.16 version: 13.6.16 @@ -182,7 +185,7 @@ importers: version: 2.1.2 karma-qunit: specifier: '*' - version: 4.1.2(karma@6.4.0)(qunit@2.20.0) + version: 4.1.2(karma@6.4.0)(qunit@2.21.1) preact: specifier: '*' version: 10.10.6 @@ -228,7 +231,7 @@ importers: version: 5.3.3 typeson: specifier: ^5.8.2 - version: 5.18.2(core-js-bundle@3.34.0)(regenerator-runtime@0.13.11) + version: 5.18.2(core-js-bundle@3.37.1)(regenerator-runtime@0.13.11) typeson-registry: specifier: ^1.0.0-alpha.21 version: 1.0.0-alpha.39 @@ -327,7 +330,7 @@ importers: version: 12.1.5(react-dom@17.0.2)(react@17.0.2) '@testing-library/user-event': specifier: ^14.4.3 - version: 14.4.3(@testing-library/dom@9.3.3) + version: 14.4.3(@testing-library/dom@10.4.0) '@types/jest': specifier: ^29.5.2 version: 29.5.2 @@ -357,7 +360,7 @@ importers: version: link:../../libs/dexie-react-hooks postcss-flexbugs-fixes: specifier: ^5.0.2 - version: 5.0.2(postcss@8.4.32) + version: 5.0.2(postcss@8.4.40) react: specifier: ^17.0.2 version: 17.0.2 @@ -369,7 +372,7 @@ importers: version: 17.0.2(react@17.0.2) react-scripts: specifier: 5.0.1 - version: 5.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.56.0)(react@17.0.2)(typescript@4.9.5)(uglify-js@3.14.2) + version: 5.0.1(@babel/plugin-syntax-flow@7.24.7)(@babel/plugin-transform-react-jsx@7.24.7)(eslint@8.57.0)(react@17.0.2)(typescript@4.9.5)(uglify-js@3.14.2) react-use: specifier: ^17.4.0 version: 17.4.0(react-dom@17.0.2)(react@17.0.2) @@ -422,11 +425,6 @@ importers: packages: - /@aashutoshrathi/word-wrap@1.2.6: - resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} - engines: {node: '>=0.10.0'} - dev: false - /@adobe/css-tools@4.2.0: resolution: {integrity: sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==} dev: false @@ -478,12 +476,12 @@ packages: '@babel/highlight': 7.18.6 dev: false - /@babel/code-frame@7.23.5: - resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + /@babel/code-frame@7.24.7: + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/highlight': 7.23.4 - chalk: 2.4.2 + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 dev: false /@babel/compat-data@7.22.3: @@ -514,7 +512,7 @@ packages: - supports-color dev: false - /@babel/eslint-parser@7.21.8(@babel/core@7.22.1)(eslint@8.56.0): + /@babel/eslint-parser@7.21.8(@babel/core@7.22.1)(eslint@8.57.0): resolution: {integrity: sha512-HLhI+2q+BP3sf78mFUZNCGc10KEmoUqtUT1OCdMZsN+qr4qFeLUod62/zAnF3jNQstwyasDkZnVXwfK2Bml7MQ==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: @@ -523,7 +521,7 @@ packages: dependencies: '@babel/core': 7.22.1 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 8.56.0 + eslint: 8.57.0 eslint-visitor-keys: 2.1.0 semver: 6.3.0 dev: false @@ -538,6 +536,16 @@ packages: jsesc: 2.5.2 dev: false + /@babel/generator@7.24.10: + resolution: {integrity: sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.9 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + dev: false + /@babel/helper-annotate-as-pure@7.18.6: resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} engines: {node: '>=6.9.0'} @@ -545,11 +553,11 @@ packages: '@babel/types': 7.22.4 dev: false - /@babel/helper-annotate-as-pure@7.22.5: - resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} + /@babel/helper-annotate-as-pure@7.24.7: + resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.24.9 dev: false /@babel/helper-builder-binary-assignment-operator-visitor@7.22.3: @@ -626,6 +634,13 @@ packages: engines: {node: '>=6.9.0'} dev: false + /@babel/helper-environment-visitor@7.24.7: + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.9 + dev: false + /@babel/helper-function-name@7.21.0: resolution: {integrity: sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==} engines: {node: '>=6.9.0'} @@ -634,6 +649,14 @@ packages: '@babel/types': 7.22.4 dev: false + /@babel/helper-function-name@7.24.7: + resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.9 + dev: false + /@babel/helper-hoist-variables@7.18.6: resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} engines: {node: '>=6.9.0'} @@ -641,6 +664,13 @@ packages: '@babel/types': 7.22.4 dev: false + /@babel/helper-hoist-variables@7.24.7: + resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.9 + dev: false + /@babel/helper-member-expression-to-functions@7.22.3: resolution: {integrity: sha512-Gl7sK04b/2WOb6OPVeNy9eFKeD3L6++CzL3ykPOWqTn08xgYYK0wz4TUh2feIImDXxcVW3/9WQ1NMKY66/jfZA==} engines: {node: '>=6.9.0'} @@ -655,11 +685,14 @@ packages: '@babel/types': 7.22.4 dev: false - /@babel/helper-module-imports@7.22.15: - resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + /@babel/helper-module-imports@7.24.7: + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/traverse': 7.24.8 + '@babel/types': 7.24.9 + transitivePeerDependencies: + - supports-color dev: false /@babel/helper-module-transforms@7.22.1: @@ -690,8 +723,8 @@ packages: engines: {node: '>=6.9.0'} dev: false - /@babel/helper-plugin-utils@7.22.5: - resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + /@babel/helper-plugin-utils@7.24.8: + resolution: {integrity: sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==} engines: {node: '>=6.9.0'} dev: false @@ -745,13 +778,20 @@ packages: '@babel/types': 7.22.4 dev: false + /@babel/helper-split-export-declaration@7.24.7: + resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.9 + dev: false + /@babel/helper-string-parser@7.21.5: resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==} engines: {node: '>=6.9.0'} dev: false - /@babel/helper-string-parser@7.23.4: - resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + /@babel/helper-string-parser@7.24.8: + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} engines: {node: '>=6.9.0'} dev: false @@ -759,8 +799,8 @@ packages: resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-identifier@7.22.20: - resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + /@babel/helper-validator-identifier@7.24.7: + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} engines: {node: '>=6.9.0'} dev: false @@ -800,13 +840,14 @@ packages: chalk: 2.4.2 js-tokens: 4.0.0 - /@babel/highlight@7.23.4: - resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + /@babel/highlight@7.24.7: + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-validator-identifier': 7.24.7 chalk: 2.4.2 js-tokens: 4.0.0 + picocolors: 1.0.1 dev: false /@babel/parser@7.22.4: @@ -817,6 +858,14 @@ packages: '@babel/types': 7.22.4 dev: false + /@babel/parser@7.24.8: + resolution: {integrity: sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.9 + dev: false + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.22.1): resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} engines: {node: '>=6.9.0'} @@ -1016,14 +1065,14 @@ packages: '@babel/helper-plugin-utils': 7.21.5 dev: false - /@babel/plugin-syntax-flow@7.23.3(@babel/core@7.22.1): - resolution: {integrity: sha512-YZiAIpkJAwQXBJLIQbRFayR5c+gJ35Vcz3bg954k7cd73zqjvhacJuL9RbrzPz8qPmZdgqP6EUKwy0PCNhaaPA==} + /@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.1): + resolution: {integrity: sha512-9G8GYT/dxn/D1IIKOUBmGX0mnmj46mGH9NnZyJLwtCpgh5f7D2VbuKodb+2s9m1Yavh1s7ASQN8lf0eqrb1LTw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.22.1 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-plugin-utils': 7.24.8 dev: false /@babel/plugin-syntax-import-assertions@7.20.0(@babel/core@7.22.1): @@ -1074,14 +1123,14 @@ packages: '@babel/helper-plugin-utils': 7.21.5 dev: false - /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.22.1): - resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} + /@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.22.1): + resolution: {integrity: sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.22.1 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-plugin-utils': 7.24.8 dev: false /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.22.1): @@ -1675,18 +1724,20 @@ packages: '@babel/types': 7.22.4 dev: false - /@babel/plugin-transform-react-jsx@7.23.4(@babel/core@7.22.1): - resolution: {integrity: sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==} + /@babel/plugin-transform-react-jsx@7.24.7(@babel/core@7.22.1): + resolution: {integrity: sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.22.1 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.22.1) - '@babel/types': 7.23.6 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.8 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.22.1) + '@babel/types': 7.24.9 + transitivePeerDependencies: + - supports-color dev: false /@babel/plugin-transform-react-pure-annotations@7.18.6(@babel/core@7.22.1): @@ -1993,8 +2044,8 @@ packages: regenerator-runtime: 0.13.11 dev: false - /@babel/runtime@7.23.6: - resolution: {integrity: sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==} + /@babel/runtime@7.24.8: + resolution: {integrity: sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==} engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.1 @@ -2009,6 +2060,15 @@ packages: '@babel/types': 7.22.4 dev: false + /@babel/template@7.24.7: + resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.24.8 + '@babel/types': 7.24.9 + dev: false + /@babel/traverse@7.22.4: resolution: {integrity: sha512-Tn1pDsjIcI+JcLKq1AVlZEr4226gpuAQTsLMorsYg9tuS/kG7nuwwJ4AB8jfQuEgb/COBwR/DqJxmoiYFu5/rQ==} engines: {node: '>=6.9.0'} @@ -2027,6 +2087,24 @@ packages: - supports-color dev: false + /@babel/traverse@7.24.8: + resolution: {integrity: sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.10 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/parser': 7.24.8 + '@babel/types': 7.24.9 + debug: 4.3.5 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/types@7.22.4: resolution: {integrity: sha512-Tx9x3UBHTTsMSW85WB2kphxYQVvrZ/t1FxD88IpSgIjiUJlCm9z+xWIDwyo1vffTwSqteqyznB8ZE9vYYk16zA==} engines: {node: '>=6.9.0'} @@ -2036,12 +2114,12 @@ packages: to-fast-properties: 2.0.0 dev: false - /@babel/types@7.23.6: - resolution: {integrity: sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==} + /@babel/types@7.24.9: + resolution: {integrity: sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.23.4 - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 dev: false @@ -2215,18 +2293,18 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} - /@eslint-community/eslint-utils@4.4.0(eslint@8.56.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.56.0 + eslint: 8.57.0 eslint-visitor-keys: 3.4.1 dev: false - /@eslint-community/regexpp@4.10.0: - resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + /@eslint-community/regexpp@4.11.0: + resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: false @@ -2257,10 +2335,10 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.5 espree: 9.6.1 globals: 13.24.0 - ignore: 5.3.0 + ignore: 5.3.1 import-fresh: 3.3.0 js-yaml: 4.1.0 minimatch: 3.1.2 @@ -2269,8 +2347,8 @@ packages: - supports-color dev: false - /@eslint/js@8.56.0: - resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==} + /@eslint/js@8.57.0: + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: false @@ -2313,13 +2391,13 @@ packages: react: 17.0.2 dev: false - /@humanwhocodes/config-array@0.11.13: - resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} + /@humanwhocodes/config-array@0.11.14: + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} deprecated: Use @eslint/config-array instead dependencies: - '@humanwhocodes/object-schema': 2.0.1 - debug: 4.3.4 + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.3.5 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -2345,8 +2423,8 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true - /@humanwhocodes/object-schema@2.0.1: - resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + /@humanwhocodes/object-schema@2.0.3: + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead dev: false @@ -2632,14 +2710,33 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/trace-mapping': 0.3.18 + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + dev: false + /@jridgewell/resolve-uri@3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + dev: false + /@jridgewell/set-array@1.1.2: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + dev: false + /@jridgewell/source-map@0.3.5: resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} dependencies: @@ -2652,12 +2749,23 @@ packages: /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + /@jridgewell/sourcemap-codec@1.5.0: + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + dev: false + /@jridgewell/trace-mapping@0.3.18: resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} dependencies: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + dev: false + /@lambdatest/node-tunnel@4.0.7: resolution: {integrity: sha512-LnQon3/b6YhbkzhUZ0nrnRRq1bZ0K4nxbJHXZcJd0FeNucH3RTERHyra69BbRyQ8Dc9YPoFFg0jGH4gnmrn6Cw==} dependencies: @@ -3269,27 +3377,27 @@ packages: tslib: 2.4.0 dev: false - /@testing-library/dom@8.20.0: - resolution: {integrity: sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==} - engines: {node: '>=12'} + /@testing-library/dom@10.4.0: + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} dependencies: - '@babel/code-frame': 7.21.4 - '@babel/runtime': 7.22.3 - '@types/aria-query': 5.0.1 - aria-query: 5.1.3 + '@babel/code-frame': 7.24.7 + '@babel/runtime': 7.24.8 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 chalk: 4.1.2 dom-accessibility-api: 0.5.16 lz-string: 1.5.0 pretty-format: 27.5.1 dev: false - /@testing-library/dom@9.3.3: - resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==} - engines: {node: '>=14'} + /@testing-library/dom@8.20.0: + resolution: {integrity: sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==} + engines: {node: '>=12'} dependencies: - '@babel/code-frame': 7.23.5 - '@babel/runtime': 7.23.6 - '@types/aria-query': 5.0.4 + '@babel/code-frame': 7.21.4 + '@babel/runtime': 7.22.3 + '@types/aria-query': 5.0.1 aria-query: 5.1.3 chalk: 4.1.2 dom-accessibility-api: 0.5.16 @@ -3326,13 +3434,13 @@ packages: react-dom: 17.0.2(react@17.0.2) dev: false - /@testing-library/user-event@14.4.3(@testing-library/dom@9.3.3): + /@testing-library/user-event@14.4.3(@testing-library/dom@10.4.0): resolution: {integrity: sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==} engines: {node: '>=12', npm: '>=6'} peerDependencies: '@testing-library/dom': '>=7.21.4' dependencies: - '@testing-library/dom': 9.3.3 + '@testing-library/dom': 10.4.0 dev: false /@tootallnate/once@1.1.2: @@ -3675,7 +3783,7 @@ packages: '@types/yargs-parser': 21.0.0 dev: false - /@typescript-eslint/eslint-plugin@5.59.9(@typescript-eslint/parser@5.59.9)(eslint@8.56.0)(typescript@4.9.5): + /@typescript-eslint/eslint-plugin@5.59.9(@typescript-eslint/parser@5.59.9)(eslint@8.57.0)(typescript@4.9.5): resolution: {integrity: sha512-4uQIBq1ffXd2YvF7MAvehWKW3zVv/w+mSfRAu+8cKbfj3nwzyqJLNcZJpQ/WZ1HLbJDiowwmQ6NO+63nCA+fqA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -3687,12 +3795,12 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 5.59.9(eslint@8.56.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.59.9(eslint@8.57.0)(typescript@4.9.5) '@typescript-eslint/scope-manager': 5.59.9 - '@typescript-eslint/type-utils': 5.59.9(eslint@8.56.0)(typescript@4.9.5) - '@typescript-eslint/utils': 5.59.9(eslint@8.56.0)(typescript@4.9.5) + '@typescript-eslint/type-utils': 5.59.9(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/utils': 5.59.9(eslint@8.57.0)(typescript@4.9.5) debug: 4.3.4 - eslint: 8.56.0 + eslint: 8.57.0 grapheme-splitter: 1.0.4 ignore: 5.2.4 natural-compare-lite: 1.4.0 @@ -3703,20 +3811,20 @@ packages: - supports-color dev: false - /@typescript-eslint/experimental-utils@5.59.9(eslint@8.56.0)(typescript@4.9.5): + /@typescript-eslint/experimental-utils@5.59.9(eslint@8.57.0)(typescript@4.9.5): resolution: {integrity: sha512-eZTK/Ci0QAqNc/q2MqMwI2+QI5ZI9HM12FcfGwbEvKif5ev/CIIYLmrlckvgPrC8XSbl39HtErR5NJiQkRkvWg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.59.9(eslint@8.56.0)(typescript@4.9.5) - eslint: 8.56.0 + '@typescript-eslint/utils': 5.59.9(eslint@8.57.0)(typescript@4.9.5) + eslint: 8.57.0 transitivePeerDependencies: - supports-color - typescript dev: false - /@typescript-eslint/parser@5.59.9(eslint@8.56.0)(typescript@4.9.5): + /@typescript-eslint/parser@5.59.9(eslint@8.57.0)(typescript@4.9.5): resolution: {integrity: sha512-FsPkRvBtcLQ/eVK1ivDiNYBjn3TGJdXy2fhXX+rc7czWl4ARwnpArwbihSOHI2Peg9WbtGHrbThfBUkZZGTtvQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -3730,7 +3838,7 @@ packages: '@typescript-eslint/types': 5.59.9 '@typescript-eslint/typescript-estree': 5.59.9(typescript@4.9.5) debug: 4.3.4 - eslint: 8.56.0 + eslint: 8.57.0 typescript: 4.9.5 transitivePeerDependencies: - supports-color @@ -3744,7 +3852,7 @@ packages: '@typescript-eslint/visitor-keys': 5.59.9 dev: false - /@typescript-eslint/type-utils@5.59.9(eslint@8.56.0)(typescript@4.9.5): + /@typescript-eslint/type-utils@5.59.9(eslint@8.57.0)(typescript@4.9.5): resolution: {integrity: sha512-ksEsT0/mEHg9e3qZu98AlSrONAQtrSTljL3ow9CGej8eRo7pe+yaC/mvTjptp23Xo/xIf2mLZKC6KPv4Sji26Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -3755,9 +3863,9 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 5.59.9(typescript@4.9.5) - '@typescript-eslint/utils': 5.59.9(eslint@8.56.0)(typescript@4.9.5) + '@typescript-eslint/utils': 5.59.9(eslint@8.57.0)(typescript@4.9.5) debug: 4.3.4 - eslint: 8.56.0 + eslint: 8.57.0 tsutils: 3.21.0(typescript@4.9.5) typescript: 4.9.5 transitivePeerDependencies: @@ -3790,19 +3898,19 @@ packages: - supports-color dev: false - /@typescript-eslint/utils@5.59.9(eslint@8.56.0)(typescript@4.9.5): + /@typescript-eslint/utils@5.59.9(eslint@8.57.0)(typescript@4.9.5): resolution: {integrity: sha512-1PuMYsju/38I5Ggblaeb98TOoUvjhRvLpLa1DoTOFaLWqaXl/1iQ1eGurTXgBY58NUdtfTXKP5xBq7q9NDaLKg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@types/json-schema': 7.0.12 '@types/semver': 7.5.0 '@typescript-eslint/scope-manager': 5.59.9 '@typescript-eslint/types': 5.59.9 '@typescript-eslint/typescript-estree': 5.59.9(typescript@4.9.5) - eslint: 8.56.0 + eslint: 8.57.0 eslint-scope: 5.1.1 semver: 7.5.1 transitivePeerDependencies: @@ -3992,12 +4100,12 @@ packages: acorn: 7.4.1 dev: true - /acorn-jsx@5.3.2(acorn@8.11.2): + /acorn-jsx@5.3.2(acorn@8.12.1): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.11.2 + acorn: 8.12.1 dev: false /acorn-walk@7.2.0: @@ -4021,8 +4129,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - /acorn@8.11.2: - resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} + /acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} engines: {node: '>=0.4.0'} hasBin: true dev: false @@ -4227,6 +4335,12 @@ packages: deep-equal: 2.2.1 dev: false + /aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + dependencies: + dequal: 2.0.3 + dev: false + /array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} dependencies: @@ -4827,6 +4941,7 @@ packages: /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} + dev: false /camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} @@ -4963,20 +5078,21 @@ packages: resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} dev: true - /cliui@6.0.0: - resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - dev: true + wrap-ansi: 7.0.0 - /cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + dev: true /clone-deep@4.0.1: resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} @@ -5158,8 +5274,8 @@ packages: toggle-selection: 1.0.6 dev: false - /core-js-bundle@3.34.0: - resolution: {integrity: sha512-6afGRU6ouqeVDVCmwXVE9H+oYmXsR77a9Iax83RcgXi3fOGgemzAzNWauwnKD4iIR8j5hzy/6bCF3d7nmAt/lA==} + /core-js-bundle@3.37.1: + resolution: {integrity: sha512-Bt9sorQku7bA6xoaY2NYdeaEnitLg9peHJ+eAijrARCQ5FhkoUW1eF4oI35XfP9kyeyljw71uCud4ju8tjGhsg==} requiresBuild: true dev: true @@ -5567,10 +5683,17 @@ packages: dependencies: ms: 2.1.2 - /decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - dev: true + /debug@4.3.5: + resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false /decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} @@ -5832,13 +5955,13 @@ packages: engines: {node: '>=14.8.0'} dev: true - /dts-bundle-generator@5.9.0: - resolution: {integrity: sha512-wzxUa9nfGL09Sg+gD3jqA8kYIA9A/olenvP2MmZ6IGTlUxR8G1z4U+0+OfUEL6OH1mzn6xsD8EOVgpNnGDi8tQ==} - engines: {node: '>=12.0.0'} + /dts-bundle-generator@9.3.1: + resolution: {integrity: sha512-1/nMT7LFOkXbrL1ZvLpzrjNbfX090LZ64nLIXVmet557mshFCGP/oTiQiZenafJZ6GsmRQLTYKSlQnkxK8tsTw==} + engines: {node: '>=14.0.0'} hasBin: true dependencies: typescript: 5.3.3 - yargs: 15.4.1 + yargs: 17.7.2 dev: true /duplexer@0.1.2: @@ -6105,6 +6228,11 @@ packages: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + dev: true + /escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -6134,7 +6262,7 @@ packages: source-map: 0.6.1 dev: false - /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.56.0)(jest@27.5.1)(typescript@4.9.5): + /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.24.7)(@babel/plugin-transform-react-jsx@7.24.7)(eslint@8.57.0)(jest@27.5.1)(typescript@4.9.5): resolution: {integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -6145,20 +6273,20 @@ packages: optional: true dependencies: '@babel/core': 7.22.1 - '@babel/eslint-parser': 7.21.8(@babel/core@7.22.1)(eslint@8.56.0) + '@babel/eslint-parser': 7.21.8(@babel/core@7.22.1)(eslint@8.57.0) '@rushstack/eslint-patch': 1.3.0 - '@typescript-eslint/eslint-plugin': 5.59.9(@typescript-eslint/parser@5.59.9)(eslint@8.56.0)(typescript@4.9.5) - '@typescript-eslint/parser': 5.59.9(eslint@8.56.0)(typescript@4.9.5) + '@typescript-eslint/eslint-plugin': 5.59.9(@typescript-eslint/parser@5.59.9)(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.59.9(eslint@8.57.0)(typescript@4.9.5) babel-preset-react-app: 10.0.1 confusing-browser-globals: 1.0.11 - eslint: 8.56.0 - eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.56.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.9)(eslint@8.56.0) - eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.59.9)(eslint@8.56.0)(jest@27.5.1)(typescript@4.9.5) - eslint-plugin-jsx-a11y: 6.7.1(eslint@8.56.0) - eslint-plugin-react: 7.32.2(eslint@8.56.0) - eslint-plugin-react-hooks: 4.6.0(eslint@8.56.0) - eslint-plugin-testing-library: 5.11.0(eslint@8.56.0)(typescript@4.9.5) + eslint: 8.57.0 + eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.24.7)(@babel/plugin-transform-react-jsx@7.24.7)(eslint@8.57.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.9)(eslint@8.57.0) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.59.9)(eslint@8.57.0)(jest@27.5.1)(typescript@4.9.5) + eslint-plugin-jsx-a11y: 6.7.1(eslint@8.57.0) + eslint-plugin-react: 7.32.2(eslint@8.57.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0) + eslint-plugin-testing-library: 5.11.0(eslint@8.57.0)(typescript@4.9.5) typescript: 4.9.5 transitivePeerDependencies: - '@babel/plugin-syntax-flow' @@ -6179,7 +6307,7 @@ packages: - supports-color dev: false - /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-node@0.3.7)(eslint@8.56.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-node@0.3.7)(eslint@8.57.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -6200,15 +6328,15 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.59.9(eslint@8.56.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.59.9(eslint@8.57.0)(typescript@4.9.5) debug: 3.2.7 - eslint: 8.56.0 + eslint: 8.57.0 eslint-import-resolver-node: 0.3.7 transitivePeerDependencies: - supports-color dev: false - /eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.56.0): + /eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.24.7)(@babel/plugin-transform-react-jsx@7.24.7)(eslint@8.57.0): resolution: {integrity: sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -6216,14 +6344,14 @@ packages: '@babel/plugin-transform-react-jsx': ^7.14.9 eslint: ^8.1.0 dependencies: - '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.22.1) - '@babel/plugin-transform-react-jsx': 7.23.4(@babel/core@7.22.1) - eslint: 8.56.0 + '@babel/plugin-syntax-flow': 7.24.7(@babel/core@7.22.1) + '@babel/plugin-transform-react-jsx': 7.24.7(@babel/core@7.22.1) + eslint: 8.57.0 lodash: 4.17.21 string-natural-compare: 3.0.1 dev: false - /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.9)(eslint@8.56.0): + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.9)(eslint@8.57.0): resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} peerDependencies: @@ -6233,15 +6361,15 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.59.9(eslint@8.56.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.59.9(eslint@8.57.0)(typescript@4.9.5) array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.56.0 + eslint: 8.57.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-node@0.3.7)(eslint@8.56.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.9)(eslint-import-resolver-node@0.3.7)(eslint@8.57.0) has: 1.0.3 is-core-module: 2.12.1 is-glob: 4.0.3 @@ -6256,7 +6384,7 @@ packages: - supports-color dev: false - /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.59.9)(eslint@8.56.0)(jest@27.5.1)(typescript@4.9.5): + /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.59.9)(eslint@8.57.0)(jest@27.5.1)(typescript@4.9.5): resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} peerDependencies: @@ -6269,16 +6397,16 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.59.9(@typescript-eslint/parser@5.59.9)(eslint@8.56.0)(typescript@4.9.5) - '@typescript-eslint/experimental-utils': 5.59.9(eslint@8.56.0)(typescript@4.9.5) - eslint: 8.56.0 + '@typescript-eslint/eslint-plugin': 5.59.9(@typescript-eslint/parser@5.59.9)(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/experimental-utils': 5.59.9(eslint@8.57.0)(typescript@4.9.5) + eslint: 8.57.0 jest: 27.5.1 transitivePeerDependencies: - supports-color - typescript dev: false - /eslint-plugin-jsx-a11y@6.7.1(eslint@8.56.0): + /eslint-plugin-jsx-a11y@6.7.1(eslint@8.57.0): resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} engines: {node: '>=4.0'} peerDependencies: @@ -6293,7 +6421,7 @@ packages: axobject-query: 3.1.1 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 8.56.0 + eslint: 8.57.0 has: 1.0.3 jsx-ast-utils: 3.3.3 language-tags: 1.0.5 @@ -6303,16 +6431,16 @@ packages: semver: 6.3.0 dev: false - /eslint-plugin-react-hooks@4.6.0(eslint@8.56.0): + /eslint-plugin-react-hooks@4.6.0(eslint@8.57.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 dependencies: - eslint: 8.56.0 + eslint: 8.57.0 dev: false - /eslint-plugin-react@7.32.2(eslint@8.56.0): + /eslint-plugin-react@7.32.2(eslint@8.57.0): resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==} engines: {node: '>=4'} peerDependencies: @@ -6322,7 +6450,7 @@ packages: array.prototype.flatmap: 1.3.1 array.prototype.tosorted: 1.1.1 doctrine: 2.1.0 - eslint: 8.56.0 + eslint: 8.57.0 estraverse: 5.3.0 jsx-ast-utils: 3.3.3 minimatch: 3.1.2 @@ -6336,14 +6464,14 @@ packages: string.prototype.matchall: 4.0.8 dev: false - /eslint-plugin-testing-library@5.11.0(eslint@8.56.0)(typescript@4.9.5): + /eslint-plugin-testing-library@5.11.0(eslint@8.57.0)(typescript@4.9.5): resolution: {integrity: sha512-ELY7Gefo+61OfXKlQeXNIDVVLPcvKTeiQOoMZG9TeuWa7Ln4dUNRv8JdRWBQI9Mbb427XGlVB1aa1QPZxBJM8Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} peerDependencies: eslint: ^7.5.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.59.9(eslint@8.56.0)(typescript@4.9.5) - eslint: 8.56.0 + '@typescript-eslint/utils': 5.59.9(eslint@8.57.0)(typescript@4.9.5) + eslint: 8.57.0 transitivePeerDependencies: - supports-color - typescript @@ -6405,7 +6533,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: false - /eslint-webpack-plugin@3.2.0(eslint@8.56.0)(webpack@5.74.0): + /eslint-webpack-plugin@3.2.0(eslint@8.57.0)(webpack@5.74.0): resolution: {integrity: sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==} engines: {node: '>= 12.13.0'} peerDependencies: @@ -6413,7 +6541,7 @@ packages: webpack: ^5.0.0 dependencies: '@types/eslint': 8.40.0 - eslint: 8.56.0 + eslint: 8.57.0 jest-worker: 28.1.3 micromatch: 4.0.5 normalize-path: 3.0.0 @@ -6515,29 +6643,29 @@ packages: - supports-color dev: true - /eslint@8.56.0: - resolution: {integrity: sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==} + /eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) - '@eslint-community/regexpp': 4.10.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.11.0 '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.56.0 - '@humanwhocodes/config-array': 0.11.13 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 '@ungap/structured-clone': 1.2.0 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.5 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - esquery: 1.5.0 + esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 6.0.1 @@ -6545,7 +6673,7 @@ packages: glob-parent: 6.0.2 globals: 13.24.0 graphemer: 1.4.0 - ignore: 5.3.0 + ignore: 5.3.1 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 @@ -6555,7 +6683,7 @@ packages: lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 - optionator: 0.9.3 + optionator: 0.9.4 strip-ansi: 6.0.1 text-table: 0.2.0 transitivePeerDependencies: @@ -6584,8 +6712,8 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.11.2 - acorn-jsx: 5.3.2(acorn@8.11.2) + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) eslint-visitor-keys: 3.4.3 dev: false @@ -6601,8 +6729,8 @@ packages: estraverse: 5.3.0 dev: true - /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + /esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} dependencies: estraverse: 5.3.0 @@ -6979,7 +7107,7 @@ packages: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} dev: true - /fork-ts-checker-webpack-plugin@6.5.3(eslint@8.56.0)(typescript@4.9.5)(webpack@5.74.0): + /fork-ts-checker-webpack-plugin@6.5.3(eslint@8.57.0)(typescript@4.9.5)(webpack@5.74.0): resolution: {integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==} engines: {node: '>=10', yarn: '>=1.0.0'} peerDependencies: @@ -6999,7 +7127,7 @@ packages: chokidar: 3.5.3 cosmiconfig: 6.0.0 deepmerge: 4.2.2 - eslint: 8.56.0 + eslint: 8.57.0 fs-extra: 9.1.0 glob: 7.2.3 memfs: 3.5.2 @@ -7629,8 +7757,8 @@ packages: engines: {node: '>= 4'} dev: false - /ignore@5.3.0: - resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} + /ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} dev: false @@ -8868,14 +8996,14 @@ packages: qunit: 2.17.2 dev: true - /karma-qunit@4.1.2(karma@6.4.0)(qunit@2.20.0): + /karma-qunit@4.1.2(karma@6.4.0)(qunit@2.21.1): resolution: {integrity: sha512-taTPqBeHCOlkeKTSzQgIKzAUb79vw3rfbCph+xwwh63tyGjNtljwx91VArhIM9DzIIR3gB9G214wQg+oXI9ycw==} peerDependencies: karma: ^4.0.0 || ^5.0.0 || ^6.0.0 qunit: ^2.0.0 dependencies: karma: 6.4.0 - qunit: 2.20.0 + qunit: 2.21.1 dev: true /karma-webdriver-launcher@1.0.8: @@ -9651,16 +9779,16 @@ packages: word-wrap: 1.2.3 dev: true - /optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + /optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} dependencies: - '@aashutoshrathi/word-wrap': 1.2.6 deep-is: 0.1.4 fast-levenshtein: 2.0.6 levn: 0.4.1 prelude-ls: 1.2.1 type-check: 0.4.0 + word-wrap: 1.2.5 dev: false /os-tmpdir@1.0.2: @@ -9804,6 +9932,10 @@ packages: /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + /picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + dev: false + /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -10042,12 +10174,12 @@ packages: postcss: 8.4.24 dev: false - /postcss-flexbugs-fixes@5.0.2(postcss@8.4.32): + /postcss-flexbugs-fixes@5.0.2(postcss@8.4.40): resolution: {integrity: sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==} peerDependencies: postcss: ^8.1.4 dependencies: - postcss: 8.4.32 + postcss: 8.4.40 dev: false /postcss-focus-visible@6.0.4(postcss@8.4.24): @@ -10627,13 +10759,13 @@ packages: source-map-js: 1.0.2 dev: false - /postcss@8.4.32: - resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==} + /postcss@8.4.40: + resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==} engines: {node: ^10 || ^12 || >=14} dependencies: nanoid: 3.3.7 - picocolors: 1.0.0 - source-map-js: 1.0.2 + picocolors: 1.0.1 + source-map-js: 1.2.0 dev: false /preact@10.10.6: @@ -10816,8 +10948,8 @@ packages: tiny-glob: 0.2.9 dev: true - /qunit@2.20.0: - resolution: {integrity: sha512-N8Fp1J55waE+QG1KwX2LOyqulZUToRrrPBqDOfYfuAMkEglFL15uwvmH1P4Tq/omQ/mGbBI8PEB3PhIfvUb+jg==} + /qunit@2.21.1: + resolution: {integrity: sha512-SMA8IBZamI9MyVB4dShGpn6+X6plO8mIyfZTQ815XBvv/nVMeUj+yxsw8SgZVnrMlrAvTziJkmjOhaDwkNMHWQ==} engines: {node: '>=10'} hasBin: true dependencies: @@ -10903,7 +11035,7 @@ packages: warning: 4.0.3 dev: false - /react-dev-utils@12.0.1(eslint@8.56.0)(typescript@4.9.5)(webpack@5.74.0): + /react-dev-utils@12.0.1(eslint@8.57.0)(typescript@4.9.5)(webpack@5.74.0): resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} engines: {node: '>=14'} peerDependencies: @@ -10922,7 +11054,7 @@ packages: escape-string-regexp: 4.0.0 filesize: 8.0.7 find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 6.5.3(eslint@8.56.0)(typescript@4.9.5)(webpack@5.74.0) + fork-ts-checker-webpack-plugin: 6.5.3(eslint@8.57.0)(typescript@4.9.5)(webpack@5.74.0) global-modules: 2.0.0 globby: 11.1.0 gzip-size: 6.0.0 @@ -10980,7 +11112,7 @@ packages: engines: {node: '>=0.10.0'} dev: false - /react-scripts@5.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.56.0)(react@17.0.2)(typescript@4.9.5)(uglify-js@3.14.2): + /react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7)(@babel/plugin-transform-react-jsx@7.24.7)(eslint@8.57.0)(react@17.0.2)(typescript@4.9.5)(uglify-js@3.14.2): resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==} engines: {node: '>=14.0.0'} hasBin: true @@ -11007,9 +11139,9 @@ packages: css-minimizer-webpack-plugin: 3.4.1(webpack@5.74.0) dotenv: 10.0.0 dotenv-expand: 5.1.0 - eslint: 8.56.0 - eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.56.0)(jest@27.5.1)(typescript@4.9.5) - eslint-webpack-plugin: 3.2.0(eslint@8.56.0)(webpack@5.74.0) + eslint: 8.57.0 + eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.24.7)(@babel/plugin-transform-react-jsx@7.24.7)(eslint@8.57.0)(jest@27.5.1)(typescript@4.9.5) + eslint-webpack-plugin: 3.2.0(eslint@8.57.0)(webpack@5.74.0) file-loader: 6.2.0(webpack@5.74.0) fs-extra: 10.0.1 html-webpack-plugin: 5.5.1(webpack@5.74.0) @@ -11026,7 +11158,7 @@ packages: prompts: 2.4.2 react: 17.0.2 react-app-polyfill: 3.0.0 - react-dev-utils: 12.0.1(eslint@8.56.0)(typescript@4.9.5)(webpack@5.74.0) + react-dev-utils: 12.0.1(eslint@8.57.0)(typescript@4.9.5)(webpack@5.74.0) react-refresh: 0.11.0 resolve: 1.22.1 resolve-url-loader: 4.0.0 @@ -11307,10 +11439,6 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - /require-main-filename@2.0.0: - resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - dev: true - /requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -11867,10 +11995,6 @@ packages: - supports-color dev: false - /set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - dev: true - /set-harmonic-interval@1.0.1: resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==} engines: {node: '>=6.9'} @@ -12052,6 +12176,11 @@ packages: engines: {node: '>=0.10.0'} dev: false + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + dev: false + /source-map-loader@3.0.2(webpack@5.74.0): resolution: {integrity: sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==} engines: {node: '>= 12.13.0'} @@ -12271,15 +12400,6 @@ packages: strip-ansi: 5.2.0 dev: true - /string-width@4.2.2: - resolution: {integrity: sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==} - engines: {node: '>=8'} - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - dev: true - /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -12936,14 +13056,14 @@ packages: whatwg-url: 8.7.0 dev: true - /typeson@5.18.2(core-js-bundle@3.34.0)(regenerator-runtime@0.13.11): + /typeson@5.18.2(core-js-bundle@3.37.1)(regenerator-runtime@0.13.11): resolution: {integrity: sha512-Vetd+OGX05P4qHyHiSLdHZ5Z5GuQDrHHwSdjkqho9NSCYVSLSfRMjklD/unpHH8tXBR9Z/R05rwJSuMpMFrdsw==} engines: {node: '>=0.1.14'} peerDependencies: core-js-bundle: ^3.6.4 regenerator-runtime: ^0.13.3 dependencies: - core-js-bundle: 3.34.0 + core-js-bundle: 3.37.1 regenerator-runtime: 0.13.11 dev: true @@ -13463,10 +13583,6 @@ packages: is-weakset: 2.0.2 dev: false - /which-module@2.0.0: - resolution: {integrity: sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==} - dev: true - /which-typed-array@1.1.9: resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} engines: {node: '>= 0.4'} @@ -13499,6 +13615,11 @@ packages: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} + /word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + dev: false + /workbox-background-sync@6.6.0: resolution: {integrity: sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==} dependencies: @@ -13741,15 +13862,6 @@ packages: workbox-core: 6.6.0 dev: false - /wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - dev: true - /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -13824,8 +13936,14 @@ packages: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} dev: false - /y18n@4.0.1: - resolution: {integrity: sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==} + /y-protocols@1.0.6(yjs@13.6.16): + resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + yjs: ^13.0.0 + dependencies: + lib0: 0.2.94 + yjs: 13.6.16 dev: true /y18n@5.0.8: @@ -13849,33 +13967,13 @@ packages: engines: {node: '>= 14'} dev: false - /yargs-parser@18.1.3: - resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} - engines: {node: '>=6'} - dependencies: - camelcase: 5.3.1 - decamelize: 1.2.0 - dev: true - /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} - /yargs@15.4.1: - resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} - engines: {node: '>=8'} - dependencies: - cliui: 6.0.0 - decamelize: 1.2.0 - find-up: 4.1.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - require-main-filename: 2.0.0 - set-blocking: 2.0.0 - string-width: 4.2.2 - which-module: 2.0.0 - y18n: 4.0.1 - yargs-parser: 18.1.3 + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} dev: true /yargs@16.2.0: @@ -13890,6 +13988,19 @@ packages: y18n: 5.0.8 yargs-parser: 20.2.9 + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + /yjs@13.6.16: resolution: {integrity: sha512-uEq+n/dFIecBElEdeQea8nDnltScBfuhCSyAxDw4CosveP9Ag0eW6iZi2mdpW7EgxSFT7VXK2MJl3tKaLTmhAQ==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} From 4ceb5887cedfddd9d7a9f6aa67046d1b398c7fd4 Mon Sep 17 00:00:00 2001 From: dfahlander Date: Sun, 28 Jul 2024 11:56:29 +0200 Subject: [PATCH 20/23] Refactored Y.js support: * Renamed updTable to updatesTable * Renamed id / rowId to parentId --- src/classes/table/table-helpers.ts | 6 +-- src/classes/table/table.ts | 4 +- .../transaction/transaction-constructor.ts | 2 +- src/classes/version/version.ts | 2 +- src/public/types/table-schema.d.ts | 2 +- src/public/types/yjs-related.ts | 8 ++-- src/yjs/compressYDocs.ts | 38 ++++++++++--------- src/yjs/observeYDocUpdates.ts | 14 +++---- 8 files changed, 39 insertions(+), 37 deletions(-) diff --git a/src/classes/table/table-helpers.ts b/src/classes/table/table-helpers.ts index 1b2bffa2b..a46047160 100644 --- a/src/classes/table/table-helpers.ts +++ b/src/classes/table/table-helpers.ts @@ -11,9 +11,9 @@ export function builtInDeletionTrigger (table: Table, keys: null | readonly any[ const { yProps } = table.schema; if (!yProps) return res; if (keys && res.numFailures > 0) keys = keys.filter((_, i) => !res.failures[i]); - return Promise.all(yProps.map(({updTable}) => + return Promise.all(yProps.map(({updatesTable}) => keys - ? table.db.table(updTable).where('k').anyOf(keys).delete() - : table.db.table(updTable).clear() + ? table.db.table(updatesTable).where('k').anyOf(keys).delete() + : table.db.table(updatesTable).clear() )).then(() => res); } \ No newline at end of file diff --git a/src/classes/table/table.ts b/src/classes/table/table.ts index 0c764a796..7968c101c 100644 --- a/src/classes/table/table.ts +++ b/src/classes/table/table.ts @@ -280,8 +280,8 @@ export class Table implements ITable { if (this.schema.yProps) { const Y = getYLibrary(db); constructor = class extends (constructor as any) {}; - this.schema.yProps.forEach(({prop, updTable}) => { - Object.defineProperty(constructor.prototype, prop, createYDocProperty(db, Y, this, prop, updTable)); + this.schema.yProps.forEach(({prop, updatesTable}) => { + Object.defineProperty(constructor.prototype, prop, createYDocProperty(db, Y, this, updatesTable)); }); } // Collect all inherited property names (including method names) by diff --git a/src/classes/transaction/transaction-constructor.ts b/src/classes/transaction/transaction-constructor.ts index 74bab3269..2011307c5 100644 --- a/src/classes/transaction/transaction-constructor.ts +++ b/src/classes/transaction/transaction-constructor.ts @@ -35,7 +35,7 @@ export function createTransactionConstructor(db: Dexie) { if (mode !== 'readonly') storeNames.forEach(storeName => { // Uncollapse storeName to include Y update tables in case deletion of a record - then we must also delete its Y updates. const yProps = dbschema[storeName]?.yProps; - if (yProps) storeNames = storeNames.concat(yProps.map(p => p.updTable)); + if (yProps) storeNames = storeNames.concat(yProps.map(p => p.updatesTable)); }); this.db = db; diff --git a/src/classes/version/version.ts b/src/classes/version/version.ts index 00038228e..7fb49dfd2 100644 --- a/src/classes/version/version.ts +++ b/src/classes/version/version.ts @@ -81,7 +81,7 @@ export class Version implements IVersion { // Could be using an index [f+i] but that wouldn't gain too much and Firefox before 126 doesnt support it. // Local updates are flagged while remote updates are not. // - { [yProp.updTable]: '++i,k' }, + { [yProp.updatesTable]: '++i,k' }, outSchema ); } diff --git a/src/public/types/table-schema.d.ts b/src/public/types/table-schema.d.ts index a447af91a..b4b4f2a5e 100644 --- a/src/public/types/table-schema.d.ts +++ b/src/public/types/table-schema.d.ts @@ -4,7 +4,7 @@ export interface TableSchema { name: string; primKey: IndexSpec; indexes: IndexSpec[]; - yProps?: {prop: string, updTable: string}[]; + yProps?: {prop: string, updatesTable: string}[]; mappedClass: Function; idxByName: {[name: string]: IndexSpec}; readHook?: (x:any) => any diff --git a/src/public/types/yjs-related.ts b/src/public/types/yjs-related.ts index 79c1de264..389fc32a6 100644 --- a/src/public/types/yjs-related.ts +++ b/src/public/types/yjs-related.ts @@ -55,11 +55,11 @@ export interface DucktypedYDoc extends DucktypedYObservable { export interface DexieYDocMeta { db: Dexie, - table: string, updatesTable: string, - prop: string, - id: any, - cacheKey: string + parentTable: string, + parentId: any + //prop: string, + //cacheKey: string } /** Docktyped Awareness */ diff --git a/src/yjs/compressYDocs.ts b/src/yjs/compressYDocs.ts index 9fc753462..7edbee59d 100644 --- a/src/yjs/compressYDocs.ts +++ b/src/yjs/compressYDocs.ts @@ -25,10 +25,10 @@ export function compressYDocs(db: Dexie, interval?: number) { /** Compress an individual Y.Doc table */ function compressYDocsTable( db: Dexie, - { updTable }: { prop: string; updTable: string }, + { updatesTable }: { prop: string; updatesTable: string }, skipIfRunnedSince?: number // milliseconds ) { - const updTbl = db.table(updTable); + const updTbl = db.table(updatesTable); return Promise.all([ // syncers (for example dexie-cloud-addon or other 3rd part syncers) They may have unsentFrom set. updTbl @@ -37,7 +37,7 @@ function compressYDocsTable( .toArray(), // lastCompressed (pointer to the last compressed update) - db.transaction('rw', updTable, () => + db.transaction('rw', updatesTable, () => updTbl.get(0).then((lastCompressed: YLastCompressed | undefined) => { if ( lastCompressed && @@ -72,7 +72,7 @@ function compressYDocsTable( // 6. Update lastCompressedId to the i of the latest compressed entry. return updTbl .where('i') - .between(lastCompressedUpdate, Infinity, false, false) + .between(lastCompressedUpdate, Infinity, false) .toArray((addedUpdates: YUpdateRow[]) => { if (addedUpdates.length <= 1) return; // For sure no updates to compress if there would be only 1. const docsToCompress: { docId: any; updates: YUpdateRow[] }[] = []; @@ -80,7 +80,7 @@ function compressYDocsTable( for (let j = 0; j < addedUpdates.length; ++j) { const updateRow = addedUpdates[j]; const { i, f, k } = updateRow; - if (i >= unsentFrom && f & 0x01) break; // An update that need to be synced was found. Stop here and let dontCompressFrom stay. + if (i >= unsentFrom && (f & 0x01)) break; // An update that need to be synced was found. Stop here and let dontCompressFrom stay. const entry = docsToCompress.find( (entry) => cmp(entry.docId, k) === 0 ); @@ -90,7 +90,7 @@ function compressYDocsTable( } let p = Promise.resolve(); for (const { docId, updates } of docsToCompress) { - p = p.then(() => compressUpdatesForDoc(db, updTable, docId, updates)); + p = p.then(() => compressUpdatesForDoc(db, updatesTable, docId, updates)); } return p.then(() => { // Update lastCompressed atomically to the value we computed. @@ -115,29 +115,31 @@ function compressYDocsTable( export function compressUpdatesForDoc( db: Dexie, - updTable: string, - docRowId: any, + updatesTable: string, + parentId: any, addedUpdatesToCompress: YUpdateRow[] ) { if (addedUpdatesToCompress.length < 1) throw new Error('Invalid input'); - return db.transaction('rw', updTable, (tx) => { - const updTbl = tx.table(updTable); - return updTbl.where({ k: docRowId }).first((mainUpdate: YUpdateRow) => { + return db.transaction('rw', updatesTable, (tx) => { + const updTbl = tx.table(updatesTable); + return updTbl.where({ k: parentId }).first((mainUpdate: YUpdateRow) => { const updates = [mainUpdate].concat(addedUpdatesToCompress); // in some situations, mainUpdate will be included twice here. But Y.js doesn't care! const Y = getYLibrary(db); const doc = new Y.Doc({ gc: true }); - updates.forEach((update) => { - if (cmp(update.k, docRowId) !== 0) { - throw new Error('Invalid update'); - } - Y.applyUpdateV2(doc, update.u); - }); + //Y.transact(doc, ()=>{ + updates.forEach((update) => { + //if (cmp(update.k, docRowId) !== 0) { + // throw new Error('Invalid update'); + //} + Y.applyUpdateV2(doc, update.u); + }); + //}, "compressYDocs"); // Don't think anyone could be listening to this local doc. const compressedUpdate = Y.encodeStateAsUpdateV2(doc); const lastUpdate = updates.pop(); return updTbl .put({ i: lastUpdate.i, - k: docRowId, + k: parentId, u: compressedUpdate, }) .then(() => updTbl.bulkDelete(updates.map((update) => update.i))); diff --git a/src/yjs/observeYDocUpdates.ts b/src/yjs/observeYDocUpdates.ts index de1fb68ba..fefcc1e4d 100644 --- a/src/yjs/observeYDocUpdates.ts +++ b/src/yjs/observeYDocUpdates.ts @@ -16,7 +16,7 @@ export function observeYDocUpdates( db: Dexie, parentTableName: string, updatesTableName: string, - id: any, + parentId: any, Y: DucktypedY ): () => void { let lastUpdateId = 0; @@ -34,14 +34,14 @@ export function observeYDocUpdates( .between(lastUpdateId, Infinity, false) .toArray() .then((updates) => - updates.filter((update) => cmp(update.k, id) === 0) + updates.filter((update) => cmp(update.k, parentId) === 0) ) - : updatesTable.where({ k: id }).toArray() + : updatesTable.where({ k: parentId }).toArray() ).then((updates) => { if (updates.length > 0) lastUpdateId = updates[updates.length - 1].i; return updates; }), - db.table(parentTableName).where(':id').equals(id).toArray(), // Why not just count() or get()? Because of cache only works with toArray() currently (optimization) + db.table(parentTableName).where(':id').equals(parentId).toArray(), // Why not just count() or get()? Because of cache only works with toArray() currently (optimization) ]); }).subscribe( ([updates, parentRow]) => { @@ -59,7 +59,7 @@ export function observeYDocUpdates( Y.applyUpdateV2(doc, update.u); }); }, - subscription, + provider, false ); } @@ -75,10 +75,10 @@ export function observeYDocUpdates( ); const onUpdate = (update: Uint8Array, origin: any) => { - if (origin === subscription) return; // Already applied. + if (origin === provider) return; // Already applied. db.table(updatesTableName) .add({ - k: id, + k: parentId, u: update, f: 1, // Flag as local update (to be included when syncing) }) From 54ef0e77fef9c322f7f431d4b307e8b3bf5262ff Mon Sep 17 00:00:00 2001 From: dfahlander Date: Sun, 28 Jul 2024 11:57:00 +0200 Subject: [PATCH 21/23] Updated test --- test/tests-yjs.js | 19 ++++++++++++++++--- test/typings-test/tsconfig.json | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/test/tests-yjs.js b/test/tests-yjs.js index a083b1a72..0179af719 100644 --- a/test/tests-yjs.js +++ b/test/tests-yjs.js @@ -5,6 +5,7 @@ import { promisedTest, } from './dexie-unittest-utils'; import * as Y from 'yjs'; +import * as awareness from 'y-protocols/awareness'; const db = new Dexie('TestYjs', { Y }); db.version(1).stores({ @@ -95,7 +96,7 @@ promisedTest('Test Y document compression', async () => { // Verify there are no updates in the updates table initially: const updateTable = db.docs.schema.yProps.find( (p) => p.prop === 'content' - ).updTable; + ).updatesTable; equal(await db.table(updateTable).count(), 0, 'No docs stored yet'); // Create three updates: @@ -168,14 +169,16 @@ promisedTest('Test that syncers prohibit GC from compressing unsynced updates', // Verify there are no updates in the updates table initially: const updateTable = db.docs.schema.yProps.find( (p) => p.prop === 'content' - ).updTable; + ).updatesTable; equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 0, 'No docs stored yet'); // Create three updates: await db.transaction('rw', db.docs, () => { + //Y.transact(doc, () => { doc.getArray('arr').insert(0, ['a', 'b', 'c']); doc.getArray('arr').insert(0, ['1', '2', '3']); doc.getArray('arr').insert(0, ['x', 'y', 'z']); + //}); }); // Verify we have 3 updates: equal(await db.table(updateTable).where('i').between(1,Infinity).count(), 3, 'Three updates stored'); @@ -195,4 +198,14 @@ promisedTest('Test that syncers prohibit GC from compressing unsynced updates', // Verify we have 0 updates after deleting the row holding our Y.Doc property: equal(await db.table(updateTable).where('i').between(1, Infinity).count(), 0, '0 updates stored'); ok(provider.destroyed, "Provider was destroyed when our document was deleted"); -}); \ No newline at end of file +}); + + +/*promisedTest('Test awareness', async () => { + const doc = new Y.Doc(); + const aw = new awareness.Awareness(doc); + console.log(Array.from(aw.getStates().keys())); + const update = awareness.encodeAwarenessUpdate(aw, Array.from(aw.getStates().keys())); + console.log("Update length", update.length); +}); +*/ \ No newline at end of file diff --git a/test/typings-test/tsconfig.json b/test/typings-test/tsconfig.json index 211ff0ded..f538b4042 100644 --- a/test/typings-test/tsconfig.json +++ b/test/typings-test/tsconfig.json @@ -8,7 +8,7 @@ "noImplicitThis": true, "outDir": "../../tools/tmp/test-typings", "moduleResolution": "node", - "lib": ["dom", "es2020"] + "lib": ["dom", "es2021"] }, "files": [ "test-typings.ts" From c013c513aaad0a9a9e6ff0f0b3d4672e97cac02f Mon Sep 17 00:00:00 2001 From: dfahlander Date: Sun, 28 Jul 2024 11:58:31 +0200 Subject: [PATCH 22/23] Made it possible to reach the Y.Doc cache from addons or outside dexie: * To lookup whether a certain document is open and find it --- src/public/index.d.ts | 9 ++++++++ src/yjs/DexieYProvider.ts | 14 ++++++------ src/yjs/createYDocProperty.ts | 27 +++++++++++------------ src/yjs/docCache.ts | 40 +++++++++++++++++++++++++++++++---- 4 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/public/index.d.ts b/src/public/index.d.ts index 1b245d963..e00df1a5d 100644 --- a/src/public/index.d.ts +++ b/src/public/index.d.ts @@ -72,8 +72,17 @@ export function remove(num: number | bigint | any[]): PropModification; declare var DexieYProvider: { (doc: DucktypedYDoc): DexieYProvider; new (doc: DucktypedYDoc): DexieYProvider; + getDocCache: (db: Dexie) => { + cache: { [key: string]: WeakRef }; + readonly size: number; + find: (updatesTable: string, parentId: any) => DucktypedYDoc | undefined; + add: (doc: DucktypedYDoc) => void; + delete: (doc: DucktypedYDoc) => void; + }; } +export { DexieYProvider, RangeSet }; + /** Exporting 'Dexie' as the default export. **/ export default Dexie; diff --git a/src/yjs/DexieYProvider.ts b/src/yjs/DexieYProvider.ts index 88f85935b..90af496d3 100644 --- a/src/yjs/DexieYProvider.ts +++ b/src/yjs/DexieYProvider.ts @@ -4,17 +4,17 @@ import type { DexieYProvider, DucktypedYDoc, } from '../public/types/yjs-related'; -import { throwIfDestroyed } from './docCache'; +import { throwIfDestroyed, getDocCache } from './docCache'; import { getYLibrary } from './getYLibrary'; import { observeYDocUpdates } from './observeYDocUpdates'; export function DexieYProvider (doc: DucktypedYDoc): DexieYProvider { - const { guid, collectionid: updatesTable, meta: { db, table }} = + const { meta: { db, parentTable, parentId, updatesTable }} = (doc as DucktypedYDoc) || {}; - if (!db || !table || !updatesTable) + if (!db || !parentTable || !updatesTable) throw new Error('Y.Doc not generated by Dexie'); - if (!db.table(table) || !db.table(updatesTable)) { - throw new Error(`Table ${table} or ${updatesTable} not found in db`); + if (!db.table(parentTable) || !db.table(updatesTable)) { + throw new Error(`Table ${parentTable} or ${updatesTable} not found in db`); } throwIfDestroyed(doc); const Y = getYLibrary(db); @@ -41,9 +41,11 @@ export function DexieYProvider (doc: DucktypedYDoc): DexieYProvider { on = this.on = createEvents(); // Releases listeners for GC } }; - const stopObserving = observeYDocUpdates(provider, doc, db, table, updatesTable, guid, Y); + const stopObserving = observeYDocUpdates(provider, doc, db, parentTable, updatesTable, parentId, Y); doc.on('destroy', provider.destroy.bind(provider)); db.on.y.fire(provider, Y); // Allow for addons to invoke their sync- and awareness providers here. return provider; } + +DexieYProvider.getDocCache = getDocCache; diff --git a/src/yjs/createYDocProperty.ts b/src/yjs/createYDocProperty.ts index c9079efaf..c18412cd1 100644 --- a/src/yjs/createYDocProperty.ts +++ b/src/yjs/createYDocProperty.ts @@ -2,40 +2,37 @@ import type { Table } from '../public/types/table'; import type { Dexie } from '../public/types/dexie'; import type { DexieYDocMeta, DucktypedY } from '../public/types/yjs-related'; import { getByKeyPath } from '../functions/utils'; -import { docCache, destroyed, registry } from './docCache'; +import { destroyed, getDocCache } from './docCache'; export function createYDocProperty( db: Dexie, Y: DucktypedY, table: Table, - prop: string, updatesTable: string ) { const pkKeyPath = table.schema.primKey.keyPath; + const docCache = getDocCache(db); return { get(this: object) { const id = getByKeyPath(this, pkKeyPath); - const cacheKey = `${table.name}[${id}].${prop}`; - let docRef = docCache[cacheKey]; - if (docRef) return docRef.deref(); - const doc = new Y.Doc({ - collectionid: updatesTable, - guid: ''+id, + let doc = docCache.find(updatesTable, id); + if (doc) return doc; + + doc = new Y.Doc({ meta: { db, - table: table.name, - cacheKey, - } as DexieYDocMeta, + updatesTable, + parentTable: table.name, + parentId: id + } satisfies DexieYDocMeta, }); - docCache[cacheKey] = new WeakRef(doc); - registry.register(doc, cacheKey); + docCache.add(doc); doc.on('destroy', () => { destroyed.add(doc); - registry.unregister(doc); - delete docCache[cacheKey]; + docCache.delete(doc); }); return doc; diff --git a/src/yjs/docCache.ts b/src/yjs/docCache.ts index ca26eb8f2..62f8abeea 100644 --- a/src/yjs/docCache.ts +++ b/src/yjs/docCache.ts @@ -1,10 +1,38 @@ +import { Dexie } from '../public/types/dexie'; import type { DucktypedYDoc } from '../public/types/yjs-related'; -// The cache -export let docCache: { [key: string]: WeakRef; } = {}; +// The Y.Doc cache containing all active documents +export function getDocCache(db: Dexie) { + 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); + const docRef = this.cache[cacheKey]; + return docRef ? docRef.deref() : undefined; + }, + add(doc: DucktypedYDoc): void { + const { updatesTable, parentId } = doc.meta; + if (!updatesTable || parentId == null) + throw new Error(`Missing Dexie-related metadata in Y.Doc`); + const cacheKey = getYDocCacheKey(updatesTable, parentId); + 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) + ]; + }, + }; +} +//export let docCache: { [key: string]: WeakRef; } = {}; // The finalization registry -export const registry = new FinalizationRegistry((heldValue) => { - delete docCache[heldValue]; +const docRegistry = new FinalizationRegistry<{cache: any, key: string}>(({cache, key}) => { + delete cache[key]; }); // The weak map //export const doc2ProviderWeakMap = new WeakMap>>(); @@ -14,3 +42,7 @@ export function throwIfDestroyed(doc: object) { if (destroyed.has(doc)) throw new Error('Y.Doc has been destroyed'); } + +export function getYDocCacheKey(yTable: string, parentId: any): string { + return `${yTable}[${parentId}]`; +} From 8bb82ccea512a51298e0a546b2787073cbba205a Mon Sep 17 00:00:00 2001 From: dfahlander Date: Sun, 28 Jul 2024 12:21:43 +0200 Subject: [PATCH 23/23] Don't expose the WeakRef type in public API (would be a breaking typings change) --- src/public/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/public/index.d.ts b/src/public/index.d.ts index e00df1a5d..69ec2b3ea 100644 --- a/src/public/index.d.ts +++ b/src/public/index.d.ts @@ -73,7 +73,6 @@ declare var DexieYProvider: { (doc: DucktypedYDoc): DexieYProvider; new (doc: DucktypedYDoc): DexieYProvider; getDocCache: (db: Dexie) => { - cache: { [key: string]: WeakRef }; readonly size: number; find: (updatesTable: string, parentId: any) => DucktypedYDoc | undefined; add: (doc: DucktypedYDoc) => void;