@verdant-web/store 4.0.0 → 4.1.0-alpha.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 (171) hide show
  1. package/LICENSE +21 -650
  2. package/dist/bundle/index.js +11 -11
  3. package/dist/bundle/index.js.map +4 -4
  4. package/dist/esm/__tests__/fixtures/testStorage.d.ts +1 -2
  5. package/dist/esm/__tests__/fixtures/testStorage.js +3 -5
  6. package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
  7. package/dist/esm/client/Client.d.ts +6 -2
  8. package/dist/esm/client/Client.js +18 -6
  9. package/dist/esm/client/Client.js.map +1 -1
  10. package/dist/esm/client/ClientDescriptor.d.ts +7 -5
  11. package/dist/esm/client/ClientDescriptor.js +18 -4
  12. package/dist/esm/client/ClientDescriptor.js.map +1 -1
  13. package/dist/esm/context/ShutdownHandler.d.ts +8 -0
  14. package/dist/esm/context/ShutdownHandler.js +24 -0
  15. package/dist/esm/context/ShutdownHandler.js.map +1 -0
  16. package/dist/esm/context/context.d.ts +15 -4
  17. package/dist/esm/entities/EntityStore.js +6 -3
  18. package/dist/esm/entities/EntityStore.js.map +1 -1
  19. package/dist/esm/files/EntityFile.d.ts +1 -0
  20. package/dist/esm/files/EntityFile.js +16 -11
  21. package/dist/esm/files/EntityFile.js.map +1 -1
  22. package/dist/esm/files/FileManager.d.ts +1 -3
  23. package/dist/esm/files/FileManager.js +12 -10
  24. package/dist/esm/files/FileManager.js.map +1 -1
  25. package/dist/esm/index.d.ts +4 -5
  26. package/dist/esm/index.js +2 -3
  27. package/dist/esm/index.js.map +1 -1
  28. package/dist/esm/internal.d.ts +6 -0
  29. package/dist/esm/internal.js +5 -0
  30. package/dist/esm/internal.js.map +1 -0
  31. package/dist/esm/persistence/MessageCreator.d.ts +3 -1
  32. package/dist/esm/persistence/MessageCreator.js +58 -55
  33. package/dist/esm/persistence/MessageCreator.js.map +1 -1
  34. package/dist/esm/persistence/PersistenceFiles.d.ts +8 -21
  35. package/dist/esm/persistence/PersistenceFiles.js +44 -30
  36. package/dist/esm/persistence/PersistenceFiles.js.map +1 -1
  37. package/dist/esm/persistence/PersistenceMetadata.d.ts +12 -11
  38. package/dist/esm/persistence/PersistenceMetadata.js +201 -137
  39. package/dist/esm/persistence/PersistenceMetadata.js.map +1 -1
  40. package/dist/esm/persistence/PersistenceQueries.d.ts +10 -11
  41. package/dist/esm/persistence/PersistenceQueries.js +33 -5
  42. package/dist/esm/persistence/PersistenceQueries.js.map +1 -1
  43. package/dist/esm/persistence/PersistenceRebaser.d.ts +5 -9
  44. package/dist/esm/persistence/PersistenceRebaser.js +63 -47
  45. package/dist/esm/persistence/PersistenceRebaser.js.map +1 -1
  46. package/dist/esm/persistence/idb/IdbService.d.ts +0 -1
  47. package/dist/esm/persistence/idb/IdbService.js +28 -16
  48. package/dist/esm/persistence/idb/IdbService.js.map +1 -1
  49. package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.d.ts +11 -31
  50. package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.js +31 -36
  51. package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.js.map +1 -1
  52. package/dist/esm/persistence/idb/idbPersistence.d.ts +17 -9
  53. package/dist/esm/persistence/idb/idbPersistence.js +80 -39
  54. package/dist/esm/persistence/idb/idbPersistence.js.map +1 -1
  55. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.d.ts +7 -10
  56. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js +45 -71
  57. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js.map +1 -1
  58. package/dist/esm/persistence/idb/metadata/openMetadataDatabase.d.ts +1 -12
  59. package/dist/esm/persistence/idb/metadata/openMetadataDatabase.js +3 -56
  60. package/dist/esm/persistence/idb/metadata/openMetadataDatabase.js.map +1 -1
  61. package/dist/esm/persistence/idb/queries/{IdbQueryDb.d.ts → IdbDocumentDb.d.ts} +7 -13
  62. package/dist/esm/persistence/idb/queries/{IdbQueryDb.js → IdbDocumentDb.js} +15 -32
  63. package/dist/esm/persistence/idb/queries/IdbDocumentDb.js.map +1 -0
  64. package/dist/esm/persistence/idb/queries/migration/db.d.ts +3 -5
  65. package/dist/esm/persistence/idb/queries/migration/db.js +13 -28
  66. package/dist/esm/persistence/idb/queries/migration/db.js.map +1 -1
  67. package/dist/esm/persistence/idb/util.d.ts +8 -4
  68. package/dist/esm/persistence/idb/util.js +64 -21
  69. package/dist/esm/persistence/idb/util.js.map +1 -1
  70. package/dist/esm/persistence/interfaces.d.ts +68 -75
  71. package/dist/esm/persistence/{idb/queries/migration → migration}/engine.d.ts +4 -7
  72. package/dist/esm/persistence/{idb/queries/migration → migration}/engine.js +18 -10
  73. package/dist/esm/persistence/migration/engine.js.map +1 -0
  74. package/dist/esm/persistence/migration/finalize.d.ts +9 -0
  75. package/dist/esm/persistence/migration/finalize.js +75 -0
  76. package/dist/esm/persistence/migration/finalize.js.map +1 -0
  77. package/dist/esm/persistence/migration/migrate.d.ts +12 -0
  78. package/dist/esm/persistence/migration/migrate.js +89 -0
  79. package/dist/esm/persistence/migration/migrate.js.map +1 -0
  80. package/dist/esm/persistence/migration/paths.js.map +1 -0
  81. package/dist/esm/persistence/migration/paths.test.js.map +1 -0
  82. package/dist/esm/persistence/migration/types.d.ts +3 -0
  83. package/dist/esm/persistence/migration/types.js.map +1 -0
  84. package/dist/esm/persistence/persistence.js +25 -15
  85. package/dist/esm/persistence/persistence.js.map +1 -1
  86. package/dist/esm/queries/FindAllQuery.js +1 -1
  87. package/dist/esm/queries/FindAllQuery.js.map +1 -1
  88. package/dist/esm/queries/FindInfiniteQuery.js +2 -2
  89. package/dist/esm/queries/FindInfiniteQuery.js.map +1 -1
  90. package/dist/esm/queries/FindOneQuery.js +1 -1
  91. package/dist/esm/queries/FindOneQuery.js.map +1 -1
  92. package/dist/esm/queries/FindPageQuery.js +1 -1
  93. package/dist/esm/queries/FindPageQuery.js.map +1 -1
  94. package/dist/esm/sync/FileSync.js +3 -3
  95. package/dist/esm/sync/FileSync.js.map +1 -1
  96. package/dist/esm/sync/PushPullSync.d.ts +2 -3
  97. package/dist/esm/sync/PushPullSync.js +4 -2
  98. package/dist/esm/sync/PushPullSync.js.map +1 -1
  99. package/dist/esm/sync/ServerSyncEndpointProvider.d.ts +3 -7
  100. package/dist/esm/sync/ServerSyncEndpointProvider.js +3 -2
  101. package/dist/esm/sync/ServerSyncEndpointProvider.js.map +1 -1
  102. package/dist/esm/sync/Sync.d.ts +6 -1
  103. package/dist/esm/sync/Sync.js +12 -4
  104. package/dist/esm/sync/Sync.js.map +1 -1
  105. package/dist/esm/sync/WebSocketSync.js +10 -4
  106. package/dist/esm/sync/WebSocketSync.js.map +1 -1
  107. package/package.json +6 -2
  108. package/src/__tests__/fixtures/testStorage.ts +6 -6
  109. package/src/client/Client.ts +26 -8
  110. package/src/client/ClientDescriptor.ts +27 -9
  111. package/src/context/ShutdownHandler.ts +26 -0
  112. package/src/context/context.ts +16 -4
  113. package/src/entities/EntityStore.ts +9 -3
  114. package/src/files/EntityFile.ts +11 -6
  115. package/src/files/FileManager.ts +13 -10
  116. package/src/index.ts +8 -9
  117. package/src/internal.ts +27 -0
  118. package/src/persistence/MessageCreator.ts +79 -73
  119. package/src/persistence/PersistenceFiles.ts +57 -31
  120. package/src/persistence/PersistenceMetadata.ts +287 -195
  121. package/src/persistence/PersistenceQueries.ts +45 -9
  122. package/src/persistence/PersistenceRebaser.ts +105 -70
  123. package/src/persistence/idb/IdbService.ts +40 -22
  124. package/src/persistence/idb/files/IdbPersistenceFileDb.ts +30 -62
  125. package/src/persistence/idb/idbPersistence.ts +123 -47
  126. package/src/persistence/idb/metadata/IdbMetadataDb.ts +75 -97
  127. package/src/persistence/idb/metadata/openMetadataDatabase.ts +2 -96
  128. package/src/persistence/idb/queries/{IdbQueryDb.ts → IdbDocumentDb.ts} +17 -57
  129. package/src/persistence/idb/queries/migration/db.ts +20 -39
  130. package/src/persistence/idb/util.ts +84 -21
  131. package/src/persistence/interfaces.ts +89 -90
  132. package/src/persistence/{idb/queries/migration → migration}/engine.ts +30 -15
  133. package/src/persistence/migration/finalize.ts +126 -0
  134. package/src/persistence/migration/migrate.ts +169 -0
  135. package/src/persistence/migration/types.ts +4 -0
  136. package/src/persistence/persistence.ts +37 -14
  137. package/src/queries/FindAllQuery.ts +1 -1
  138. package/src/queries/FindInfiniteQuery.ts +2 -2
  139. package/src/queries/FindOneQuery.ts +1 -1
  140. package/src/queries/FindPageQuery.ts +1 -1
  141. package/src/sync/FileSync.ts +21 -15
  142. package/src/sync/PushPullSync.ts +3 -4
  143. package/src/sync/ServerSyncEndpointProvider.ts +6 -8
  144. package/src/sync/Sync.ts +20 -7
  145. package/src/sync/WebSocketSync.ts +10 -4
  146. package/dist/esm/client/constants.d.ts +0 -1
  147. package/dist/esm/client/constants.js +0 -2
  148. package/dist/esm/client/constants.js.map +0 -1
  149. package/dist/esm/persistence/idb/queries/IdbQueryDb.js.map +0 -1
  150. package/dist/esm/persistence/idb/queries/migration/engine.js.map +0 -1
  151. package/dist/esm/persistence/idb/queries/migration/migrations.d.ts +0 -15
  152. package/dist/esm/persistence/idb/queries/migration/migrations.js +0 -243
  153. package/dist/esm/persistence/idb/queries/migration/migrations.js.map +0 -1
  154. package/dist/esm/persistence/idb/queries/migration/openQueryDatabase.d.ts +0 -8
  155. package/dist/esm/persistence/idb/queries/migration/openQueryDatabase.js +0 -24
  156. package/dist/esm/persistence/idb/queries/migration/openQueryDatabase.js.map +0 -1
  157. package/dist/esm/persistence/idb/queries/migration/paths.js.map +0 -1
  158. package/dist/esm/persistence/idb/queries/migration/paths.test.js.map +0 -1
  159. package/dist/esm/persistence/idb/queries/migration/types.d.ts +0 -6
  160. package/dist/esm/persistence/idb/queries/migration/types.js.map +0 -1
  161. package/src/client/constants.ts +0 -1
  162. package/src/persistence/idb/queries/migration/migrations.ts +0 -345
  163. package/src/persistence/idb/queries/migration/openQueryDatabase.ts +0 -54
  164. package/src/persistence/idb/queries/migration/types.ts +0 -8
  165. /package/dist/esm/persistence/{idb/queries/migration → migration}/paths.d.ts +0 -0
  166. /package/dist/esm/persistence/{idb/queries/migration → migration}/paths.js +0 -0
  167. /package/dist/esm/persistence/{idb/queries/migration → migration}/paths.test.d.ts +0 -0
  168. /package/dist/esm/persistence/{idb/queries/migration → migration}/paths.test.js +0 -0
  169. /package/dist/esm/persistence/{idb/queries/migration → migration}/types.js +0 -0
  170. /package/src/persistence/{idb/queries/migration → migration}/paths.test.ts +0 -0
  171. /package/src/persistence/{idb/queries/migration → migration}/paths.ts +0 -0
