@verdant-web/store 5.3.0 → 5.4.2

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.
Files changed (89) hide show
  1. package/dist/bundle/index.js +8 -8
  2. package/dist/bundle/index.js.map +4 -4
  3. package/dist/esm/__tests__/queries.test.js +35 -0
  4. package/dist/esm/__tests__/queries.test.js.map +1 -1
  5. package/dist/esm/client/Client.js +12 -4
  6. package/dist/esm/client/Client.js.map +1 -1
  7. package/dist/esm/entities/Entity.d.ts +2 -5
  8. package/dist/esm/entities/Entity.js +17 -10
  9. package/dist/esm/entities/Entity.js.map +1 -1
  10. package/dist/esm/entities/EntityMetadata.js +14 -2
  11. package/dist/esm/entities/EntityMetadata.js.map +1 -1
  12. package/dist/esm/entities/EntityStore.js +1 -0
  13. package/dist/esm/entities/EntityStore.js.map +1 -1
  14. package/dist/esm/persistence/MessageCreator.js +1 -1
  15. package/dist/esm/persistence/MessageCreator.js.map +1 -1
  16. package/dist/esm/persistence/PersistenceMetadata.d.ts +6 -1
  17. package/dist/esm/persistence/PersistenceMetadata.js +40 -2
  18. package/dist/esm/persistence/PersistenceMetadata.js.map +1 -1
  19. package/dist/esm/persistence/PersistenceRebaser.js +1 -1
  20. package/dist/esm/persistence/PersistenceRebaser.js.map +1 -1
  21. package/dist/esm/persistence/idb/IdbService.js +1 -1
  22. package/dist/esm/persistence/idb/IdbService.js.map +1 -1
  23. package/dist/esm/persistence/idb/indexNames.d.ts +5 -0
  24. package/dist/esm/persistence/idb/indexNames.js +6 -0
  25. package/dist/esm/persistence/idb/indexNames.js.map +1 -0
  26. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.d.ts +6 -3
  27. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js +33 -8
  28. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js.map +1 -1
  29. package/dist/esm/persistence/idb/metadata/openMetadataDatabase.js +5 -5
  30. package/dist/esm/persistence/idb/metadata/openMetadataDatabase.js.map +1 -1
  31. package/dist/esm/persistence/idb/queries/ranges.js +3 -4
  32. package/dist/esm/persistence/idb/queries/ranges.js.map +1 -1
  33. package/dist/esm/persistence/interfaces.d.ts +2 -0
  34. package/dist/esm/persistence/migration/engine.js +3 -4
  35. package/dist/esm/persistence/migration/engine.js.map +1 -1
  36. package/dist/esm/queries/BaseQuery.d.ts +6 -0
  37. package/dist/esm/queries/BaseQuery.js +39 -5
  38. package/dist/esm/queries/BaseQuery.js.map +1 -1
  39. package/dist/esm/queries/CollectionQueries.d.ts +8 -7
  40. package/dist/esm/queries/CollectionQueries.js +8 -6
  41. package/dist/esm/queries/CollectionQueries.js.map +1 -1
  42. package/dist/esm/queries/FindAllQuery.d.ts +3 -1
  43. package/dist/esm/queries/FindAllQuery.js +6 -4
  44. package/dist/esm/queries/FindAllQuery.js.map +1 -1
  45. package/dist/esm/queries/FindInfiniteQuery.js +6 -6
  46. package/dist/esm/queries/FindInfiniteQuery.js.map +1 -1
  47. package/dist/esm/queries/FindOneQuery.js +3 -3
  48. package/dist/esm/queries/FindOneQuery.js.map +1 -1
  49. package/dist/esm/queries/FindPageQuery.js +3 -3
  50. package/dist/esm/queries/FindPageQuery.js.map +1 -1
  51. package/dist/esm/queries/GetQuery.js +1 -1
  52. package/dist/esm/queries/GetQuery.js.map +1 -1
  53. package/dist/esm/queries/QueryCache.d.ts +4 -1
  54. package/dist/esm/queries/QueryCache.js +15 -1
  55. package/dist/esm/queries/QueryCache.js.map +1 -1
  56. package/dist/esm/queries/diagnostics.d.ts +19 -0
  57. package/dist/esm/queries/diagnostics.js +11 -0
  58. package/dist/esm/queries/diagnostics.js.map +1 -0
  59. package/dist/esm/queries/ranges.js +3 -4
  60. package/dist/esm/queries/ranges.js.map +1 -1
  61. package/dist/esm/sync/WebSocketSync.js +4 -2
  62. package/dist/esm/sync/WebSocketSync.js.map +1 -1
  63. package/package.json +7 -2
  64. package/src/__tests__/queries.test.ts +42 -0
  65. package/src/client/Client.ts +15 -4
  66. package/src/entities/Entity.ts +31 -22
  67. package/src/entities/EntityMetadata.ts +15 -3
  68. package/src/entities/EntityStore.ts +1 -0
  69. package/src/persistence/MessageCreator.ts +1 -1
  70. package/src/persistence/PersistenceMetadata.ts +67 -7
  71. package/src/persistence/PersistenceRebaser.ts +1 -1
  72. package/src/persistence/idb/IdbService.ts +1 -1
  73. package/src/persistence/idb/indexNames.ts +5 -0
  74. package/src/persistence/idb/metadata/IdbMetadataDb.ts +71 -14
  75. package/src/persistence/idb/metadata/openMetadataDatabase.ts +15 -7
  76. package/src/persistence/idb/queries/ranges.ts +2 -3
  77. package/src/persistence/interfaces.ts +5 -1
  78. package/src/persistence/migration/engine.ts +3 -4
  79. package/src/queries/BaseQuery.ts +41 -5
  80. package/src/queries/CollectionQueries.ts +12 -9
  81. package/src/queries/FindAllQuery.ts +14 -7
  82. package/src/queries/FindInfiniteQuery.ts +22 -18
  83. package/src/queries/FindOneQuery.ts +9 -7
  84. package/src/queries/FindPageQuery.ts +11 -9
  85. package/src/queries/GetQuery.ts +1 -1
  86. package/src/queries/QueryCache.ts +19 -2
  87. package/src/queries/diagnostics.ts +32 -0
  88. package/src/queries/ranges.ts +2 -3
  89. package/src/sync/WebSocketSync.ts +2 -0
