shelving 1.280.0 → 1.282.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/db/index.d.ts CHANGED
@@ -6,16 +6,17 @@ export * from "./migrate/PostgresMigrator.js";
6
6
  export * from "./migrate/SQLiteMigrator.js";
7
7
  export * from "./migrate/SQLMigrator.js";
8
8
  export * from "./provider/CacheDBProvider.js";
9
- export * from "./provider/ChangesDBProvider.js";
10
9
  export * from "./provider/DBProvider.js";
11
10
  export * from "./provider/DebugDBProvider.js";
12
11
  export * from "./provider/MemoryDBProvider.js";
13
12
  export * from "./provider/MockDBProvider.js";
14
13
  export * from "./provider/PostgresProvider.js";
14
+ export * from "./provider/RecordingDBProvider.js";
15
15
  export * from "./provider/SQLiteProvider.js";
16
16
  export * from "./provider/SQLProvider.js";
17
17
  export * from "./provider/StorageDBProvider.js";
18
18
  export * from "./provider/ThroughDBProvider.js";
19
+ export * from "./provider/UndoDBProvider.js";
19
20
  export * from "./provider/ValidationDBProvider.js";
20
21
  export * from "./store/ItemStore.js";
21
22
  export * from "./store/QueryStore.js";
package/db/index.js CHANGED
@@ -6,16 +6,17 @@ export * from "./migrate/PostgresMigrator.js";
6
6
  export * from "./migrate/SQLiteMigrator.js";
7
7
  export * from "./migrate/SQLMigrator.js";
8
8
  export * from "./provider/CacheDBProvider.js";
9
- export * from "./provider/ChangesDBProvider.js";
10
9
  export * from "./provider/DBProvider.js";
11
10
  export * from "./provider/DebugDBProvider.js";
12
11
  export * from "./provider/MemoryDBProvider.js";
13
12
  export * from "./provider/MockDBProvider.js";
14
13
  export * from "./provider/PostgresProvider.js";
14
+ export * from "./provider/RecordingDBProvider.js";
15
15
  export * from "./provider/SQLiteProvider.js";
16
16
  export * from "./provider/SQLProvider.js";
17
17
  export * from "./provider/StorageDBProvider.js";
18
18
  export * from "./provider/ThroughDBProvider.js";
19
+ export * from "./provider/UndoDBProvider.js";
19
20
  export * from "./provider/ValidationDBProvider.js";
20
21
  export * from "./store/ItemStore.js";
21
22
  export * from "./store/QueryStore.js";
@@ -1,27 +1,23 @@
1
1
  import type { Data } from "../../util/data.js";
2
2
  import type { Identifier, Item, Items, ItemsSequence, OptionalItem, OptionalItemSequence } from "../../util/item.js";
3
3
  import type { Query } from "../../util/query.js";
4
- import type { Sourceable } from "../../util/source.js";
5
4
  import type { Updates } from "../../util/update.js";
6
5
  import type { Collection } from "../collection/Collection.js";
7
- import { DBProvider } from "./DBProvider.js";
6
+ import type { DBProvider } from "./DBProvider.js";
8
7
  import { MemoryDBProvider } from "./MemoryDBProvider.js";
8
+ import { ThroughDBProvider } from "./ThroughDBProvider.js";
9
9
  /**
10
10
  * Database provider that keeps a copy of asynchronous remote data in a local synchronous cache.
11
11
  *
12
12
  * - Wraps a `source` provider and mirrors every read and write into an in-memory `MemoryDBProvider`, so subsequent reads can be served synchronously and live subscriptions stay seeded.
13
13
  * - Reads fetch from `source`, then refresh the cache; writes hit `source`, then mirror the change into the cache.
14
+ * - Fetch-first item writes: `updateItem()` and `deleteItem()` fetch the item first (caching it) and skip the source write when it doesn't exist. Query writes are inherited two-step, resolving through this provider's own `getQuery()` — so the matched items are cached, and each per-item write mirrors exactly. The fetch and the writes are separate steps, so wrap them in `transact()` when they must be atomic.
15
+ * - Transactions run on `source` via `transact()` with a transaction-scoped mirror — only a committed transaction's writes reach the cache.
14
16
  * - Discover the cache from a wrapping layer with `getSource(CacheDBProvider, provider)` to seed stores from `.memory`.
15
17
  *
16
18
  * @see https://shelving.cc/db/CacheDBProvider
17
19
  */
18
- export declare class CacheDBProvider<I extends Identifier, T extends Data> extends DBProvider<I, T> implements Sourceable<DBProvider<I, T>> {
19
- /**
20
- * The wrapped source provider that data is fetched from and written to.
21
- *
22
- * @see https://shelving.cc/db/CacheDBProvider/source
23
- */
24
- readonly source: DBProvider<I, T>;
20
+ export declare class CacheDBProvider<I extends Identifier, T extends Data> extends ThroughDBProvider<I, T> {
25
21
  /**
26
22
  * The in-memory provider holding the local synchronous cache of `source` data.
27
23
  *
@@ -40,20 +36,21 @@ export declare class CacheDBProvider<I extends Identifier, T extends Data> exten
40
36
  addItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, data: TT): Promise<II>;
41
37
  /** Mirror the set item into the cache. */
42
38
  setItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, data: TT): Promise<void>;
43
- /** Mirror the updates into the cache. */
39
+ /** Fetch the item first (caching it), then update it only if it exists. */
44
40
  updateItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, updates: Updates<Item<II, TT>>): Promise<void>;
45
- /** Remove the deleted item from the cache. */
41
+ /** Fetch the item first, then delete it only if it exists. */
46
42
  deleteItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
47
- countQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<number>;
48
43
  /** Read from `source`, then refresh the cache. */
49
44
  getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
50
45
  /** Mirror each emission into the cache. */
51
46
  getQuerySequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
