@verdant-web/store 3.8.0 → 3.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE +650 -21
  2. package/dist/bundle/index.js +12 -12
  3. package/dist/bundle/index.js.map +4 -4
  4. package/dist/esm/__tests__/{documents.test.js → entities.test.js} +30 -2
  5. package/dist/esm/__tests__/entities.test.js.map +1 -0
  6. package/dist/esm/__tests__/fixtures/testStorage.js +3 -1
  7. package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
  8. package/dist/esm/client/Client.d.ts +3 -3
  9. package/dist/esm/client/Client.js +5 -5
  10. package/dist/esm/client/Client.js.map +1 -1
  11. package/dist/esm/client/ClientDescriptor.d.ts +1 -0
  12. package/dist/esm/client/ClientDescriptor.js +6 -3
  13. package/dist/esm/client/ClientDescriptor.js.map +1 -1
  14. package/dist/esm/context.d.ts +1 -0
  15. package/dist/esm/entities/Entity.d.ts +7 -3
  16. package/dist/esm/entities/Entity.js +11 -6
  17. package/dist/esm/entities/Entity.js.map +1 -1
  18. package/dist/esm/entities/Entity.test.js +1 -1
  19. package/dist/esm/entities/Entity.test.js.map +1 -1
  20. package/dist/esm/entities/EntityMetadata.d.ts +1 -0
  21. package/dist/esm/entities/EntityMetadata.js +1 -0
  22. package/dist/esm/entities/EntityMetadata.js.map +1 -1
  23. package/dist/esm/entities/EntityStore.d.ts +3 -3
  24. package/dist/esm/entities/EntityStore.js +6 -1
  25. package/dist/esm/entities/EntityStore.js.map +1 -1
  26. package/dist/esm/entities/entityFieldSubscriber.d.ts +5 -0
  27. package/dist/esm/entities/entityFieldSubscriber.js +19 -0
  28. package/dist/esm/entities/entityFieldSubscriber.js.map +1 -0
  29. package/dist/esm/files/EntityFile.d.ts +3 -0
  30. package/dist/esm/files/EntityFile.js +19 -8
  31. package/dist/esm/files/EntityFile.js.map +1 -1
  32. package/dist/esm/files/FileManager.js +6 -1
  33. package/dist/esm/files/FileManager.js.map +1 -1
  34. package/dist/esm/files/FileStorage.d.ts +2 -0
  35. package/dist/esm/files/FileStorage.js +12 -0
  36. package/dist/esm/files/FileStorage.js.map +1 -1
  37. package/dist/esm/idb.d.ts +6 -0
  38. package/dist/esm/idb.js +17 -1
  39. package/dist/esm/idb.js.map +1 -1
  40. package/dist/esm/metadata/openMetadataDatabase.js +6 -1
  41. package/dist/esm/metadata/openMetadataDatabase.js.map +1 -1
  42. package/dist/esm/migration/db.d.ts +9 -3
  43. package/dist/esm/migration/db.js +23 -11
  44. package/dist/esm/migration/db.js.map +1 -1
  45. package/dist/esm/migration/engine.d.ts +15 -0
  46. package/dist/esm/migration/engine.js +159 -0
  47. package/dist/esm/migration/engine.js.map +1 -0
  48. package/dist/esm/migration/migrations.d.ts +17 -0
  49. package/dist/esm/migration/migrations.js +242 -0
  50. package/dist/esm/migration/migrations.js.map +1 -0
  51. package/dist/esm/migration/openQueryDatabase.d.ts +10 -0
  52. package/dist/esm/migration/openQueryDatabase.js +27 -0
  53. package/dist/esm/migration/openQueryDatabase.js.map +1 -0
  54. package/dist/esm/migration/openWIPDatabase.d.ts +11 -0
  55. package/dist/esm/migration/openWIPDatabase.js +65 -0
  56. package/dist/esm/migration/openWIPDatabase.js.map +1 -0
  57. package/dist/esm/migration/types.d.ts +3 -0
  58. package/dist/esm/migration/types.js +2 -0
  59. package/dist/esm/migration/types.js.map +1 -0
  60. package/dist/esm/queries/QueryableStorage.js +1 -1
  61. package/dist/esm/queries/QueryableStorage.js.map +1 -1
  62. package/dist/esm/sync/PushPullSync.d.ts +3 -2
  63. package/dist/esm/sync/PushPullSync.js +7 -1
  64. package/dist/esm/sync/PushPullSync.js.map +1 -1
  65. package/dist/esm/sync/Sync.d.ts +2 -0
  66. package/dist/esm/sync/Sync.js +1 -0
  67. package/dist/esm/sync/Sync.js.map +1 -1
  68. package/package.json +2 -2
  69. package/src/__tests__/{documents.test.ts → entities.test.ts} +44 -2
  70. package/src/__tests__/fixtures/testStorage.ts +3 -1
  71. package/src/client/Client.ts +6 -8
  72. package/src/client/ClientDescriptor.ts +7 -6
  73. package/src/context.ts +1 -0
  74. package/src/entities/Entity.test.ts +1 -1
  75. package/src/entities/Entity.ts +20 -8
  76. package/src/entities/EntityMetadata.ts +2 -0
  77. package/src/entities/EntityStore.ts +9 -3
  78. package/src/entities/entityFieldSubscriber.ts +31 -0
  79. package/src/files/EntityFile.ts +10 -0
  80. package/src/files/FileManager.ts +11 -1
  81. package/src/files/FileStorage.ts +31 -0
  82. package/src/idb.ts +20 -1
  83. package/src/metadata/openMetadataDatabase.ts +7 -1
  84. package/src/migration/db.ts +62 -20
  85. package/src/migration/engine.ts +248 -0
  86. package/src/migration/migrations.ts +347 -0
  87. package/src/migration/openQueryDatabase.ts +63 -0
  88. package/src/migration/openWIPDatabase.ts +97 -0
  89. package/src/migration/types.ts +4 -0
  90. package/src/queries/QueryableStorage.ts +1 -1
  91. package/src/sync/PushPullSync.ts +10 -0
  92. package/src/sync/Sync.ts +3 -0
  93. package/dist/esm/__tests__/documents.test.js.map +0 -1
  94. package/dist/esm/migration/openDatabase.d.ts +0 -20
  95. package/dist/esm/migration/openDatabase.js +0 -463
  96. package/dist/esm/migration/openDatabase.js.map +0 -1
  97. package/src/migration/openDatabase.ts +0 -749
  98. /package/dist/esm/__tests__/{documents.test.d.ts → entities.test.d.ts} +0 -0