@@ -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: 'oid_timestamp',
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
- l_t: isLocal_timestamp,
88
- d_t: documentOid_timestamp,
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('l_t', 'l_t', { unique: false });
104
- operations.createIndex('o_t', 'o_t', { unique: false });
105
- operations.createIndex('d_t', 'd_t', { unique: false });
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
- // FIXME:
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
- // FIXME: this could be optimized (making n queries for authz
178
- // when the snapshots themselves are derived from the same data...)
179
- // maybe don't use the findAll query, and instead go a level
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
@@ -2,6 +2,7 @@ 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 { getDiagnosticResult, QueryDiagnostic, QueryTiming } from './diagnostics.js';
5
6
  import { filterResultSet } from './utils.js';
6
7
 
7
8
  export type BaseQueryEvents = {
@@ -37,12 +38,18 @@ export abstract class BaseQuery<T> extends Disposable {
37
38
  private _allUnsubscribedHandler?: (query: BaseQuery<T>) => void;
38
39
  private _status: QueryStatus = 'initial';
39
40
  private _executionPromise: Promise<T> | null = null;
41
+ private _timing: QueryTiming = {
42
+ sweep: null,
43
+ hydration: null,
44
+ total: null,
45
+ };
40
46
 
41
47
  protected context;
42
48
 
43
49
  readonly collection;
44
50
  readonly key;
45
51
  readonly isListQuery;
52
+ readonly type;
46
53
 
47
54
  constructor({
48
55
  initial,
@@ -65,6 +72,7 @@ export abstract class BaseQuery<T> extends Disposable {
65
72
  this.context = context;
66
73
  this.key = key;
67
74
  this.collection = collection;
75
+ this.type = this.constructor.name;
68
76
  const shouldUpdateFn =
69
77
  shouldUpdate ||
70
78
  ((collections: string[]) => collections.includes(collection));
@@ -223,10 +231,9 @@ export abstract class BaseQuery<T> extends Disposable {
223
231
  };
224
232
 
225
233
  execute = () => {
226
- const startTime = new Date();
234
+ const startTime = performance.now();
227
235
  this.context.log(
228
236
  'debug',
229
- `[${startTime.toLocaleTimeString()}]`,
230
237
  'Executing query',
231
238
  this.key,
232
239
  );
@@ -255,11 +262,10 @@ export abstract class BaseQuery<T> extends Disposable {
255
262
  }
256
263
  })
257
264
  .finally(() => {
258
- const endTime = new Date();
259
- const duration = endTime.getTime() - startTime.getTime();
265
+ const duration = performance.now() - startTime;
266
+ this._timing.total = duration;
260
267
  this.context.log(
261
268
  'debug',
262
- `[${endTime.toLocaleTimeString()}]`,
263
269
  'Query executed',
264
270
  this.key,
265
271
  `Duration: ${duration}ms`,
@@ -267,6 +273,36 @@ export abstract class BaseQuery<T> extends Disposable {
267
273
  });
268
274
  return this._executionPromise;
269
275
  };
276
+
277
+ protected measureSweep = async <V>(work: () => Promise<V>): Promise<V> => {
278
+ const start = performance.now();
279
+ try {
280
+ return await work();
281
+ } finally {
282
+ this._timing.sweep = performance.now() - start;
283
+ }
284
+ };
285
+
286
+ protected measureHydration = async <V>(
287
+ work: () => Promise<V>,
288
+ ): Promise<V> => {
289
+ const start = performance.now();
290
+ try {
291
+ return await work();
292
+ } finally {
293
+ this._timing.hydration = performance.now() - start;
294
+ }
295
+ };
296
+
297
+ getDiagnostic = (active: boolean): QueryDiagnostic => ({
298
+ key: this.key,
299
+ collection: this.collection,
300
+ type: this.type,
301
+ status: this.status,
302
+ active,
303
+ result: getDiagnosticResult(this.current),
304
+ timing: { ...this._timing },
305
+ });
270
306
  protected abstract run(): Promise<void>;
271
307
 
272
308
  [ON_ALL_UNSUBSCRIBED] = (handler: (query: BaseQuery<T>) => void) => {
@@ -4,16 +4,16 @@ import {
4
4
  hashObject,
5
5
  } from '@verdant-web/common';
6
6
  import { Context } from '../context/context.js';
7
- import { EntityStore } from '../entities/EntityStore.js';
8
- import { GetQuery } from './GetQuery.js';
9
- import { QueryCache } from './QueryCache.js';
10
- import { FindOneQuery } from './FindOneQuery.js';
11
- import { FindPageQuery } from './FindPageQuery.js';
12
- import { FindInfiniteQuery } from './FindInfiniteQuery.js';
13
- import { FindAllQuery } from './FindAllQuery.js';
14
7
  import { DocumentManager } from '../entities/DocumentManager.js';
8
+ import { EntityStore } from '../entities/EntityStore.js';
15
9
  import { ObjectEntity } from '../index.js';
16
10
  import { UPDATE } from './BaseQuery.js';
11
+ import { FindAllQuery } from './FindAllQuery.js';
12
+ import { FindInfiniteQuery } from './FindInfiniteQuery.js';
13
+ import { FindOneQuery } from './FindOneQuery.js';
14
+ import { FindPageQuery } from './FindPageQuery.js';
15
+ import { GetQuery } from './GetQuery.js';
16
+ import { QueryCache } from './QueryCache.js';
17
17
 
18
18
  export class CollectionQueries<
19
19
  T extends ObjectEntity<any, any>,
@@ -123,9 +123,11 @@ export class CollectionQueries<
123
123
  findAll = ({
124
124
  index,
125
125
  key: providedKey,
126
- }: { index?: Filter; key?: string } = {}) => {
126
+ limit,
127
+ }: { index?: Filter; key?: string; limit?: number } = {}) => {
127
128
  const key =
128
- providedKey || `findAll:${this.collection}:${this.serializeIndex(index)}`;
129
+ providedKey ||
130
+ `findAll:${this.collection}:${limit}:${this.serializeIndex(index)}`;
129
131
  return this.cache.getOrSet(
130
132
  key,
131
133
  () =>
@@ -135,6 +137,7 @@ export class CollectionQueries<
135
137
  hydrate: this.hydrate,
136
138
  context: this.context,
137
139
  key,
140
+ limit,
138
141
  }),
139
142
  (existing) => {
140
143
  existing[UPDATE](index);
@@ -5,35 +5,42 @@ import { areIndexesEqual } from './utils.js';
5
5
  export class FindAllQuery<T> extends BaseQuery<T[]> {
6
6
  private index;
7
7
  private hydrate;
8
+ private limit;
8
9
 
9
10
  constructor({
10
11
  index,
11
12
  hydrate,
13
+ limit,
12
14
  ...rest
13
15
  }: {
14
16
  index?: CollectionFilter;
15
17
  hydrate: (oid: string) => Promise<T>;
18
+ limit?: number;
16
19
  } & Omit<BaseQueryOptions<T[]>, 'initial'>) {
17
20
  super({
18
21
  initial: [],
19
22
  ...rest,
20
23
  });
24
+ this.limit = limit;
21
25
  this.index = index;
22
26
  this.hydrate = hydrate;
23
27
  }
24
28
 
25
29
  protected run = async () => {
26
- const { result: oids } = await (
27
- await this.context.documents
28
- ).findAllOids({
29
- collection: this.collection,
30
- index: this.index,
31
- });
30
+ const { result: oids } = await this.measureSweep(async () =>
31
+ (await this.context.documents).findAllOids({
32
+ collection: this.collection,
33
+ index: this.index,
34
+ limit: this.limit,
35
+ }),
36
+ );
32
37
  this.context.log(
33
38
  'debug',
34
39
  `FindAllQuery: ${oids.length} oids found: ${oids}`,
35
40
  );
36
- this.setValue(await Promise.all(oids.map(this.hydrate)));
41
+ this.setValue(
42
+ await this.measureHydration(() => Promise.all(oids.map(this.hydrate))),
43
+ );
37
44
  };
38
45
 
39
46
  [UPDATE] = (index: CollectionFilter | undefined) => {
@@ -37,32 +37,36 @@ export class FindInfiniteQuery<T> extends BaseQuery<T[]> {
37
37
  }
38
38
 
39
39
  protected run = async () => {
40
- const { result, hasNextPage } = await (
41
- await this.context.documents
42
- ).findAllOids({
43
- collection: this.collection,
44
- limit: this._pageSize * this._upToPage,
45
- offset: 0,
46
- index: this.index,
47
- });
40
+ const { result, hasNextPage } = await this.measureSweep(async () =>
41
+ (await this.context.documents).findAllOids({
42
+ collection: this.collection,
43
+ limit: this._pageSize * this._upToPage,
44
+ offset: 0,
45
+ index: this.index,
46
+ }),
47
+ );
48
48
  this._hasNextPage = hasNextPage;
49
- this.setValue(await Promise.all(result.map(this.hydrate)));
49
+ this.setValue(
50
+ await this.measureHydration(() => Promise.all(result.map(this.hydrate))),
51
+ );
50
52
  };
51
53
 
52
54
  public loadMore = async () => {
53
- const { result, hasNextPage } = await (
54
- await this.context.documents
55
- ).findAllOids({
56
- collection: this.collection,
57
- limit: this._pageSize,
58
- offset: this._pageSize * this._upToPage,
59
- index: this.index,
60
- });
55
+ const { result, hasNextPage } = await this.measureSweep(async () =>
56
+ (await this.context.documents).findAllOids({
57
+ collection: this.collection,
58
+ limit: this._pageSize,
59
+ offset: this._pageSize * this._upToPage,
60
+ index: this.index,
61
+ }),
62
+ );
61
63
  this._hasNextPage = hasNextPage;
62
64
  this._upToPage++;
63
65
  this.setValue([
64
66
  ...this.current,
65
- ...(await Promise.all(result.map(this.hydrate))),
67
+ ...(await this.measureHydration(() =>
68
+ Promise.all(result.map(this.hydrate)),
69
+ )),
66
70
  ]);
67
71
  };
68
72
 
@@ -23,13 +23,15 @@ export class FindOneQuery<T> extends BaseQuery<T | null> {
23
23
  }
24
24
 
25
25
  protected run = async () => {
26
- const oid = await (
27
- await this.context.documents
28
- ).findOneOid({
29
- collection: this.collection,
30
- index: this.index,
31
- });
32
- this.setValue(oid ? await this.hydrate(oid) : null);
26
+ const oid = await this.measureSweep(async () =>
27
+ (await this.context.documents).findOneOid({
28
+ collection: this.collection,
29
+ index: this.index,
30
+ }),
31
+ );
32
+ this.setValue(
33
+ oid ? await this.measureHydration(() => this.hydrate(oid)) : null,
34
+ );
33
35
  };
34
36
 
35
37
  [UPDATE] = (index: CollectionFilter | undefined) => {
@@ -48,16 +48,18 @@ export class FindPageQuery<T> extends BaseQuery<T[]> {
48
48
  }
49
49
 
50
50
  protected run = async () => {
51
- const { result, hasNextPage } = await (
52
- await this.context.documents
53
- ).findAllOids({
54
- collection: this.collection,
55
- index: this.index,
56
- limit: this._pageSize,
57
- offset: this._page * this._pageSize,
58
- });
51
+ const { result, hasNextPage } = await this.measureSweep(async () =>
52
+ (await this.context.documents).findAllOids({
53
+ collection: this.collection,
54
+ index: this.index,
55
+ limit: this._pageSize,
56
+ offset: this._page * this._pageSize,
57
+ }),
58
+ );
59
59
  this._hasNextPage = hasNextPage;
60
- this.setValue(await Promise.all(result.map(this.hydrate)));
60
+ this.setValue(
61
+ await this.measureHydration(() => Promise.all(result.map(this.hydrate))),
62
+ );
61
63
  };
62
64
 
63
65
  nextPage = async () => {
@@ -22,7 +22,7 @@ export class GetQuery<T> extends BaseQuery<T | null> {
22
22
  }
23
23
 
24
24
  protected run = async () => {
25
- const value = await this.hydrate(this.oid);
25
+ const value = await this.measureHydration(() => this.hydrate(this.oid));
26
26
  this.setValue(value);
27
27
  };
28
28
  }
@@ -1,6 +1,7 @@
1
1
  import { Context } from '../context/context.js';
2
2
  import { Disposable } from '../utils/Disposable.js';
3
3
  import { BaseQuery, ON_ALL_UNSUBSCRIBED } from './BaseQuery.js';
4
+ import { QueryDiagnostic, QueryDiagnostics } from './diagnostics.js';
4
5
 
5
6
  export class QueryCache extends Disposable {
6
7
  private _cache: Map<string, BaseQuery<any>> = new Map();
@@ -8,6 +9,7 @@ export class QueryCache extends Disposable {
8
9
  private context;
9
10
  /** A set of query keys to keep alive even if they unsubscribe */
10
11
  private _holds = new Set<string>();
12
+ private _inactiveDiagnostics = new Map<string, QueryDiagnostic>();
11
13
 
12
14
  constructor({
13
15
  evictionTime = 5 * 1000,
@@ -38,6 +40,7 @@ export class QueryCache extends Disposable {
38
40
 
39
41
  set<V extends BaseQuery<any>>(value: V) {
40
42
  this._cache.set(value.key, value);
43
+ this._inactiveDiagnostics.delete(value.key);
41
44
  value[ON_ALL_UNSUBSCRIBED](this.enqueueQueryEviction);
42
45
  // immediately enqueue a check to see if this query should be evicted --
43
46
  // this basically gives code X seconds to subscribe to the query before
@@ -47,6 +50,15 @@ export class QueryCache extends Disposable {
47
50
  return value;
48
51
  }
49
52
 
53
+ get diagnostics(): QueryDiagnostics {
54
+ return {
55
+ queries: [
56
+ ...this._cache.values().map((query) => query.getDiagnostic(true)),
57
+ ...this._inactiveDiagnostics.values(),
58
+ ],
59
+ };
60
+ }
61
+
50
62
  getOrSet<V extends BaseQuery<any>>(
51
63
  key: string,
52
64
  create: () => V,
@@ -78,19 +90,23 @@ export class QueryCache extends Disposable {
78
90
  // got a different version of this query.
79
91
  if (this._cache.get(query.key) === query) {
80
92
  this._cache.delete(query.key);
93
+ this._inactiveDiagnostics.set(query.key, query.getDiagnostic(false));
81
94
  this.context.log('debug', 'QueryCache: evicted query', query.key);
82
95
  }
83
96
  }, this._evictionTime);
84
97
  };
85
98
 
86
- dropAll = () => {
99
+ dropAll = () => {
87
100
  this.context.log(
88
101
  'debug',
89
102
  'QueryCache: drop all',
90
103
  this._cache.size,
91
104
  'queries',
92
105
  );
93
- this._cache.forEach((query) => query.dispose());
106
+ this._cache.forEach((query) => {
107
+ this._inactiveDiagnostics.set(query.key, query.getDiagnostic(false));
108
+ query.dispose();
109
+ });
94
110
  this._cache.clear();
95
111
  };
96
112
 
@@ -135,4 +151,5 @@ export type PublicQueryCacheAPI = Pick<
135
151
  | 'keepAlives'
136
152
  | 'forceRefreshAll'
137
153
  | 'activeKeys'
154
+ | 'diagnostics'
138
155
  >;
@@ -0,0 +1,32 @@
1
+ import { Entity } from '../entities/Entity.js';
2
+ import type { QueryStatus } from './BaseQuery.js';
3
+
4
+ export type QueryTiming = {
5
+ sweep: number | null;
6
+ hydration: number | null;
7
+ total: number | null;
8
+ };
9
+
10
+ export type QueryDiagnostic = {
11
+ key: string;
12
+ collection: string;
13
+ type: string;
14
+ status: QueryStatus;
15
+ active: boolean;
16
+ result: unknown;
17
+ timing: QueryTiming;
18
+ };
19
+
20
+ export type QueryDiagnostics = {
21
+ queries: QueryDiagnostic[];
22
+ };
23
+
24
+ export const getDiagnosticResult = (value: unknown): unknown => {
25
+ if (Array.isArray(value)) {
26
+ return value.map(getDiagnosticResult);
27
+ }
28
+ if (value instanceof Entity) {
29
+ return value.getSnapshot();
30
+ }
31
+ return value;
32
+ };
@@ -46,15 +46,14 @@ const rangeIndexToIdbKeyRange = (filter: RangeCollectionIndexFilter) => {
46
46
  };
47
47
 
48
48
  const compoundIndexToIdbKeyRange = (
49
- // FIXME:
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}`,
@@ -314,6 +314,8 @@ export class WebSocketSync
314
314
  await this.sendDisconnecting();
315
315
  this.socket?.removeEventListener('message', this.onMessage);
316
316
  this.socket?.removeEventListener('close', this.onClose);
317
+ this.socket?.removeEventListener('error', this.onError);
318
+ this.socket?.removeEventListener('open', this.onOpen);
317
319
  if (this.socket?.readyState === WEBSOCKET_OPEN) {
318
320
  this.socket.close();
319
321
  }