@chipmobilesdk/rn-local-db 0.1.1 → 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.
@@ -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
- const collectionFor = <T extends StorableRecordData>(name: string): CollectionOps<T> => {
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
- return instrument(
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 attachments: Attachments = {
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
- async transaction<R>(fn: (tx: Transaction) => Promise<R>): Promise<R> {
380
+ transaction<R>(fn: (tx: Transaction) => Promise<R>): Promise<R> {
238
381
  assertOpen();
239
- // Snapshot-and-restore rather than a write log: simpler to
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
- async batch(operations: BatchOperation[]): Promise<BatchOutcome> {
385
+ batch(operations: BatchOperation[]): Promise<BatchOutcome> {
284
386
  assertOpen();
285
387
  assertBatchWithinBudget(operations.length);
286
- return handle.transaction(async (tx: Transaction) => {
287
- const outcomes = [];
288
- for (const operation of operations) {
289
- const ops = tx.collection(operation.collection);
290
- switch (operation.type) {
291
- case 'insert':
292
- outcomes.push(await ops.insert(operation.record));
293
- break;
294
- case 'update':
295
- outcomes.push(await ops.update(operation.id, { data: operation.data }));
296
- break;
297
- case 'upsert':
298
- outcomes.push(await ops.upsert(operation.record));
299
- break;
300
- case 'delete':
301
- outcomes.push(await ops.delete(operation.id));
302
- break;
303
- case 'softDelete':
304
- outcomes.push(await ops.softDelete(operation.id));
305
- break;
306
- default:
307
- break;
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
- return { applied: outcomes.length, outcomes };
311
- });
413
+ return { applied: outcomes.length, outcomes };
414
+ }),
415
+ );
312
416
  },
313
417
 
314
418
  attachments: () => attachments,