@verdant-web/store 5.4.0 → 5.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle/index.js +8 -8
- package/dist/bundle/index.js.map +4 -4
- package/dist/esm/__tests__/queries.test.js +16 -0
- package/dist/esm/__tests__/queries.test.js.map +1 -1
- package/dist/esm/client/Client.js +12 -4
- package/dist/esm/client/Client.js.map +1 -1
- package/dist/esm/entities/Entity.d.ts +2 -5
- package/dist/esm/entities/Entity.js +17 -10
- package/dist/esm/entities/Entity.js.map +1 -1
- package/dist/esm/entities/EntityMetadata.js +14 -2
- package/dist/esm/entities/EntityMetadata.js.map +1 -1
- package/dist/esm/entities/EntityStore.js +1 -0
- package/dist/esm/entities/EntityStore.js.map +1 -1
- package/dist/esm/persistence/MessageCreator.js +1 -1
- package/dist/esm/persistence/MessageCreator.js.map +1 -1
- package/dist/esm/persistence/PersistenceMetadata.d.ts +6 -1
- package/dist/esm/persistence/PersistenceMetadata.js +40 -2
- package/dist/esm/persistence/PersistenceMetadata.js.map +1 -1
- package/dist/esm/persistence/PersistenceRebaser.js +1 -1
- package/dist/esm/persistence/PersistenceRebaser.js.map +1 -1
- package/dist/esm/persistence/idb/IdbService.js +1 -1
- package/dist/esm/persistence/idb/IdbService.js.map +1 -1
- package/dist/esm/persistence/idb/indexNames.d.ts +5 -0
- package/dist/esm/persistence/idb/indexNames.js +6 -0
- package/dist/esm/persistence/idb/indexNames.js.map +1 -0
- package/dist/esm/persistence/idb/metadata/IdbMetadataDb.d.ts +6 -3
- package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js +33 -8
- package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js.map +1 -1
- package/dist/esm/persistence/idb/metadata/openMetadataDatabase.js +5 -5
- package/dist/esm/persistence/idb/metadata/openMetadataDatabase.js.map +1 -1
- package/dist/esm/persistence/idb/queries/ranges.js +3 -4
- package/dist/esm/persistence/idb/queries/ranges.js.map +1 -1
- package/dist/esm/persistence/interfaces.d.ts +2 -0
- package/dist/esm/persistence/migration/engine.js +3 -4
- package/dist/esm/persistence/migration/engine.js.map +1 -1
- package/dist/esm/queries/BaseQuery.d.ts +8 -0
- package/dist/esm/queries/BaseQuery.js +49 -5
- package/dist/esm/queries/BaseQuery.js.map +1 -1
- package/dist/esm/queries/FindAllQuery.js +3 -3
- package/dist/esm/queries/FindAllQuery.js.map +1 -1
- package/dist/esm/queries/FindInfiniteQuery.js +6 -6
- package/dist/esm/queries/FindInfiniteQuery.js.map +1 -1
- package/dist/esm/queries/FindOneQuery.js +3 -3
- package/dist/esm/queries/FindOneQuery.js.map +1 -1
- package/dist/esm/queries/FindPageQuery.js +3 -3
- package/dist/esm/queries/FindPageQuery.js.map +1 -1
- package/dist/esm/queries/GetQuery.js +1 -1
- package/dist/esm/queries/GetQuery.js.map +1 -1
- package/dist/esm/queries/QueryCache.d.ts +4 -1
- package/dist/esm/queries/QueryCache.js +15 -1
- package/dist/esm/queries/QueryCache.js.map +1 -1
- package/dist/esm/queries/diagnostics.d.ts +24 -0
- package/dist/esm/queries/diagnostics.js +11 -0
- package/dist/esm/queries/diagnostics.js.map +1 -0
- package/dist/esm/queries/ranges.js +3 -4
- package/dist/esm/queries/ranges.js.map +1 -1
- package/dist/esm/sync/WebSocketSync.js +4 -2
- package/dist/esm/sync/WebSocketSync.js.map +1 -1
- package/package.json +7 -2
- package/src/__tests__/queries.test.ts +19 -0
- package/src/client/Client.ts +15 -4
- package/src/entities/Entity.ts +31 -22
- package/src/entities/EntityMetadata.ts +15 -3
- package/src/entities/EntityStore.ts +1 -0
- package/src/persistence/MessageCreator.ts +1 -1
- package/src/persistence/PersistenceMetadata.ts +67 -7
- package/src/persistence/PersistenceRebaser.ts +1 -1
- package/src/persistence/idb/IdbService.ts +1 -1
- package/src/persistence/idb/indexNames.ts +5 -0
- package/src/persistence/idb/metadata/IdbMetadataDb.ts +71 -14
- package/src/persistence/idb/metadata/openMetadataDatabase.ts +15 -7
- package/src/persistence/idb/queries/ranges.ts +2 -3
- package/src/persistence/interfaces.ts +5 -1
- package/src/persistence/migration/engine.ts +3 -4
- package/src/queries/BaseQuery.ts +56 -10
- package/src/queries/FindAllQuery.ts +10 -8
- package/src/queries/FindInfiniteQuery.ts +22 -18
- package/src/queries/FindOneQuery.ts +9 -7
- package/src/queries/FindPageQuery.ts +11 -9
- package/src/queries/GetQuery.ts +1 -1
- package/src/queries/QueryCache.ts +19 -2
- package/src/queries/diagnostics.ts +38 -0
- package/src/queries/ranges.ts +2 -3
- package/src/sync/WebSocketSync.ts +2 -0
package/src/client/Client.ts
CHANGED
|
@@ -101,10 +101,8 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
101
101
|
ctx: this.context,
|
|
102
102
|
files: this._fileManager,
|
|
103
103
|
});
|
|
104
|
-
// note:
|
|
105
|
-
//
|
|
106
|
-
// refresh.
|
|
107
|
-
// FIXME: make this less fragile
|
|
104
|
+
// note: historically querycache needed to be constructed after entities,
|
|
105
|
+
// not sure if this is still the case.
|
|
108
106
|
this._queryCache = new QueryCache({
|
|
109
107
|
context: this.context,
|
|
110
108
|
evictionTime: this.context.config.queries?.evictionTime,
|
|
@@ -142,6 +140,11 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
142
140
|
documentManager: this.documentManager,
|
|
143
141
|
});
|
|
144
142
|
}
|
|
143
|
+
|
|
144
|
+
if (typeof window !== 'undefined') {
|
|
145
|
+
(window as Window & { __VERDANT_CLIENT__?: Client }).__VERDANT_CLIENT__ =
|
|
146
|
+
this;
|
|
147
|
+
}
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
private importingPromise = Promise.resolve();
|
|
@@ -325,6 +328,14 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
325
328
|
this.context.persistenceShutdownHandler.shutdown();
|
|
326
329
|
this.context.internalEvents.disable();
|
|
327
330
|
this.context.entityEvents.disable();
|
|
331
|
+
if (typeof window !== 'undefined') {
|
|
332
|
+
const browserWindow = window as Window & {
|
|
333
|
+
__VERDANT_CLIENT__?: Client;
|
|
334
|
+
};
|
|
335
|
+
if (browserWindow.__VERDANT_CLIENT__ === this) {
|
|
336
|
+
delete browserWindow.__VERDANT_CLIENT__;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
328
339
|
|
|
329
340
|
// the idea here is to flush the microtask queue -
|
|
330
341
|
// we may have queued tasks related to queries that
|
package/src/entities/Entity.ts
CHANGED
|
@@ -103,6 +103,9 @@ export class Entity<
|
|
|
103
103
|
// provided from external creators, this is a method to delete
|
|
104
104
|
// this entity.
|
|
105
105
|
private _deleteSelf: () => void;
|
|
106
|
+
// set manually when deleting the entity, this silences pruning warnings
|
|
107
|
+
// for missing data while the deletion is being processed.
|
|
108
|
+
private _deleting = false;
|
|
106
109
|
|
|
107
110
|
constructor({
|
|
108
111
|
oid,
|
|
@@ -239,13 +242,15 @@ export class Entity<
|
|
|
239
242
|
for (let i = 0; i < rawView.length; i++) {
|
|
240
243
|
const child = this.get(i);
|
|
241
244
|
if (this.childIsNull(child) && !isNullable(schema)) {
|
|
242
|
-
this.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
if (!this._deleting) {
|
|
246
|
+
this.ctx.log(
|
|
247
|
+
'error',
|
|
248
|
+
'Child missing in non-nullable field',
|
|
249
|
+
this.oid,
|
|
250
|
+
'index:',
|
|
251
|
+
i,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
249
254
|
|
|
250
255
|
// this item will be pruned.
|
|
251
256
|
} else {
|
|
@@ -286,13 +291,15 @@ export class Entity<
|
|
|
286
291
|
}
|
|
287
292
|
const child = this.get(key as any);
|
|
288
293
|
if (this.childIsNull(child) && !isNullable(schema)) {
|
|
289
|
-
this.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
if (!this._deleting) {
|
|
295
|
+
this.ctx.log(
|
|
296
|
+
'error',
|
|
297
|
+
'Child entity is missing for non-nullable field',
|
|
298
|
+
this.oid,
|
|
299
|
+
'key:',
|
|
300
|
+
key,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
296
303
|
if (this.schema.type !== 'map') {
|
|
297
304
|
/**
|
|
298
305
|
* PRUNE - this is a prune point. we can't continue
|
|
@@ -334,14 +341,6 @@ export class Entity<
|
|
|
334
341
|
return this.viewData.deleted || this.view === null;
|
|
335
342
|
}
|
|
336
343
|
|
|
337
|
-
/**
|
|
338
|
-
* Doesn't compute view data; simply uses available cached
|
|
339
|
-
* view info to determine deletion status.
|
|
340
|
-
*/
|
|
341
|
-
private get quickDeleted() {
|
|
342
|
-
return this._viewData?.deleted || this.cachedView === null;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
344
|
get invalid() {
|
|
346
345
|
return !!this.validate();
|
|
347
346
|
}
|
|
@@ -603,6 +602,12 @@ export class Entity<
|
|
|
603
602
|
if (this.access) {
|
|
604
603
|
for (const op of operations) {
|
|
605
604
|
op.authz = this.access;
|
|
605
|
+
|
|
606
|
+
// if op is init, reset _deleting flag - this is not a functional flag, only
|
|
607
|
+
// hides some warnings. if we're here, we must be re-initializing and we can reset it.
|
|
608
|
+
if (op.data.op === 'initialize') {
|
|
609
|
+
this._deleting = false;
|
|
610
|
+
}
|
|
606
611
|
}
|
|
607
612
|
}
|
|
608
613
|
|
|
@@ -1397,6 +1402,10 @@ export class Entity<
|
|
|
1397
1402
|
this.metadataFamily.discardPendingOperation(operation);
|
|
1398
1403
|
this.invalidateCachedView();
|
|
1399
1404
|
};
|
|
1405
|
+
|
|
1406
|
+
__markDeleting__ = () => {
|
|
1407
|
+
this._deleting = true;
|
|
1408
|
+
};
|
|
1400
1409
|
}
|
|
1401
1410
|
|
|
1402
1411
|
function assertNotSymbol<T>(key: T): asserts key is Exclude<T, symbol> {
|
|
@@ -37,6 +37,10 @@ export class EntityMetadata {
|
|
|
37
37
|
// soon as the user makes modifications to the entity, this
|
|
38
38
|
// ephemeral pruned data is applied underneath it.
|
|
39
39
|
private ephemeralOperations?: Operation[] = [];
|
|
40
|
+
// pending operations are 'in flight' into persistence, but
|
|
41
|
+
// if you killed the power now they'd be lost. they don't have an
|
|
42
|
+
// officially applied timestamp yet either, so they are applied
|
|
43
|
+
// last, after all confirmed/ephemerals.
|
|
40
44
|
private pendingOperations: Operation[] = [];
|
|
41
45
|
readonly oid;
|
|
42
46
|
|
|
@@ -209,7 +213,6 @@ export class EntityMetadata {
|
|
|
209
213
|
this.confirmedOperations.push(op);
|
|
210
214
|
totalAdded++;
|
|
211
215
|
}
|
|
212
|
-
// FIXME: seems inefficient
|
|
213
216
|
// remove this incoming op from pending if it's in there
|
|
214
217
|
const pendingPrior = this.pendingOperations.length;
|
|
215
218
|
this.discardPendingOperation(op);
|
|
@@ -235,10 +238,19 @@ export class EntityMetadata {
|
|
|
235
238
|
return old;
|
|
236
239
|
};
|
|
237
240
|
|
|
241
|
+
// while this may not be the most efficient way to remove an operation,
|
|
242
|
+
// in practice the ops we discard are generally some of the first in
|
|
243
|
+
// the list, so the find is quick.
|
|
238
244
|
discardPendingOperation = (operation: Operation) => {
|
|
239
|
-
|
|
240
|
-
|
|
245
|
+
// only one op can match, so - avoiding filter() here,
|
|
246
|
+
// we want short circuiting.
|
|
247
|
+
const indexOfOperation = this.pendingOperations.findIndex(
|
|
248
|
+
(op) => op.timestamp === operation.timestamp,
|
|
241
249
|
);
|
|
250
|
+
if (indexOfOperation === -1) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.pendingOperations.splice(indexOfOperation, 1);
|
|
242
254
|
};
|
|
243
255
|
|
|
244
256
|
private applyOperations = (
|
|
@@ -59,7 +59,7 @@ export class MessageCreator {
|
|
|
59
59
|
storeNames: ['operations', 'baselines'],
|
|
60
60
|
},
|
|
61
61
|
async (tx) => {
|
|
62
|
-
//
|
|
62
|
+
// TODO: this branch gives bad vibes. should we always
|
|
63
63
|
// send all operations from other replicas too? is there
|
|
64
64
|
// ever a case where we have a "since" timestamp and there
|
|
65
65
|
// are foreign ops that match it?
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
applyPatch,
|
|
3
3
|
assert,
|
|
4
4
|
assignOid,
|
|
5
|
+
decomposeOid,
|
|
5
6
|
DocumentBaseline,
|
|
6
7
|
getOidRoot,
|
|
7
8
|
ObjectIdentifier,
|
|
@@ -293,15 +294,31 @@ export class PersistenceMetadata {
|
|
|
293
294
|
);
|
|
294
295
|
};
|
|
295
296
|
|
|
297
|
+
// worst case of this is rather expensive but currently only used for migration
|
|
298
|
+
// and deletion. if usage is expanded, consider caching...
|
|
296
299
|
getDocumentAuthz = async (oid: ObjectIdentifier) => {
|
|
297
300
|
let authz;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
301
|
+
|
|
302
|
+
// find the latest initialize op - this is most up to date authz.
|
|
303
|
+
await this.db.iterateEntityOperations(
|
|
304
|
+
oid,
|
|
305
|
+
(op) => {
|
|
306
|
+
if (op.data.op === 'initialize') {
|
|
307
|
+
authz = op.authz;
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
reverse: true,
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// found initialize, this is the answer.
|
|
317
|
+
if (authz) return authz;
|
|
318
|
+
|
|
319
|
+
// all initialize ops may have been rebased, in which case we use the baseline.
|
|
320
|
+
const baseline = await this.db.getBaseline(oid);
|
|
321
|
+
return baseline?.authz;
|
|
305
322
|
};
|
|
306
323
|
|
|
307
324
|
insertData = async (
|
|
@@ -512,4 +529,47 @@ export class PersistenceMetadata {
|
|
|
512
529
|
this.updateLocalReplica({ ackedLogicalTime: timestamp });
|
|
513
530
|
}
|
|
514
531
|
};
|
|
532
|
+
|
|
533
|
+
// upkeep - not called automatically as of today
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Removes all metadata entries related to collections which are no
|
|
537
|
+
* longer present in the schema.
|
|
538
|
+
*/
|
|
539
|
+
__unstable__purgeRemovedCollections = async () => {
|
|
540
|
+
const schema = this.ctx.schema;
|
|
541
|
+
const collectionsInSchema = new Set(Object.keys(schema.collections));
|
|
542
|
+
const oidsToPurge = new Set<ObjectIdentifier>();
|
|
543
|
+
await this.db.iterateAllBaselines((baseline) => {
|
|
544
|
+
const { collection } = decomposeOid(baseline.oid);
|
|
545
|
+
if (!collectionsInSchema.has(collection)) {
|
|
546
|
+
this.ctx.log(
|
|
547
|
+
'debug',
|
|
548
|
+
`Deleting baseline for removed collection ${collection}`,
|
|
549
|
+
baseline.oid,
|
|
550
|
+
);
|
|
551
|
+
oidsToPurge.add(baseline.oid);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
await this.db.iterateAllOperations((op) => {
|
|
555
|
+
const { collection } = decomposeOid(op.oid);
|
|
556
|
+
if (!collectionsInSchema.has(collection)) {
|
|
557
|
+
this.ctx.log(
|
|
558
|
+
'debug',
|
|
559
|
+
`Deleting operation for removed collection ${collection}`,
|
|
560
|
+
op.oid,
|
|
561
|
+
);
|
|
562
|
+
oidsToPurge.add(op.oid);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
for (const oid of oidsToPurge) {
|
|
567
|
+
await this.db.purgeDocumentData(oid);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
this.ctx.log(
|
|
571
|
+
'debug',
|
|
572
|
+
`Purged ${oidsToPurge.size} document metadata groups for removed collections`,
|
|
573
|
+
);
|
|
574
|
+
};
|
|
515
575
|
}
|
|
@@ -136,7 +136,7 @@ export class PersistenceRebaser {
|
|
|
136
136
|
await this.db.iterateEntityOperations(
|
|
137
137
|
oid,
|
|
138
138
|
(patch) => {
|
|
139
|
-
//
|
|
139
|
+
// seems like the wrong place to do this
|
|
140
140
|
// but it's here as a safety measure...
|
|
141
141
|
if (!baseline || patch.timestamp > baseline.timestamp) {
|
|
142
142
|
current = applyPatch(current, patch.data, deletedRefs);
|
|
@@ -18,7 +18,7 @@ export class IdbService extends Disposable {
|
|
|
18
18
|
super();
|
|
19
19
|
const abortController = new AbortController();
|
|
20
20
|
this.globalAbortController = abortController;
|
|
21
|
-
//
|
|
21
|
+
// TODO: replace with event? I can't get this to work. (Illegal invocation on abort?)
|
|
22
22
|
// this.addDispose(abort);
|
|
23
23
|
this.db.addEventListener('versionchange', this.onVersionChange);
|
|
24
24
|
this.addDispose(() => {
|
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
createLowerBoundIndexValue,
|
|
4
4
|
createUpperBoundIndexValue,
|
|
5
5
|
DocumentBaseline,
|
|
6
|
-
getLegacyDotOidSubIdRange,
|
|
7
6
|
getOidRoot,
|
|
8
7
|
getOidSubIdRange,
|
|
9
8
|
ObjectIdentifier,
|
|
@@ -19,13 +18,14 @@ import {
|
|
|
19
18
|
PersistenceMetadataDb,
|
|
20
19
|
} from '../../interfaces.js';
|
|
21
20
|
import { IdbService } from '../IdbService.js';
|
|
21
|
+
import { OPERATION_INDEX_NAMES } from '../indexNames.js';
|
|
22
22
|
import { closeDatabase, getSizeOfObjectStore } from '../util.js';
|
|
23
23
|
|
|
24
24
|
export type StoredClientOperation = ClientOperation & {
|
|
25
25
|
/** This acts as the primary key */
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
[OPERATION_INDEX_NAMES.OID__TIMESTAMP]: string;
|
|
27
|
+
[OPERATION_INDEX_NAMES.IS_LOCAL__TIMESTAMP]: string;
|
|
28
|
+
[OPERATION_INDEX_NAMES.ROOT_OID__TIMESTAMP]: string;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
export type StoredSchema = {
|
|
@@ -123,12 +123,9 @@ export class IdbMetadataDb
|
|
|
123
123
|
(store) => {
|
|
124
124
|
const root = getOidRoot(rootOid);
|
|
125
125
|
const [start, end] = getOidSubIdRange(rootOid);
|
|
126
|
-
// FIXME: get rid of legacy dot OIDs...
|
|
127
|
-
const [dotStart, dotEnd] = getLegacyDotOidSubIdRange(rootOid);
|
|
128
126
|
return [
|
|
129
127
|
store.openCursor(IDBKeyRange.only(root)),
|
|
130
128
|
store.openCursor(IDBKeyRange.bound(start, end, false, false)),
|
|
131
|
-
store.openCursor(IDBKeyRange.bound(dotStart, dotEnd, false, false)),
|
|
132
129
|
];
|
|
133
130
|
},
|
|
134
131
|
iterator,
|
|
@@ -204,7 +201,7 @@ export class IdbMetadataDb
|
|
|
204
201
|
return this.iterate<StoredClientOperation>(
|
|
205
202
|
'operations',
|
|
206
203
|
(store) => {
|
|
207
|
-
const index = store.index(
|
|
204
|
+
const index = store.index(OPERATION_INDEX_NAMES.ROOT_OID__TIMESTAMP);
|
|
208
205
|
const start = createLowerBoundIndexValue(rootOid);
|
|
209
206
|
const end = opts?.to
|
|
210
207
|
? createCompoundIndexValue(rootOid, opts.to)
|
|
@@ -221,7 +218,7 @@ export class IdbMetadataDb
|
|
|
221
218
|
iterateEntityOperations = (
|
|
222
219
|
oid: string,
|
|
223
220
|
iterator: (op: StoredClientOperation) => void,
|
|
224
|
-
opts?: CommonQueryOptions & { to?: string | null },
|
|
221
|
+
opts?: CommonQueryOptions & { to?: string | null; reverse?: boolean },
|
|
225
222
|
): Promise<void> => {
|
|
226
223
|
// NOTE: this is simplified from original impl.
|
|
227
224
|
// perhaps I missed some nuance as to why it was
|
|
@@ -236,7 +233,7 @@ export class IdbMetadataDb
|
|
|
236
233
|
: createUpperBoundIndexValue(oid);
|
|
237
234
|
|
|
238
235
|
const range = IDBKeyRange.bound(start, end, false, false);
|
|
239
|
-
return store.openCursor(range);
|
|
236
|
+
return store.openCursor(range, opts?.reverse ? 'prev' : 'next');
|
|
240
237
|
},
|
|
241
238
|
iterator,
|
|
242
239
|
opts,
|
|
@@ -296,7 +293,7 @@ export class IdbMetadataDb
|
|
|
296
293
|
? createCompoundIndexValue(true, opts.after)
|
|
297
294
|
: createLowerBoundIndexValue(true);
|
|
298
295
|
const end = createUpperBoundIndexValue(true);
|
|
299
|
-
const index = store.index(
|
|
296
|
+
const index = store.index(OPERATION_INDEX_NAMES.IS_LOCAL__TIMESTAMP);
|
|
300
297
|
return index.openCursor(
|
|
301
298
|
// NOTE: differs from original impl -- last arg is 'false' instead of 'true'
|
|
302
299
|
IDBKeyRange.bound(start, end, !!opts?.after, false),
|
|
@@ -425,11 +422,71 @@ export class IdbMetadataDb
|
|
|
425
422
|
): StoredClientOperation => {
|
|
426
423
|
return {
|
|
427
424
|
...op,
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
425
|
+
[OPERATION_INDEX_NAMES.OID__TIMESTAMP]: createCompoundIndexValue(
|
|
426
|
+
op.oid,
|
|
427
|
+
op.timestamp,
|
|
428
|
+
) as string,
|
|
429
|
+
[OPERATION_INDEX_NAMES.IS_LOCAL__TIMESTAMP]: createCompoundIndexValue(
|
|
430
|
+
op.isLocal,
|
|
431
|
+
op.timestamp,
|
|
432
|
+
) as string,
|
|
433
|
+
[OPERATION_INDEX_NAMES.ROOT_OID__TIMESTAMP]: createCompoundIndexValue(
|
|
434
|
+
getOidRoot(op.oid),
|
|
435
|
+
op.timestamp,
|
|
436
|
+
) as string,
|
|
431
437
|
};
|
|
432
438
|
};
|
|
439
|
+
|
|
440
|
+
purgeDocumentData(
|
|
441
|
+
oid: ObjectIdentifier,
|
|
442
|
+
opts?: CommonQueryOptions<IDBTransaction> | undefined,
|
|
443
|
+
): Promise<void> {
|
|
444
|
+
const rootOid = getOidRoot(oid);
|
|
445
|
+
const tx = opts?.transaction as IDBTransaction | undefined;
|
|
446
|
+
|
|
447
|
+
return this.transaction(
|
|
448
|
+
{
|
|
449
|
+
mode: 'readwrite',
|
|
450
|
+
storeNames: ['baselines', 'operations'],
|
|
451
|
+
},
|
|
452
|
+
async (ownedTx) => {
|
|
453
|
+
const transaction = tx ?? ownedTx;
|
|
454
|
+
|
|
455
|
+
await this.iterate<DocumentBaseline>(
|
|
456
|
+
'baselines',
|
|
457
|
+
(store) => {
|
|
458
|
+
const [start, end] = getOidSubIdRange(rootOid);
|
|
459
|
+
return [
|
|
460
|
+
store.openCursor(IDBKeyRange.only(rootOid)),
|
|
461
|
+
store.openCursor(IDBKeyRange.bound(start, end, false, false)),
|
|
462
|
+
];
|
|
463
|
+
},
|
|
464
|
+
(baseline, store) => {
|
|
465
|
+
store.delete(baseline.oid);
|
|
466
|
+
},
|
|
467
|
+
{ mode: 'readwrite', transaction },
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
await this.iterate<StoredClientOperation>(
|
|
471
|
+
'operations',
|
|
472
|
+
(store) => {
|
|
473
|
+
const index = store.index(
|
|
474
|
+
OPERATION_INDEX_NAMES.ROOT_OID__TIMESTAMP,
|
|
475
|
+
);
|
|
476
|
+
const start = createLowerBoundIndexValue(rootOid);
|
|
477
|
+
const end = createUpperBoundIndexValue(rootOid);
|
|
478
|
+
return index.openCursor(
|
|
479
|
+
IDBKeyRange.bound(start, end, false, false),
|
|
480
|
+
);
|
|
481
|
+
},
|
|
482
|
+
(op, store) => {
|
|
483
|
+
store.delete(op[OPERATION_INDEX_NAMES.OID__TIMESTAMP]);
|
|
484
|
+
},
|
|
485
|
+
{ mode: 'readwrite', transaction },
|
|
486
|
+
);
|
|
487
|
+
},
|
|
488
|
+
);
|
|
489
|
+
}
|
|
433
490
|
}
|
|
434
491
|
|
|
435
492
|
const writeOpts = { mode: 'readwrite' } as const;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { replaceLegacyOidsInObject } from '@verdant-web/common';
|
|
2
|
-
import { getMetadataDbName } from '../util.js';
|
|
3
2
|
import { Context } from '../../../context/context.js';
|
|
3
|
+
import { OPERATION_INDEX_NAMES } from '../indexNames.js';
|
|
4
|
+
import { getMetadataDbName } from '../util.js';
|
|
4
5
|
|
|
5
6
|
const migrations = [version1, version2, version3, version4, version5, version6];
|
|
6
7
|
export const CURRENT_METADATA_VERSION = migrations.length;
|
|
@@ -55,7 +56,7 @@ async function version1(db: IDBDatabase, tx: IDBTransaction) {
|
|
|
55
56
|
keyPath: 'oid',
|
|
56
57
|
});
|
|
57
58
|
const operationsStore = db.createObjectStore('operations', {
|
|
58
|
-
keyPath:
|
|
59
|
+
keyPath: OPERATION_INDEX_NAMES.OID__TIMESTAMP,
|
|
59
60
|
});
|
|
60
61
|
const infoStore = db.createObjectStore('info', { keyPath: 'type' });
|
|
61
62
|
baselinesStore.createIndex('timestamp', 'timestamp');
|
|
@@ -84,8 +85,8 @@ async function version2(db: IDBDatabase, tx: IDBTransaction) {
|
|
|
84
85
|
cursor.value;
|
|
85
86
|
cursor.update({
|
|
86
87
|
...value,
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
[OPERATION_INDEX_NAMES.IS_LOCAL__TIMESTAMP]: isLocal_timestamp,
|
|
89
|
+
[OPERATION_INDEX_NAMES.ROOT_OID__TIMESTAMP]: documentOid_timestamp,
|
|
89
90
|
});
|
|
90
91
|
cursor.continue();
|
|
91
92
|
} else {
|
|
@@ -100,9 +101,16 @@ async function version2(db: IDBDatabase, tx: IDBTransaction) {
|
|
|
100
101
|
operations.deleteIndex('isLocal_timestamp');
|
|
101
102
|
operations.deleteIndex('documentOid_timestamp');
|
|
102
103
|
// create the new indexes
|
|
103
|
-
operations.createIndex(
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
operations.createIndex(
|
|
105
|
+
OPERATION_INDEX_NAMES.IS_LOCAL__TIMESTAMP,
|
|
106
|
+
OPERATION_INDEX_NAMES.IS_LOCAL__TIMESTAMP,
|
|
107
|
+
{ unique: false },
|
|
108
|
+
);
|
|
109
|
+
operations.createIndex(
|
|
110
|
+
OPERATION_INDEX_NAMES.ROOT_OID__TIMESTAMP,
|
|
111
|
+
OPERATION_INDEX_NAMES.ROOT_OID__TIMESTAMP,
|
|
112
|
+
{ unique: false },
|
|
113
|
+
);
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
/**
|
|
@@ -46,15 +46,14 @@ const rangeIndexToIdbKeyRange = (filter: RangeCollectionIndexFilter) => {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
const compoundIndexToIdbKeyRange = (
|
|
49
|
-
|
|
50
|
-
schema: any,
|
|
49
|
+
schema: StorageSchema,
|
|
51
50
|
collection: string,
|
|
52
51
|
filter: CollectionCompoundIndexFilter,
|
|
53
52
|
) => {
|
|
54
53
|
// validate the usage of the compound index:
|
|
55
54
|
// - all match fields must be contiguous at the start of the compound order
|
|
56
55
|
const indexDefinition =
|
|
57
|
-
schema.collections[collection].compounds[filter.where];
|
|
56
|
+
schema.collections[collection].compounds?.[filter.where];
|
|
58
57
|
assert(
|
|
59
58
|
indexDefinition,
|
|
60
59
|
`Index ${filter.where} does not exist on collection ${collection}`,
|
|
@@ -105,7 +105,7 @@ export interface PersistenceMetadataDb<
|
|
|
105
105
|
iterateEntityOperations(
|
|
106
106
|
oid: string,
|
|
107
107
|
iterator: Iterator<ClientOperation>,
|
|
108
|
-
opts?: CommonQueryOptions<Tx> & { to?: string | null },
|
|
108
|
+
opts?: CommonQueryOptions<Tx> & { to?: string | null; reverse?: boolean },
|
|
109
109
|
): Promise<void>;
|
|
110
110
|
iterateCollectionOperations(
|
|
111
111
|
collection: string,
|
|
@@ -138,6 +138,10 @@ export interface PersistenceMetadataDb<
|
|
|
138
138
|
ops: ClientOperation[],
|
|
139
139
|
opts?: CommonQueryOptions<Tx>,
|
|
140
140
|
): Promise<ObjectIdentifier[]>;
|
|
141
|
+
purgeDocumentData(
|
|
142
|
+
oid: ObjectIdentifier,
|
|
143
|
+
opts?: CommonQueryOptions<Tx>,
|
|
144
|
+
): Promise<void>;
|
|
141
145
|
|
|
142
146
|
/* WARNING: deletes all data */
|
|
143
147
|
reset(opts?: { clearReplica?: boolean; transaction?: Tx }): Promise<void>;
|
|
@@ -174,10 +174,9 @@ export async function getMigrationEngine({
|
|
|
174
174
|
!!rootOid,
|
|
175
175
|
`Document is missing an OID: ${JSON.stringify(doc)}`,
|
|
176
176
|
);
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
// lower to retain access to lower level data here?
|
|
177
|
+
// while querying again for data we already discarded in the findAll
|
|
178
|
+
// above isn't ideal, in practice the get authz method is
|
|
179
|
+
// fairly quick because it short-circuits.
|
|
181
180
|
const authz = await meta.getDocumentAuthz(rootOid);
|
|
182
181
|
const original = cloneDeep(doc);
|
|
183
182
|
// @ts-ignore - excessive type resolution
|
package/src/queries/BaseQuery.ts
CHANGED
|
@@ -2,6 +2,12 @@ import { EventSubscriber } from '@verdant-web/common';
|
|
|
2
2
|
import { Context } from '../context/context.js';
|
|
3
3
|
import { Entity } from '../entities/Entity.js';
|
|
4
4
|
import { Disposable } from '../utils/Disposable.js';
|
|
5
|
+
import {
|
|
6
|
+
getDiagnosticResult,
|
|
7
|
+
QueryDiagnostic,
|
|
8
|
+
QueryRun,
|
|
9
|
+
QueryTiming,
|
|
10
|
+
} from './diagnostics.js';
|
|
5
11
|
import { filterResultSet } from './utils.js';
|
|
6
12
|
|
|
7
13
|
export type BaseQueryEvents = {
|
|
@@ -37,12 +43,20 @@ export abstract class BaseQuery<T> extends Disposable {
|
|
|
37
43
|
private _allUnsubscribedHandler?: (query: BaseQuery<T>) => void;
|
|
38
44
|
private _status: QueryStatus = 'initial';
|
|
39
45
|
private _executionPromise: Promise<T> | null = null;
|
|
46
|
+
private _timing: QueryTiming = {
|
|
47
|
+
sweep: null,
|
|
48
|
+
hydration: null,
|
|
49
|
+
total: null,
|
|
50
|
+
};
|
|
51
|
+
private _startedAt: number | null = null;
|
|
52
|
+
private _timingHistory: QueryRun[] = [];
|
|
40
53
|
|
|
41
54
|
protected context;
|
|
42
55
|
|
|
43
56
|
readonly collection;
|
|
44
57
|
readonly key;
|
|
45
58
|
readonly isListQuery;
|
|
59
|
+
readonly type;
|
|
46
60
|
|
|
47
61
|
constructor({
|
|
48
62
|
initial,
|
|
@@ -65,6 +79,7 @@ export abstract class BaseQuery<T> extends Disposable {
|
|
|
65
79
|
this.context = context;
|
|
66
80
|
this.key = key;
|
|
67
81
|
this.collection = collection;
|
|
82
|
+
this.type = this.constructor.name;
|
|
68
83
|
const shouldUpdateFn =
|
|
69
84
|
shouldUpdate ||
|
|
70
85
|
((collections: string[]) => collections.includes(collection));
|
|
@@ -223,13 +238,9 @@ export abstract class BaseQuery<T> extends Disposable {
|
|
|
223
238
|
};
|
|
224
239
|
|
|
225
240
|
execute = () => {
|
|
226
|
-
const startTime =
|
|
227
|
-
this.
|
|
228
|
-
|
|
229
|
-
`[${startTime.toLocaleTimeString()}]`,
|
|
230
|
-
'Executing query',
|
|
231
|
-
this.key,
|
|
232
|
-
);
|
|
241
|
+
const startTime = performance.now();
|
|
242
|
+
this._startedAt = Date.now();
|
|
243
|
+
this.context.log('debug', 'Executing query', this.key);
|
|
233
244
|
|
|
234
245
|
if (this.status === 'initial') {
|
|
235
246
|
this.status = 'initializing';
|
|
@@ -255,11 +266,14 @@ export abstract class BaseQuery<T> extends Disposable {
|
|
|
255
266
|
}
|
|
256
267
|
})
|
|
257
268
|
.finally(() => {
|
|
258
|
-
const
|
|
259
|
-
|
|
269
|
+
const duration = performance.now() - startTime;
|
|
270
|
+
this._timing.total = duration;
|
|
271
|
+
this._timingHistory = [
|
|
272
|
+
...this._timingHistory,
|
|
273
|
+
{ startedAt: this._startedAt ?? Date.now(), ...this._timing },
|
|
274
|
+
].slice(-100);
|
|
260
275
|
this.context.log(
|
|
261
276
|
'debug',
|
|
262
|
-
`[${endTime.toLocaleTimeString()}]`,
|
|
263
277
|
'Query executed',
|
|
264
278
|
this.key,
|
|
265
279
|
`Duration: ${duration}ms`,
|
|
@@ -267,6 +281,38 @@ export abstract class BaseQuery<T> extends Disposable {
|
|
|
267
281
|
});
|
|
268
282
|
return this._executionPromise;
|
|
269
283
|
};
|
|
284
|
+
|
|
285
|
+
protected measureSweep = async <V>(work: () => Promise<V>): Promise<V> => {
|
|
286
|
+
const start = performance.now();
|
|
287
|
+
try {
|
|
288
|
+
return await work();
|
|
289
|
+
} finally {
|
|
290
|
+
this._timing.sweep = performance.now() - start;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
protected measureHydration = async <V>(
|
|
295
|
+
work: () => Promise<V>,
|
|
296
|
+
): Promise<V> => {
|
|
297
|
+
const start = performance.now();
|
|
298
|
+
try {
|
|
299
|
+
return await work();
|
|
300
|
+
} finally {
|
|
301
|
+
this._timing.hydration = performance.now() - start;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
getDiagnostic = (active: boolean): QueryDiagnostic => ({
|
|
306
|
+
key: this.key,
|
|
307
|
+
startedAt: this._startedAt,
|
|
308
|
+
collection: this.collection,
|
|
309
|
+
type: this.type,
|
|
310
|
+
status: this.status,
|
|
311
|
+
active,
|
|
312
|
+
result: getDiagnosticResult(this.current),
|
|
313
|
+
timing: { ...this._timing },
|
|
314
|
+
runs: this._timingHistory.map((run) => ({ ...run })),
|
|
315
|
+
});
|
|
270
316
|
protected abstract run(): Promise<void>;
|
|
271
317
|
|
|
272
318
|
[ON_ALL_UNSUBSCRIBED] = (handler: (query: BaseQuery<T>) => void) => {
|