@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
@@ -6,23 +6,26 @@ import {
6
6
  ObjectIdentifier,
7
7
  } from '@verdant-web/common';
8
8
  import { Context } from '../../../context/context.js';
9
- import {
10
- AbstractTransaction,
11
- CommonQueryOptions,
12
- PersistenceQueryDb,
13
- QueryMode,
14
- } from '../../interfaces.js';
9
+ import { PersistenceDocumentDb } from '../../interfaces.js';
15
10
  import { IdbService } from '../IdbService.js';
16
11
  import { getRange } from './ranges.js';
17
12
  import { closeDatabase, getSizeOfObjectStore, isAbortError } from '../util.js';
18
13
 
19
- export class IdbQueryDb extends IdbService implements PersistenceQueryDb {
14
+ export class IdbDocumentDb extends IdbService implements PersistenceDocumentDb {
20
15
  private ctx;
21
- constructor(db: IDBDatabase, context: Omit<Context, 'queries' | 'files'>) {
16
+ constructor(db: IDBDatabase, context: Omit<Context, 'documents' | 'files'>) {
22
17
  super(db, { log: context.log });
23
18
  this.ctx = context;
19
+ this.addDispose(() => {
20
+ this.ctx.log('info', 'Closing document database for', this.ctx.namespace);
21
+ return closeDatabase(this.db);
22
+ });
24
23
  }
25
24
 
25
+ close = async () => {
26
+ await this.dispose();
27
+ };
28
+
26
29
  stats = async (): Promise<
27
30
  Record<string, { count: number; size: number }>
28
31
  > => {
@@ -37,17 +40,6 @@ export class IdbQueryDb extends IdbService implements PersistenceQueryDb {
37
40
  return collections;
38
41
  };
39
42
 
40
- transaction = (opts: {
41
- mode?: QueryMode;
42
- storeNames: string[];
43
- abort?: AbortSignal;
44
- }): AbstractTransaction => {
45
- return this.createTransaction(opts.storeNames, {
46
- mode: opts.mode,
47
- abort: opts.abort,
48
- });
49
- };
50
-
51
43
  findOneOid = async (opts: {
52
44
  collection: string;
53
45
  index?: CollectionFilter;
@@ -148,36 +140,15 @@ export class IdbQueryDb extends IdbService implements PersistenceQueryDb {
148
140
 
149
141
  saveEntities = async (
150
142
  entities: { oid: ObjectIdentifier; getSnapshot: () => any }[],
151
- opts?: CommonQueryOptions & { abort?: AbortSignal },
143
+ optsAndInfo: { abort?: AbortSignal; collections: string[] },
152
144
  ): Promise<void> => {
153
- if (entities.length === 0) return;
154
-
155
- let collections = Array.from(
156
- new Set(entities.map((e) => decomposeOid(e.oid).collection)),
157
- );
158
-
159
- const toRemove = collections.filter((c) => !this.ctx.schema.collections[c]);
160
- if (toRemove.length > 0) {
161
- this.ctx.log(
162
- 'warn',
163
- `Ignoring entities from collections that no longer exist: ${toRemove.join(
164
- ', ',
165
- )}`,
166
- );
167
- }
168
- const withRemoved = new Set(collections);
169
- toRemove.forEach((c) => withRemoved.delete(c));
170
- collections = Array.from(withRemoved);
171
-
172
145
  const options = {
173
- transaction: this.createTransaction(collections, {
146
+ transaction: this.createTransaction(optsAndInfo.collections, {
174
147
  mode: 'readwrite',
175
- abort: opts?.abort,
148
+ abort: optsAndInfo.abort,
176
149
  }),
177
150
  };
178
151
 
179
- // FIXME: not test is making it to this line
180
-
181
152
  await Promise.all(
182
153
  entities.map(async (e) => {
183
154
  const snapshot = e.getSnapshot();
@@ -198,19 +169,11 @@ export class IdbQueryDb extends IdbService implements PersistenceQueryDb {
198
169
  }),
199
170
  );
200
171
  options.transaction.commit();
201
- this.ctx.entityEvents.emit('collectionsChanged', collections);
202
- for (const entity of entities) {
203
- this.ctx.entityEvents.emit('documentChanged', entity.oid);
204
- }
205
172
  };
206
173
 
207
- reset = async (opts?: {
208
- transaction?: AbstractTransaction;
209
- }): Promise<void> => {
174
+ reset = async (): Promise<void> => {
210
175
  const names = Object.keys(this.ctx.schema.collections);
211
- const tx =
212
- (opts?.transaction as IDBTransaction) ||
213
- this.createTransaction(names, { mode: 'readwrite' });
176
+ const tx = this.createTransaction(names, { mode: 'readwrite' });
214
177
  await Promise.all(
215
178
  names.map((name) =>
216
179
  this.run(name, (store) => store.clear(), { transaction: tx }),
@@ -220,10 +183,6 @@ export class IdbQueryDb extends IdbService implements PersistenceQueryDb {
220
183
  this.ctx.log('info', '💨 Reset queryable storage');
221
184
  };
222
185
 
223
- dispose = () => {
224
- return closeDatabase(this.db);
225
- };
226
-
227
186
  private saveDocument = async (
228
187
  oid: ObjectIdentifier,
229
188
  doc: any,
@@ -241,6 +200,7 @@ export class IdbQueryDb extends IdbService implements PersistenceQueryDb {
241
200
  const schema = this.ctx.schema.collections[collection];
242
201
  // no need to validate before storing; the entity's snapshot is already validated.
243
202
  const indexes = getIndexValues(schema, doc);
203
+ indexes['@@@snapshot'] = JSON.stringify(doc);
244
204
  await this.run(collection, (store) => store.put(indexes), {
245
205
  mode: 'readwrite',
246
206
  transaction,
@@ -3,22 +3,9 @@ import {
3
3
  storeRequestPromise,
4
4
  openDatabase as baseOpenDatabase,
5
5
  getDocumentDbName,
6
+ closeDatabase,
6
7
  } from '../../util.js';
7
- import { OpenDocumentDbContext } from './types.js';
8
-
9
- export async function getDatabaseVersion(
10
- indexedDB: IDBFactory,
11
- namespace: string,
12
- ): Promise<number> {
13
- const databaseName = getDocumentDbName(namespace);
14
- const dbInfo = await indexedDB.databases();
15
- const existingDb = dbInfo.find((info) => info.name === databaseName);
16
- if (existingDb) {
17
- return existingDb.version ?? 0;
18
- }
19
-
20
- return 0;
21
- }
8
+ import { Context } from '../../../../internal.js';
22
9
 
23
10
  /**
24
11
  * Upgrades the database to the given version, using the given upgrader function.
@@ -34,22 +21,22 @@ export async function upgradeDatabase(
34
21
  ) => void,
35
22
  log?: (...args: any[]) => void,
36
23
  ): Promise<IDBDatabase> {
24
+ log?.('debug', 'Upgrading database', namespace, 'to version', version);
37
25
  function openAndUpgrade(
38
26
  resolve: (db: IDBDatabase) => void,
39
27
  reject: (err: Error) => void,
40
28
  ) {
41
- const request = indexedDb.open(
42
- [namespace, 'collections'].join('_'),
43
- version,
44
- );
29
+ const request = indexedDb.open(getDocumentDbName(namespace), version);
45
30
  let wasUpgraded = false;
46
31
  request.onupgradeneeded = (event) => {
47
32
  const transaction = request.transaction!;
48
33
  upgrader(transaction, request.result, event);
49
34
  wasUpgraded = true;
50
35
  };
51
- request.onsuccess = (event) => {
36
+ request.onsuccess = async (event) => {
52
37
  if (wasUpgraded) {
38
+ // close the database
39
+ await closeDatabase(request.result);
53
40
  resolve(request.result);
54
41
  } else {
55
42
  reject(
@@ -63,39 +50,29 @@ export async function upgradeDatabase(
63
50
  reject(request.error || new Error('Unknown error'));
64
51
  };
65
52
  request.onblocked = (event) => {
66
- log?.('Database upgrade blocked, waiting...');
67
- // setTimeout(() => {
68
- // openAndUpgrade(resolve, reject);
69
- // }, 200);
53
+ log?.('Database upgrade blocked!');
54
+ reject(
55
+ new Error(
56
+ 'Database upgrade blocked. The app may be open in another tab?',
57
+ ),
58
+ );
70
59
  };
71
60
  }
72
61
  return new Promise<IDBDatabase>(openAndUpgrade);
73
62
  }
74
63
 
75
- export async function acquireLock(
76
- namespace: string,
77
- procedure: () => Promise<void>,
78
- ) {
79
- if (typeof navigator !== 'undefined' && navigator.locks) {
80
- await navigator.locks.request(`verdant_migration_${namespace}`, procedure);
81
- } else {
82
- // TODO: is there a fallback?
83
- await procedure();
84
- }
85
- }
86
-
87
64
  export async function openDatabase({
88
65
  indexedDB = globalIDB,
89
66
  namespace,
90
67
  version,
91
- context,
68
+ log,
92
69
  }: {
93
70
  indexedDB?: IDBFactory;
94
71
  namespace: string;
95
72
  version: number;
96
- context: OpenDocumentDbContext;
73
+ log?: Context['log'];
97
74
  }): Promise<IDBDatabase> {
98
- context.log('debug', 'Opening database', namespace, 'at version', version);
75
+ log?.('debug', 'Opening database', namespace, 'at version', version);
99
76
  const db = await baseOpenDatabase(
100
77
  getDocumentDbName(namespace),
101
78
  version,
@@ -106,6 +83,10 @@ export async function openDatabase({
106
83
  db.close();
107
84
  });
108
85
 
86
+ db.addEventListener('close', () => {
87
+ log?.('warn', 'Database closed', namespace);
88
+ });
89
+
109
90
  return db;
110
91
  }
111
92
 
@@ -1,4 +1,5 @@
1
1
  import { roughSizeOfObject } from '@verdant-web/common';
2
+ import { Context } from '../../internal.js';
2
3
 
3
4
  export const globalIDB =
4
5
  typeof window !== 'undefined' ? window.indexedDB : (undefined as any);
@@ -182,38 +183,100 @@ export function createAbortableTransaction(
182
183
  }
183
184
 
184
185
  /**
185
- * Empties all data in a database without changing
186
- * its structure.
186
+ * Deletes any existing database with name `toName` and
187
+ * copies the index structure and all data
188
+ * from `from` to a new database.
189
+ *
190
+ * Does NOT run Verdant migrations. Use to copy existing
191
+ * data as-is.
187
192
  */
188
- export function emptyDatabase(db: IDBDatabase) {
189
- const storeNames = Array.from(db.objectStoreNames);
190
- const tx = db.transaction(storeNames, 'readwrite');
191
- for (const storeName of storeNames) {
192
- tx.objectStore(storeName).clear();
193
+ export async function overwriteDatabase(
194
+ from: IDBDatabase,
195
+ toName: string,
196
+ ctx: Pick<Context, 'log'>,
197
+ indexedDB = window.indexedDB,
198
+ ) {
199
+ const databases = await getAllDatabaseNamesAndVersions(indexedDB);
200
+ if (databases.some((d) => d.name === toName)) {
201
+ await deleteDatabase(toName, indexedDB);
202
+ ctx.log('debug', 'Deleted existing database', toName);
193
203
  }
194
- return new Promise<void>((resolve, reject) => {
195
- tx.oncomplete = () => resolve();
196
- tx.onerror = () => reject(tx.error);
204
+
205
+ const to = await new Promise<IDBDatabase>((resolve, reject) => {
206
+ ctx.log('debug', 'Opening reset database', toName, 'at', from.version);
207
+ const openRequest = indexedDB.open(toName, from.version);
208
+ openRequest.onupgradeneeded = () => {
209
+ ctx.log(
210
+ 'debug',
211
+ 'Upgrading database',
212
+ toName,
213
+ 'to version',
214
+ from.version,
215
+ );
216
+ // copy all indexes from original
217
+ const original = from;
218
+ const upgradeTx = openRequest.transaction;
219
+ if (!upgradeTx) {
220
+ throw new Error('No transaction');
221
+ }
222
+ for (const storeName of Array.from(original.objectStoreNames)) {
223
+ const originalObjectStore = original
224
+ .transaction(storeName)
225
+ .objectStore(storeName);
226
+ // create object store
227
+ upgradeTx.db.createObjectStore(storeName, {
228
+ keyPath: originalObjectStore.keyPath,
229
+ autoIncrement: originalObjectStore.autoIncrement,
230
+ });
231
+ const store = upgradeTx.objectStore(storeName);
232
+ const originalStore = original
233
+ .transaction(storeName)
234
+ .objectStore(storeName);
235
+ for (const index of Array.from(originalStore.indexNames)) {
236
+ const originalIndex = originalStore.index(index);
237
+ ctx.log('debug', 'Copying index', index);
238
+ store.createIndex(index, originalIndex.keyPath, {
239
+ unique: originalIndex.unique,
240
+ multiEntry: originalIndex.multiEntry,
241
+ });
242
+ }
243
+ }
244
+ };
245
+ openRequest.onsuccess = () => {
246
+ ctx.log('debug', 'Opened reset database', toName);
247
+ resolve(openRequest.result);
248
+ };
249
+ openRequest.onerror = () =>
250
+ reject(openRequest.error ?? new Error('Unknown database upgrade error'));
197
251
  });
198
- }
199
252
 
200
- export async function copyDatabase(from: IDBDatabase, to: IDBDatabase) {
201
- await emptyDatabase(to);
202
253
  const records = await getAllFromObjectStores(
203
254
  from,
204
255
  Array.from(from.objectStoreNames),
205
256
  );
206
- const writeTx = to.transaction(Array.from(to.objectStoreNames), 'readwrite');
207
- for (let i = 0; i < records.length; i++) {
208
- const store = writeTx.objectStore(from.objectStoreNames[i]);
209
- for (const record of records[i]) {
210
- store.add(record);
257
+ await new Promise<void>((resolve, reject) => {
258
+ const writeTx = to.transaction(
259
+ Array.from(to.objectStoreNames),
260
+ 'readwrite',
261
+ );
262
+ for (let i = 0; i < records.length; i++) {
263
+ const store = writeTx.objectStore(from.objectStoreNames[i]);
264
+ for (const record of records[i]) {
265
+ store.add(record);
266
+ }
211
267
  }
212
- }
213
- return new Promise<void>((resolve, reject) => {
214
268
  writeTx.oncomplete = () => resolve();
215
- writeTx.onerror = () => reject(writeTx.error);
269
+ writeTx.onerror = (ev) => {
270
+ const err =
271
+ writeTx.error ??
272
+ (ev.target as any).transaction?.error ??
273
+ new Error('Unknown error');
274
+ ctx.log('critical', 'Error copying data', err);
275
+ reject(err);
276
+ };
216
277
  });
278
+
279
+ await closeDatabase(to);
217
280
  }
218
281
 
219
282
  export function openDatabase(
@@ -2,20 +2,19 @@ import {
2
2
  CollectionFilter,
3
3
  DocumentBaseline,
4
4
  FileData,
5
+ Migration,
5
6
  ObjectIdentifier,
6
7
  Operation,
7
8
  } from '@verdant-web/common';
8
9
  import { Context, InitialContext } from '../context/context.js';
9
10
 
10
11
  export interface AckInfo {
11
- type: 'ack';
12
12
  globalAckTimestamp: string | null;
13
13
  }
14
14
 
15
15
  export interface LocalReplicaInfo {
16
- type: 'localReplicaInfo';
17
16
  id: string;
18
- userId: string | undefined;
17
+ userId: string | null;
19
18
  ackedLogicalTime: string | null;
20
19
  lastSyncedLogicalTime: string | null;
21
20
  }
@@ -37,90 +36,97 @@ export interface ExportedData {
37
36
  files: File[];
38
37
  }
39
38
 
40
- export type AbstractTransaction = unknown;
39
+ export type AbstractTransaction = any;
41
40
  export type QueryMode = 'readwrite' | 'readonly';
42
- export interface CommonQueryOptions {
43
- transaction?: AbstractTransaction;
41
+ export interface CommonQueryOptions<
42
+ Tx extends AbstractTransaction = AbstractTransaction,
43
+ > {
44
+ transaction?: Tx;
44
45
  mode?: QueryMode;
45
46
  }
46
47
  export type Iterator<T> = (item: T) => void | boolean;
47
48
 
48
- export interface PersistenceMetadataDb {
49
- transaction(opts: {
50
- mode?: QueryMode;
51
- storeNames: string[];
52
- abort?: AbortSignal;
53
- }): AbstractTransaction;
54
- dispose(): void | Promise<void>;
49
+ export interface PersistenceMetadataDb<
50
+ Tx extends AbstractTransaction = AbstractTransaction,
51
+ > {
52
+ transaction<T = void>(
53
+ opts: {
54
+ mode?: QueryMode;
55
+ storeNames: string[];
56
+ abort?: AbortSignal;
57
+ },
58
+ procedure: (tx: Tx) => Promise<T>,
59
+ ): Promise<T>;
55
60
 
56
61
  // infos
57
62
  getAckInfo(): Promise<AckInfo>;
58
63
  setGlobalAck(ack: string): Promise<void>;
59
- getLocalReplica(opts?: CommonQueryOptions): Promise<LocalReplicaInfo>;
64
+ getLocalReplica(
65
+ opts?: CommonQueryOptions<Tx>,
66
+ ): Promise<LocalReplicaInfo | undefined | null>;
60
67
  updateLocalReplica(
61
- data: Partial<LocalReplicaInfo>,
62
- opts?: CommonQueryOptions,
68
+ data: LocalReplicaInfo,
69
+ opts?: CommonQueryOptions<Tx>,
63
70
  ): Promise<void>;
64
71
 
65
72
  // baselines
66
73
  iterateDocumentBaselines(
67
74
  rootOid: string,
68
75
  iterator: Iterator<DocumentBaseline>,
69
- opts?: CommonQueryOptions,
76
+ opts?: CommonQueryOptions<Tx>,
70
77
  ): Promise<void>;
71
78
  iterateCollectionBaselines(
72
79
  collection: string,
73
80
  iterator: Iterator<DocumentBaseline>,
74
- opts?: CommonQueryOptions,
81
+ opts?: CommonQueryOptions<Tx>,
75
82
  ): Promise<void>;
76
83
  iterateAllBaselines(
77
84
  iterator: Iterator<DocumentBaseline>,
78
- opts?: CommonQueryOptions,
85
+ opts?: CommonQueryOptions<Tx>,
79
86
  ): Promise<void>;
80
87
  getBaseline(
81
88
  oid: string,
82
- opts?: CommonQueryOptions,
83
- ): Promise<DocumentBaseline>;
89
+ opts?: CommonQueryOptions<Tx>,
90
+ ): Promise<DocumentBaseline | null>;
84
91
  setBaselines(
85
92
  baselines: DocumentBaseline[],
86
- opts?: CommonQueryOptions,
93
+ opts?: CommonQueryOptions<Tx>,
87
94
  ): Promise<void>;
88
- deleteBaseline(oid: string, opts?: CommonQueryOptions): Promise<void>;
95
+ deleteBaseline(oid: string, opts?: CommonQueryOptions<Tx>): Promise<void>;
89
96
 
90
97
  // operations
91
98
  iterateDocumentOperations(
92
99
  rootOid: string,
93
100
  iterator: Iterator<ClientOperation>,
94
- opts?: CommonQueryOptions & {
101
+ opts?: CommonQueryOptions<Tx> & {
95
102
  to?: string | null;
96
103
  },
97
104
  ): Promise<void>;
98
105
  iterateEntityOperations(
99
106
  oid: string,
100
107
  iterator: Iterator<ClientOperation>,
101
- opts?: CommonQueryOptions & { to?: string | null },
108
+ opts?: CommonQueryOptions<Tx> & { to?: string | null },
102
109
  ): Promise<void>;
103
110
  iterateCollectionOperations(
104
111
  collection: string,
105
112
  iterator: Iterator<ClientOperation>,
106
- opts?: CommonQueryOptions,
113
+ opts?: CommonQueryOptions<Tx>,
107
114
  ): Promise<void>;
108
115
  iterateLocalOperations(
109
116
  iterator: Iterator<ClientOperation>,
110
- opts?: CommonQueryOptions & {
117
+ opts?: CommonQueryOptions<Tx> & {
111
118
  before?: string | null;
112
119
  after?: string | null;
113
120
  },
114
121
  ): Promise<void>;
115
122
  /** Iterates over operations for an entity for processing and deletes them as it goes. */
116
- consumeEntityOperations(
123
+ deleteEntityOperations(
117
124
  oid: string,
118
- iterator: Iterator<ClientOperation>,
119
- opts?: CommonQueryOptions & { to?: string | null },
125
+ opts: CommonQueryOptions<Tx> & { to: string | null },
120
126
  ): Promise<void>;
121
127
  iterateAllOperations(
122
128
  iterator: Iterator<ClientOperation>,
123
- opts?: CommonQueryOptions & {
129
+ opts?: CommonQueryOptions<Tx> & {
124
130
  before?: string | null;
125
131
  from?: string | null;
126
132
  },
@@ -130,14 +136,11 @@ export interface PersistenceMetadataDb {
130
136
  */
131
137
  addOperations(
132
138
  ops: ClientOperation[],
133
- opts?: CommonQueryOptions,
139
+ opts?: CommonQueryOptions<Tx>,
134
140
  ): Promise<ObjectIdentifier[]>;
135
141
 
136
142
  /* WARNING: deletes all data */
137
- reset(opts?: {
138
- clearReplica?: boolean;
139
- transaction?: AbstractTransaction;
140
- }): Promise<void>;
143
+ reset(opts?: { clearReplica?: boolean; transaction?: Tx }): Promise<void>;
141
144
 
142
145
  stats(): Promise<{
143
146
  operationsSize: { count: number; size: number };
@@ -145,14 +148,7 @@ export interface PersistenceMetadataDb {
145
148
  }>;
146
149
  }
147
150
 
148
- export interface PersistenceQueryDb {
149
- transaction(opts: {
150
- mode?: QueryMode;
151
- storeNames: string[];
152
- abort?: AbortSignal;
153
- }): AbstractTransaction;
154
- dispose(): void | Promise<void>;
155
-
151
+ export interface PersistenceDocumentDb {
156
152
  findOneOid(opts: {
157
153
  collection: string;
158
154
  index?: CollectionFilter;
@@ -166,12 +162,14 @@ export interface PersistenceQueryDb {
166
162
 
167
163
  saveEntities(
168
164
  entities: { oid: ObjectIdentifier; getSnapshot: () => any }[],
169
- opts?: CommonQueryOptions & { abort?: AbortSignal },
165
+ optsAndInfo: { abort?: AbortSignal; collections: string[] },
170
166
  ): Promise<void>;
171
167
 
172
- reset(opts?: { transaction?: AbstractTransaction }): Promise<void>;
168
+ reset(): Promise<void>;
173
169
 
174
170
  stats(): Promise<Record<string, { count: number; size: number }>>;
171
+
172
+ close(): Promise<void>;
175
173
  }
176
174
 
177
175
  export interface PersistedFileData extends FileData {
@@ -179,62 +177,63 @@ export interface PersistedFileData extends FileData {
179
177
  }
180
178
 
181
179
  export interface PersistenceFileDb {
182
- transaction(opts: {
183
- mode?: QueryMode;
184
- storeNames: string[];
185
- abort?: AbortSignal;
186
- }): AbstractTransaction;
187
- dispose(): void | Promise<void>;
188
-
189
- add(
190
- file: FileData,
191
- options?: { transaction?: AbstractTransaction; downloadRemote?: boolean },
192
- ): Promise<void>;
193
- markUploaded(
194
- fileId: string,
195
- options?: { transaction?: AbstractTransaction },
196
- ): Promise<void>;
197
- get(
198
- fileId: string,
199
- options?: { transaction?: AbstractTransaction },
200
- ): Promise<PersistedFileData | null>;
201
- delete(
202
- fileId: string,
203
- options?: { transaction?: AbstractTransaction },
204
- ): Promise<void>;
205
- markPendingDelete(
206
- fileId: string,
207
- options?: { transaction?: AbstractTransaction },
208
- ): Promise<void>;
209
- listUnsynced(options?: {
210
- transaction?: AbstractTransaction;
211
- }): Promise<PersistedFileData[]>;
212
- resetSyncedStatusSince(
213
- since: string | null,
214
- options?: { transaction?: AbstractTransaction },
215
- ): Promise<void>;
180
+ add(file: FileData, options?: { downloadRemote?: boolean }): Promise<void>;
181
+ markUploaded(fileId: string): Promise<void>;
182
+ get(fileId: string): Promise<PersistedFileData | null>;
183
+ delete(fileId: string): Promise<void>;
184
+ markPendingDelete(fileId: string): Promise<void>;
185
+ listUnsynced(): Promise<PersistedFileData[]>;
186
+ resetSyncedStatusSince(since: string | null): Promise<void>;
216
187
  iterateOverPendingDelete(
217
- iterator: (file: PersistedFileData, store: IDBObjectStore) => void,
218
- options?: { transaction?: IDBTransaction },
188
+ iterator: (file: PersistedFileData) => void,
219
189
  ): Promise<void>;
220
- getAll(options?: {
221
- transaction?: AbstractTransaction;
222
- }): Promise<PersistedFileData[]>;
190
+ loadFileContents(file: FileData, ctx: Context): Promise<Blob>;
191
+ getAll(): Promise<PersistedFileData[]>;
223
192
  stats(): Promise<{ size: { count: number; size: number } }>;
224
193
  }
225
194
 
226
- export interface PersistenceImplementation {
195
+ export interface PersistenceNamespace {
227
196
  openMetadata(ctx: InitialContext): Promise<PersistenceMetadataDb>;
228
- openQueries(ctx: Omit<Context, 'queries'>): Promise<PersistenceQueryDb>;
197
+ /**
198
+ * Open the Documents database according to the schema in the given
199
+ * context. By the time this is called with a version, relevant migrations
200
+ * will have been applied.
201
+ */
202
+ openDocuments(
203
+ ctx: Omit<Context, 'documents' | 'files'>,
204
+ ): Promise<PersistenceDocumentDb>;
205
+ /**
206
+ * Apply a migration to the namespace provided in ctx.
207
+ * This should make any transformations necessary to the
208
+ * document database to accommodate the new schema indexes.
209
+ * The migration itself contains a lot of information about
210
+ * what changed between versions.
211
+ *
212
+ * This method should also store the new version to persisted
213
+ * metadata, however your implementation chooses to do that.
214
+ */
215
+ applyMigration(ctx: InitialContext, migration: Migration<any>): Promise<void>;
229
216
  openFiles(
230
- ctx: Omit<Context, 'files' | 'queries'>,
217
+ ctx: Omit<Context, 'files' | 'documents'>,
231
218
  ): Promise<PersistenceFileDb>;
232
- /** Copies all data (metadata/document queries/files) from one namespace to another. */
233
- copyNamespace(from: string, to: string, ctx: InitialContext): Promise<void>;
219
+ }
220
+
221
+ export interface PersistenceImplementation {
222
+ name: string;
223
+ openNamespace(
224
+ namespace: string,
225
+ ctx: Pick<Context, 'log' | 'persistenceShutdownHandler'>,
226
+ ): Promise<PersistenceNamespace>;
234
227
  /** Returns a list of all persisted namespaces visible to this app. */
235
228
  getNamespaces(): Promise<string[]>;
236
229
  /** Deletes all data from a particular namespace. */
237
230
  deleteNamespace(namespace: string, ctx: InitialContext): Promise<void>;
238
231
  /** Gets the schema version of the given namespace */
239
232
  getNamespaceVersion(namespace: string): Promise<number>;
233
+ /**
234
+ * Copies all data from one namespace to another. It should
235
+ * overwrite the target namespace such that data and database
236
+ * schema are identical.
237
+ */
238
+ copyNamespace(from: string, to: string, ctx: InitialContext): Promise<void>;
240
239
  }