52
- /** Mirror the change into the cache. */
53
- setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
54
- /** Mirror the updates into the cache. */
55
- updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
56
- /** Remove the deleted items from the cache. */
57
- deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
47
+ cloneWith(source: DBProvider<I, T>): this;
48
+ /**
49
+ * Runs the transaction on `source`, recording the callback's operations, then commits the recorded writes into the cache once the source commits.
50
+ * - The callback's provider is this cache over the source's transaction (with its own transaction-scoped mirror), so fetch-first writes and query resolution behave exactly as they do outside a transaction — and the fetch-then-write steps are atomic because both run in the source transaction.
51
+ * - Uncommitted data never touches the cache: a thrown callback commits nothing, and if the backend retries the callback only the committed attempt's writes are mirrored.
52
+ * - Update writes commit to the cache as deltas, so they refresh cached items and skip uncached ones — an item only read inside the transaction stays uncached until its next read.
53
+ */
54
+ transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
58
55
  [Symbol.asyncDispose](): Promise<void>;
59
56
  }
@@ -1,22 +1,19 @@
1
1
  import { awaitDispose } from "../../util/dispose.js";
2
- import { DBProvider } from "./DBProvider.js";
3
2
  import { MemoryDBProvider } from "./MemoryDBProvider.js";
3
+ import { RecordingDBProvider } from "./RecordingDBProvider.js";
4
+ import { ThroughDBProvider } from "./ThroughDBProvider.js";
4
5
  /**
5
6
  * Database provider that keeps a copy of asynchronous remote data in a local synchronous cache.
6
7
  *
7
8
  * - Wraps a `source` provider and mirrors every read and write into an in-memory `MemoryDBProvider`, so subsequent reads can be served synchronously and live subscriptions stay seeded.
8
9
  * - Reads fetch from `source`, then refresh the cache; writes hit `source`, then mirror the change into the cache.
10
+ * - Fetch-first item writes: `updateItem()` and `deleteItem()` fetch the item first (caching it) and skip the source write when it doesn't exist. Query writes are inherited two-step, resolving through this provider's own `getQuery()` — so the matched items are cached, and each per-item write mirrors exactly. The fetch and the writes are separate steps, so wrap them in `transact()` when they must be atomic.
11
+ * - Transactions run on `source` via `transact()` with a transaction-scoped mirror — only a committed transaction's writes reach the cache.
9
12
  * - Discover the cache from a wrapping layer with `getSource(CacheDBProvider, provider)` to seed stores from `.memory`.
10
13
  *
11
14
  * @see https://shelving.cc/db/CacheDBProvider
12
15
  */