@@ -14,15 +14,16 @@ import {
14
14
  AbstractTransaction,
15
15
  ClientOperation,
16
16
  CommonQueryOptions,
17
+ LocalReplicaInfo,
17
18
  MetadataExport,
18
19
  PersistenceMetadataDb,
19
20
  } from './interfaces.js';
20
21
  import { InitialContext } from '../context/context.js';
21
22
  import { PersistenceRebaser } from './PersistenceRebaser.js';
22
23
  import { MessageCreator } from './MessageCreator.js';
23
- import { Disposable } from '../utils/Disposable.js';
24
+ import cuid from 'cuid';
24
25
 
25
- export class PersistenceMetadata extends Disposable {
26
+ export class PersistenceMetadata {
26
27
  private rebaser: PersistenceRebaser;
27
28
  /** Available to others, like sync... */
28
29
  readonly messageCreator: MessageCreator;
@@ -30,14 +31,9 @@ export class PersistenceMetadata extends Disposable {
30
31
  syncMessage: (message: ClientMessage) => void;
31
32
  }>();
32
33
 
33
- constructor(
34
- private db: PersistenceMetadataDb,
35
- private ctx: InitialContext,
36
- ) {
37
- super();
38
- this.rebaser = new PersistenceRebaser(db, ctx);
39
- this.messageCreator = new MessageCreator(db, ctx);
40
- this.compose(this.db);
34
+ constructor(private db: PersistenceMetadataDb, private ctx: InitialContext) {
35
+ this.rebaser = new PersistenceRebaser(db, this, ctx);
36
+ this.messageCreator = new MessageCreator(db, this, ctx);
41
37
  }
42
38
 
43
39
  private insertOperations = async (
@@ -60,7 +56,10 @@ export class PersistenceMetadata extends Disposable {
60
56
  }
61
57
 
62
58
  // we can now enqueue and check for rebase opportunities
63
- if (!this.ctx.config.persistence?.disableRebasing) {
59
+ if (
60
+ !this.ctx.config.persistence?.disableRebasing &&
61
+ !this.ctx.pauseRebasing
62
+ ) {
64
63
  this.rebaser.tryAutonomousRebase();
65
64
  }
66
65
 
@@ -78,7 +77,10 @@ export class PersistenceMetadata extends Disposable {
78
77
  (operation as ClientOperation).isLocal = true;
79
78
  }
80
79
  await this.insertOperations(operations as ClientOperation[], options);
81
-
80
+ this.ctx.log(
81
+ 'debug',
82
+ `Inserted ${operations.length} local operations; sending sync message`,
83
+ );
82
84
  const message = await this.messageCreator.createOperation({ operations });
83
85
  this.events.emit('syncMessage', message);
84
86
  };
@@ -124,72 +126,80 @@ export class PersistenceMetadata extends Disposable {
124
126
  assert(documentOid === rootOid, 'Must be root document OID');
125
127
  oids.add(documentOid);
126
128
  // readwrite mode to block on other write transactions
127
- const transaction = this.db.transaction({
128
- storeNames: ['baselines', 'operations'],
129
- });
130
- await Promise.all([
131
- this.db.iterateDocumentBaselines(
132
- documentOid,
133
- (baseline) => {
134
- oids.add(baseline.oid);
135
- },
136
- { transaction },
137
- ),
138
- this.db.iterateDocumentOperations(
139
- documentOid,
140
- (patch) => {
141
- oids.add(patch.oid);
142
- },
143
- { transaction },
144
- ),
145
- ]);
146
- const authz = await this.getDocumentAuthz(documentOid);
147
- const ops = new Array<Operation>();
148
- for (const oid of oids) {
149
- ops.push({
150
- oid,
151
- timestamp: this.ctx.time.now,
152
- data: { op: 'delete' },
153
- authz,
154
- });
155
- }
156
- return this.insertLocalOperations(ops);
129
+ return this.db.transaction(
130
+ {
131
+ storeNames: ['baselines', 'operations'],
132
+ },
133
+ async (transaction) => {
134
+ await Promise.all([
135
+ this.db.iterateDocumentBaselines(
136
+ documentOid,
137
+ (baseline) => {
138
+ oids.add(baseline.oid);
139
+ },
140
+ { transaction },
141
+ ),
142
+ this.db.iterateDocumentOperations(
143
+ documentOid,
144
+ (patch) => {
145
+ oids.add(patch.oid);
146
+ },
147
+ { transaction },
148
+ ),
149
+ ]);
150
+ const authz = await this.getDocumentAuthz(documentOid);
151
+ const ops = new Array<Operation>();
152
+ for (const oid of oids) {
153
+ ops.push({
154
+ oid,
155
+ timestamp: this.ctx.time.now,
156
+ data: { op: 'delete' },
157
+ authz,
158
+ });
159
+ }
160
+ return this.insertLocalOperations(ops, { transaction });
161
+ },
162
+ );
157
163
  };
158
164
 
159
165
  deleteCollection = async (collection: string) => {
160
166
  const oids = new Set<ObjectIdentifier>();
161
- const transaction = this.db.transaction({
162
- storeNames: ['baselines', 'operations'],
163
- mode: 'readwrite',
164
- });
165
- await Promise.all([
166
- this.db.iterateCollectionBaselines(
167
- collection,
168
- (baseline) => {
169
- oids.add(baseline.oid);
170
- },
171
- { transaction },
172
- ),
173
- this.db.iterateCollectionOperations(
174
- collection,
175
- (patch) => {
176
- oids.add(patch.oid);
177
- },
178
- { transaction },
179
- ),
180
- ]);
181
-
182
- const ops = new Array<Operation>();
183
- for (const oid of oids) {
184
- ops.push({
185
- oid,
186
- timestamp: this.ctx.time.now,
187
- data: { op: 'delete' },
188
- authz: undefined,
189
- });
190
- }
167
+ return this.db.transaction(
168
+ {
169
+ storeNames: ['baselines', 'operations'],
170
+ mode: 'readwrite',
171
+ },
172
+ async (transaction) => {
173
+ await Promise.all([
174
+ this.db.iterateCollectionBaselines(
175
+ collection,
176
+ (baseline) => {
177
+ oids.add(baseline.oid);
178
+ },
179
+ { transaction },
180
+ ),
181
+ this.db.iterateCollectionOperations(
182
+ collection,
183
+ (patch) => {
184
+ oids.add(patch.oid);
185
+ },
186
+ { transaction },
187
+ ),
188
+ ]);
189
+
190
+ const ops = new Array<Operation>();
191
+ for (const oid of oids) {
192
+ ops.push({
193
+ oid,
194
+ timestamp: this.ctx.time.now,
195
+ data: { op: 'delete' },
196
+ authz: undefined,
197
+ });
198
+ }
191
199
 
192
- return this.insertLocalOperations(ops);
200
+ return this.insertLocalOperations(ops, { transaction });
201
+ },
202
+ );
193
203
  };
194
204
 
195
205
  getDocumentSnapshot = async (
@@ -198,83 +208,91 @@ export class PersistenceMetadata extends Disposable {
198
208
  ) => {
199
209
  const documentOid = getOidRoot(oid);
200
210
  assert(documentOid === oid, 'Must be root document OID');
201
- const transaction = this.db.transaction({
202
- storeNames: ['baselines', 'operations'],
203
- mode: 'readwrite',
204
- });
205
- const baselines: DocumentBaseline[] = [];
206
- await this.db.iterateDocumentBaselines(
207
- documentOid,
208
- (b) => {
209
- baselines.push(b);
210
- },
211
+ return this.db.transaction(
211
212
  {
212
- transaction,
213
+ storeNames: ['baselines', 'operations'],
214
+ mode: 'readwrite',
213
215
  },
214
- );
215
- const objectMap = new Map<ObjectIdentifier, any>();
216
- for (const baseline of baselines) {
217
- if (baseline.snapshot) {
218
- assignOid(baseline.snapshot, baseline.oid);
219
- }
220
- objectMap.set(baseline.oid, baseline.snapshot);
221
- }
222
- await this.db.iterateDocumentOperations(
223
- documentOid,
224
- (op) => {
225
- const obj = objectMap.get(op.oid) || undefined;
226
- const newObj = applyPatch(obj, op.data);
227
- if (newObj) {
228
- assignOid(newObj, op.oid);
216
+ async (transaction) => {
217
+ const baselines: DocumentBaseline[] = [];
218
+ await this.db.iterateDocumentBaselines(
219
+ documentOid,
220
+ (b) => {
221
+ baselines.push(b);
222
+ },
223
+ {
224
+ transaction,
225
+ },
226
+ );
227
+ const objectMap = new Map<ObjectIdentifier, any>();
228
+ for (const baseline of baselines) {
229
+ if (baseline.snapshot) {
230
+ assignOid(baseline.snapshot, baseline.oid);
231
+ }
232
+ objectMap.set(baseline.oid, baseline.snapshot);
229
233
  }
230
- objectMap.set(op.oid, newObj);
231
- },
232
- {
233
- transaction,
234
- // only apply operations up to the current time
235
- to: options.to || this.ctx.time.now,
234
+ await this.db.iterateDocumentOperations(
235
+ documentOid,
236
+ (op) => {
237
+ const obj = objectMap.get(op.oid) || undefined;
238
+ const newObj = applyPatch(obj, op.data);
239
+ if (newObj) {
240
+ assignOid(newObj, op.oid);
241
+ }
242
+ objectMap.set(op.oid, newObj);
243
+ },
244
+ {
245
+ transaction,
246
+ // only apply operations up to the current time
247
+ to: options.to || this.ctx.time.now,
248
+ },
249
+ );
250
+ const root = objectMap.get(documentOid);
251
+ if (root) {
252
+ substituteRefsWithObjects(root, objectMap);
253
+ }
254
+ return root;
236
255
  },
237
256
  );
238
- const root = objectMap.get(documentOid);
239
- if (root) {
240
- substituteRefsWithObjects(root, objectMap);
241
- }
242
- return root;
243
257
  };
244
258
 
245
259
  getDocumentData = async (
246
260
  oid: ObjectIdentifier,
247
261
  options?: { abort?: AbortSignal },
248
262
  ) => {
249
- const transaction = this.db.transaction({
250
- storeNames: ['baselines', 'operations'],
251
- abort: options?.abort,
252
- });
253
- const baselines: DocumentBaseline[] = [];
254
- const operations: Record<ObjectIdentifier, Operation[]> = {};
255
- await Promise.all([
256
- this.db.iterateDocumentBaselines(
257
- oid,
258
- (baseline) => {
259
- baselines.push(baseline);
260
- },
261
- {
262
- transaction,
263
- },
264
- ),
265
- this.db.iterateDocumentOperations(
266
- oid,
267
- (op) => {
268
- operations[op.oid] ??= [];
269
- operations[op.oid].push(op);
270
- },
271
- { transaction },
272
- ),
273
- ]);
274
- return {
275
- baselines,
276
- operations,
277
- };
263
+ return this.db.transaction(
264
+ {
265
+ storeNames: ['baselines', 'operations'],
266
+ abort: options?.abort,
267
+ },
268
+ async (transaction) => {
269
+ const baselines: DocumentBaseline[] = [];
270
+ const operations: Record<ObjectIdentifier, Operation[]> = {};
271
+ await Promise.all([
272
+ this.db.iterateDocumentBaselines(
273
+ oid,
274
+ (baseline) => {
275
+ baselines.push(baseline);
276
+ },
277
+ {
278
+ transaction,
279
+ },
280
+ ),
281
+ this.db.iterateDocumentOperations(
282
+ oid,
283
+ (op) => {
284
+ operations[op.oid] ??= [];
285
+ operations[op.oid].push(op);
286
+ },
287
+ { transaction },
288
+ ),
289
+ ]);
290
+ return {
291
+ baselines,
292
+ operations,
293
+ };
294
+ },
295
+ );
278
296
  };
279
297
 
280
298
  getDocumentAuthz = async (oid: ObjectIdentifier) => {
@@ -296,28 +314,37 @@ export class PersistenceMetadata extends Disposable {
296
314
  },
297
315
  options?: { abort?: AbortSignal },
298
316
  ) => {
299
- const transaction = this.db.transaction({
300
- storeNames: ['baselines', 'operations'],
301
- abort: options?.abort,
302
- mode: 'readwrite',
303
- });
304
- if (data.baselines) {
305
- await this.insertRemoteBaselines(data.baselines, { transaction });
306
- }
307
- if (options?.abort?.aborted) return;
308
- if (data.operations) {
309
- if (data.isLocal) {
310
- await this.insertLocalOperations(data.operations, { transaction });
311
- } else {
312
- await this.insertRemoteOperations(data.operations, { transaction });
313
- }
314
- }
317
+ return this.db.transaction(
318
+ {
319
+ storeNames: ['baselines', 'operations'],
320
+ abort: options?.abort,
321
+ mode: 'readwrite',
322
+ },
323
+ async (transaction) => {
324
+ this.ctx.log('debug', 'Begin insert data transaction');
325
+ if (data.baselines) {
326
+ await this.insertRemoteBaselines(data.baselines, { transaction });
327
+ }
328
+ this.ctx.log('debug', 'Inserted baselines (if any)');
329
+ if (options?.abort?.aborted) throw new Error('Aborted');
330
+ if (data.operations) {
331
+ if (data.isLocal) {
332
+ this.ctx.log('debug', 'Inserting local operations');
333
+ await this.insertLocalOperations(data.operations, { transaction });
334
+ } else {
335
+ this.ctx.log('debug', 'Inserting remote operations');
336
+ await this.insertRemoteOperations(data.operations, { transaction });
337
+ }
338
+ }
339
+ this.ctx.log('debug', 'End insert data transaction');
340
+ },
341
+ );
315
342
  };
316
343
 
317
344
  updateLastSynced = async (timestamp: string) => {
318
345
  if (this.ctx.closing) return;
319
346
 
320
- return this.db.updateLocalReplica({
347
+ return this.updateLocalReplica({
321
348
  lastSyncedLogicalTime: timestamp,
322
349
  });
323
350
  };
@@ -329,8 +356,50 @@ export class PersistenceMetadata extends Disposable {
329
356
  }
330
357
  };
331
358
 
332
- getLocalReplica = async (options?: CommonQueryOptions) => {
333
- return this.db.getLocalReplica(options);
359
+ // caching local replica as it's accessed often and only changed
360
+ // via this class.
361
+ private _cachedLocalReplica: LocalReplicaInfo | null = null;
362
+ private _creatingLocalReplica: Promise<LocalReplicaInfo> | undefined =
363
+ undefined;
364
+ getLocalReplica = async (
365
+ options?: CommonQueryOptions,
366
+ ): Promise<LocalReplicaInfo> => {
367
+ if (this._cachedLocalReplica) return this._cachedLocalReplica;
368
+
369
+ const lookup = await this.db.getLocalReplica(options);
370
+ if (lookup) {
371
+ this.ctx.log('debug', 'Read local replica', lookup);
372
+ this._cachedLocalReplica = lookup;
373
+ return lookup;
374
+ }
375
+
376
+ if (this._creatingLocalReplica) {
377
+ return this._creatingLocalReplica;
378
+ }
379
+
380
+ this._creatingLocalReplica = (async () => {
381
+ const replicaId = cuid();
382
+ const replicaInfo: LocalReplicaInfo = {
383
+ id: replicaId,
384
+ userId: null,
385
+ ackedLogicalTime: null,
386
+ lastSyncedLogicalTime: null,
387
+ };
388
+ await this.db.updateLocalReplica(replicaInfo);
389
+ this._cachedLocalReplica = replicaInfo;
390
+ return replicaInfo;
391
+ })();
392
+ return this._creatingLocalReplica;
393
+ };
394
+ updateLocalReplica = async (
395
+ data: Partial<LocalReplicaInfo>,
396
+ opts?: { transaction?: AbstractTransaction },
397
+ ) => {
398
+ const original = await this.getLocalReplica(opts);
399
+ assert(!!original, 'Local replica must exist');
400
+ Object.assign(original, data);
401
+ this._cachedLocalReplica = original;
402
+ await this.db.updateLocalReplica(original, opts);
334
403
  };
335
404
 
336
405
  // used to construct sync messages
@@ -338,54 +407,77 @@ export class PersistenceMetadata extends Disposable {
338
407
  iterateAllOperations = this.db.iterateAllOperations;
339
408
  iterateAllBaselines = this.db.iterateAllBaselines;
340
409
 
341
- reset = this.db.reset;
410
+ reset = async () => {
411
+ if (this.ctx.closing) return;
412
+ await this.db.reset();
413
+ };
342
414
  stats = this.db.stats;
343
415
 
344
416
  export = async (): Promise<MetadataExport> => {
345
- const db = this.db;
346
417
  const baselines = new Array<DocumentBaseline>();
347
418
  const operations = new Array<ClientOperation>();
348
- const transaction = db.transaction({
349
- storeNames: ['baselines', 'operations'],
350
- mode: 'readwrite',
351
- });
352
- await this.iterateAllOperations(
353
- (op) => {
354
- operations.push(op);
419
+ return this.db.transaction(
420
+ {
421
+ storeNames: ['baselines', 'operations'],
422
+ mode: 'readwrite',
355
423
  },
356
- { transaction },
357
- );
358
- await this.iterateAllBaselines(
359
- (baseline) => {
360
- baselines.push(baseline);
424
+ async (transaction) => {
425
+ await this.iterateAllOperations(
426
+ (op) => {
427
+ operations.push(op);
428
+ },
429
+ { transaction },
430
+ );
431
+ await this.iterateAllBaselines(
432
+ (baseline) => {
433
+ baselines.push(baseline);
434
+ },
435
+ { transaction },
436
+ );
437
+ const localReplica = await this.getLocalReplica();
438
+ return {
439
+ operations,
440
+ baselines,
441
+ localReplica,
442
+ schemaVersion: this.ctx.schema.version,
443
+ };
361
444
  },
362
- { transaction },
363
445
  );
364
- const localReplica = await this.db.getLocalReplica();
365
- return {
366
- operations,
367
- baselines,
368
- localReplica,
369
- schemaVersion: this.ctx.schema.version,
370
- };
371
446
  };
372
447
 
373
448
  resetFrom = async (data: MetadataExport) => {
374
- const db = this.db;
375
- const transaction = db.transaction({
376
- storeNames: ['baselines', 'operations', 'info'],
377
- mode: 'readwrite',
378
- });
379
- await this.db.reset({ clearReplica: true, transaction });
449
+ this._cachedLocalReplica = null;
450
+
451
+ // await this.db.transaction(
452
+ // { mode: 'readwrite', storeNames: ['baselines', 'operations', 'info'] },
453
+ // async (tx) => {
454
+ // await this.db.reset({ clearReplica: true, transaction: tx });
455
+
456
+ // if (data.localReplica) {
457
+ // await this.updateLocalReplica(
458
+ // {
459
+ // ackedLogicalTime: data.localReplica.ackedLogicalTime,
460
+ // lastSyncedLogicalTime: data.localReplica.lastSyncedLogicalTime,
461
+ // },
462
+ // {
463
+ // transaction: tx,
464
+ // },
465
+ // );
466
+ // }
467
+ // },
468
+ // );
469
+
470
+ // transaction wasn't working for IDB (invalid state -- it was closing early?)
471
+ await this.db.reset({ clearReplica: true });
472
+
380
473
  if (data.localReplica) {
381
- await this.db.updateLocalReplica(
382
- {
383
- ackedLogicalTime: data.localReplica.ackedLogicalTime,
384
- lastSyncedLogicalTime: data.localReplica.lastSyncedLogicalTime,
385
- },
386
- { transaction },
387
- );
474
+ await this.updateLocalReplica({
475
+ ackedLogicalTime: data.localReplica.ackedLogicalTime,
476
+ lastSyncedLogicalTime: data.localReplica.lastSyncedLogicalTime,
477
+ });
388
478
  }
479
+ // after transaction completes, insert new data.
480
+ // TODO: does this have to be split up like this?
389
481
  this.ctx.log('debug', 'Resetting metadata from export', data);
390
482
  await this.insertData({
391
483
  operations: data.operations,
@@ -404,7 +496,7 @@ export class PersistenceMetadata extends Disposable {
404
496
  };
405
497
 
406
498
  private ack = async (timestamp: string) => {
407
- const localReplicaInfo = await this.db.getLocalReplica();
499
+ const localReplicaInfo = await this.getLocalReplica();
408
500
  // can't ack timestamps from the future.
409
501
  if (timestamp > this.ctx.time.now) return;
410
502
 
@@ -419,7 +511,7 @@ export class PersistenceMetadata extends Disposable {
419
511
  (!localReplicaInfo.ackedLogicalTime ||
420
512
  timestamp > localReplicaInfo.ackedLogicalTime)
421
513
  ) {
422
- this.db.updateLocalReplica({ ackedLogicalTime: timestamp });
514
+ this.updateLocalReplica({ ackedLogicalTime: timestamp });
423
515
  }
424
516
  };
425
517
  }
@@ -1,19 +1,55 @@
1
- import { PersistenceQueryDb } from './interfaces.js';
1
+ import { PersistenceDocumentDb } from './interfaces.js';
2
2
  import { Context } from '../context/context.js';
3
- import { Disposable } from '../utils/Disposable.js';
3
+ import { decomposeOid, ObjectIdentifier } from '@verdant-web/common';
4
4
 
5
- export class PersistenceQueries extends Disposable {
5
+ export class PersistenceDocuments {
6
6
  constructor(
7
- private db: PersistenceQueryDb,
7
+ private db: PersistenceDocumentDb,
8
8
  private ctx: Omit<Context, 'queries'>,
9
- ) {
10
- super();
11
- this.compose(this.db);
12
- }
9
+ ) {}
13
10
 
14
11
  reset = this.db.reset.bind(this.db);
15
12
 
16
- saveEntities = this.db.saveEntities;
13
+ close = this.db.close.bind(this.db);
14
+
15
+ saveEntities = async (
16
+ entities: { oid: ObjectIdentifier; getSnapshot: () => any }[],
17
+ options?: { abort?: AbortSignal },
18
+ ) => {
19
+ if (entities.length === 0) return;
20
+
21
+ // filter entities to remove collections which don't
22
+ // exist in the schema anymore
23
+ const currentCollectionSet = new Set(
24
+ Object.keys(this.ctx.schema.collections),
25
+ );
26
+ const collections: string[] = [];
27
+ const filteredEntities = entities.filter((entity) => {
28
+ const { collection } = decomposeOid(entity.oid);
29
+ if (!currentCollectionSet.has(collection)) {
30
+ this.ctx.log(
31
+ 'warn',
32
+ `Entity ${entity.oid} is in a collection that no longer exists in the schema. It will not be saved.`,
33
+ );
34
+ return false;
35
+ }
36
+ if (!collections.includes(collection)) collections.push(collection);
37
+ return true;
38
+ });
39
+
40
+ if (collections.length === 0) return;
41
+
42
+ this.ctx.log('debug', 'Saving', filteredEntities.length, 'entities');
43
+ await this.db.saveEntities(filteredEntities, {
44
+ abort: options?.abort,
45
+ collections,
46
+ });
47
+ this.ctx.log('debug', 'Saved', filteredEntities.length, 'entities');
48
+ this.ctx.entityEvents.emit('collectionsChanged', collections);
49
+ for (const entity of entities) {
50
+ this.ctx.entityEvents.emit('documentChanged', entity.oid);
51
+ }
52
+ };
17
53
 
18
54
  findOneOid = this.db.findOneOid.bind(this.db);
19
55
  findAllOids = this.db.findAllOids.bind(this.db);