@verdant-web/store 2.5.8 → 2.7.0

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 (145) hide show
  1. package/dist/bundle/index.js +15 -10
  2. package/dist/bundle/index.js.map +4 -4
  3. package/dist/cjs/{entities/FakeWeakRef.d.ts → FakeWeakRef.d.ts} +2 -2
  4. package/dist/cjs/{entities/FakeWeakRef.js → FakeWeakRef.js} +4 -4
  5. package/dist/cjs/FakeWeakRef.js.map +1 -0
  6. package/dist/cjs/IDBService.d.ts +1 -1
  7. package/dist/cjs/IDBService.js +18 -1
  8. package/dist/cjs/IDBService.js.map +1 -1
  9. package/dist/cjs/__tests__/documents.test.js +17 -0
  10. package/dist/cjs/__tests__/documents.test.js.map +1 -1
  11. package/dist/cjs/__tests__/fixtures/testStorage.d.ts +1 -1
  12. package/dist/cjs/__tests__/fixtures/testStorage.js +3 -2
  13. package/dist/cjs/__tests__/fixtures/testStorage.js.map +1 -1
  14. package/dist/cjs/__tests__/mutations.test.d.ts +1 -0
  15. package/dist/cjs/__tests__/mutations.test.js +42 -0
  16. package/dist/cjs/__tests__/mutations.test.js.map +1 -0
  17. package/dist/cjs/__tests__/queries.test.js +2 -0
  18. package/dist/cjs/__tests__/queries.test.js.map +1 -1
  19. package/dist/cjs/client/Client.d.ts +6 -4
  20. package/dist/cjs/client/Client.js +24 -16
  21. package/dist/cjs/client/Client.js.map +1 -1
  22. package/dist/cjs/client/ClientDescriptor.d.ts +15 -4
  23. package/dist/cjs/client/ClientDescriptor.js +117 -36
  24. package/dist/cjs/client/ClientDescriptor.js.map +1 -1
  25. package/dist/cjs/context.d.ts +1 -0
  26. package/dist/cjs/entities/DocumentFamiliyCache.d.ts +22 -2
  27. package/dist/cjs/entities/DocumentFamiliyCache.js +39 -21
  28. package/dist/cjs/entities/DocumentFamiliyCache.js.map +1 -1
  29. package/dist/cjs/entities/Entity.d.ts +7 -2
  30. package/dist/cjs/entities/Entity.js +33 -3
  31. package/dist/cjs/entities/Entity.js.map +1 -1
  32. package/dist/cjs/entities/EntityStore.d.ts +2 -1
  33. package/dist/cjs/entities/EntityStore.js +50 -20
  34. package/dist/cjs/entities/EntityStore.js.map +1 -1
  35. package/dist/cjs/idb.d.ts +2 -0
  36. package/dist/cjs/idb.js +9 -1
  37. package/dist/cjs/idb.js.map +1 -1
  38. package/dist/cjs/index.d.ts +1 -1
  39. package/dist/cjs/index.js +2 -1
  40. package/dist/cjs/index.js.map +1 -1
  41. package/dist/cjs/metadata/BaselinesStore.js +15 -5
  42. package/dist/cjs/metadata/BaselinesStore.js.map +1 -1
  43. package/dist/cjs/metadata/openMetadataDatabase.d.ts +11 -2
  44. package/dist/cjs/metadata/openMetadataDatabase.js +56 -3
  45. package/dist/cjs/metadata/openMetadataDatabase.js.map +1 -1
  46. package/dist/cjs/migration/db.d.ts +1 -1
  47. package/dist/cjs/migration/db.js +5 -2
  48. package/dist/cjs/migration/db.js.map +1 -1
  49. package/dist/cjs/migration/openDatabase.d.ts +8 -0
  50. package/dist/cjs/migration/openDatabase.js +217 -165
  51. package/dist/cjs/migration/openDatabase.js.map +1 -1
  52. package/dist/cjs/queries/BaseQuery.js +12 -1
  53. package/dist/cjs/queries/BaseQuery.js.map +1 -1
  54. package/dist/cjs/sync/Sync.d.ts +6 -5
  55. package/dist/cjs/sync/Sync.js.map +1 -1
  56. package/dist/cjs/sync/WebSocketSync.js +4 -3
  57. package/dist/cjs/sync/WebSocketSync.js.map +1 -1
  58. package/dist/esm/{entities/FakeWeakRef.d.ts → FakeWeakRef.d.ts} +2 -2
  59. package/dist/esm/{entities/FakeWeakRef.js → FakeWeakRef.js} +2 -2
  60. package/dist/esm/FakeWeakRef.js.map +1 -0
  61. package/dist/esm/IDBService.d.ts +1 -1
  62. package/dist/esm/IDBService.js +18 -1
  63. package/dist/esm/IDBService.js.map +1 -1
  64. package/dist/esm/__tests__/documents.test.js +17 -0
  65. package/dist/esm/__tests__/documents.test.js.map +1 -1
  66. package/dist/esm/__tests__/fixtures/testStorage.d.ts +1 -1
  67. package/dist/esm/__tests__/fixtures/testStorage.js +4 -3
  68. package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
  69. package/dist/esm/__tests__/mutations.test.d.ts +1 -0
  70. package/dist/esm/__tests__/mutations.test.js +40 -0
  71. package/dist/esm/__tests__/mutations.test.js.map +1 -0
  72. package/dist/esm/__tests__/queries.test.js +2 -0
  73. package/dist/esm/__tests__/queries.test.js.map +1 -1
  74. package/dist/esm/client/Client.d.ts +6 -4
  75. package/dist/esm/client/Client.js +25 -17
  76. package/dist/esm/client/Client.js.map +1 -1
  77. package/dist/esm/client/ClientDescriptor.d.ts +15 -4
  78. package/dist/esm/client/ClientDescriptor.js +121 -40
  79. package/dist/esm/client/ClientDescriptor.js.map +1 -1
  80. package/dist/esm/context.d.ts +1 -0
  81. package/dist/esm/entities/DocumentFamiliyCache.d.ts +22 -2
  82. package/dist/esm/entities/DocumentFamiliyCache.js +39 -21
  83. package/dist/esm/entities/DocumentFamiliyCache.js.map +1 -1
  84. package/dist/esm/entities/Entity.d.ts +7 -2
  85. package/dist/esm/entities/Entity.js +33 -3
  86. package/dist/esm/entities/Entity.js.map +1 -1
  87. package/dist/esm/entities/EntityStore.d.ts +2 -1
  88. package/dist/esm/entities/EntityStore.js +51 -21
  89. package/dist/esm/entities/EntityStore.js.map +1 -1
  90. package/dist/esm/idb.d.ts +2 -0
  91. package/dist/esm/idb.js +6 -0
  92. package/dist/esm/idb.js.map +1 -1
  93. package/dist/esm/index.d.ts +1 -1
  94. package/dist/esm/index.js +1 -1
  95. package/dist/esm/index.js.map +1 -1
  96. package/dist/esm/metadata/BaselinesStore.js +16 -6
  97. package/dist/esm/metadata/BaselinesStore.js.map +1 -1
  98. package/dist/esm/metadata/openMetadataDatabase.d.ts +11 -2
  99. package/dist/esm/metadata/openMetadataDatabase.js +54 -2
  100. package/dist/esm/metadata/openMetadataDatabase.js.map +1 -1
  101. package/dist/esm/migration/db.d.ts +1 -1
  102. package/dist/esm/migration/db.js +5 -2
  103. package/dist/esm/migration/db.js.map +1 -1
  104. package/dist/esm/migration/openDatabase.d.ts +8 -0
  105. package/dist/esm/migration/openDatabase.js +215 -164
  106. package/dist/esm/migration/openDatabase.js.map +1 -1
  107. package/dist/esm/queries/BaseQuery.js +12 -1
  108. package/dist/esm/queries/BaseQuery.js.map +1 -1
  109. package/dist/esm/sync/Sync.d.ts +6 -5
  110. package/dist/esm/sync/Sync.js.map +1 -1
  111. package/dist/esm/sync/WebSocketSync.js +4 -3
  112. package/dist/esm/sync/WebSocketSync.js.map +1 -1
  113. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  114. package/dist/tsconfig.tsbuildinfo +1 -1
  115. package/package.json +9 -4
  116. package/src/{entities/FakeWeakRef.ts → FakeWeakRef.ts} +2 -2
  117. package/src/IDBService.ts +20 -2
  118. package/src/__tests__/documents.test.ts +19 -0
  119. package/src/__tests__/fixtures/testStorage.ts +4 -7
  120. package/src/__tests__/mutations.test.ts +51 -0
  121. package/src/__tests__/queries.test.ts +3 -0
  122. package/src/client/Client.ts +29 -21
  123. package/src/client/ClientDescriptor.ts +176 -53
  124. package/src/context.ts +1 -0
  125. package/src/entities/DocumentFamiliyCache.ts +66 -21
  126. package/src/entities/Entity.ts +41 -6
  127. package/src/entities/EntityStore.ts +68 -21
  128. package/src/idb.ts +10 -0
  129. package/src/index.ts +1 -0
  130. package/src/metadata/BaselinesStore.ts +17 -6
  131. package/src/metadata/openMetadataDatabase.ts +96 -13
  132. package/src/migration/db.ts +14 -1
  133. package/src/migration/openDatabase.ts +350 -219
  134. package/src/queries/BaseQuery.ts +14 -1
  135. package/src/sync/Sync.ts +13 -9
  136. package/src/sync/WebSocketSync.ts +1 -0
  137. package/dist/cjs/entities/FakeWeakRef.js.map +0 -1
  138. package/dist/cjs/indexes.d.ts +0 -3
  139. package/dist/cjs/indexes.js +0 -20
  140. package/dist/cjs/indexes.js.map +0 -1
  141. package/dist/esm/entities/FakeWeakRef.js.map +0 -1
  142. package/dist/esm/indexes.d.ts +0 -3
  143. package/dist/esm/indexes.js +0 -15
  144. package/dist/esm/indexes.js.map +0 -1
  145. package/src/indexes.ts +0 -31