package/src/idb.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { roughSizeOfObject } from '@verdant-web/common';
2
2
 
3
+ export const globalIDB =
4
+ typeof window !== 'undefined' ? window.indexedDB : (undefined as any);
5
+
3
6
  export function isAbortError(err: unknown) {
4
7
  return err instanceof Error && err.name === 'AbortError';
5
8
  }
@@ -109,7 +112,7 @@ export async function closeDatabase(db: IDBDatabase) {
109
112
 
110
113
  export async function deleteAllDatabases(
111
114
  namespace: string,
112
- indexedDB: IDBFactory = window.indexedDB,
115
+ indexedDB: IDBFactory = globalIDB,
113
116
  ) {
114
117
  const req1 = indexedDB.deleteDatabase([namespace, 'meta'].join('_'));
115
118
  const req2 = indexedDB.deleteDatabase([namespace, 'collections'].join('_'));
@@ -163,3 +166,19 @@ export function createAbortableTransaction(
163
166
  }
164
167
  return tx;
165
168
  }
169
+
170
+ /**
171
+ * Empties all data in a database without changing
172
+ * its structure.
173
+ */
174
+ export function emptyDatabase(db: IDBDatabase) {
175
+ const storeNames = Array.from(db.objectStoreNames);
176
+ const tx = db.transaction(storeNames, 'readwrite');
177
+ for (const storeName of storeNames) {
178
+ tx.objectStore(storeName).clear();
179
+ }
180
+ return new Promise<void>((resolve, reject) => {
181
+ tx.oncomplete = () => resolve();
182
+ tx.onerror = () => reject(tx.error);
183
+ });
184
+ }
@@ -1,7 +1,7 @@
1
1
  import { replaceLegacyOidsInObject } from '@verdant-web/common';
2
2
  import { closeDatabase, storeRequestPromise } from '../idb.js';
3
3
 
4
- const migrations = [version1, version2, version3, version4, version5];
4
+ const migrations = [version1, version2, version3, version4, version5, version6];
5
5
 
6
6
  export function openMetadataDatabase({
7
7
  indexedDB = window.indexedDB,
@@ -258,3 +258,9 @@ async function version5(db: IDBDatabase, tx: IDBTransaction) {
258
258
  };
259
259
  });
260
260
  }
