@chipmobilesdk/rn-local-db 0.1.2 → 0.2.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.
- package/CHANGELOG.md +50 -0
- package/README.md +74 -72
- package/package.json +1 -1
- package/src/adapter/deletion.ts +24 -10
- package/src/adapter/lifecycle.ts +121 -45
- package/src/adapter/transaction.ts +6 -0
- package/src/artifacts/availability.ts +3 -5
- package/src/artifacts/cascade.ts +42 -6
- package/src/artifacts/store.ts +34 -12
- package/src/contract/attachment.ts +6 -0
- package/src/contract/migration.ts +3 -1
- package/src/contract/record.ts +9 -0
- package/src/contract/transaction.ts +8 -0
- package/src/crypto/chunked-cipher.ts +28 -22
- package/src/crypto/derive.ts +2 -1
- package/src/platform/bytes.ts +144 -0
- package/src/query/budgets.ts +2 -5
- package/src/query/cursor.ts +3 -2
- package/src/sqlite/batch.ts +28 -18
- package/src/sqlite/connection.ts +66 -44
- package/src/sqlite/index-coverage.ts +4 -1
- package/src/sqlite/row-codec.ts +23 -3
- package/src/sqlite/transaction-queue.ts +87 -0
- package/src/testing/conformance/attachments.ts +1 -1
- package/src/testing/memory-adapter/index.ts +176 -72
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* (research.md R8). Documented divergences are listed in the package README.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { createTransactionQueue } from '../../sqlite/transaction-queue';
|
|
9
10
|
import type {
|
|
10
11
|
AttachmentInput,
|
|
11
12
|
AttachmentReference,
|
|
@@ -116,7 +117,47 @@ export function createMemoryAdapter(options: MemoryAdapterOptions = {}): Storage
|
|
|
116
117
|
}
|
|
117
118
|
};
|
|
118
119
|
|
|
119
|
-
|
|
120
|
+
/**
|
|
121
|
+
* The same isolation the SQLite connection gives: a transaction holds the
|
|
122
|
+
* handle until it settles, and work through the handle meanwhile waits
|
|
123
|
+
* rather than landing inside the transaction's snapshot and vanishing
|
|
124
|
+
* with its rollback. Only what the transaction hands out runs inside it.
|
|
125
|
+
*/
|
|
126
|
+
const queue = createTransactionQueue();
|
|
127
|
+
const queued = <O extends object>(ops: O): O =>
|
|
128
|
+
Object.fromEntries(
|
|
129
|
+
Object.entries(ops).map(([key, value]) => [
|
|
130
|
+
key,
|
|
131
|
+
typeof value === 'function'
|
|
132
|
+
? (...args: unknown[]) =>
|
|
133
|
+
queue.run(() => (value as (...a: unknown[]) => Promise<unknown>)(...args))
|
|
134
|
+
: value,
|
|
135
|
+
]),
|
|
136
|
+
) as O;
|
|
137
|
+
|
|
138
|
+
/** Artifact deletions held back until the transaction that caused them commits. */
|
|
139
|
+
type Deferred = { add(id: string): void };
|
|
140
|
+
|
|
141
|
+
/** FR-070 for the memory adapter, with the SQLite adapter's ordering and reporting. */
|
|
142
|
+
const cascadeFor = (collection: string, id: string, deferred?: Deferred): DeletionOutcome => {
|
|
143
|
+
const removedArtifacts: string[] = [];
|
|
144
|
+
for (const reference of [...store.attachments.values()]) {
|
|
145
|
+
if (reference.owner?.collection !== collection || reference.owner?.id !== id) continue;
|
|
146
|
+
store.attachments.delete(reference.id);
|
|
147
|
+
if (deferred) {
|
|
148
|
+
deferred.add(reference.id);
|
|
149
|
+
} else {
|
|
150
|
+
store.artifacts.delete(reference.id);
|
|
151
|
+
}
|
|
152
|
+
removedArtifacts.push(reference.id);
|
|
153
|
+
}
|
|
154
|
+
return { removedRecords: 0, removedArtifacts, orphanedReferences: [], failedRemovals: [] };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const rawCollectionFor = <T extends StorableRecordData>(
|
|
158
|
+
name: string,
|
|
159
|
+
deferred?: Deferred,
|
|
160
|
+
): CollectionOps<T> => {
|
|
120
161
|
assertOpen();
|
|
121
162
|
const schema = schemas.get(name);
|
|
122
163
|
if (!schema) {
|
|
@@ -125,7 +166,7 @@ export function createMemoryAdapter(options: MemoryAdapterOptions = {}): Storage
|
|
|
125
166
|
detail: { collection: name },
|
|
126
167
|
});
|
|
127
168
|
}
|
|
128
|
-
|
|
169
|
+
const ops = instrument(
|
|
129
170
|
name,
|
|
130
171
|
'memory',
|
|
131
172
|
createMemoryCollection<T>({ store, schema, now, assertOpen }) as unknown as Record<
|
|
@@ -133,15 +174,28 @@ export function createMemoryAdapter(options: MemoryAdapterOptions = {}): Storage
|
|
|
133
174
|
(...args: never[]) => unknown
|
|
134
175
|
>,
|
|
135
176
|
) as unknown as CollectionOps<T>;
|
|
177
|
+
if (config.cascadeAttachments !== true) {
|
|
178
|
+
return ops;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
...ops,
|
|
182
|
+
async delete(id: string) {
|
|
183
|
+
const outcome = await ops.delete(id);
|
|
184
|
+
return { ...outcome, cascade: cascadeFor(name, id, deferred) };
|
|
185
|
+
},
|
|
186
|
+
};
|
|
136
187
|
};
|
|
137
188
|
|
|
189
|
+
const collectionFor = <T extends StorableRecordData>(name: string): CollectionOps<T> =>
|
|
190
|
+
queued(rawCollectionFor<T>(name));
|
|
191
|
+
|
|
138
192
|
const backupPosture: EffectivePosture = {
|
|
139
193
|
requested: store.backupIncluded ? 'included' : 'excluded',
|
|
140
194
|
observed: store.backupIncluded ? 'included' : 'excluded',
|
|
141
195
|
enforcedBy: 'package',
|
|
142
196
|
};
|
|
143
197
|
|
|
144
|
-
const
|
|
198
|
+
const rawAttachments = (deferred?: Deferred): Attachments => ({
|
|
145
199
|
async write(input: AttachmentInput): Promise<AttachmentReference> {
|
|
146
200
|
assertOpen();
|
|
147
201
|
throwIfFaulted('attachmentWrite');
|
|
@@ -191,6 +245,16 @@ export function createMemoryAdapter(options: MemoryAdapterOptions = {}): Storage
|
|
|
191
245
|
async remove(id: string): Promise<DeletionOutcome> {
|
|
192
246
|
assertOpen();
|
|
193
247
|
assertValidAttachmentId(id);
|
|
248
|
+
if (deferred) {
|
|
249
|
+
const hadReference = store.attachments.delete(id);
|
|
250
|
+
if (hadReference) deferred.add(id);
|
|
251
|
+
return {
|
|
252
|
+
removedRecords: 0,
|
|
253
|
+
removedArtifacts: hadReference ? [id] : [],
|
|
254
|
+
orphanedReferences: [],
|
|
255
|
+
failedRemovals: [],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
194
258
|
const removed = store.artifacts.delete(id);
|
|
195
259
|
store.attachments.delete(id);
|
|
196
260
|
return {
|
|
@@ -225,6 +289,85 @@ export function createMemoryAdapter(options: MemoryAdapterOptions = {}): Storage
|
|
|
225
289
|
}
|
|
226
290
|
return { orphanedArtifacts, danglingReferences, removedArtifacts };
|
|
227
291
|
},
|
|
292
|
+
});
|
|
293
|
+
const attachments = queued(rawAttachments());
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* One transaction against the store. Snapshot-and-restore rather than a
|
|
297
|
+
* write log: simpler to reason about, and rollback fidelity is what US4
|
|
298
|
+
* tests. Runs only inside `exclusive`, so the snapshot never captures,
|
|
299
|
+
* nor its restore erases, a write made from outside.
|
|
300
|
+
*/
|
|
301
|
+
const runTransaction = async <R>(fn: (tx: Transaction) => Promise<R>): Promise<R> => {
|
|
302
|
+
const snapshot = cloneCollections(store);
|
|
303
|
+
const attachmentSnapshot = new Map(store.attachments);
|
|
304
|
+
const pendingArtifactRemovals: string[] = [];
|
|
305
|
+
const deferred: Deferred = { add: id => pendingArtifactRemovals.push(id) };
|
|
306
|
+
let settled = false;
|
|
307
|
+
|
|
308
|
+
const assertLive = (): void => {
|
|
309
|
+
if (settled) {
|
|
310
|
+
raise(DiagnosticCodes.TRANSACTION_STATE, {
|
|
311
|
+
message: 'transaction already settled',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const rollback = (): void => {
|
|
316
|
+
store.collections = snapshot as never;
|
|
317
|
+
store.attachments = attachmentSnapshot;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// A collection taken from the transaction keeps refusing once it
|
|
321
|
+
// settles, as the SQLite executor does; otherwise a write after abort
|
|
322
|
+
// would land straight in the store.
|
|
323
|
+
const live = <O extends object>(ops: O): O =>
|
|
324
|
+
Object.fromEntries(
|
|
325
|
+
Object.entries(ops).map(([key, value]) => [
|
|
326
|
+
key,
|
|
327
|
+
typeof value === 'function'
|
|
328
|
+
? // Async so a refusal rejects, as the SQLite executor's does.
|
|
329
|
+
async (...args: unknown[]) => {
|
|
330
|
+
assertLive();
|
|
331
|
+
return (value as (...a: unknown[]) => unknown)(...args);
|
|
332
|
+
}
|
|
333
|
+
: value,
|
|
334
|
+
]),
|
|
335
|
+
) as O;
|
|
336
|
+
|
|
337
|
+
const tx: Transaction = {
|
|
338
|
+
collection: <T extends StorableRecordData>(name: string) => {
|
|
339
|
+
assertLive();
|
|
340
|
+
return live(rawCollectionFor<T>(name, deferred));
|
|
341
|
+
},
|
|
342
|
+
attachments: () => {
|
|
343
|
+
assertLive();
|
|
344
|
+
return rawAttachments(deferred);
|
|
345
|
+
},
|
|
346
|
+
async abort() {
|
|
347
|
+
assertLive();
|
|
348
|
+
settled = true;
|
|
349
|
+
rollback();
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const result = await fn(tx);
|
|
355
|
+
if (!settled) {
|
|
356
|
+
throwIfFaulted('commit');
|
|
357
|
+
settled = true;
|
|
358
|
+
// Committed: the artifacts whose references went may go too.
|
|
359
|
+
for (const id of pendingArtifactRemovals) {
|
|
360
|
+
store.artifacts.delete(id);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (!settled) {
|
|
366
|
+
settled = true;
|
|
367
|
+
rollback();
|
|
368
|
+
}
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
228
371
|
};
|
|
229
372
|
|
|
230
373
|
const handle: DatabaseHandle = {
|
|
@@ -234,81 +377,42 @@ export function createMemoryAdapter(options: MemoryAdapterOptions = {}): Storage
|
|
|
234
377
|
isEncrypted: store.encrypted,
|
|
235
378
|
collection: collectionFor,
|
|
236
379
|
|
|
237
|
-
|
|
380
|
+
transaction<R>(fn: (tx: Transaction) => Promise<R>): Promise<R> {
|
|
238
381
|
assertOpen();
|
|
239
|
-
|
|
240
|
-
// reason about, and rollback fidelity is what US4 tests.
|
|
241
|
-
const snapshot = cloneCollections(store);
|
|
242
|
-
const attachmentSnapshot = new Map(store.attachments);
|
|
243
|
-
let settled = false;
|
|
244
|
-
|
|
245
|
-
const tx: Transaction = {
|
|
246
|
-
collection: <T extends StorableRecordData>(name: string) => {
|
|
247
|
-
if (settled) {
|
|
248
|
-
raise(DiagnosticCodes.TRANSACTION_STATE, {
|
|
249
|
-
message: 'transaction already settled',
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
return collectionFor<T>(name);
|
|
253
|
-
},
|
|
254
|
-
async abort() {
|
|
255
|
-
if (settled) {
|
|
256
|
-
raise(DiagnosticCodes.TRANSACTION_STATE, {
|
|
257
|
-
message: 'transaction already settled',
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
settled = true;
|
|
261
|
-
store.collections = snapshot as never;
|
|
262
|
-
store.attachments = attachmentSnapshot;
|
|
263
|
-
},
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
const result = await fn(tx);
|
|
268
|
-
if (!settled) {
|
|
269
|
-
throwIfFaulted('commit');
|
|
270
|
-
settled = true;
|
|
271
|
-
}
|
|
272
|
-
return result;
|
|
273
|
-
} catch (error) {
|
|
274
|
-
if (!settled) {
|
|
275
|
-
settled = true;
|
|
276
|
-
store.collections = snapshot as never;
|
|
277
|
-
store.attachments = attachmentSnapshot;
|
|
278
|
-
}
|
|
279
|
-
throw error;
|
|
280
|
-
}
|
|
382
|
+
return queue.runTransaction(() => runTransaction(fn));
|
|
281
383
|
},
|
|
282
384
|
|
|
283
|
-
|
|
385
|
+
batch(operations: BatchOperation[]): Promise<BatchOutcome> {
|
|
284
386
|
assertOpen();
|
|
285
387
|
assertBatchWithinBudget(operations.length);
|
|
286
|
-
return
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
388
|
+
return queue.runTransaction(() =>
|
|
389
|
+
runTransaction(async (tx: Transaction) => {
|
|
390
|
+
const outcomes = [];
|
|
391
|
+
for (const operation of operations) {
|
|
392
|
+
const ops = tx.collection(operation.collection);
|
|
393
|
+
switch (operation.type) {
|
|
394
|
+
case 'insert':
|
|
395
|
+
outcomes.push(await ops.insert(operation.record));
|
|
396
|
+
break;
|
|
397
|
+
case 'update':
|
|
398
|
+
outcomes.push(await ops.update(operation.id, { data: operation.data }));
|
|
399
|
+
break;
|
|
400
|
+
case 'upsert':
|
|
401
|
+
outcomes.push(await ops.upsert(operation.record));
|
|
402
|
+
break;
|
|
403
|
+
case 'delete':
|
|
404
|
+
outcomes.push(await ops.delete(operation.id));
|
|
405
|
+
break;
|
|
406
|
+
case 'softDelete':
|
|
407
|
+
outcomes.push(await ops.softDelete(operation.id));
|
|
408
|
+
break;
|
|
409
|
+
default:
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
308
412
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
413
|
+
return { applied: outcomes.length, outcomes };
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
312
416
|
},
|
|
313
417
|
|
|
314
418
|
attachments: () => attachments,
|