@@ -1,13 +1,29 @@
1
- import { EventSubscriber, Migration, StorageSchema } from '@verdant-web/common';
1
+ import {
2
+ EventSubscriber,
3
+ Migration,
4
+ StorageSchema,
5
+ hashObject,
6
+ } from '@verdant-web/common';
2
7
  import { Context } from '../context.js';
3
8
  import { FileManagerConfig } from '../files/FileManager.js';
4
9
  import { Metadata } from '../metadata/Metadata.js';
5
- import { openMetadataDatabase } from '../metadata/openMetadataDatabase.js';
6
- import { openDocumentDatabase } from '../migration/openDatabase.js';
10
+ import {
11
+ openMetadataDatabase,
12
+ openWIPMetadataDatabase,
13
+ } from '../metadata/openMetadataDatabase.js';
14
+ import {
15
+ openDocumentDatabase,
16
+ openWIPDocumentDatabase,
17
+ } from '../migration/openDatabase.js';
7
18
  import { ServerSyncOptions } from '../sync/Sync.js';
8
19
  import { UndoHistory } from '../UndoHistory.js';
9
20
  import { Client } from './Client.js';
10
- import { deleteAllDatabases } from '../idb.js';
21
+ import {
22
+ deleteAllDatabases,
23
+ deleteDatabase,
24
+ getAllDatabaseNamesAndVersions,
25
+ } from '../idb.js';
26
+ import { FakeWeakRef } from '../FakeWeakRef.js';
11
27
 
