@verdant-web/store 4.0.0 → 4.1.0-alpha.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/LICENSE +21 -650
- package/dist/bundle/index.js +11 -11
- package/dist/bundle/index.js.map +4 -4
- package/dist/esm/__tests__/fixtures/testStorage.d.ts +1 -2
- package/dist/esm/__tests__/fixtures/testStorage.js +3 -5
- package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
- package/dist/esm/client/Client.d.ts +6 -2
- package/dist/esm/client/Client.js +18 -6
- package/dist/esm/client/Client.js.map +1 -1
- package/dist/esm/client/ClientDescriptor.d.ts +7 -5
- package/dist/esm/client/ClientDescriptor.js +18 -4
- package/dist/esm/client/ClientDescriptor.js.map +1 -1
- package/dist/esm/context/ShutdownHandler.d.ts +8 -0
- package/dist/esm/context/ShutdownHandler.js +24 -0
- package/dist/esm/context/ShutdownHandler.js.map +1 -0
- package/dist/esm/context/context.d.ts +15 -4
- package/dist/esm/entities/EntityStore.js +6 -3
- package/dist/esm/entities/EntityStore.js.map +1 -1
- package/dist/esm/files/EntityFile.d.ts +1 -0
- package/dist/esm/files/EntityFile.js +16 -11
- package/dist/esm/files/EntityFile.js.map +1 -1
- package/dist/esm/files/FileManager.d.ts +1 -3
- package/dist/esm/files/FileManager.js +12 -10
- package/dist/esm/files/FileManager.js.map +1 -1
- package/dist/esm/index.d.ts +4 -5
- package/dist/esm/index.js +2 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal.d.ts +6 -0
- package/dist/esm/internal.js +5 -0
- package/dist/esm/internal.js.map +1 -0
- package/dist/esm/persistence/MessageCreator.d.ts +3 -1
- package/dist/esm/persistence/MessageCreator.js +58 -55
- package/dist/esm/persistence/MessageCreator.js.map +1 -1
- package/dist/esm/persistence/PersistenceFiles.d.ts +8 -21
- package/dist/esm/persistence/PersistenceFiles.js +44 -30
- package/dist/esm/persistence/PersistenceFiles.js.map +1 -1
- package/dist/esm/persistence/PersistenceMetadata.d.ts +12 -11
- package/dist/esm/persistence/PersistenceMetadata.js +201 -137
- package/dist/esm/persistence/PersistenceMetadata.js.map +1 -1
- package/dist/esm/persistence/PersistenceQueries.d.ts +10 -11
- package/dist/esm/persistence/PersistenceQueries.js +33 -5
- package/dist/esm/persistence/PersistenceQueries.js.map +1 -1
- package/dist/esm/persistence/PersistenceRebaser.d.ts +5 -9
- package/dist/esm/persistence/PersistenceRebaser.js +63 -47
- package/dist/esm/persistence/PersistenceRebaser.js.map +1 -1
- package/dist/esm/persistence/idb/IdbService.d.ts +0 -1
- package/dist/esm/persistence/idb/IdbService.js +28 -16
- package/dist/esm/persistence/idb/IdbService.js.map +1 -1
- package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.d.ts +11 -31
- package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.js +31 -36
- package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.js.map +1 -1
- package/dist/esm/persistence/idb/idbPersistence.d.ts +17 -9
- package/dist/esm/persistence/idb/idbPersistence.js +80 -39
- package/dist/esm/persistence/idb/idbPersistence.js.map +1 -1
- package/dist/esm/persistence/idb/metadata/IdbMetadataDb.d.ts +7 -10
- package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js +45 -71
- package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js.map +1 -1
- package/dist/esm/persistence/idb/metadata/openMetadataDatabase.d.ts +1 -12
- package/dist/esm/persistence/idb/metadata/openMetadataDatabase.js +3 -56
- package/dist/esm/persistence/idb/metadata/openMetadataDatabase.js.map +1 -1
- package/dist/esm/persistence/idb/queries/{IdbQueryDb.d.ts → IdbDocumentDb.d.ts} +7 -13
- package/dist/esm/persistence/idb/queries/{IdbQueryDb.js → IdbDocumentDb.js} +15 -32
- package/dist/esm/persistence/idb/queries/IdbDocumentDb.js.map +1 -0
- package/dist/esm/persistence/idb/queries/migration/db.d.ts +3 -5
- package/dist/esm/persistence/idb/queries/migration/db.js +13 -28
- package/dist/esm/persistence/idb/queries/migration/db.js.map +1 -1
- package/dist/esm/persistence/idb/util.d.ts +8 -4
- package/dist/esm/persistence/idb/util.js +64 -21
- package/dist/esm/persistence/idb/util.js.map +1 -1
- package/dist/esm/persistence/interfaces.d.ts +68 -75
- package/dist/esm/persistence/{idb/queries/migration → migration}/engine.d.ts +4 -7
- package/dist/esm/persistence/{idb/queries/migration → migration}/engine.js +18 -10
- package/dist/esm/persistence/migration/engine.js.map +1 -0
- package/dist/esm/persistence/migration/finalize.d.ts +9 -0
- package/dist/esm/persistence/migration/finalize.js +75 -0
- package/dist/esm/persistence/migration/finalize.js.map +1 -0
- package/dist/esm/persistence/migration/migrate.d.ts +12 -0
- package/dist/esm/persistence/migration/migrate.js +89 -0
- package/dist/esm/persistence/migration/migrate.js.map +1 -0
- package/dist/esm/persistence/migration/paths.js.map +1 -0
- package/dist/esm/persistence/migration/paths.test.js.map +1 -0
- package/dist/esm/persistence/migration/types.d.ts +3 -0
- package/dist/esm/persistence/migration/types.js.map +1 -0
- package/dist/esm/persistence/persistence.js +25 -15
- package/dist/esm/persistence/persistence.js.map +1 -1
- package/dist/esm/queries/FindAllQuery.js +1 -1
- package/dist/esm/queries/FindAllQuery.js.map +1 -1
- package/dist/esm/queries/FindInfiniteQuery.js +2 -2
- package/dist/esm/queries/FindInfiniteQuery.js.map +1 -1
- package/dist/esm/queries/FindOneQuery.js +1 -1
- package/dist/esm/queries/FindOneQuery.js.map +1 -1
- package/dist/esm/queries/FindPageQuery.js +1 -1
- package/dist/esm/queries/FindPageQuery.js.map +1 -1
- package/dist/esm/sync/FileSync.js +3 -3
- package/dist/esm/sync/FileSync.js.map +1 -1
- package/dist/esm/sync/PushPullSync.d.ts +2 -3
- package/dist/esm/sync/PushPullSync.js +4 -2
- package/dist/esm/sync/PushPullSync.js.map +1 -1
- package/dist/esm/sync/ServerSyncEndpointProvider.d.ts +3 -7
- package/dist/esm/sync/ServerSyncEndpointProvider.js +3 -2
- package/dist/esm/sync/ServerSyncEndpointProvider.js.map +1 -1
- package/dist/esm/sync/Sync.d.ts +6 -1
- package/dist/esm/sync/Sync.js +12 -4
- package/dist/esm/sync/Sync.js.map +1 -1
- package/dist/esm/sync/WebSocketSync.js +10 -4
- package/dist/esm/sync/WebSocketSync.js.map +1 -1
- package/dist/esm/utils/wip.js +1 -1
- package/package.json +6 -2
- package/src/__tests__/fixtures/testStorage.ts +6 -6
- package/src/client/Client.ts +26 -8
- package/src/client/ClientDescriptor.ts +27 -9
- package/src/context/ShutdownHandler.ts +26 -0
- package/src/context/context.ts +16 -4
- package/src/entities/EntityStore.ts +9 -3
- package/src/files/EntityFile.ts +11 -6
- package/src/files/FileManager.ts +13 -10
- package/src/index.ts +8 -9
- package/src/internal.ts +27 -0
- package/src/persistence/MessageCreator.ts +79 -73
- package/src/persistence/PersistenceFiles.ts +57 -31
- package/src/persistence/PersistenceMetadata.ts +287 -195
- package/src/persistence/PersistenceQueries.ts +45 -9
- package/src/persistence/PersistenceRebaser.ts +105 -70
- package/src/persistence/idb/IdbService.ts +40 -22
- package/src/persistence/idb/files/IdbPersistenceFileDb.ts +30 -62
- package/src/persistence/idb/idbPersistence.ts +123 -47
- package/src/persistence/idb/metadata/IdbMetadataDb.ts +75 -97
- package/src/persistence/idb/metadata/openMetadataDatabase.ts +2 -96
- package/src/persistence/idb/queries/{IdbQueryDb.ts → IdbDocumentDb.ts} +17 -57
- package/src/persistence/idb/queries/migration/db.ts +20 -39
- package/src/persistence/idb/util.ts +84 -21
- package/src/persistence/interfaces.ts +89 -90
- package/src/persistence/{idb/queries/migration → migration}/engine.ts +30 -15
- package/src/persistence/migration/finalize.ts +126 -0
- package/src/persistence/migration/migrate.ts +169 -0
- package/src/persistence/migration/types.ts +4 -0
- package/src/persistence/persistence.ts +37 -14
- package/src/queries/FindAllQuery.ts +1 -1
- package/src/queries/FindInfiniteQuery.ts +2 -2
- package/src/queries/FindOneQuery.ts +1 -1
- package/src/queries/FindPageQuery.ts +1 -1
- package/src/sync/FileSync.ts +21 -15
- package/src/sync/PushPullSync.ts +3 -4
- package/src/sync/ServerSyncEndpointProvider.ts +6 -8
- package/src/sync/Sync.ts +20 -7
- package/src/sync/WebSocketSync.ts +10 -4
- package/src/utils/wip.ts +1 -1
- package/dist/esm/client/constants.d.ts +0 -1
- package/dist/esm/client/constants.js +0 -2
- package/dist/esm/client/constants.js.map +0 -1
- package/dist/esm/persistence/idb/queries/IdbQueryDb.js.map +0 -1
- package/dist/esm/persistence/idb/queries/migration/engine.js.map +0 -1
- package/dist/esm/persistence/idb/queries/migration/migrations.d.ts +0 -15
- package/dist/esm/persistence/idb/queries/migration/migrations.js +0 -243
- package/dist/esm/persistence/idb/queries/migration/migrations.js.map +0 -1
- package/dist/esm/persistence/idb/queries/migration/openQueryDatabase.d.ts +0 -8
- package/dist/esm/persistence/idb/queries/migration/openQueryDatabase.js +0 -24
- package/dist/esm/persistence/idb/queries/migration/openQueryDatabase.js.map +0 -1
- package/dist/esm/persistence/idb/queries/migration/paths.js.map +0 -1
- package/dist/esm/persistence/idb/queries/migration/paths.test.js.map +0 -1
- package/dist/esm/persistence/idb/queries/migration/types.d.ts +0 -6
- package/dist/esm/persistence/idb/queries/migration/types.js.map +0 -1
- package/src/client/constants.ts +0 -1
- package/src/persistence/idb/queries/migration/migrations.ts +0 -345
- package/src/persistence/idb/queries/migration/openQueryDatabase.ts +0 -54
- package/src/persistence/idb/queries/migration/types.ts +0 -8
- /package/dist/esm/persistence/{idb/queries/migration → migration}/paths.d.ts +0 -0
- /package/dist/esm/persistence/{idb/queries/migration → migration}/paths.js +0 -0
- /package/dist/esm/persistence/{idb/queries/migration → migration}/paths.test.d.ts +0 -0
- /package/dist/esm/persistence/{idb/queries/migration → migration}/paths.test.js +0 -0
- /package/dist/esm/persistence/{idb/queries/migration → migration}/types.js +0 -0
- /package/src/persistence/{idb/queries/migration → migration}/paths.test.ts +0 -0
- /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
|
|
14
|
+
export class IdbDocumentDb extends IdbService implements PersistenceDocumentDb {
|
|
20
15
|
private ctx;
|
|
21
|
-
constructor(db: IDBDatabase, context: Omit<Context, '
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
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 {
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
68
|
+
log,
|
|
92
69
|
}: {
|
|
93
70
|
indexedDB?: IDBFactory;
|
|
94
71
|
namespace: string;
|
|
95
72
|
version: number;
|
|
96
|
-
|
|
73
|
+
log?: Context['log'];
|
|
97
74
|
}): Promise<IDBDatabase> {
|
|
98
|
-
|
|
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
|
-
*
|
|
186
|
-
*
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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 = () =>
|
|
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 |
|
|
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 =
|
|
39
|
+
export type AbstractTransaction = any;
|
|
41
40
|
export type QueryMode = 'readwrite' | 'readonly';
|
|
42
|
-
export interface CommonQueryOptions
|
|
43
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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(
|
|
64
|
+
getLocalReplica(
|
|
65
|
+
opts?: CommonQueryOptions<Tx>,
|
|
66
|
+
): Promise<LocalReplicaInfo | undefined | null>;
|
|
60
67
|
updateLocalReplica(
|
|
61
|
-
data:
|
|
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
|
-
|
|
123
|
+
deleteEntityOperations(
|
|
117
124
|
oid: string,
|
|
118
|
-
|
|
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
|
|
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
|
-
|
|
165
|
+
optsAndInfo: { abort?: AbortSignal; collections: string[] },
|
|
170
166
|
): Promise<void>;
|
|
171
167
|
|
|
172
|
-
reset(
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
218
|
-
options?: { transaction?: IDBTransaction },
|
|
188
|
+
iterator: (file: PersistedFileData) => void,
|
|
219
189
|
): Promise<void>;
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
195
|
+
export interface PersistenceNamespace {
|
|
227
196
|
openMetadata(ctx: InitialContext): Promise<PersistenceMetadataDb>;
|
|
228
|
-
|
|
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' | '
|
|
217
|
+
ctx: Omit<Context, 'files' | 'documents'>,
|
|
231
218
|
): Promise<PersistenceFileDb>;
|
|
232
|
-
|
|
233
|
-
|
|
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
|
}
|