From 1f7c33e4074d9dcb28d9027ccf306e4a10311927 Mon Sep 17 00:00:00 2001 From: dfahlander Date: Sat, 15 Jul 2023 16:59:41 +0200 Subject: [PATCH 1/6] Resolves #1765 The `db` passed to db.on.ready() callback must not block until db is ready because the callback's code is part of making the db ready. A bug prevented liveQueries based on the unblocked db to emit values before on-ready callback resolves its promise. When using requireAuth: true, the entire authentication flow is done as a part of the on('ready') flow. A part of the flow was waiting for a liveQuery observable to emit currentUser value before continuing, leading to a deadlock. There was a general bug that the "_vip" state of the db wasn't propagated to transactions and tables. This is solved by letting the "_vip" version of the db (the one passed to on-ready callback) be a Proxy that makes sure to return vipped tables and transactions so that all code using that db instance will not deadlock before the on-ready callback completes. --- src/classes/dexie/dexie.ts | 23 +++++++++++++++++++---- src/helpers/vipify.ts | 10 ++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 src/helpers/vipify.ts diff --git a/src/classes/dexie/dexie.ts b/src/classes/dexie/dexie.ts index 43d81893f..3e7c65844 100644 --- a/src/classes/dexie/dexie.ts +++ b/src/classes/dexie/dexie.ts @@ -45,6 +45,7 @@ import { IndexableType } from '../../public'; import { observabilityMiddleware } from '../../live-query/observability-middleware'; import { cacheExistingValuesMiddleware } from '../../dbcore/cache-existing-values-middleware'; import { cacheMiddleware } from "../../live-query/cache/cache-middleware"; +import { vipify } from "../../helpers/vipify"; export interface DbReadyState { dbOpenError: any; @@ -199,11 +200,11 @@ export class Dexie implements IDexie { this._maxKey = getMaxKey(options.IDBKeyRange as typeof IDBKeyRange); - this._createTransaction = ( + this._createTransaction = function ( mode: IDBTransactionMode, storeNames: string[], dbschema: DbSchema, - parentTransaction?: Transaction) => new this.Transaction(mode, storeNames, dbschema, this._options.chromeTransactionDurability, parentTransaction); + parentTransaction?: Transaction) { return new this.Transaction(mode, storeNames, dbschema, this._options.chromeTransactionDurability, parentTransaction) }; this._fireOnBlocked = ev => { this.on("blocked").fire(ev); @@ -220,7 +221,21 @@ export class Dexie implements IDexie { this.use(virtualIndexMiddleware); this.use(hooksMiddleware); - this.vip = Object.create(this, {_vip: {value: true}}) as Dexie; + const vipDB = new Proxy(this, { + get: (_, prop, receiver) => { + if (prop === '_vip') return true; + if (prop === 'table') return (tableName: string) => vipify(this.table(tableName), vipDB); + const rv = Reflect.get(_, prop, receiver); + if (rv instanceof Table) return vipify(rv, vipDB); + if (prop === 'tables') return (rv as Table[]).map(t => vipify(t, vipDB)); + if (prop === '_createTransaction') return function() { + const tx: Transaction = (rv as typeof this._createTransaction).apply(this, arguments); + return vipify(tx, vipDB); + } + return rv; + } + }); + this.vip = vipDB; // Call each addon: addons.forEach(addon => addon(this)); @@ -298,7 +313,7 @@ export class Dexie implements IDexie { if (idx >= 0) connections.splice(idx, 1); if (this.idbdb) { try { this.idbdb.close(); } catch (e) { } - this._novip.idbdb = null; // db._novip is because db can be an Object.create(origDb). + this.idbdb = null; } // Reset dbReadyPromise promise: state.dbReadyPromise = new Promise(resolve => { diff --git a/src/helpers/vipify.ts b/src/helpers/vipify.ts new file mode 100644 index 000000000..16e60c8cc --- /dev/null +++ b/src/helpers/vipify.ts @@ -0,0 +1,10 @@ +import { type Dexie } from "../classes/dexie"; +import { type Table } from "../classes/table"; +import { type Transaction } from "../classes/transaction"; + +export function vipify( + target: Table | Transaction, + vipDb: Dexie +): Table { + return Object.create(target, {db: {value: vipDb}}); +} From ad21ff9ea59e2bbc2d59773af2c5dc936a2d7766 Mon Sep 17 00:00:00 2001 From: dfahlander Date: Sat, 15 Jul 2023 17:06:51 +0200 Subject: [PATCH 2/6] Simplified arguments now that db._vip is a pure Proxy rather than a protype-derived object. --- src/classes/dexie/dexie-open.ts | 4 ++-- src/classes/dexie/generate-middleware-stacks.ts | 2 +- src/classes/dexie/transaction-helpers.ts | 9 +++++---- src/classes/version/schema-helpers.ts | 11 +++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/classes/dexie/dexie-open.ts b/src/classes/dexie/dexie-open.ts index 0432bff5d..9dfc89430 100644 --- a/src/classes/dexie/dexie-open.ts +++ b/src/classes/dexie/dexie-open.ts @@ -79,7 +79,7 @@ export function dexieOpen (db: Dexie) { upgradeTransaction.onerror = eventRejectHandler(reject); var oldVer = e.oldVersion > Math.pow(2, 62) ? 0 : e.oldVersion; // Safari 8 fix. wasCreated = oldVer < 1; - db._novip.idbdb = req.result;// db._novip is because db can be an Object.create(origDb). + db.idbdb = req.result; runUpgraders(db, oldVer / 10, upgradeTransaction, reject); } }, reject); @@ -87,7 +87,7 @@ export function dexieOpen (db: Dexie) { req.onsuccess = wrap (() => { // Core opening procedure complete. Now let's just record some stuff. upgradeTransaction = null; - const idbdb = db._novip.idbdb = req.result; // db._novip is because db can be an Object.create(origDb). + const idbdb = db.idbdb = req.result; const objectStoreNames = slice(idbdb.objectStoreNames); if (objectStoreNames.length > 0) try { diff --git a/src/classes/dexie/generate-middleware-stacks.ts b/src/classes/dexie/generate-middleware-stacks.ts index 456fdc521..fc5a9100f 100644 --- a/src/classes/dexie/generate-middleware-stacks.ts +++ b/src/classes/dexie/generate-middleware-stacks.ts @@ -29,7 +29,7 @@ function createMiddlewareStacks( }; } -export function generateMiddlewareStacks({_novip: db}: Dexie, tmpTrans: IDBTransaction) { +export function generateMiddlewareStacks(db: Dexie, tmpTrans: IDBTransaction) { const idbdb = tmpTrans.db; const stacks = createMiddlewareStacks(db._middlewares, idbdb, db._deps, tmpTrans); db.core = stacks.dbcore!; diff --git a/src/classes/dexie/transaction-helpers.ts b/src/classes/dexie/transaction-helpers.ts index 32cc5aedb..81b670de1 100644 --- a/src/classes/dexie/transaction-helpers.ts +++ b/src/classes/dexie/transaction-helpers.ts @@ -92,10 +92,11 @@ export function enterTransactionScope( }, zoneProps); return (returnValue && typeof returnValue.then === 'function' ? // Promise returned. User uses promise-style transactions. - Promise.resolve(returnValue).then(x => trans.active ? - x // Transaction still active. Continue. - : rejection(new exceptions.PrematureCommit( - "Transaction committed too early. See http://bit.ly/2kdckMn"))) + Promise.resolve(returnValue).then(x => { + if (trans.active) return x; // Transaction still active. Continue. + throw new exceptions.PrematureCommit( + "Transaction committed too early. See http://bit.ly/2kdckMn"); + }) // No promise returned. Wait for all outstanding promises before continuing. : promiseFollowed.then(() => returnValue) ).then(x => { diff --git a/src/classes/version/schema-helpers.ts b/src/classes/version/schema-helpers.ts index 15a28ab01..96aadcf91 100644 --- a/src/classes/version/schema-helpers.ts +++ b/src/classes/version/schema-helpers.ts @@ -9,12 +9,11 @@ import { exceptions } from '../../errors'; import { TableSchema } from '../../public/types/table-schema'; import { IndexSpec } from '../../public/types/index-spec'; import { hasIEDeleteObjectStoreBug, isIEOrEdge } from '../../globals/constants'; -import { safariMultiStoreFix } from '../../functions/quirks'; import { createIndexSpec, nameFromKeyPath } from '../../helpers/index-spec'; import { createTableSchema } from '../../helpers/table-schema'; import { generateMiddlewareStacks } from '../dexie/generate-middleware-stacks'; -export function setApiOnPlace({_novip: db}: Dexie, objs: Object[], tableNames: string[], dbschema: DbSchema) { +export function setApiOnPlace(db: Dexie, objs: Object[], tableNames: string[], dbschema: DbSchema) { tableNames.forEach(tableName => { const schema = dbschema[tableName]; objs.forEach(obj => { @@ -41,7 +40,7 @@ export function setApiOnPlace({_novip: db}: Dexie, objs: Object[], tableNames: s }); } -export function removeTablesApi({_novip: db}: Dexie, objs: Object[]) { +export function removeTablesApi(db: Dexie, objs: Object[]) { objs.forEach(obj => { for (let key in obj) { if (obj[key] instanceof db.Table) delete obj[key]; @@ -78,7 +77,7 @@ export function runUpgraders(db: Dexie, oldVersion: number, idbUpgradeTrans: IDB export type UpgradeQueueItem = (idbtrans: IDBTransaction) => PromiseLike | void; export function updateTablesAndIndexes( - {_novip: db}: Dexie, + db: Dexie, oldVersion: number, trans: Transaction, idbUpgradeTrans: IDBTransaction) @@ -339,7 +338,7 @@ function buildGlobalSchema( return globalSchema; } -export function readGlobalSchema({_novip: db}: Dexie, idbdb: IDBDatabase, tmpTrans: IDBTransaction) { +export function readGlobalSchema(db: Dexie, idbdb: IDBDatabase, tmpTrans: IDBTransaction) { db.verno = idbdb.version / 10; const globalSchema = db._dbSchema = buildGlobalSchema(db, idbdb, tmpTrans); db._storeNames = slice(idbdb.objectStoreNames, 0); @@ -352,7 +351,7 @@ export function verifyInstalledSchema(db: Dexie, tmpTrans: IDBTransaction): bool return !(diff.add.length || diff.change.some(ch => ch.add.length || ch.change.length)); } -export function adjustToExistingIndexNames({_novip: db}: Dexie, schema: DbSchema, idbtrans: IDBTransaction) { +export function adjustToExistingIndexNames(db: Dexie, schema: DbSchema, idbtrans: IDBTransaction) { // Issue #30 Problem with existing db - adjust to existing index names when migrating from non-dexie db const storeNames = idbtrans.db.objectStoreNames; From a64ef87951a4addcfa9f0c54eda26f0764cead8d Mon Sep 17 00:00:00 2001 From: dfahlander Date: Sat, 15 Jul 2023 17:08:27 +0200 Subject: [PATCH 3/6] When error occur, a consequential error happened: postMessage failed to post an object with function properties. Make sure not to post the DexieError but a simpler one, copying the message only. --- addons/dexie-cloud/src/sync/sync.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/dexie-cloud/src/sync/sync.ts b/addons/dexie-cloud/src/sync/sync.ts index d7ee12b88..d5b170cc0 100644 --- a/addons/dexie-cloud/src/sync/sync.ts +++ b/addons/dexie-cloud/src/sync/sync.ts @@ -108,7 +108,7 @@ export function sync( }); db.syncStateChangedEvent.next({ phase: isOnline ? 'error' : 'offline', - error, + error: new Error('' + error?.message || error), }); return Promise.reject(error); }); From 9e49a5bf4773e0fbf319eaa93969d08ba57540ab Mon Sep 17 00:00:00 2001 From: dfahlander Date: Sat, 15 Jul 2023 17:09:04 +0200 Subject: [PATCH 4/6] Let the "ResetDatabaseButton" reload the page to ensure application state reset. --- .../dexie-cloud-todo-app/src/components/ResetDatabaseButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/dexie-cloud-todo-app/src/components/ResetDatabaseButton.tsx b/samples/dexie-cloud-todo-app/src/components/ResetDatabaseButton.tsx index dd2fac20c..1de11b1e4 100644 --- a/samples/dexie-cloud-todo-app/src/components/ResetDatabaseButton.tsx +++ b/samples/dexie-cloud-todo-app/src/components/ResetDatabaseButton.tsx @@ -9,7 +9,7 @@ export function ResetDatabaseButton() { className="large-button" onClick={async () => { await db.delete(); - await db.open(); + location.reload(); // Reload the page to reset application state hard. }} > Factory reset client From b75d6accff6c5862214dcc65c22ee92fdb6c948f Mon Sep 17 00:00:00 2001 From: dfahlander Date: Sat, 15 Jul 2023 17:55:37 +0200 Subject: [PATCH 5/6] One unit test fails. Must use Proxy instead of Object.create() for vipified tables / transactions. This will makes uses less complex, especially places where code is setting props on the vipified objects without having to get adjusted for the vip case. --- src/helpers/vipify.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/helpers/vipify.ts b/src/helpers/vipify.ts index 16e60c8cc..19543185b 100644 --- a/src/helpers/vipify.ts +++ b/src/helpers/vipify.ts @@ -2,9 +2,17 @@ import { type Dexie } from "../classes/dexie"; import { type Table } from "../classes/table"; import { type Transaction } from "../classes/transaction"; -export function vipify( - target: Table | Transaction, +export function vipify( + target: T, vipDb: Dexie -): Table { - return Object.create(target, {db: {value: vipDb}}); +): T { + return new Proxy(target, { + get (target, prop, receiver) { + // The "db" prop of the table or transaction is the only one we need to + // override. The rest of the props can be accessed from the original + // object. + if (prop === 'db') return vipDb; + return Reflect.get(target, prop, receiver); + } + }); } From 18992920250b9d50de04c0af7a8d97f03d54de47 Mon Sep 17 00:00:00 2001 From: dfahlander Date: Sat, 15 Jul 2023 18:08:35 +0200 Subject: [PATCH 6/6] Reverted unintentional code changes --- src/classes/dexie/dexie.ts | 4 ++-- src/classes/dexie/transaction-helpers.ts | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/classes/dexie/dexie.ts b/src/classes/dexie/dexie.ts index 3e7c65844..009bb3429 100644 --- a/src/classes/dexie/dexie.ts +++ b/src/classes/dexie/dexie.ts @@ -200,11 +200,11 @@ export class Dexie implements IDexie { this._maxKey = getMaxKey(options.IDBKeyRange as typeof IDBKeyRange); - this._createTransaction = function ( + this._createTransaction = ( mode: IDBTransactionMode, storeNames: string[], dbschema: DbSchema, - parentTransaction?: Transaction) { return new this.Transaction(mode, storeNames, dbschema, this._options.chromeTransactionDurability, parentTransaction) }; + parentTransaction?: Transaction) => new this.Transaction(mode, storeNames, dbschema, this._options.chromeTransactionDurability, parentTransaction); this._fireOnBlocked = ev => { this.on("blocked").fire(ev); diff --git a/src/classes/dexie/transaction-helpers.ts b/src/classes/dexie/transaction-helpers.ts index 81b670de1..32cc5aedb 100644 --- a/src/classes/dexie/transaction-helpers.ts +++ b/src/classes/dexie/transaction-helpers.ts @@ -92,11 +92,10 @@ export function enterTransactionScope( }, zoneProps); return (returnValue && typeof returnValue.then === 'function' ? // Promise returned. User uses promise-style transactions. - Promise.resolve(returnValue).then(x => { - if (trans.active) return x; // Transaction still active. Continue. - throw new exceptions.PrematureCommit( - "Transaction committed too early. See http://bit.ly/2kdckMn"); - }) + Promise.resolve(returnValue).then(x => trans.active ? + x // Transaction still active. Continue. + : rejection(new exceptions.PrematureCommit( + "Transaction committed too early. See http://bit.ly/2kdckMn"))) // No promise returned. Wait for all outstanding promises before continuing. : promiseFollowed.then(() => returnValue) ).then(x => {