@verdant-web/store 2.2.0 → 2.3.1
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.
- package/dist/cjs/client/Client.js +4 -16
- package/dist/cjs/client/Client.js.map +1 -1
- package/dist/cjs/client/ClientDescriptor.d.ts +1 -0
- package/dist/cjs/client/ClientDescriptor.js +7 -3
- package/dist/cjs/client/ClientDescriptor.js.map +1 -1
- package/dist/cjs/files/EntityFile.d.ts +5 -1
- package/dist/cjs/files/EntityFile.js +6 -1
- package/dist/cjs/files/EntityFile.js.map +1 -1
- package/dist/cjs/files/FileManager.js +9 -2
- package/dist/cjs/files/FileManager.js.map +1 -1
- package/dist/cjs/files/FileStorage.d.ts +2 -1
- package/dist/cjs/files/FileStorage.js +13 -2
- package/dist/cjs/files/FileStorage.js.map +1 -1
- package/dist/cjs/files/utils.js +1 -1
- package/dist/cjs/files/utils.js.map +1 -1
- package/dist/cjs/idb.d.ts +1 -0
- package/dist/cjs/idb.js +17 -1
- package/dist/cjs/idb.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/metadata/Metadata.d.ts +4 -0
- package/dist/cjs/metadata/Metadata.js +9 -2
- package/dist/cjs/metadata/Metadata.js.map +1 -1
- package/dist/cjs/migration/db.d.ts +8 -0
- package/dist/cjs/migration/db.js +109 -0
- package/dist/cjs/migration/db.js.map +1 -0
- package/dist/cjs/migration/errors.d.ts +5 -0
- package/dist/cjs/migration/errors.js +12 -0
- package/dist/cjs/migration/errors.js.map +1 -0
- package/dist/cjs/{openDocumentDatabase.d.ts → migration/openDatabase.d.ts} +2 -2
- package/dist/cjs/{openDocumentDatabase.js → migration/openDatabase.js} +47 -150
- package/dist/cjs/migration/openDatabase.js.map +1 -0
- package/dist/cjs/migration/paths.d.ts +6 -0
- package/dist/cjs/migration/paths.js +53 -0
- package/dist/cjs/migration/paths.js.map +1 -0
- package/dist/cjs/migration/paths.test.d.ts +1 -0
- package/dist/cjs/migration/paths.test.js +91 -0
- package/dist/cjs/migration/paths.test.js.map +1 -0
- package/dist/cjs/reactives/DocumentFamiliyCache.d.ts +5 -1
- package/dist/cjs/reactives/DocumentFamiliyCache.js +11 -7
- package/dist/cjs/reactives/DocumentFamiliyCache.js.map +1 -1
- package/dist/cjs/sync/Heartbeat.js +1 -1
- package/dist/cjs/sync/Heartbeat.js.map +1 -1
- package/dist/cjs/sync/PresenceManager.js +3 -0
- package/dist/cjs/sync/PresenceManager.js.map +1 -1
- package/dist/cjs/sync/WebSocketSync.js +6 -0
- package/dist/cjs/sync/WebSocketSync.js.map +1 -1
- package/dist/esm/client/Client.js +3 -15
- package/dist/esm/client/Client.js.map +1 -1
- package/dist/esm/client/ClientDescriptor.d.ts +1 -0
- package/dist/esm/client/ClientDescriptor.js +6 -2
- package/dist/esm/client/ClientDescriptor.js.map +1 -1
- package/dist/esm/files/EntityFile.d.ts +5 -1
- package/dist/esm/files/EntityFile.js +6 -1
- package/dist/esm/files/EntityFile.js.map +1 -1
- package/dist/esm/files/FileManager.js +9 -2
- package/dist/esm/files/FileManager.js.map +1 -1
- package/dist/esm/files/FileStorage.d.ts +2 -1
- package/dist/esm/files/FileStorage.js +13 -2
- package/dist/esm/files/FileStorage.js.map +1 -1
- package/dist/esm/files/utils.js +1 -1
- package/dist/esm/files/utils.js.map +1 -1
- package/dist/esm/idb.d.ts +1 -0
- package/dist/esm/idb.js +15 -0
- package/dist/esm/idb.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/metadata/Metadata.d.ts +4 -0
- package/dist/esm/metadata/Metadata.js +9 -2
- package/dist/esm/metadata/Metadata.js.map +1 -1
- package/dist/esm/migration/db.d.ts +8 -0
- package/dist/esm/migration/db.js +101 -0
- package/dist/esm/migration/db.js.map +1 -0
- package/dist/esm/migration/errors.d.ts +5 -0
- package/dist/esm/migration/errors.js +8 -0
- package/dist/esm/migration/errors.js.map +1 -0
- package/dist/esm/{openDocumentDatabase.d.ts → migration/openDatabase.d.ts} +2 -2
- package/dist/esm/{openDocumentDatabase.js → migration/openDatabase.js} +39 -142
- package/dist/esm/migration/openDatabase.js.map +1 -0
- package/dist/esm/migration/paths.d.ts +6 -0
- package/dist/esm/migration/paths.js +49 -0
- package/dist/esm/migration/paths.js.map +1 -0
- package/dist/esm/migration/paths.test.d.ts +1 -0
- package/dist/esm/migration/paths.test.js +89 -0
- package/dist/esm/migration/paths.test.js.map +1 -0
- package/dist/esm/reactives/DocumentFamiliyCache.d.ts +5 -1
- package/dist/esm/reactives/DocumentFamiliyCache.js +11 -7
- package/dist/esm/reactives/DocumentFamiliyCache.js.map +1 -1
- package/dist/esm/sync/Heartbeat.js +1 -1
- package/dist/esm/sync/Heartbeat.js.map +1 -1
- package/dist/esm/sync/PresenceManager.js +3 -0
- package/dist/esm/sync/PresenceManager.js.map +1 -1
- package/dist/esm/sync/WebSocketSync.js +6 -0
- package/dist/esm/sync/WebSocketSync.js.map +1 -1
- package/dist/tsconfig-cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/client/Client.ts +7 -17
- package/src/client/ClientDescriptor.ts +7 -3
- package/src/files/EntityFile.ts +14 -1
- package/src/files/FileManager.ts +8 -2
- package/src/files/FileStorage.ts +18 -2
- package/src/files/utils.ts +1 -1
- package/src/idb.ts +19 -0
- package/src/index.ts +1 -0
- package/src/metadata/Metadata.ts +9 -2
- package/src/migration/db.ts +147 -0
- package/src/migration/errors.ts +7 -0
- package/src/{openDocumentDatabase.ts → migration/openDatabase.ts} +69 -201
- package/src/migration/paths.test.ts +93 -0
- package/src/migration/paths.ts +73 -0
- package/src/reactives/DocumentFamiliyCache.ts +31 -9
- package/src/sync/Heartbeat.ts +1 -1
- package/src/sync/PresenceManager.ts +3 -0
- package/src/sync/WebSocketSync.ts +9 -0
- package/dist/cjs/openDocumentDatabase.js.map +0 -1
- package/dist/esm/openDocumentDatabase.js.map +0 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export async function getDatabaseVersion(
|
|
2
|
+
indexedDB: IDBFactory,
|
|
3
|
+
namespace: string,
|
|
4
|
+
version: number,
|
|
5
|
+
log?: (...args: any[]) => void,
|
|
6
|
+
): Promise<number> {
|
|
7
|
+
function openAndGetVersion(
|
|
8
|
+
resolve: (res: [number, IDBDatabase]) => void,
|
|
9
|
+
reject: (err: Error) => void,
|
|
10
|
+
) {
|
|
11
|
+
let currentVersion: number;
|
|
12
|
+
let database: IDBDatabase;
|
|
13
|
+
const request = indexedDB.open(
|
|
14
|
+
[namespace, 'collections'].join('_'),
|
|
15
|
+
version,
|
|
16
|
+
);
|
|
17
|
+
request.onupgradeneeded = async (event) => {
|
|
18
|
+
currentVersion = event.oldVersion;
|
|
19
|
+
const transaction = request.transaction!;
|
|
20
|
+
database = request.result;
|
|
21
|
+
transaction.abort();
|
|
22
|
+
};
|
|
23
|
+
request.onsuccess = (event) => {
|
|
24
|
+
resolve([request.result.version, request.result]);
|
|
25
|
+
};
|
|
26
|
+
request.onblocked = (event) => {
|
|
27
|
+
// retry if blocked
|
|
28
|
+
log?.('Database blocked, waiting...');
|
|
29
|
+
// setTimeout(() => {
|
|
30
|
+
// openAndGetVersion(resolve, reject);
|
|
31
|
+
// }, 200);
|
|
32
|
+
};
|
|
33
|
+
request.onerror = (event) => {
|
|
34
|
+
// FIXME: this fails if the code is older than the local database
|
|
35
|
+
resolve([currentVersion!, database!]);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const [currentVersion, db] = await new Promise<[number, IDBDatabase]>(
|
|
39
|
+
openAndGetVersion,
|
|
40
|
+
);
|
|
41
|
+
await closeDatabase(db);
|
|
42
|
+
return currentVersion;
|
|
43
|
+
}
|
|
44
|
+
|
|
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
|
+
/**
|
|
52
|
+
* Upgrades the database to the given version, using the given upgrader function.
|
|
53
|
+
*/
|
|
54
|
+
export async function upgradeDatabase(
|
|
55
|
+
indexedDb: IDBFactory,
|
|
56
|
+
namespace: string,
|
|
57
|
+
version: number,
|
|
58
|
+
upgrader: (
|
|
59
|
+
transaction: IDBTransaction,
|
|
60
|
+
db: IDBDatabase,
|
|
61
|
+
event: IDBVersionChangeEvent,
|
|
62
|
+
) => void,
|
|
63
|
+
log?: (...args: any[]) => void,
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
function openAndUpgrade(resolve: () => void, reject: (err: Error) => void) {
|
|
66
|
+
const request = indexedDb.open(
|
|
67
|
+
[namespace, 'collections'].join('_'),
|
|
68
|
+
version,
|
|
69
|
+
);
|
|
70
|
+
let wasUpgraded = false;
|
|
71
|
+
request.onupgradeneeded = (event) => {
|
|
72
|
+
const transaction = request.transaction!;
|
|
73
|
+
upgrader(transaction, request.result, event);
|
|
74
|
+
wasUpgraded = true;
|
|
75
|
+
};
|
|
76
|
+
request.onsuccess = (event) => {
|
|
77
|
+
request.result.close();
|
|
78
|
+
if (wasUpgraded) {
|
|
79
|
+
resolve();
|
|
80
|
+
} else {
|
|
81
|
+
reject(
|
|
82
|
+
new Error(
|
|
83
|
+
'Database was not upgraded when a version change was expected',
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
request.onerror = (event) => {
|
|
89
|
+
reject(request.error || new Error('Unknown error'));
|
|
90
|
+
};
|
|
91
|
+
request.onblocked = (event) => {
|
|
92
|
+
log?.('Database upgrade blocked, waiting...');
|
|
93
|
+
// setTimeout(() => {
|
|
94
|
+
// openAndUpgrade(resolve, reject);
|
|
95
|
+
// }, 200);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return new Promise(openAndUpgrade);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function acquireLock(
|
|
102
|
+
namespace: string,
|
|
103
|
+
procedure: () => Promise<void>,
|
|
104
|
+
) {
|
|
105
|
+
if (typeof navigator !== 'undefined' && navigator.locks) {
|
|
106
|
+
await navigator.locks.request(`verdant_migration_${namespace}`, procedure);
|
|
107
|
+
} else {
|
|
108
|
+
// TODO: is there a fallback?
|
|
109
|
+
await procedure();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function openDatabase(
|
|
114
|
+
indexedDb: IDBFactory,
|
|
115
|
+
namespace: string,
|
|
116
|
+
version: number,
|
|
117
|
+
): Promise<IDBDatabase> {
|
|
118
|
+
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
|
119
|
+
const request = indexedDb.open(
|
|
120
|
+
[namespace, 'collections'].join('_'),
|
|
121
|
+
version,
|
|
122
|
+
);
|
|
123
|
+
request.onupgradeneeded = async (event) => {
|
|
124
|
+
const transaction = request.transaction!;
|
|
125
|
+
transaction.abort();
|
|
126
|
+
|
|
127
|
+
reject(
|
|
128
|
+
new Error('Migration error: database version changed while migrating'),
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
request.onsuccess = (event) => {
|
|
132
|
+
resolve(request.result);
|
|
133
|
+
};
|
|
134
|
+
request.onblocked = (event) => {
|
|
135
|
+
reject(new Error('Migration error: database blocked'));
|
|
136
|
+
};
|
|
137
|
+
request.onerror = (event) => {
|
|
138
|
+
reject(new Error('Migration error: database error'));
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
db.addEventListener('versionchange', (event) => {
|
|
143
|
+
db.close();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return db;
|
|
147
|
+
}
|
|
@@ -5,21 +5,33 @@ import {
|
|
|
5
5
|
ObjectIdentifier,
|
|
6
6
|
StorageSchema,
|
|
7
7
|
addFieldDefaults,
|
|
8
|
+
assert,
|
|
8
9
|
assignIndexValues,
|
|
10
|
+
assignOid,
|
|
9
11
|
assignOidPropertiesToAllSubObjects,
|
|
10
12
|
assignOidsToAllSubObjects,
|
|
11
13
|
cloneDeep,
|
|
12
14
|
createOid,
|
|
13
15
|
decomposeOid,
|
|
14
16
|
diffToPatches,
|
|
17
|
+
getOid,
|
|
18
|
+
hasOid,
|
|
15
19
|
initialToPatches,
|
|
16
20
|
migrationRange,
|
|
17
21
|
removeOidPropertiesFromAllSubObjects,
|
|
18
22
|
removeOidsFromAllSubObjects,
|
|
19
23
|
} from '@verdant-web/common';
|
|
20
|
-
import { Context } from '
|
|
21
|
-
import { Metadata } from '
|
|
22
|
-
import { findAllOids, findOneOid } from '
|
|
24
|
+
import { Context } from '../context.js';
|
|
25
|
+
import { Metadata } from '../metadata/Metadata.js';
|
|
26
|
+
import { findAllOids, findOneOid } from '../queries2/dbQueries.js';
|
|
27
|
+
import {
|
|
28
|
+
acquireLock,
|
|
29
|
+
closeDatabase,
|
|
30
|
+
getDatabaseVersion,
|
|
31
|
+
openDatabase,
|
|
32
|
+
upgradeDatabase,
|
|
33
|
+
} from './db.js';
|
|
34
|
+
import { getMigrationPath } from './paths.js';
|
|
23
35
|
|
|
24
36
|
const globalIDB =
|
|
25
37
|
typeof window !== 'undefined' ? window.indexedDB : (undefined as any);
|
|
@@ -53,13 +65,11 @@ export async function openDocumentDatabase({
|
|
|
53
65
|
version,
|
|
54
66
|
);
|
|
55
67
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
throw new Error(`No migration found for version(s) ${toRunVersions}`);
|
|
62
|
-
}
|
|
68
|
+
const toRun = getMigrationPath({
|
|
69
|
+
currentVersion,
|
|
70
|
+
targetVersion: version,
|
|
71
|
+
migrations,
|
|
72
|
+
});
|
|
63
73
|
|
|
64
74
|
if (toRun.length > 0) {
|
|
65
75
|
await acquireLock(context.namespace, async () => {
|
|
@@ -240,86 +250,6 @@ export async function openDocumentDatabase({
|
|
|
240
250
|
}
|
|
241
251
|
}
|
|
242
252
|
|
|
243
|
-
async function getDatabaseVersion(
|
|
244
|
-
indexedDB: IDBFactory,
|
|
245
|
-
namespace: string,
|
|
246
|
-
version: number,
|
|
247
|
-
log?: (...args: any[]) => void,
|
|
248
|
-
): Promise<number> {
|
|
249
|
-
function openAndGetVersion(
|
|
250
|
-
resolve: (res: [number, IDBDatabase]) => void,
|
|
251
|
-
reject: (err: Error) => void,
|
|
252
|
-
) {
|
|
253
|
-
let currentVersion: number;
|
|
254
|
-
let database: IDBDatabase;
|
|
255
|
-
const request = indexedDB.open(
|
|
256
|
-
[namespace, 'collections'].join('_'),
|
|
257
|
-
version,
|
|
258
|
-
);
|
|
259
|
-
request.onupgradeneeded = async (event) => {
|
|
260
|
-
currentVersion = event.oldVersion;
|
|
261
|
-
const transaction = request.transaction!;
|
|
262
|
-
database = request.result;
|
|
263
|
-
transaction.abort();
|
|
264
|
-
};
|
|
265
|
-
request.onsuccess = (event) => {
|
|
266
|
-
resolve([request.result.version, request.result]);
|
|
267
|
-
};
|
|
268
|
-
request.onblocked = (event) => {
|
|
269
|
-
// retry if blocked
|
|
270
|
-
log?.('Database blocked, waiting...');
|
|
271
|
-
// setTimeout(() => {
|
|
272
|
-
// openAndGetVersion(resolve, reject);
|
|
273
|
-
// }, 200);
|
|
274
|
-
};
|
|
275
|
-
request.onerror = (event) => {
|
|
276
|
-
// FIXME: this fails if the code is older than the local database
|
|
277
|
-
resolve([currentVersion!, database!]);
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
const [currentVersion, db] = await new Promise<[number, IDBDatabase]>(
|
|
281
|
-
openAndGetVersion,
|
|
282
|
-
);
|
|
283
|
-
await closeDatabase(db);
|
|
284
|
-
return currentVersion;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function openDatabase(
|
|
288
|
-
indexedDb: IDBFactory,
|
|
289
|
-
namespace: string,
|
|
290
|
-
version: number,
|
|
291
|
-
): Promise<IDBDatabase> {
|
|
292
|
-
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
|
293
|
-
const request = indexedDb.open(
|
|
294
|
-
[namespace, 'collections'].join('_'),
|
|
295
|
-
version,
|
|
296
|
-
);
|
|
297
|
-
request.onupgradeneeded = async (event) => {
|
|
298
|
-
const transaction = request.transaction!;
|
|
299
|
-
transaction.abort();
|
|
300
|
-
|
|
301
|
-
reject(
|
|
302
|
-
new Error('Migration error: database version changed while migrating'),
|
|
303
|
-
);
|
|
304
|
-
};
|
|
305
|
-
request.onsuccess = (event) => {
|
|
306
|
-
resolve(request.result);
|
|
307
|
-
};
|
|
308
|
-
request.onblocked = (event) => {
|
|
309
|
-
reject(new Error('Migration error: database blocked'));
|
|
310
|
-
};
|
|
311
|
-
request.onerror = (event) => {
|
|
312
|
-
reject(new Error('Migration error: database error'));
|
|
313
|
-
};
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
db.addEventListener('versionchange', (event) => {
|
|
317
|
-
db.close();
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
return db;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
253
|
function getMigrationMutations({
|
|
324
254
|
migration,
|
|
325
255
|
meta,
|
|
@@ -360,28 +290,21 @@ function getMigrationMutations({
|
|
|
360
290
|
}, {} as any);
|
|
361
291
|
}
|
|
362
292
|
|
|
363
|
-
function
|
|
364
|
-
meta,
|
|
293
|
+
function getMigrationQueries({
|
|
365
294
|
migration,
|
|
366
295
|
context,
|
|
296
|
+
meta,
|
|
367
297
|
}: {
|
|
368
|
-
|
|
369
|
-
migration: Migration;
|
|
370
|
-
meta: Metadata;
|
|
298
|
+
migration: Migration<any>;
|
|
371
299
|
context: Context;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const newOids = new Array<ObjectIdentifier>();
|
|
378
|
-
|
|
379
|
-
const queries = migration.oldCollections.reduce((acc, collectionName) => {
|
|
300
|
+
meta: Metadata;
|
|
301
|
+
}) {
|
|
302
|
+
return migration.oldCollections.reduce((acc, collectionName) => {
|
|
380
303
|
acc[collectionName] = {
|
|
381
304
|
get: async (id: string) => {
|
|
382
305
|
const oid = createOid(collectionName, id);
|
|
383
306
|
const doc = await meta.getDocumentSnapshot(oid);
|
|
384
|
-
removeOidsFromAllSubObjects(doc);
|
|
307
|
+
// removeOidsFromAllSubObjects(doc);
|
|
385
308
|
return doc;
|
|
386
309
|
},
|
|
387
310
|
findOne: async (filter: CollectionFilter) => {
|
|
@@ -392,7 +315,7 @@ function getMigrationEngine({
|
|
|
392
315
|
});
|
|
393
316
|
if (!oid) return null;
|
|
394
317
|
const doc = await meta.getDocumentSnapshot(oid);
|
|
395
|
-
removeOidsFromAllSubObjects(doc);
|
|
318
|
+
// removeOidsFromAllSubObjects(doc);
|
|
396
319
|
return doc;
|
|
397
320
|
},
|
|
398
321
|
findAll: async (filter: CollectionFilter) => {
|
|
@@ -404,12 +327,35 @@ function getMigrationEngine({
|
|
|
404
327
|
const docs = await Promise.all(
|
|
405
328
|
oids.map((oid) => meta.getDocumentSnapshot(oid)),
|
|
406
329
|
);
|
|
407
|
-
docs.forEach((doc) => removeOidsFromAllSubObjects(doc));
|
|
330
|
+
// docs.forEach((doc) => removeOidsFromAllSubObjects(doc));
|
|
408
331
|
return docs;
|
|
409
332
|
},
|
|
410
333
|
};
|
|
411
334
|
return acc;
|
|
412
335
|
}, {} as any);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function getMigrationEngine({
|
|
339
|
+
meta,
|
|
340
|
+
migration,
|
|
341
|
+
context,
|
|
342
|
+
}: {
|
|
343
|
+
log?: (...args: any[]) => void;
|
|
344
|
+
migration: Migration;
|
|
345
|
+
meta: Metadata;
|
|
346
|
+
context: Context;
|
|
347
|
+
}): MigrationEngine<any, any> {
|
|
348
|
+
function getMigrationNow() {
|
|
349
|
+
return meta.time.zero(migration.version);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const newOids = new Array<ObjectIdentifier>();
|
|
353
|
+
|
|
354
|
+
const queries = getMigrationQueries({
|
|
355
|
+
migration,
|
|
356
|
+
context,
|
|
357
|
+
meta,
|
|
358
|
+
});
|
|
413
359
|
const mutations = getMigrationMutations({
|
|
414
360
|
migration,
|
|
415
361
|
getMigrationNow,
|
|
@@ -418,45 +364,27 @@ function getMigrationEngine({
|
|
|
418
364
|
});
|
|
419
365
|
const awaitables = new Array<Promise<any>>();
|
|
420
366
|
const engine: MigrationEngine<StorageSchema, StorageSchema> = {
|
|
367
|
+
log: context.log,
|
|
421
368
|
newOids,
|
|
422
369
|
migrate: async (collection, strategy) => {
|
|
423
|
-
const docs = await
|
|
424
|
-
const transaction = context.documentDb.transaction(
|
|
425
|
-
collection,
|
|
426
|
-
'readonly',
|
|
427
|
-
);
|
|
428
|
-
|
|
429
|
-
const store = transaction.objectStore(collection);
|
|
430
|
-
const cursorReq = store.openCursor();
|
|
431
|
-
|
|
432
|
-
const documentsToMigrate: any[] = [];
|
|
433
|
-
|
|
434
|
-
cursorReq.onsuccess = async (event) => {
|
|
435
|
-
const cursor = cursorReq.result;
|
|
436
|
-
if (cursor) {
|
|
437
|
-
documentsToMigrate.push(cursor.value);
|
|
438
|
-
cursor.continue();
|
|
439
|
-
} else {
|
|
440
|
-
resolve(documentsToMigrate);
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
cursorReq.onerror = (event) => {
|
|
444
|
-
reject(cursorReq.error);
|
|
445
|
-
};
|
|
446
|
-
});
|
|
370
|
+
const docs = await queries[collection].findAll();
|
|
447
371
|
|
|
448
372
|
await Promise.all(
|
|
449
|
-
docs.map(async (doc) => {
|
|
373
|
+
docs.map(async (doc: any) => {
|
|
374
|
+
assert(
|
|
375
|
+
hasOid(doc),
|
|
376
|
+
`Document is missing an OID: ${JSON.stringify(doc)}`,
|
|
377
|
+
);
|
|
450
378
|
const original = cloneDeep(doc);
|
|
451
379
|
// remove any indexes before computing the diff
|
|
452
|
-
const collectionSpec = migration.oldSchema.collections[collection];
|
|
453
|
-
const indexKeys = [
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
];
|
|
457
|
-
indexKeys.forEach((key) => {
|
|
458
|
-
|
|
459
|
-
});
|
|
380
|
+
// const collectionSpec = migration.oldSchema.collections[collection];
|
|
381
|
+
// const indexKeys = [
|
|
382
|
+
// ...Object.keys(collectionSpec.synthetics || {}),
|
|
383
|
+
// ...Object.keys(collectionSpec.compounds || {}),
|
|
384
|
+
// ];
|
|
385
|
+
// indexKeys.forEach((key) => {
|
|
386
|
+
// delete doc[key];
|
|
387
|
+
// });
|
|
460
388
|
// @ts-ignore - excessive type resolution
|
|
461
389
|
const newValue = await strategy(doc);
|
|
462
390
|
if (newValue) {
|
|
@@ -493,6 +421,7 @@ function getMigrationEngine({
|
|
|
493
421
|
function getVersion1MigrationEngine({
|
|
494
422
|
meta,
|
|
495
423
|
migration,
|
|
424
|
+
context,
|
|
496
425
|
}: {
|
|
497
426
|
context: OpenDocumentDbContext;
|
|
498
427
|
migration: Migration;
|
|
@@ -519,6 +448,7 @@ function getVersion1MigrationEngine({
|
|
|
519
448
|
meta,
|
|
520
449
|
});
|
|
521
450
|
const engine: MigrationEngine<StorageSchema, StorageSchema> = {
|
|
451
|
+
log: context.log,
|
|
522
452
|
newOids,
|
|
523
453
|
migrate: (collection, strategy) => {
|
|
524
454
|
throw new Error(
|
|
@@ -532,59 +462,6 @@ function getVersion1MigrationEngine({
|
|
|
532
462
|
return engine;
|
|
533
463
|
}
|
|
534
464
|
|
|
535
|
-
async function closeDatabase(db: IDBDatabase) {
|
|
536
|
-
db.close();
|
|
537
|
-
// FIXME: this isn't right!!!!
|
|
538
|
-
await new Promise<void>((resolve) => resolve());
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
async function upgradeDatabase(
|
|
542
|
-
indexedDb: IDBFactory,
|
|
543
|
-
namespace: string,
|
|
544
|
-
version: number,
|
|
545
|
-
upgrader: (
|
|
546
|
-
transaction: IDBTransaction,
|
|
547
|
-
db: IDBDatabase,
|
|
548
|
-
event: IDBVersionChangeEvent,
|
|
549
|
-
) => void,
|
|
550
|
-
log?: (...args: any[]) => void,
|
|
551
|
-
): Promise<void> {
|
|
552
|
-
function openAndUpgrade(resolve: () => void, reject: (err: Error) => void) {
|
|
553
|
-
const request = indexedDb.open(
|
|
554
|
-
[namespace, 'collections'].join('_'),
|
|
555
|
-
version,
|
|
556
|
-
);
|
|
557
|
-
let wasUpgraded = false;
|
|
558
|
-
request.onupgradeneeded = (event) => {
|
|
559
|
-
const transaction = request.transaction!;
|
|
560
|
-
upgrader(transaction, request.result, event);
|
|
561
|
-
wasUpgraded = true;
|
|
562
|
-
};
|
|
563
|
-
request.onsuccess = (event) => {
|
|
564
|
-
request.result.close();
|
|
565
|
-
if (wasUpgraded) {
|
|
566
|
-
resolve();
|
|
567
|
-
} else {
|
|
568
|
-
reject(
|
|
569
|
-
new Error(
|
|
570
|
-
'Database was not upgraded when a version change was expected',
|
|
571
|
-
),
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
};
|
|
575
|
-
request.onerror = (event) => {
|
|
576
|
-
reject(request.error || new Error('Unknown error'));
|
|
577
|
-
};
|
|
578
|
-
request.onblocked = (event) => {
|
|
579
|
-
log?.('Database upgrade blocked, waiting...');
|
|
580
|
-
// setTimeout(() => {
|
|
581
|
-
// openAndUpgrade(resolve, reject);
|
|
582
|
-
// }, 200);
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
return new Promise(openAndUpgrade);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
465
|
async function getAllKeys(store: IDBObjectStore) {
|
|
589
466
|
return new Promise<IDBValidKey[]>((resolve, reject) => {
|
|
590
467
|
const request = store.getAllKeys();
|
|
@@ -620,12 +497,3 @@ async function putView(store: IDBObjectStore, view: any) {
|
|
|
620
497
|
};
|
|
621
498
|
});
|
|
622
499
|
}
|
|
623
|
-
|
|
624
|
-
async function acquireLock(namespace: string, procedure: () => Promise<void>) {
|
|
625
|
-
if (typeof navigator !== 'undefined' && navigator.locks) {
|
|
626
|
-
await navigator.locks.request(`lo-fi_migration_${namespace}`, procedure);
|
|
627
|
-
} else {
|
|
628
|
-
// TODO: is there a fallback?
|
|
629
|
-
await procedure();
|
|
630
|
-
}
|
|
631
|
-
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Migration } from '@verdant-web/common';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { getMigrationPath } from './paths.js';
|
|
4
|
+
|
|
5
|
+
function mockMigration(from: number, to: number) {
|
|
6
|
+
return {
|
|
7
|
+
oldSchema: {
|
|
8
|
+
version: from,
|
|
9
|
+
},
|
|
10
|
+
newSchema: {
|
|
11
|
+
version: to,
|
|
12
|
+
},
|
|
13
|
+
} as any as Migration;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('migration pathfinding', () => {
|
|
17
|
+
it('should follow a linear chain', () => {
|
|
18
|
+
const migrations = [
|
|
19
|
+
mockMigration(0, 1),
|
|
20
|
+
mockMigration(1, 2),
|
|
21
|
+
mockMigration(2, 3),
|
|
22
|
+
mockMigration(3, 4),
|
|
23
|
+
mockMigration(4, 5),
|
|
24
|
+
];
|
|
25
|
+
const path = getMigrationPath({
|
|
26
|
+
currentVersion: 0,
|
|
27
|
+
targetVersion: 5,
|
|
28
|
+
migrations,
|
|
29
|
+
});
|
|
30
|
+
expect(path).toEqual(migrations);
|
|
31
|
+
});
|
|
32
|
+
it('should choose the shortest path', () => {
|
|
33
|
+
const migrations = [
|
|
34
|
+
mockMigration(0, 1),
|
|
35
|
+
mockMigration(1, 2),
|
|
36
|
+
mockMigration(2, 3),
|
|
37
|
+
mockMigration(3, 4),
|
|
38
|
+
mockMigration(4, 5),
|
|
39
|
+
mockMigration(0, 5),
|
|
40
|
+
];
|
|
41
|
+
const path = getMigrationPath({
|
|
42
|
+
currentVersion: 0,
|
|
43
|
+
targetVersion: 5,
|
|
44
|
+
migrations,
|
|
45
|
+
});
|
|
46
|
+
expect(path).toEqual([mockMigration(0, 5)]);
|
|
47
|
+
});
|
|
48
|
+
it('should be resilient to dead ends', () => {
|
|
49
|
+
const migrations = [
|
|
50
|
+
mockMigration(0, 1),
|
|
51
|
+
mockMigration(1, 2),
|
|
52
|
+
mockMigration(4, 5),
|
|
53
|
+
mockMigration(5, 6),
|
|
54
|
+
mockMigration(1, 4),
|
|
55
|
+
];
|
|
56
|
+
const path = getMigrationPath({
|
|
57
|
+
currentVersion: 0,
|
|
58
|
+
targetVersion: 6,
|
|
59
|
+
migrations,
|
|
60
|
+
});
|
|
61
|
+
expect(path).toEqual([
|
|
62
|
+
mockMigration(0, 1),
|
|
63
|
+
mockMigration(1, 4),
|
|
64
|
+
mockMigration(4, 5),
|
|
65
|
+
mockMigration(5, 6),
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
it('should error when no path exists to target', () => {
|
|
69
|
+
const migrations = [
|
|
70
|
+
mockMigration(0, 1),
|
|
71
|
+
mockMigration(2, 3),
|
|
72
|
+
mockMigration(3, 4),
|
|
73
|
+
mockMigration(4, 5),
|
|
74
|
+
mockMigration(5, 6),
|
|
75
|
+
];
|
|
76
|
+
expect(() => {
|
|
77
|
+
getMigrationPath({
|
|
78
|
+
currentVersion: 0,
|
|
79
|
+
targetVersion: 6,
|
|
80
|
+
migrations,
|
|
81
|
+
});
|
|
82
|
+
}).toThrow();
|
|
83
|
+
});
|
|
84
|
+
it('should return empty when versions are the same', () => {
|
|
85
|
+
expect(
|
|
86
|
+
getMigrationPath({
|
|
87
|
+
currentVersion: 1,
|
|
88
|
+
targetVersion: 1,
|
|
89
|
+
migrations: [],
|
|
90
|
+
}),
|
|
91
|
+
).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Migration } from '@verdant-web/common';
|
|
2
|
+
import { MigrationPathError } from './errors.js';
|
|
3
|
+
|
|
4
|
+
export function getMigrationPath({
|
|
5
|
+
currentVersion,
|
|
6
|
+
targetVersion,
|
|
7
|
+
migrations,
|
|
8
|
+
}: {
|
|
9
|
+
currentVersion: number;
|
|
10
|
+
targetVersion: number;
|
|
11
|
+
migrations: Migration[];
|
|
12
|
+
}) {
|
|
13
|
+
const path = getNextPathStep({
|
|
14
|
+
currentVersion,
|
|
15
|
+
targetVersion,
|
|
16
|
+
migrations,
|
|
17
|
+
});
|
|
18
|
+
if (!path) {
|
|
19
|
+
throw new MigrationPathError(
|
|
20
|
+
`No migration path found from ${currentVersion} to ${targetVersion}! This is a bug. If you're seeing this, contact the developer and provide them with the full contents of this message.`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return path;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getNextPathStep({
|
|
27
|
+
currentVersion,
|
|
28
|
+
targetVersion,
|
|
29
|
+
migrations,
|
|
30
|
+
}: {
|
|
31
|
+
currentVersion: number;
|
|
32
|
+
targetVersion: number;
|
|
33
|
+
migrations: Migration[];
|
|
34
|
+
}): Migration[] | null {
|
|
35
|
+
if (currentVersion === targetVersion) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const fromHere = migrations
|
|
40
|
+
.filter((m) => m.oldSchema.version === currentVersion)
|
|
41
|
+
.sort((a, b) => b.newSchema.version - a.newSchema.version);
|
|
42
|
+
|
|
43
|
+
// keep trying next steps, starting from the largest step,
|
|
44
|
+
// until we find one that leads to the target version down the line
|
|
45
|
+
while (fromHere.length > 0) {
|
|
46
|
+
const next = fromHere.shift()!;
|
|
47
|
+
// this one goes too far (probably never relevant, but still)
|
|
48
|
+
if (next.newSchema.version > targetVersion) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
// exact match - we're done, return the path
|
|
52
|
+
if (next.newSchema.version === targetVersion) {
|
|
53
|
+
return [next];
|
|
54
|
+
}
|
|
55
|
+
// look ahead a down the line. do we reach the target? if so,
|
|
56
|
+
// we choose this path.
|
|
57
|
+
const nextPath = getNextPathStep({
|
|
58
|
+
currentVersion: next.newSchema.version,
|
|
59
|
+
targetVersion,
|
|
60
|
+
migrations,
|
|
61
|
+
});
|
|
62
|
+
if (nextPath) {
|
|
63
|
+
return [next, ...nextPath];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// otherwise, try the next one with a smaller increment
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// no paths from here match at all! if another layer is calling this one,
|
|
70
|
+
// it will fallback to its next longest step. otherwise there may
|
|
71
|
+
// be no paths at all...
|
|
72
|
+
return null;
|
|
73
|
+
}
|