261
+
262
+ // version 5->6: add timestamp to file metadata
263
+ async function version6(db: IDBDatabase, tx: IDBTransaction) {
264
+ const files = tx.objectStore('files');
265
+ files.createIndex('timestamp', 'timestamp');
266
+ }
@@ -1,3 +1,6 @@
1
+ import { closeDatabase, globalIDB, storeRequestPromise } from '../idb.js';
2
+ import { OpenDocumentDbContext } from './types.js';
3
+
1
4
  export async function getDatabaseVersion(
2
5
  indexedDB: IDBFactory,
3
6
  namespace: string,
@@ -42,12 +45,6 @@ export async function getDatabaseVersion(
42
45
  return currentVersion;
43
46
  }
44
47
 
45
- export async function closeDatabase(db: IDBDatabase) {
46
- db.close();
47
- // FIXME: this isn't right!!!!
48
- await new Promise<void>((resolve) => resolve());
49
- }
50
-
51
48
  /**
52
49
  * Upgrades the database to the given version, using the given upgrader function.
53
50
  */
@@ -61,8 +58,11 @@ export async function upgradeDatabase(
61
58
  event: IDBVersionChangeEvent,
62
59
  ) => void,
63
60
  log?: (...args: any[]) => void,
64
- ): Promise<void> {
65
- function openAndUpgrade(resolve: () => void, reject: (err: Error) => void) {
61
+ ): Promise<IDBDatabase> {
62
+ function openAndUpgrade(
63
+ resolve: (db: IDBDatabase) => void,
64
+ reject: (err: Error) => void,
65
+ ) {
66
66
  const request = indexedDb.open(
67
67
  [namespace, 'collections'].join('_'),
68
68
  version,
@@ -74,9 +74,8 @@ export async function upgradeDatabase(
74
74
  wasUpgraded = true;
75
75
  };
76
76
  request.onsuccess = (event) => {
77
- request.result.close();
78
77
  if (wasUpgraded) {
79
- resolve();
78
+ resolve(request.result);
80
79
  } else {
81
80
  reject(
82
81
  new Error(
@@ -95,7 +94,7 @@ export async function upgradeDatabase(
95
94
  // }, 200);
96
95
  };
97
96
  }
98
- return new Promise(openAndUpgrade);
97
+ return new Promise<IDBDatabase>(openAndUpgrade);
99
98
  }
100
99
 
101
100
  export async function acquireLock(
@@ -110,15 +109,20 @@ export async function acquireLock(
110
109
  }
111
110
  }
112
111
 
113
- export async function openDatabase(
114
- indexedDb: IDBFactory,
115
- namespace: string,
116
- version: number,
117
- log?: (...args: any[]) => void,
118
- ): Promise<IDBDatabase> {
119
- log?.('debug', 'Opening database', namespace, 'at version', version);
112
+ export async function openDatabase({
113
+ indexedDB = globalIDB,
114
+ namespace,
115
+ version,
116
+ context,
117
+ }: {
118
+ indexedDB?: IDBFactory;
119
+ namespace: string;
120
+ version: number;
121
+ context: OpenDocumentDbContext;
122
+ }): Promise<IDBDatabase> {
123
+ context.log('debug', 'Opening database', namespace, 'at version', version);
120
124
  const db = await new Promise<IDBDatabase>((resolve, reject) => {
121
- const request = indexedDb.open(
125
+ const request = indexedDB.open(
122
126
  [namespace, 'collections'].join('_'),
123
127
  version,
124
128
  );
@@ -126,7 +130,7 @@ export async function openDatabase(
126
130
  const transaction = request.transaction!;
127
131
  transaction.abort();
128
132
 
129
- log?.(
133
+ context.log(
130
134
  'error',
131
135
  'Database upgrade needed, but not expected',
132
136
  'Expected',
@@ -158,3 +162,41 @@ export async function openDatabase(
158
162
 
159
163
  return db;
160
164
  }
165
+
166
+ export async function copyAll(
167
+ sourceDatabase: IDBDatabase,
168
+ targetDatabase: IDBDatabase,
169
+ ) {
170
+ // DOMStringList... doesn't have iterable... why
171
+ const sourceStoreNames = new Array<string>();
172
+ for (let i = 0; i < sourceDatabase.objectStoreNames.length; i++) {
173
+ sourceStoreNames.push(sourceDatabase.objectStoreNames[i]);
174
+ }
175
+
176
+ const copyFromTransaction = sourceDatabase.transaction(
177
+ sourceStoreNames,
178
+ 'readonly',
179
+ );
180
+ const copyFromStores = sourceStoreNames.map((name) =>
181
+ copyFromTransaction.objectStore(name),
182
+ );
183
+ const allObjects = await Promise.all(
184
+ copyFromStores.map((store) => storeRequestPromise(store.getAll())),
185
+ );
186
+
187
+ const copyToTransaction = targetDatabase.transaction(
188
+ sourceStoreNames,
189
+ 'readwrite',
190
+ );
191
+ const copyToStores = sourceStoreNames.map((name) =>
192
+ copyToTransaction.objectStore(name),
193
+ );
194
+
195
+ for (let i = 0; i < copyToStores.length; i++) {
196
+ await Promise.all(
197
+ allObjects[i].map((obj) => {
198
+ return storeRequestPromise(copyToStores[i].put(obj));
199
+ }),
200
+ );
201
+ }
202
+ }
@@ -0,0 +1,248 @@
1
+ import {
2
+ CollectionFilter,
3
+ Migration,
4
+ MigrationEngine,
5
+ ObjectIdentifier,
6
+ addFieldDefaults,
7
+ assert,
8
+ assignOidsToAllSubObjects,
9
+ cloneDeep,
10
+ createOid,
11
+ diffToPatches,
12
+ getOid,
13
+ initialToPatches,
14
+ removeOidPropertiesFromAllSubObjects,
15
+ } from '@verdant-web/common';
16
+ import { Context } from '../context.js';
17
+ import { Metadata } from '../metadata/Metadata.js';
18
+ import { findAllOids, findOneOid } from '../queries/dbQueries.js';
19
+ import { OpenDocumentDbContext } from './types.js';
20
+
21
+ function getMigrationMutations({
22
+ migration,
23
+ meta,
24
+ getMigrationNow,
25
+ newOids,
26
+ }: {
27
+ migration: Migration<any>;
28
+ newOids: string[];
29
+ getMigrationNow: () => string;
30
+ meta: Metadata;
31
+ }) {
32
+ return migration.allCollections.reduce((acc, collectionName) => {
33
+ acc[collectionName] = {
34
+ put: async (doc: any) => {
35
+ // add defaults
36
+ addFieldDefaults(migration.newSchema.collections[collectionName], doc);
37
+ const primaryKey =
38
+ doc[migration.newSchema.collections[collectionName].primaryKey];
39
+ const oid = createOid(collectionName, primaryKey);
40
+ newOids.push(oid);
41
+ await meta.insertLocalOperations(
42
+ initialToPatches(doc, oid, getMigrationNow),
43
+ );
44
+ return doc;
45
+ },
46
+ delete: async (id: string) => {
47
+ const rootOid = createOid(collectionName, id);
48
+ const allOids = await meta.getAllDocumentRelatedOids(rootOid);
49
+ return meta.insertLocalOperations(
50
+ allOids.map((oid) => ({
51
+ oid,
52
+ timestamp: getMigrationNow(),
53
+ data: { op: 'delete' },
54
+ })),
55
+ );
56
+ },
57
+ };
58
+ return acc;
59
+ }, {} as any);
60
+ }
61
+
62
+ function getMigrationQueries({
63
+ migration,
64
+ context,
65
+ meta,
66
+ }: {
67
+ migration: Migration<any>;
68
+ context: Context;
69
+ meta: Metadata;
70
+ }) {
71
+ return migration.oldCollections.reduce((acc, collectionName) => {
72
+ acc[collectionName] = {
73
+ get: async (id: string) => {
74
+ const oid = createOid(collectionName, id);
75
+ const doc = await meta.getDocumentSnapshot(oid, {
76
+ // only get the snapshot up to the previous version (newer operations may have synced)
77
+ to: meta.time.now(migration.oldSchema.version),
78
+ });
79
+ return doc;
80
+ },
81
+ findOne: async (filter: CollectionFilter) => {
82
+ const oid = await findOneOid({
83
+ collection: collectionName,
84
+ index: filter,
85
+ context,
86
+ });
87
+ if (!oid) return null;
88
+ const doc = await meta.getDocumentSnapshot(oid, {
89
+ // only get the snapshot up to the previous version (newer operations may have synced)
90
+ to: meta.time.now(migration.oldSchema.version),
91
+ });
92
+ return doc;
93
+ },
94
+ findAll: async (filter: CollectionFilter) => {
95
+ const oids = await findAllOids({
96
+ collection: collectionName,
97
+ index: filter,
98
+ context,
99
+ });
100
+ const docs = await Promise.all(
101
+ oids.map((oid) =>
102
+ meta.getDocumentSnapshot(oid, {
103
+ // only get the snapshot up to the previous version (newer operations may have synced)
104
+ to: meta.time.now(migration.oldSchema.version),
105
+ }),
106
+ ),
107
+ );
108
+ return docs;
109
+ },
110
+ };
111
+ return acc;
112
+ }, {} as any);
113
+ }
114
+
115
+ export function getMigrationEngine({
116
+ meta,
117
+ migration,
118
+ context,
119
+ }: {
120
+ log?: (...args: any[]) => void;
121
+ migration: Migration;
122
+ meta: Metadata;
123
+ context: Context;
124
+ }): MigrationEngine {
125
+ function getMigrationNow() {
126
+ return meta.time.zero(migration.version);
127
+ }
128
+
129
+ const newOids = new Array<ObjectIdentifier>();
130
+
131
+ const queries = getMigrationQueries({
132
+ migration,
133
+ context,
134
+ meta,
135
+ });
136
+ const mutations = getMigrationMutations({
137
+ migration,
138
+ getMigrationNow,
139
+ newOids,
140
+ meta,
141
+ });
142
+ const deleteCollection = async (collection: string) => {
143
+ const allOids = await meta.getAllCollectionRelatedOids(collection);
144
+ return meta.insertLocalOperations(
145
+ allOids.map((oid) => ({
146
+ oid,
147
+ timestamp: getMigrationNow(),
148
+ data: { op: 'delete' },
149
+ })),
150
+ );
151
+ };
152
+ const awaitables = new Array<Promise<any>>();
153
+ const engine: MigrationEngine = {
154
+ log: context.log,
155
+ newOids,
156
+ deleteCollection,
157
+ migrate: async (collection, strategy) => {
158
+ const docs = await queries[collection].findAll();
159
+
160
+ await Promise.all(
161
+ docs.filter(Boolean).map(async (doc: any) => {
162
+ const rootOid = getOid(doc);
163
+ assert(
164
+ !!rootOid,
165
+ `Document is missing an OID: ${JSON.stringify(doc)}`,
166
+ );
167
+ const original = cloneDeep(doc);
168
+ // @ts-ignore - excessive type resolution
169
+ const newValue = await strategy(doc);
170
+ if (newValue) {
171
+ // the migration has altered the shape of our document. we need
172
+ // to create the operation from the diff and write it to meta as
173
+ // a migration patch
174
+ removeOidPropertiesFromAllSubObjects(original);
175
+ removeOidPropertiesFromAllSubObjects(newValue);
176
+ assignOidsToAllSubObjects(newValue);
177
+ const patches = diffToPatches(
178
+ original,
179
+ newValue,
180
+ getMigrationNow,
181
+ undefined,
182
+ [],
183
+ {
184
+ mergeUnknownObjects: true,
185
+ },
186
+ );
187
+ if (patches.length > 0) {
188
+ await meta.insertLocalOperations(patches);
189
+ }
190
+ }
191
+ }),
192
+ );
193
+ },
194
+ queries,
195
+ mutations,
196
+ awaitables,
197
+ };
198
+ return engine;
199
+ }
200
+
201
+ export function getInitialMigrationEngine({
202
+ meta,
203
+ migration,
204
+ context,
205
+ }: {
206
+ context: OpenDocumentDbContext;
207
+ migration: Migration;
208
+ meta: Metadata;
209
+ }): MigrationEngine {
210
+ function getMigrationNow() {
211
+ return meta.time.zero(migration.version);
212
+ }
213
+
214
+ const newOids = new Array<ObjectIdentifier>();
215
+
216
+ const queries = new Proxy({} as any, {
217
+ get() {
218
+ throw new Error(
219
+ 'Queries are not available in initial migrations; there is no database yet!',
220
+ );
221
+ },
222
+ }) as any;
223
+
224
+ const mutations = getMigrationMutations({
225
+ migration,
226
+ getMigrationNow,
227
+ newOids,
228
+ meta,
229
+ });
230
+ const engine: MigrationEngine = {
231
+ log: context.log,
232
+ newOids,
233
+ deleteCollection: () => {
234
+ throw new Error(
235
+ 'Calling deleteCollection() in initial migrations is not supported! Use initial migrations to seed initial data using mutations.',
236
+ );
237
+ },
238
+ migrate: () => {
239
+ throw new Error(
240
+ 'Calling migrate() in initial migrations is not supported! Use initial migrations to seed initial data using mutations.',
241
+ );
242
+ },
243
+ queries,
244
+ mutations,
245
+ awaitables: [],
246
+ };
247
+ return engine;
248
+ }