12
28
  export interface ClientDescriptorOptions<Presence = any, Profile = any> {
13
29
  /** The schema used to create this client */
@@ -43,6 +59,14 @@ export interface ClientDescriptorOptions<Presence = any, Profile = any> {
43
59
  * Configuration for file management
44
60
  */
45
61
  files?: FileManagerConfig;
62
+ /**
63
+ * Enables experimental WeakRef usage to cull documents
64
+ * from cache that aren't being used. This is a performance
65
+ * optimization which has been tested under all Verdant's test
66
+ * suites but I still want to keep testing it in the real world
67
+ * before turning it on.
68
+ */
69
+ EXPERIMENTAL_weakRefs?: boolean;
46
70
  }
47
71
 
48
72
  /**
@@ -50,13 +74,17 @@ export interface ClientDescriptorOptions<Presence = any, Profile = any> {
50
74
  * Storage creation promise and exposes some metadata which can
51
75
  * be useful immediately.
52
76
  */
53
- export class ClientDescriptor<Presence = any, Profile = any> {
54
- private readonly _readyPromise: Promise<Client>;
77
+ export class ClientDescriptor<
78
+ Presence = any,
79
+ Profile = any,
80
+ ClientImpl extends Client = Client,
81
+ > {
82
+ private readonly _readyPromise: Promise<ClientImpl>;
55
83
  // assertions because these are defined by plucking them from
56
84
  // Promise initializer
57
- private resolveReady!: (storage: Client) => void;
85
+ private resolveReady!: (storage: ClientImpl) => void;
58
86
  private rejectReady!: (err: Error) => void;
59
- private _resolvedValue: Client | undefined;
87
+ private _resolvedValue: ClientImpl | undefined;
60
88
  private _initializing = false;
61
89
  private _namespace: string;
62
90
 
@@ -88,51 +116,13 @@ export class ClientDescriptor<Presence = any, Profile = any> {
88
116
  }
89
117
  this._initializing = true;
90
118
  try {
91
- const metaDbName = [init.namespace, 'meta'].join('_');
92
- const { db: metaDb } = await openMetadataDatabase(this._namespace, {
93
- indexedDB: init.indexedDb,
94
- log: init.log,
95
- databaseName: metaDbName,
96
- });
97
-
98
- const context: Omit<Context, 'documentDb'> = {
99
- namespace: this._namespace,
100
- metaDb,
101
- schema: init.schema,
102
- log: init.log || (() => {}),
103
- undoHistory: init.undoHistory || new UndoHistory(),
104
- entityEvents: new EventSubscriber(),
105
- globalEvents: new EventSubscriber(),
106
- };
107
- const meta = new Metadata({
108
- context,
109
- disableRebasing: init.disableRebasing,
110
- });
111
-
112
- // verify schema integrity
113
- await meta.updateSchema(init.schema, init.overrideSchemaConflict);
114
-
115
- const documentDb = await openDocumentDatabase({
116
- context,
117
- version: init.schema.version,
118
- meta,
119
- migrations: init.migrations,
120
- indexedDB: init.indexedDb,
121
- });
122
-
123
- const fullContext: Context = Object.assign(context, { documentDb });
124
-
125
- const storage = new Client(
126
- {
127
- syncConfig: init.sync,
128
- migrations: init.migrations,
129
- files: init.files,
130
- },
131
- fullContext,
132
- {
133
- meta,
134
- },
135
- );
119
+ let storage: ClientImpl;
120
+ if (init.schema.wip) {
121
+ storage = await this.initializeWIPDatabases(init);
122
+ } else {
123
+ storage = await this.initializeDatabases(init);
124
+ this.cleanupWIPDatabases(init);
125
+ }
136
126
 
137
127
  this.resolveReady(storage);
138
128
  this._resolvedValue = storage;
@@ -145,6 +135,139 @@ export class ClientDescriptor<Presence = any, Profile = any> {
145
135
  }
146
136
  };
147
137
 
138
+ private initializeDatabases = async (init: ClientDescriptorOptions) => {
139
+ const { db: metaDb } = await openMetadataDatabase({
140
+ indexedDB: init.indexedDb,
141
+ log: init.log,
142
+ namespace: init.namespace,
143
+ });
144
+
145
+ const context: Omit<Context, 'documentDb'> = {
146
+ namespace: this._namespace,
147
+ metaDb,
148
+ schema: init.schema,
149
+ log: init.log || (() => {}),
150
+ undoHistory: init.undoHistory || new UndoHistory(),
151
+ entityEvents: new EventSubscriber(),
152
+ globalEvents: new EventSubscriber(),
153
+ weakRef: (value) => {
154
+ if (init.EXPERIMENTAL_weakRefs) {
155
+ return new WeakRef(value);
156
+ } else {
157
+ return new FakeWeakRef(value) as unknown as WeakRef<typeof value>;
158
+ }
159
+ },
160
+ };
161
+ const meta = new Metadata({
162
+ context,
163
+ disableRebasing: init.disableRebasing,
164
+ });
165
+
166
+ // verify schema integrity
167
+ await meta.updateSchema(init.schema, init.overrideSchemaConflict);
168
+
169
+ const documentDb = await openDocumentDatabase({
170
+ context,
171
+ version: init.schema.version,
172
+ meta,
173
+ migrations: init.migrations,
174
+ indexedDB: init.indexedDb,
175
+ });
176
+
177
+ const fullContext: Context = Object.assign(context, { documentDb });
178
+
179
+ const storage = new Client(
180
+ {
181
+ syncConfig: init.sync,
182
+ migrations: init.migrations,
183
+ files: init.files,
184
+ },
185
+ fullContext,
186
+ {
187
+ meta,
188
+ },
189
+ ) as ClientImpl;
190
+
191
+ return storage;
192
+ };
193
+
194
+ private initializeWIPDatabases = async (init: ClientDescriptorOptions) => {
195
+ const schemaHash = hashObject(init.schema);
196
+ console.info(`WIP schema in use. Opening database with hash ${schemaHash}`);
197
+
198
+ const wipNamespace = `@@wip_${init.namespace}_${schemaHash}`;
199
+ const { db: metaDb } = await openWIPMetadataDatabase({
200
+ indexedDB: init.indexedDb,
201
+ log: init.log,
202
+ namespace: init.namespace,
203
+ wipNamespace: wipNamespace,
204
+ });
205
+
206
+ const context: Omit<Context, 'documentDb'> = {
207
+ namespace: this._namespace,
208
+ metaDb,
209
+ schema: init.schema,
210
+ log: init.log || (() => {}),
211
+ undoHistory: init.undoHistory || new UndoHistory(),
212
+ entityEvents: new EventSubscriber(),
213
+ globalEvents: new EventSubscriber(),
214
+ weakRef: (value) => {
215
+ if (init.EXPERIMENTAL_weakRefs) {
216
+ return new WeakRef(value);
217
+ } else {
218
+ return new FakeWeakRef(value) as unknown as WeakRef<typeof value>;
219
+ }
220
+ },
221
+ };
222
+ const meta = new Metadata({
223
+ context,
224
+ disableRebasing: init.disableRebasing,
225
+ });
226
+
227
+ // verify schema integrity
228
+ await meta.updateSchema(init.schema, init.overrideSchemaConflict);
229
+
230
+ const documentDb = await openWIPDocumentDatabase({
231
+ context,
232
+ version: init.schema.version,
233
+ meta,
234
+ migrations: init.migrations,
235
+ indexedDB: init.indexedDb,
236
+ wipNamespace,
237
+ });
238
+
239
+ const fullContext: Context = Object.assign(context, { documentDb });
240
+
241
+ const storage = new Client(
242
+ {
243
+ syncConfig: init.sync,
244
+ migrations: init.migrations,
245
+ files: init.files,
246
+ },
247
+ fullContext,
248
+ {
249
+ meta,
250
+ },
251
+ ) as ClientImpl;
252
+
253
+ return storage;
254
+ };
255
+
256
+ private cleanupWIPDatabases = async (init: ClientDescriptorOptions) => {
257
+ const databaseInfo = await getAllDatabaseNamesAndVersions(init.indexedDb);
258
+ const wipDatabases = databaseInfo
259
+ .filter((db) => db.name?.startsWith('@@wip_'))
260
+ .map((db) => db.name!);
261
+ // don't clear a current WIP database.
262
+ const wipDatabasesToDelete = wipDatabases.filter(
263
+ (db) =>
264
+ !db.startsWith(`@@wip_${init.namespace}_${hashObject(init.schema)}`),
265
+ );
266
+ for (const db of wipDatabasesToDelete) {
267
+ await deleteDatabase(db, init.indexedDb);
268
+ }
269
+ };
270
+
148
271
  get current() {
149
272
  // exposing an immediate value if already resolved lets us
150
273
  // skip the promise microtask when accessing this externally if
package/src/context.ts CHANGED
@@ -26,4 +26,5 @@ export interface Context {
26
26
  */
27
27
  futureSeen: (timestamp: string) => void;
28
28
  }>;
29
+ weakRef<T extends object>(value: T): WeakRef<T>;
29
30
  }
@@ -11,9 +11,9 @@ import {
11
11
  } from '@verdant-web/common';
12
12
  import { Entity, refreshEntity, StoreTools } from './Entity.js';
13
13
  import type { EntityStore } from './EntityStore.js';
14
- import { WeakRef } from './FakeWeakRef.js';
15
14
  import { Context } from '../context.js';
16
15
  import { TaggedOperation } from '../types.js';
16
+ import { Resolvable } from '../utils/Resolvable.js';
17
17
 
18
18
  /**
19
19
  * Local operations: operations on this client that haven't
@@ -44,6 +44,14 @@ export class DocumentFamilyCache extends EventSubscriber<
44
44
  private context;
45
45
  private storeTools: StoreTools;
46
46
 
47
+ private _initialized = new Resolvable<boolean>();
48
+ get initializedPromise() {
49
+ return this._initialized.promise;
50
+ }
51
+ setInitialized = () => {
52
+ this._initialized.resolve(true);
53
+ };
54
+
47
55
  constructor({
48
56
  oid,
49
57
  store,
@@ -69,6 +77,10 @@ export class DocumentFamilyCache extends EventSubscriber<
69
77
  this.context = context;
70
78
  }
71
79
 
80
+ get weakRef() {
81
+ return this.context.weakRef;
82
+ }
83
+
72
84
  insertLocalOperations = (operations: Operation[]) => {
73
85
  const oidSet = new Set<ObjectIdentifier>();
74
86
  for (const operation of operations) {
@@ -236,6 +248,14 @@ export class DocumentFamilyCache extends EventSubscriber<
236
248
  };
237
249
 
238
250
  computeView = (oid: ObjectIdentifier) => {
251
+ if (
252
+ this.baselinesMap.size === 0 &&
253
+ this.operationsMap.size === 0 &&
254
+ this.localOperationsMap.size === 0
255
+ ) {
256
+ this.context.log('debug', `Entity ${oid} accessed with no data at all`);
257
+ return { view: null, deleted: true, lastTimestamp: null };
258
+ }
239
259
  const confirmed = this.computeConfirmedView(oid);
240
260
  const unconfirmedOperations = this.localOperationsMap.get(oid) || [];
241
261
  if (confirmed.empty && !unconfirmedOperations.length) {
@@ -303,6 +323,7 @@ export class DocumentFamilyCache extends EventSubscriber<
303
323
  oid: ObjectIdentifier,
304
324
  schema: StorageFieldSchema,
305
325
  parent?: Entity,
326
+ readonlyKeys?: string[],
306
327
  ): Entity => {
307
328
  let entityRef = this.entities.get(oid);
308
329
  let entity = entityRef?.deref();
@@ -313,10 +334,11 @@ export class DocumentFamilyCache extends EventSubscriber<
313
334
  fieldSchema: schema,
314
335
  store: this.storeTools,
315
336
  parent,
337
+ readonlyKeys,
316
338
  });
317
339
 
318
340
  // immediately add to cache and queue a removal if nobody subscribed
319
- this.entities.set(oid, new WeakRef(entity));
341
+ this.entities.set(oid, this.context.weakRef(entity));
320
342
  }
321
343
 
322
344
  return entity as any;
@@ -331,33 +353,56 @@ export class DocumentFamilyCache extends EventSubscriber<
331
353
  this.entities.clear();
332
354
  };
333
355
 
334
- reset = (
335
- operations: TaggedOperation[],
336
- baselines: DocumentBaseline[],
356
+ reset = ({
357
+ operations,
358
+ baselines,
359
+ dropExistingUnconfirmed: dropUnconfirmed = false,
360
+ unconfirmedOperations,
361
+ dropAll,
362
+ }: {
363
+ operations: TaggedOperation[];
364
+ unconfirmedOperations?: Operation[];
365
+ baselines: DocumentBaseline[];
337
366
  /**
338
367
  * Whether to drop operations which are only in-memory. Unconfirmed operations
339
368
  * will not be restored from storage until they are persisted, so it's not advisable
340
369
  * to use this unless the intention is to completely clear the entities.
341
370
  */
342
- dropUnconfirmed = false,
343
- ) => {
344
- const info = { isLocal: false, affectedOids: new Set<ObjectIdentifier>() };
345
- this.baselinesMap = new Map(
346
- baselines.map((baseline) => [baseline.oid, baseline]),
371
+ dropExistingUnconfirmed?: boolean;
372
+ /**
373
+ * Drop unconfirmed and confirmed data before resetting to incoming data.
374
+ * This is dangerous due to race conditions. Only use when a full reset is
375
+ * required.
376
+ */
377
+ dropAll?: boolean;
378
+ }) => {
379
+ this.context.log(
380
+ 'debug',
381
+ `Resetting cache for ${this.oid} with ${operations.length} ops and ${baselines.length} baselines, dropUnconfirmed=${dropUnconfirmed}`,
347
382
  );
348
- if (dropUnconfirmed) {
349
- this.operationsMap = new Map();
350
- } else {
351
- // clear out all confirmed operations, leaving only unconfirmed
352
- // which have been added in memory but not yet persisted in storage
353
- for (const oid of this.operationsMap.keys()) {
354
- this.operationsMap.set(
355
- oid,
356
- this.operationsMap.get(oid)?.filter((op) => !op.confirmed) ?? [],
357
- );
383
+ const info = { isLocal: false, affectedOids: new Set<ObjectIdentifier>() };
384
+
385
+ // NOTE: not clearing these maps... there are even more
386
+ // race conditions where we begin opening a cache, queue up a
387
+ // reset, then receive incoming operations before the reset
388
+ // actually hits this function, so those incoming ops are
389
+ // dropped.
390
+ // FIXME: include this in a future refactor of this
391
+ // whole system.
392
+
393
+ if (dropAll) this.baselinesMap.clear();
394
+ this.insertBaselines(baselines, info);
395
+
396
+ if (dropAll) this.operationsMap.clear();
397
+ this.insertOperations(operations, info);
398
+
399
+ if (unconfirmedOperations || dropUnconfirmed) {
400
+ this.localOperationsMap.clear();
401
+ if (unconfirmedOperations) {
402
+ this.insertLocalOperations(unconfirmedOperations);
358
403
  }
359
404
  }
360
- this.insertOperations(operations, info);
405
+
361
406
  for (const oid of this.entities.keys()) {
362
407
  const entityRef = this.entities.get(oid);
363
408
  const entity = entityRef?.deref();
@@ -6,6 +6,7 @@ import {
6
6
  decomposeOid,
7
7
  EventSubscriber,
8
8
  FileData,
9
+ FileRef,
9
10
  isFileRef,
10
11
  isObjectRef,
11
12
  maybeGetOid,
@@ -19,7 +20,6 @@ import {
19
20
  } from '@verdant-web/common';
20
21
  import { EntityFile } from '../files/EntityFile.js';
21
22
  import { processValueFiles } from '../files/utils.js';
22
- import { WeakRef } from './FakeWeakRef.js';
23
23
 
24
24
  export const ADD_OPERATIONS = '@@addOperations';
25
25
  export const DELETE = '@@delete';
@@ -39,6 +39,7 @@ export interface CacheTools {
39
39
  parent?: Entity,
40
40
  ): Entity;
41
41
  hasOid(oid: ObjectIdentifier): boolean;
42
+ weakRef<T extends object>(value: T): WeakRef<T>;
42
43
  }
43
44
 
44
45
  export interface StoreTools {
@@ -103,10 +104,11 @@ type EntityEvents = {
103
104
  type BaseEntityValue = { [Key: string]: any } | any[];
104
105
 
105
106
  export class Entity<
106
- Init = any,
107
- KeyValue extends BaseEntityValue = any,
108
- Snapshot extends any = DataFromInit<Init>,
109
- > implements
107
+ Init = any,
108
+ KeyValue extends BaseEntityValue = any,
109
+ Snapshot extends any = DataFromInit<Init>,
110
+ >
111
+ implements
110
112
  ObjectEntity<Init, KeyValue, Snapshot>,
111
113
  ListEntity<Init, KeyValue, Snapshot>
112
114
  {
@@ -120,6 +122,7 @@ export class Entity<
120
122
  protected readonly cache: CacheTools;
121
123
  protected _deleted = false;
122
124
  protected parent: WeakRef<Entity<any, any>> | undefined;
125
+ protected readonly readonlyKeys: (keyof Init)[];
123
126
 
124
127
  private cachedSnapshot: any = null;
125
128
  private cachedDestructure: KeyValue | null = null;
@@ -205,6 +208,7 @@ export class Entity<
205
208
  cache,
206
209
  parent,
207
210
  onAllUnsubscribed,
211
+ readonlyKeys = [],
208
212
  }: {
209
213
  oid: ObjectIdentifier;
210
214
  store: StoreTools;
@@ -212,14 +216,16 @@ export class Entity<
212
216
  cache: CacheTools;
213
217
  parent?: Entity<any, any>;
214
218
  onAllUnsubscribed?: () => void;
219
+ readonlyKeys?: (keyof Init)[];
215
220
  }) {
216
221
  this.oid = oid;
217
222
  const { collection } = decomposeOid(oid);
218
223
  this.collection = collection;
219
- this.parent = parent && new WeakRef(parent);
220
224
  this.store = store;
221
225
  this.fieldSchema = fieldSchema;
226
+ this.readonlyKeys = readonlyKeys;
222
227
  this.cache = cache;
228
+ this.parent = parent && this.cache.weakRef(parent);
223
229
  const { view, deleted, lastTimestamp } = this.cache.computeView(oid);
224
230
  this._current = view;
225
231
  this._deleted = deleted;
@@ -402,6 +408,17 @@ export class Entity<
402
408
  return result;
403
409
  };
404
410
 
411
+ private getFileSnapshot(item: FileRef) {
412
+ const file = this.store.getFile(item.id);
413
+ if (file.url) {
414
+ return { id: item.id, url: file.url };
415
+ } else if (file.loading || file.failed) {
416
+ return { id: item.id, url: undefined };
417
+ } else {
418
+ return { id: item.id, url: null };
419
+ }
420
+ }
421
+
405
422
  /**
406
423
  * Returns a copy of the entity and all sub-objects as
407
424
  * a plain object or array.
@@ -421,6 +438,8 @@ export class Entity<
421
438
  snapshot = this.value.map((item, idx) => {
422
439
  if (isObjectRef(item)) {
423
440
  return this.getSubObject(item.id, idx)?.getSnapshot();
441
+ } else if (isFileRef(item)) {
442
+ return this.getFileSnapshot(item);
424
443
  }
425
444
  return item;
426
445
  }) as Snapshot;
@@ -429,6 +448,8 @@ export class Entity<
429
448
  for (const [key, value] of Object.entries(snapshot)) {
430
449
  if (isObjectRef(value)) {
431
450
  snapshot[key] = this.getSubObject(value.id, key)?.getSnapshot();
451
+ } else if (isFileRef(value)) {
452
+ snapshot[key] = this.getFileSnapshot(value);
432
453
  }
433
454
  }
434
455
  }
@@ -451,6 +472,9 @@ export class Entity<
451
472
  return Object.values(this.getAll());
452
473
  };
453
474
  set = <Key extends keyof Init>(key: Key, value: Init[Key]) => {
475
+ if (this.readonlyKeys.includes(key)) {
476
+ throw new Error(`Cannot set readonly key ${key.toString()}`);
477
+ }
454
478
  this.addPatches(
455
479
  this.store.patchCreator.createSet(
456
480
  this.oid,
@@ -480,6 +504,9 @@ export class Entity<
480
504
  }
481
505
  };
482
506
  private getDeleteMode = (key: any) => {
507
+ if (this.readonlyKeys.includes(key)) {
508
+ return false;
509
+ }
483
510
  // 'any' is always deletable, and map values can be removed completely
484
511
  if (this.fieldSchema.type === 'any' || this.fieldSchema.type === 'map') {
485
512
  return 'delete';
@@ -532,6 +559,9 @@ export class Entity<
532
559
  );
533
560
  }
534
561
  for (const [key, field] of Object.entries(value)) {
562
+ if (this.readonlyKeys.includes(key as any)) {
563
+ throw new Error(`Cannot set readonly key ${key.toString()}`);
564
+ }
535
565
  const fieldSchema = this.getChildFieldSchema(key);
536
566
  if (fieldSchema) {
537
567
  traverseCollectionFieldsAndApplyDefaults(field, fieldSchema);
@@ -718,6 +748,10 @@ export class Entity<
718
748
  find = (predicate: (value: ListItemValue<KeyValue>) => boolean) => {
719
749
  return this.getAsWrapped().find(predicate);
720
750
  };
751
+
752
+ includes = (item: ListItemValue<KeyValue>) => {
753
+ return this.has(item);
754
+ };
721
755
  }
722
756
 
723
757
  export interface BaseEntity<
@@ -786,6 +820,7 @@ export interface ListEntity<
786
820
  find(
787
821
  predicate: (value: ListItemValue<Value>) => boolean,
788
822
  ): ListItemValue<Value> | undefined;
823
+ includes(value: ListItemValue<Value>): boolean;
789
824
  }
790
825
 
791
826
  export type AnyEntity<