13
- export class CacheDBProvider extends DBProvider {
14
- /**
15
- * The wrapped source provider that data is fetched from and written to.
16
- *
17
- * @see https://shelving.cc/db/CacheDBProvider/source
18
- */
19
- source;
16
+ export class CacheDBProvider extends ThroughDBProvider {
20
17
  /**
21
18
  * The in-memory provider holding the local synchronous cache of `source` data.
22
19
  *
@@ -27,74 +24,79 @@ export class CacheDBProvider extends DBProvider {
27
24
  * @param cache In-memory provider to use as the cache (a fresh `MemoryDBProvider` by default).
28
25
  */
29
26
  constructor(source, cache = new MemoryDBProvider()) {
30
- super();
31
- this.source = source;
27
+ super(source);
32
28
  this.memory = cache;
33
29
  }
34
30
  /** Read from `source`, then refresh the cache. */
35
31
  async getItem(collection, id) {
36
- const item = await this.source.getItem(collection, id);
32
+ const item = await super.getItem(collection, id);
37
33
  const table = this.memory.getTable(collection);
38
34
  item ? table.setItem(id, item) : table.deleteItem(id);
39
35
  return item;
40
36
  }
41
37
  /** Mirror each emission into the cache. */
42
38
  getItemSequence(collection, id) {
43
- return this.memory.getTable(collection).setItemSequence(id, this.source.getItemSequence(collection, id));
39
+ return this.memory.getTable(collection).setItemSequence(id, super.getItemSequence(collection, id));
44
40
  }
45
41
  /** Mirror the added item into the cache. */
46
42
  async addItem(collection, data) {
47
- const id = await this.source.addItem(collection, data);
43
+ const id = await super.addItem(collection, data);
48
44
  this.memory.getTable(collection).setItem(id, data);
49
45
  return id;
50
46
  }
51
47
  /** Mirror the set item into the cache. */
52
48
  async setItem(collection, id, data) {
53
- await this.source.setItem(collection, id, data);
49
+ await super.setItem(collection, id, data);
54
50
  this.memory.getTable(collection).setItem(id, data);
55
51
  }
56
- /** Mirror the updates into the cache. */
52
+ /** Fetch the item first (caching it), then update it only if it exists. */
57
53
  async updateItem(collection, id, updates) {
58
- await this.source.updateItem(collection, id, updates);
54
+ const item = await this.getItem(collection, id);
55
+ if (!item)
56
+ return;
57
+ await super.updateItem(collection, id, updates);
59
58
  this.memory.getTable(collection).updateItem(id, updates);
60
59
  }
61
- /** Remove the deleted item from the cache. */
60
+ /** Fetch the item first, then delete it only if it exists. */
62
61
  async deleteItem(collection, id) {
63
- await this.source.deleteItem(collection, id);
62
+ const item = await this.getItem(collection, id);
63
+ if (!item)
64
+ return;
65
+ await super.deleteItem(collection, id);
64
66
  this.memory.getTable(collection).deleteItem(id);
65
67
  }
66
- countQuery(collection, query) {
67
- return this.source.countQuery(collection, query);
68
- }
69
68
  /** Read from `source`, then refresh the cache. */
70
69
  async getQuery(collection, query) {
71
- const items = await this.source.getQuery(collection, query);
70
+ const items = await super.getQuery(collection, query);
72
71
  this.memory.getTable(collection).setItems(items);
73
72
  return items;
74
73
  }
75
74
  /** Mirror each emission into the cache. */
76
75
  getQuerySequence(collection, query) {
77
- return this.memory.getTable(collection).setItemsSequence(this.source.getQuerySequence(collection, query));
76
+ return this.memory.getTable(collection).setItemsSequence(super.getQuerySequence(collection, query));
78
77
  }
79
- /** Mirror the change into the cache. */
80
- async setQuery(collection, query, data) {
81
- await this.source.setQuery(collection, query, data);
82
- this.memory.getTable(collection).setQuery(query, data);
78
+ // Override so transaction copies get their own transaction-scoped mirror — uncommitted writes must never touch the real cache.
79
+ cloneWith(source) {
80
+ const clone = super.cloneWith(source);
81
+ Object.defineProperty(clone, "memory", { value: new MemoryDBProvider(), enumerable: true });
82
+ return clone;
83
83
  }
84
- /** Mirror the updates into the cache. */
85
- async updateQuery(collection, query, updates) {
86
- await this.source.updateQuery(collection, query, updates);
87
- this.memory.getTable(collection).updateQuery(query, updates);
88
- }
89
- /** Remove the deleted items from the cache. */
90
- async deleteQuery(collection, query) {
91
- await this.source.deleteQuery(collection, query);
92
- this.memory.getTable(collection).deleteQuery(query);
84
+ /**
85
+ * Runs the transaction on `source`, recording the callback's operations, then commits the recorded writes into the cache once the source commits.
86
+ * - The callback's provider is this cache over the source's transaction (with its own transaction-scoped mirror), so fetch-first writes and query resolution behave exactly as they do outside a transaction — and the fetch-then-write steps are atomic because both run in the source transaction.
87
+ * - Uncommitted data never touches the cache: a thrown callback commits nothing, and if the backend retries the callback only the committed attempt's writes are mirrored.
88
+ * - Update writes commit to the cache as deltas, so they refresh cached items and skip uncached ones — an item only read inside the transaction stays uncached until its next read.
89
+ */
90
+ async transact(callback) {
91
+ let transaction;
92
+ const result = await this.source.transact(provider => callback((transaction = new RecordingDBProvider(this.cloneWith(provider)))));
93
+ if (transaction)
94
+ await transaction.replayWrites(this.memory); // Commit the recorded writes into the cache.
95
+ return result;
93
96
  }
94
97
  // Implement `AsyncDisposable`
95
98
  async [Symbol.asyncDispose]() {
96
- await awaitDispose(this.source, // Dispose the source API provider.
97
- this.memory, // Dispose the source API provider.
99
+ await awaitDispose(this.memory, // Dispose the cache memory provider.
98
100
  super[Symbol.asyncDispose]());
99
101
  }
100
102
  }
@@ -11,7 +11,7 @@ import { DBProvider } from "./DBProvider.js";
11
11
  * - Extremely fast (ideal as the cache behind `CacheDBProvider`!), but does not persist data after the process or browser window closes.
12
12
  * - Identity-preserving: `getItem()` etc. return the exact same object instance that was passed into `setItem()`.
13
13
  * - Supports live subscriptions, so it can back `ItemStore` / `QueryStore` reads.
14
- * - Supports transactions: `transact()` runs the callback against a snapshot clone, captures its writes with `ChangesDBProvider`, and replays them onto this provider on success — sequences and nested transactions work inside the callback, scoped to the transaction.
14
+ * - Supports transactions: `transact()` runs the callback against a snapshot clone, records its operations with `RecordingDBProvider`, and replays the recorded writes onto this provider on success — sequences and nested transactions work inside the callback, scoped to the transaction.
15
15
  *
16
16
  * @see https://shelving.cc/db/MemoryDBProvider
17
17
  */
@@ -69,11 +69,11 @@ export declare class MemoryDBProvider<I extends Identifier = Identifier, T exten
69
69
  */
70
70
  clone(): MemoryDBProvider<I, T>;
71
71
  /**
72
- * Runs the callback against a shallow clone of this provider, capturing its writes with `ChangesDBProvider`, then replays them onto this provider when the callback resolves.
73
- * - If the callback throws, the clone and its captured changes are discarded and nothing is committed.
72
+ * Runs the callback against a shallow clone of this provider, recording its operations with `RecordingDBProvider`, then replays the recorded writes onto this provider when the callback resolves.
73
+ * - If the callback throws, the clone and its recorded operations are discarded and nothing is committed.
74
74
  * - Reads inside the callback see a snapshot from when the transaction began, plus the transaction's own writes. Realtime sequences and nested `transact()` also work inside the callback, scoped to the transaction — portable code must not rely on any of this (see `DBProvider.transact()`).
75
75
  * - The clone is disposed when the transaction completes or fails, ending any sequences opened inside the callback.
76
- * - Writes made to this provider while the callback is running are kept — the captured changes replay on top in order (updates apply as deltas, sets and deletes overwrite), with no conflict detection; overlapping transactions replay in completion order (last write wins per item).
76
+ * - Writes made to this provider while the callback is running are kept — the recorded writes replay on top in order (updates apply as deltas, sets and deletes overwrite), with no conflict detection; overlapping transactions replay in completion order (last write wins per item).
77
77
  * - Query writes resolve two-step against the clone, so they commit to exactly the items they matched inside the transaction, even if concurrent writes changed which items match.
78
78
  */
79
79
  transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
@@ -8,15 +8,15 @@ import { countItems } from "../../util/iterate.js";
8
8
  import { queryItems, queryWritableItems } from "../../util/query.js";
9
9
  import { getRandom, getRandomKey } from "../../util/random.js";
10
10
  import { updateData } from "../../util/update.js";
11
- import { ChangesDBProvider } from "./ChangesDBProvider.js";
12
11
  import { DBProvider } from "./DBProvider.js";
12
+ import { RecordingDBProvider } from "./RecordingDBProvider.js";
13
13
  /**
14
14
  * Synchronous in-memory database provider, storing each collection in a `MemoryTable`.
15
15
  *
16
16
  * - Extremely fast (ideal as the cache behind `CacheDBProvider`!), but does not persist data after the process or browser window closes.
17
17
  * - Identity-preserving: `getItem()` etc. return the exact same object instance that was passed into `setItem()`.
18
18
  * - Supports live subscriptions, so it can back `ItemStore` / `QueryStore` reads.
19
- * - Supports transactions: `transact()` runs the callback against a snapshot clone, captures its writes with `ChangesDBProvider`, and replays them onto this provider on success — sequences and nested transactions work inside the callback, scoped to the transaction.
19
+ * - Supports transactions: `transact()` runs the callback against a snapshot clone, records its operations with `RecordingDBProvider`, and replays the recorded writes onto this provider on success — sequences and nested transactions work inside the callback, scoped to the transaction.
20
20
  *
21
21
  * @see https://shelving.cc/db/MemoryDBProvider
22
22
  */
@@ -108,19 +108,19 @@ export class MemoryDBProvider extends DBProvider {
108
108
  return clone;
109
109
  }
110
110
  /**
111
- * Runs the callback against a shallow clone of this provider, capturing its writes with `ChangesDBProvider`, then replays them onto this provider when the callback resolves.
112
- * - If the callback throws, the clone and its captured changes are discarded and nothing is committed.
111
+ * Runs the callback against a shallow clone of this provider, recording its operations with `RecordingDBProvider`, then replays the recorded writes onto this provider when the callback resolves.
112
+ * - If the callback throws, the clone and its recorded operations are discarded and nothing is committed.
113
113
  * - Reads inside the callback see a snapshot from when the transaction began, plus the transaction's own writes. Realtime sequences and nested `transact()` also work inside the callback, scoped to the transaction — portable code must not rely on any of this (see `DBProvider.transact()`).
114
114
  * - The clone is disposed when the transaction completes or fails, ending any sequences opened inside the callback.
115
- * - Writes made to this provider while the callback is running are kept — the captured changes replay on top in order (updates apply as deltas, sets and deletes overwrite), with no conflict detection; overlapping transactions replay in completion order (last write wins per item).
115
+ * - Writes made to this provider while the callback is running are kept — the recorded writes replay on top in order (updates apply as deltas, sets and deletes overwrite), with no conflict detection; overlapping transactions replay in completion order (last write wins per item).
116
116
  * - Query writes resolve two-step against the clone, so they commit to exactly the items they matched inside the transaction, even if concurrent writes changed which items match.
117
117
  */
118
118
  async transact(callback) {
119
119
  const clone = this.clone();
120
120
  try {
121
- const transaction = new ChangesDBProvider(clone);
121
+ const transaction = new RecordingDBProvider(clone);
122
122
  const result = await callback(transaction);
123
- await transaction.replay(this); // Commit the captured changes.
123
+ await transaction.replayWrites(this); // Commit the recorded writes.
124
124
  return result;
125
125
  }
126
126
  finally {
@@ -0,0 +1,115 @@
1
+ import type { ImmutableArray, MutableArray } from "../../util/array.js";
2
+ import type { Data } from "../../util/data.js";
3
+ import type { Identifier, Item, Items, OptionalItem } from "../../util/item.js";
4
+ import type { Query } from "../../util/query.js";
5
+ import type { Updates } from "../../util/update.js";
6
+ import type { Collection } from "../collection/Collection.js";
7
+ import type { DBProvider } from "./DBProvider.js";
8
+ import { ThroughDBProvider } from "./ThroughDBProvider.js";
9
+ /**
10
+ * Structured log entry recording a single database operation performed through a `RecordingDBProvider`.
11
+ *
12
+ * - `action` is the kind of operation; `collection` is the `Collection` it applies to; `id` is the item involved; `data` and `updates` carry whichever fields apply to that operation.
13
+ * - A `"get"` operation records a read — `data` is the item that was observed, or `undefined` if the read confirmed the item absent.
14
+ *
15
+ * @see https://shelving.cc/db/DBOperation
16
+ */
17
+ export type DBOperation<I extends Identifier = Identifier, T extends Data = Data> = {
18
+ readonly action: "get" | "add" | "set" | "update" | "delete";
19
+ readonly collection: Collection<string, I, T>;
20
+ readonly id: I;
21
+ readonly data?: unknown;
22
+ readonly updates?: unknown;
23
+ };
24
+ /**
25
+ * Readonly array of `DBOperation` entries, e.g. the log recorded by a `RecordingDBProvider`.
26
+ *
27
+ * @see https://shelving.cc/db/DBOperations
28
+ */
29
+ export type DBOperations<I extends Identifier = Identifier, T extends Data = Data> = ImmutableArray<DBOperation<I, T>>;
30
+ /**
31
+ * Replay a list of database operations onto a provider, re-issuing each one in order.
32
+ *
33
+ * - Writes re-issue as the corresponding item write — `"add"` replays as `DBProvider.setItem()` with the logged id, so the target keeps the same generated ids.
34
+ * - `"get"` reads apply what they observed — the item is set, or deleted when the read confirmed it absent. That's right for refreshing a mirror or cache, and wrong for an authoritative target, where an observed snapshot would overwrite newer data — replay only the writes there (see `RecordingDBProvider.replayWrites()`).
35
+ * - Operations re-issue as a sequence of awaited writes — the replay itself is not atomic.
36
+ *
37
+ * @param provider Provider to replay the operations onto.
38
+ * @param operations Operations to replay, in order.
39
+ * @example await replayOperations(mirror, recording.operations);
40
+ * @see https://shelving.cc/db/replayOperations
41
+ */
42
+ export declare function replayOperations<I extends Identifier, T extends Data>(provider: DBProvider<I, T>, operations: DBOperations<I, T>): Promise<void>;
43
+ /**
44
+ * Database provider that records every operation it performs to its `operations` log.
45
+ *
46
+ * - Wraps a `source` provider, delegates each operation, then appends a `DBOperation` entry describing what happened.
47
+ * - Records reads as well as writes: `getItem()` logs a `"get"` with the item it observed (or its confirmed absence), and `getQuery()` logs a `"get"` per item returned. Derived reads and two-step query writes are inherited, so everything they do is recorded per item too. Realtime sequences are not recorded.
48
+ * - Replay the log onto another provider with `replay()`, `replayWrites()`, or `replayReads()`.
49
+ * - Useful for building audit logging, change feeds, optimistic updates (see `UndoDBProvider`), or assertions in tests.
50
+ *
51
+ * @see https://shelving.cc/db/RecordingDBProvider
52
+ */
53
+ export declare class RecordingDBProvider<I extends Identifier, T extends Data> extends ThroughDBProvider<I, T> {
54
+ /**
55
+ * The log of operations performed through this provider, in the order they happened.
56
+ *
57
+ * @see https://shelving.cc/db/RecordingDBProvider/operations
58
+ */
59
+ get operations(): DBOperations<I, T>;
60
+ readonly _operations: MutableArray<DBOperation<I, T>>;
61
+ /**
62
+ * The write operations from the `operations` log, in the order they happened.
63
+ *
64
+ * @see https://shelving.cc/db/RecordingDBProvider/writes
65
+ */
66
+ get writes(): DBOperations<I, T>;
67
+ /**
68
+ * The `"get"` read operations from the `operations` log, in the order they happened.
69
+ *
70
+ * @see https://shelving.cc/db/RecordingDBProvider/reads
71
+ */
72
+ get reads(): DBOperations<I, T>;
73
+ /** Log a `"get"` operation recording the item that was observed (or its confirmed absence). */
74
+ getItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<OptionalItem<II, TT>>;
75
+ /** Log a `"get"` operation for each item the query observed. */
76
+ getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
77
+ /** Log an `"add"` operation after writing. */
78
+ addItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, data: TT): Promise<II>;
79
+ /** Log a `"set"` operation after writing. */
80
+ setItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, data: TT): Promise<void>;
81
+ /** Log an `"update"` operation after writing. */
82
+ updateItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, updates: Updates<Item<II, TT>>): Promise<void>;
83
+ /** Log a `"delete"` operation after writing. */
84
+ deleteItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
85
+ /**
86
+ * Replay every recorded operation onto another provider, in order.
87
+ * - Reads apply what they observed and writes re-issue, so this refreshes a mirror or cache exactly — see `replayOperations()`.
88
+ * - Use `replayWrites()` for an authoritative target, where applying observed reads would overwrite newer data.
89
+ *
90
+ * @param provider Provider to replay the operations onto.
91
+ * @example await recording.replay(mirror);
92
+ * @see https://shelving.cc/db/RecordingDBProvider/replay
93
+ */
94
+ replay(provider: DBProvider<I, T>): Promise<void>;
95
+ /**
96
+ * Replay only the recorded write operations onto another provider, in order.
97
+ * - The right call for authoritative targets — applying a log to a real database, audit replay, syncing a second source of truth — where updates should compose onto current state and observed reads must never overwrite newer data.
98
+ *
99
+ * @param provider Provider to replay the writes onto.
100
+ * @example await recording.replayWrites(db);
101
+ * @see https://shelving.cc/db/RecordingDBProvider/replayWrites
102
+ */
103
+ replayWrites(provider: DBProvider<I, T>): Promise<void>;
104
+ /**
105
+ * Replay only the recorded `"get"` read operations onto another provider, in order.
106
+ * - Applies what each read observed (setting the item, or deleting it when the read confirmed absence) — e.g. warming a cache from a recorded session.
107
+ *
108
+ * @param provider Provider to replay the reads onto.
109
+ * @example await recording.replayReads(cache.memory);
110
+ * @see https://shelving.cc/db/RecordingDBProvider/replayReads
111
+ */
112
+ replayReads(provider: DBProvider<I, T>): Promise<void>;
113
+ cloneWith(source: DBProvider<I, T>): this;
114
+ transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
115
+ }
@@ -0,0 +1,146 @@
1
+ import { ThroughDBProvider } from "./ThroughDBProvider.js";
2
+ /**
3
+ * Replay a list of database operations onto a provider, re-issuing each one in order.
4
+ *
5
+ * - Writes re-issue as the corresponding item write — `"add"` replays as `DBProvider.setItem()` with the logged id, so the target keeps the same generated ids.
6
+ * - `"get"` reads apply what they observed — the item is set, or deleted when the read confirmed it absent. That's right for refreshing a mirror or cache, and wrong for an authoritative target, where an observed snapshot would overwrite newer data — replay only the writes there (see `RecordingDBProvider.replayWrites()`).
7
+ * - Operations re-issue as a sequence of awaited writes — the replay itself is not atomic.
8
+ *
9
+ * @param provider Provider to replay the operations onto.
10
+ * @param operations Operations to replay, in order.
11
+ * @example await replayOperations(mirror, recording.operations);
12
+ * @see https://shelving.cc/db/replayOperations
13
+ */
14
+ export async function replayOperations(provider, operations) {
15
+ // `as` casts needed: the log stores `data` and `updates` loosely as `unknown`.
16
+ for (const { action, collection, id, data, updates } of operations) {
17
+ if (action === "update")
18
+ await provider.updateItem(collection, id, updates);
19
+ else if (action === "delete")
20
+ await provider.deleteItem(collection, id);
21
+ else if (action === "get" && !data)
22
+ await provider.deleteItem(collection, id);
23
+ else
24
+ await provider.setItem(collection, id, data); // Observed items, adds, and sets all replay as the item's full data.
25
+ }
26
+ }
27
+ /**
28
+ * Database provider that records every operation it performs to its `operations` log.
29
+ *
30
+ * - Wraps a `source` provider, delegates each operation, then appends a `DBOperation` entry describing what happened.
31
+ * - Records reads as well as writes: `getItem()` logs a `"get"` with the item it observed (or its confirmed absence), and `getQuery()` logs a `"get"` per item returned. Derived reads and two-step query writes are inherited, so everything they do is recorded per item too. Realtime sequences are not recorded.
32
+ * - Replay the log onto another provider with `replay()`, `replayWrites()`, or `replayReads()`.
33
+ * - Useful for building audit logging, change feeds, optimistic updates (see `UndoDBProvider`), or assertions in tests.
34
+ *
35
+ * @see https://shelving.cc/db/RecordingDBProvider
36
+ */
37
+ export class RecordingDBProvider extends ThroughDBProvider {
38
+ /**
39
+ * The log of operations performed through this provider, in the order they happened.
40
+ *
41
+ * @see https://shelving.cc/db/RecordingDBProvider/operations
42
+ */
43
+ get operations() {
44
+ return this._operations;
45
+ }
46
+ _operations = [];
47
+ /**
48
+ * The write operations from the `operations` log, in the order they happened.
49
+ *
50
+ * @see https://shelving.cc/db/RecordingDBProvider/writes
51
+ */
52
+ get writes() {
53
+ return this._operations.filter(({ action }) => action !== "get");
54
+ }
55
+ /**
56
+ * The `"get"` read operations from the `operations` log, in the order they happened.
57
+ *
58
+ * @see https://shelving.cc/db/RecordingDBProvider/reads
59
+ */
60
+ get reads() {
61
+ return this._operations.filter(({ action }) => action === "get");
62
+ }
63
+ /** Log a `"get"` operation recording the item that was observed (or its confirmed absence). */
64
+ async getItem(collection, id) {
65
+ const item = await super.getItem(collection, id);
66
+ this._operations.push({ action: "get", collection, id, data: item });
67
+ return item;
68
+ }
69
+ /** Log a `"get"` operation for each item the query observed. */
70
+ async getQuery(collection, query) {
71
+ const items = await super.getQuery(collection, query);
72
+ for (const item of items)
73
+ this._operations.push({ action: "get", collection, id: item.id, data: item });
74
+ return items;
75
+ }
76
+ /** Log an `"add"` operation after writing. */
77
+ async addItem(collection, data) {
78
+ const id = await super.addItem(collection, data);
79
+ this._operations.push({ action: "add", collection, id, data });
80
+ return id;
81
+ }
82
+ /** Log a `"set"` operation after writing. */
83
+ async setItem(collection, id, data) {
84
+ await super.setItem(collection, id, data);
85
+ this._operations.push({ action: "set", collection, id, data });
86
+ }
87
+ /** Log an `"update"` operation after writing. */
88
+ async updateItem(collection, id, updates) {
89
+ await super.updateItem(collection, id, updates);
90
+ this._operations.push({ action: "update", collection, id, updates });
91
+ }
92
+ /** Log a `"delete"` operation after writing. */
93
+ async deleteItem(collection, id) {
94
+ await super.deleteItem(collection, id);
95
+ this._operations.push({ action: "delete", collection, id });
96
+ }
97
+ /**
98
+ * Replay every recorded operation onto another provider, in order.
99
+ * - Reads apply what they observed and writes re-issue, so this refreshes a mirror or cache exactly — see `replayOperations()`.
100
+ * - Use `replayWrites()` for an authoritative target, where applying observed reads would overwrite newer data.
101
+ *
102
+ * @param provider Provider to replay the operations onto.
103
+ * @example await recording.replay(mirror);
104
+ * @see https://shelving.cc/db/RecordingDBProvider/replay
105
+ */
106
+ replay(provider) {
107
+ return replayOperations(provider, this._operations);
108
+ }
109
+ /**
110
+ * Replay only the recorded write operations onto another provider, in order.
111
+ * - The right call for authoritative targets — applying a log to a real database, audit replay, syncing a second source of truth — where updates should compose onto current state and observed reads must never overwrite newer data.
112
+ *
113
+ * @param provider Provider to replay the writes onto.
114
+ * @example await recording.replayWrites(db);
115
+ * @see https://shelving.cc/db/RecordingDBProvider/replayWrites
116
+ */
117
+ replayWrites(provider) {
118
+ return replayOperations(provider, this.writes);
119
+ }
120
+ /**
121
+ * Replay only the recorded `"get"` read operations onto another provider, in order.
122
+ * - Applies what each read observed (setting the item, or deleting it when the read confirmed absence) — e.g. warming a cache from a recorded session.
123
+ *
124
+ * @param provider Provider to replay the reads onto.
125
+ * @example await recording.replayReads(cache.memory);
126
+ * @see https://shelving.cc/db/RecordingDBProvider/replayReads
127
+ */
128
+ replayReads(provider) {
129
+ return replayOperations(provider, this.reads);
130
+ }
131
+ // Override so that transaction copies get their own log.
132
+ cloneWith(source) {
133
+ const clone = super.cloneWith(source);
134
+ Object.defineProperty(clone, "_operations", { value: [], enumerable: false });
135
+ return clone;
136
+ }
137
+ // Override to log the transaction's operations after it commits — a failed transaction logs nothing.
138
+ // The merge must happen after `source.transact()` resolves: backends may retry the callback or fail the commit itself, and only the committed attempt's operations belong in the log.
139
+ async transact(callback) {
140
+ let transaction;
141
+ const result = await this.source.transact(provider => callback((transaction = this.cloneWith(provider))));
142
+ if (transaction)
143
+ this._operations.push(...transaction.operations);
144
+ return result;
145
+ }
146
+ }
@@ -0,0 +1,44 @@
1
+ import type { Data } from "../../util/data.js";
2
+ import type { Identifier, Item } from "../../util/item.js";
3
+ import type { Updates } from "../../util/update.js";
4
+ import type { Collection } from "../collection/Collection.js";
5
+ import { RecordingDBProvider } from "./RecordingDBProvider.js";
6
+ /**
7
+ * Database provider that records every operation and can undo its own writes.
8
+ *
9
+ * - Extends `RecordingDBProvider`, additionally reading each item before the first write that touches it — so the log always contains every touched item's original state.
10
+ * - Call `undo()` to restore the wrapped provider to the state the log first observed, e.g. rolling back optimistic local updates after a failed server call.
11
+ * - The extra read is skipped when the log already establishes the item's state (an earlier read observed it, or an earlier add created it) — over a local `MemoryDBProvider` the reads are effectively free anyway.
12
+ *
13
+ * @example
14
+ * const local = new UndoDBProvider(memory);
15
+ * await runServiceLogic(local); // Applies locally right away.
16
+ * try {
17
+ * await api.push(local.writes); // Send the writes to the server.
18
+ * } catch {
19
+ * await local.undo(); // Server failed — restore the local copy.
20
+ * }
21
+ *
22
+ * @see https://shelving.cc/db/UndoDBProvider
23
+ */
24
+ export declare class UndoDBProvider<I extends Identifier, T extends Data> extends RecordingDBProvider<I, T> {
25
+ /** Whether the log already establishes the original state of an item (an earlier read observed it, or an earlier add created it). */
26
+ protected _isEstablished(collection: Collection<string, I, T>, id: I): boolean;
27
+ /** Read the item first (recording its original state) if the log doesn't already establish it. */
28
+ setItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, data: TT): Promise<void>;
29
+ /** Read the item first (recording its original state) if the log doesn't already establish it. */
30
+ updateItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, updates: Updates<Item<II, TT>>): Promise<void>;
31
+ /** Read the item first (recording its original state) if the log doesn't already establish it. */
32
+ deleteItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
33
+ /**
34
+ * Restore the wrapped provider to the state the log first observed, undoing this provider's writes.
35
+ *
36
+ * - Applies the state-establishing operations in reverse order, so the earliest observed state of each item wins: reads restore the item that was observed (or delete it when the read confirmed it absent), and adds delete (the item did not exist before).
37
+ * - Restores touched items unconditionally, so concurrent writes made to those items since the recording are overwritten.
38
+ * - Writes directly to `source` without recording, so the log still describes the original operations afterwards.
39
+ *
40
+ * @example await provider.undo();
41
+ * @see https://shelving.cc/db/UndoDBProvider/undo
42
+ */
43
+ undo(): Promise<void>;
44
+ }
@@ -0,0 +1,62 @@
1
+ import { RecordingDBProvider } from "./RecordingDBProvider.js";
2
+ /**
3
+ * Database provider that records every operation and can undo its own writes.
4
+ *
5
+ * - Extends `RecordingDBProvider`, additionally reading each item before the first write that touches it — so the log always contains every touched item's original state.
6
+ * - Call `undo()` to restore the wrapped provider to the state the log first observed, e.g. rolling back optimistic local updates after a failed server call.
7
+ * - The extra read is skipped when the log already establishes the item's state (an earlier read observed it, or an earlier add created it) — over a local `MemoryDBProvider` the reads are effectively free anyway.
8
+ *
9
+ * @example
10
+ * const local = new UndoDBProvider(memory);
11
+ * await runServiceLogic(local); // Applies locally right away.
12
+ * try {
13
+ * await api.push(local.writes); // Send the writes to the server.
14
+ * } catch {
15
+ * await local.undo(); // Server failed — restore the local copy.
16
+ * }
17
+ *
18
+ * @see https://shelving.cc/db/UndoDBProvider
19
+ */
20
+ export class UndoDBProvider extends RecordingDBProvider {
21
+ /** Whether the log already establishes the original state of an item (an earlier read observed it, or an earlier add created it). */
22
+ _isEstablished(collection, id) {
23
+ return this._operations.some(operation => operation.collection === collection && operation.id === id && (operation.action === "get" || operation.action === "add"));
24
+ }
25
+ /** Read the item first (recording its original state) if the log doesn't already establish it. */
26
+ async setItem(collection, id, data) {
27
+ if (!this._isEstablished(collection, id))
28
+ await this.getItem(collection, id);
29
+ await super.setItem(collection, id, data);
30
+ }
31
+ /** Read the item first (recording its original state) if the log doesn't already establish it. */
32
+ async updateItem(collection, id, updates) {
33
+ if (!this._isEstablished(collection, id))
34
+ await this.getItem(collection, id);
35
+ await super.updateItem(collection, id, updates);
36
+ }
37
+ /** Read the item first (recording its original state) if the log doesn't already establish it. */
38
+ async deleteItem(collection, id) {
39
+ if (!this._isEstablished(collection, id))
40
+ await this.getItem(collection, id);
41
+ await super.deleteItem(collection, id);
42
+ }
43
+ /**
44
+ * Restore the wrapped provider to the state the log first observed, undoing this provider's writes.
45
+ *
46
+ * - Applies the state-establishing operations in reverse order, so the earliest observed state of each item wins: reads restore the item that was observed (or delete it when the read confirmed it absent), and adds delete (the item did not exist before).
47
+ * - Restores touched items unconditionally, so concurrent writes made to those items since the recording are overwritten.
48
+ * - Writes directly to `source` without recording, so the log still describes the original operations afterwards.
49
+ *
50
+ * @example await provider.undo();
51
+ * @see https://shelving.cc/db/UndoDBProvider/undo
52
+ */
53
+ async undo() {
54
+ // `as` cast needed: the log stores `data` loosely as `unknown`.
55
+ for (const { action, collection, id, data } of this._operations.toReversed()) {
56
+ if (action === "get" && data)
57
+ await this.source.setItem(collection, id, data);
58
+ else if (action === "get" || action === "add")
59
+ await this.source.deleteItem(collection, id);
60
+ }
61
+ }
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.280.0",
3
+ "version": "1.282.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,62 +0,0 @@
1
- import type { MutableArray } from "../../util/array.js";
2
- import type { Data } from "../../util/data.js";
3
- import type { Identifier, Item } from "../../util/item.js";
4
- import type { Updates } from "../../util/update.js";
5
- import type { Collection } from "../collection/Collection.js";
6
- import type { DBProvider } from "./DBProvider.js";
7
- import { ThroughDBProvider } from "./ThroughDBProvider.js";
8
- /**
9
- * Structured log entry recording a single database write performed through a `ChangesDBProvider`.
10
- *
11
- * - `action` is the kind of write; `collection` is the `Collection` the write applies to; `id` is the item that was written; `data` and `updates` carry whichever fields apply to that write.
12
- *
13
- * @see https://shelving.cc/db/DBChange
14
- */
15
- export type DBChange<I extends Identifier, T extends Data = Data> = {
16
- readonly action: "add" | "set" | "update" | "delete";
17
- readonly collection: Collection<string, I, T>;
18
- readonly id: I;
19
- readonly data?: unknown;
20
- readonly updates?: unknown;
21
- };
22
- /**
23
- * Database provider that records every write it performs to its `changes` log.
24
- *
25
- * - Wraps a `source` provider, delegates each write, then appends a `DBChange` entry describing what happened.
26
- * - Every change is an explicit per-item write with an `id` — query writes are inherited two-step from `ThroughDBProvider`, so they arrive here as the individual item writes they resolved to.
27
- * - Replay the log onto another provider with `ChangesDBProvider.replay()`.
28
- * - Useful for building audit logging, change feeds, or assertions in tests; reads are passed straight through and not logged.
29
- *
30
- * @see https://shelving.cc/db/ChangesDBProvider
31
- */
32
- export declare class ChangesDBProvider<I extends Identifier, T extends Data> extends ThroughDBProvider<I, T> {
33
- /**
34
- * The log of writes performed through this provider, in the order they happened.
35
- *
36
- * @see https://shelving.cc/db/ChangesDBProvider/changes
37
- */
38
- get changes(): ReadonlyArray<DBChange<I, T>>;
39
- readonly _changes: MutableArray<DBChange<I, T>>;
40
- /** Log an `"add"` change after writing. */
41
- addItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, data: TT): Promise<II>;
42
- /** Log a `"set"` change after writing. */
43
- setItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, data: TT): Promise<void>;
44
- /** Log an `"update"` change after writing. */
45
- updateItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, updates: Updates<Item<II, TT>>): Promise<void>;
46
- /** Log a `"delete"` change after writing. */
47
- deleteItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
48
- /**
49
- * Replay the `changes` log onto another provider, re-issuing each write in order.
50
- *
51
- * - Useful for audit replay or syncing a secondary store — and the commit mechanism for `MemoryDBProvider.transact()`.
52
- * - `"add"` changes replay as `DBProvider.setItem()` with the logged id, so the target keeps the same generated ids.
53
- * - The changes replay as a sequence of awaited writes — the replay itself is not atomic.
54
- *
55
- * @param provider Provider to replay the changes onto.
56
- * @example await changes.replay(mirror);
57
- * @see https://shelving.cc/db/ChangesDBProvider/replay
58
- */
59
- replay(provider: DBProvider<I, T>): Promise<void>;
60
- cloneWith(source: DBProvider<I, T>): this;
61
- transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
62
- }
@@ -1,80 +0,0 @@
1
- import { ThroughDBProvider } from "./ThroughDBProvider.js";
2
- /**
3
- * Database provider that records every write it performs to its `changes` log.
4
- *
5
- * - Wraps a `source` provider, delegates each write, then appends a `DBChange` entry describing what happened.
6
- * - Every change is an explicit per-item write with an `id` — query writes are inherited two-step from `ThroughDBProvider`, so they arrive here as the individual item writes they resolved to.
7
- * - Replay the log onto another provider with `ChangesDBProvider.replay()`.
8
- * - Useful for building audit logging, change feeds, or assertions in tests; reads are passed straight through and not logged.
9
- *
10
- * @see https://shelving.cc/db/ChangesDBProvider
11
- */
12
- export class ChangesDBProvider extends ThroughDBProvider {
13
- /**
14
- * The log of writes performed through this provider, in the order they happened.
15
- *
16
- * @see https://shelving.cc/db/ChangesDBProvider/changes
17
- */
18
- get changes() {
19
- return this._changes;
20
- }
21
- _changes = [];
22
- /** Log an `"add"` change after writing. */
23
- async addItem(collection, data) {
24
- const id = await super.addItem(collection, data);
25
- this._changes.push({ action: "add", collection, id, data });
26
- return id;
27
- }
28
- /** Log a `"set"` change after writing. */
29
- async setItem(collection, id, data) {
30
- await super.setItem(collection, id, data);
31
- this._changes.push({ action: "set", collection, id, data });
32
- }
33
- /** Log an `"update"` change after writing. */
34
- async updateItem(collection, id, updates) {
35
- await super.updateItem(collection, id, updates);
36
- this._changes.push({ action: "update", collection, id, updates });
37
- }
38
- /** Log a `"delete"` change after writing. */
39
- async deleteItem(collection, id) {
40
- await super.deleteItem(collection, id);
41
- this._changes.push({ action: "delete", collection, id });
42
- }
43
- /**
44
- * Replay the `changes` log onto another provider, re-issuing each write in order.
45
- *
46
- * - Useful for audit replay or syncing a secondary store — and the commit mechanism for `MemoryDBProvider.transact()`.
47
- * - `"add"` changes replay as `DBProvider.setItem()` with the logged id, so the target keeps the same generated ids.
48
- * - The changes replay as a sequence of awaited writes — the replay itself is not atomic.
49
- *
50
- * @param provider Provider to replay the changes onto.
51
- * @example await changes.replay(mirror);
52
- * @see https://shelving.cc/db/ChangesDBProvider/replay
53
- */
54
- async replay(provider) {
55
- // `as` casts needed: the log stores `data` and `updates` loosely as `unknown`.
56
- for (const { action, collection, id, data, updates } of this._changes) {
57
- if (action === "delete")
58
- await provider.deleteItem(collection, id);
59
- else if (action === "update")
60
- await provider.updateItem(collection, id, updates);
61
- else
62
- await provider.setItem(collection, id, data);
63
- }
64
- }
65
- // Override so that transaction copies get their own log.
66
- cloneWith(source) {
67
- const clone = super.cloneWith(source);
68
- Object.defineProperty(clone, "_changes", { value: [], enumerable: false });
69
- return clone;
70
- }
71
- // Override to log the transaction's writes after it commits — a failed transaction logs nothing.
72
- // The merge must happen after `source.transact()` resolves: backends may retry the callback or fail the commit itself, and only the committed attempt's writes belong in the log.
73
- async transact(callback) {
74
- let transaction;
75
- const result = await this.source.transact(provider => callback((transaction = this.cloneWith(provider))));
76
- if (transaction)
77
- this._changes.push(...transaction.changes);
78
- return result;
79
- }
80
- }