shelving 1.279.0 → 1.281.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";
@@ -119,7 +119,8 @@ export declare abstract class DBProvider<I extends Identifier = Identifier, T ex
119
119
  abstract getQuerySequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
120
120
  /**
121
121
  * Set (overwrite) the data for every item matching a query.
122
- * - Not guaranteed atomic: an implementation may resolve the matching items first and then write per item (two-step — see `ThroughDBProvider`), so wrap the call in `transact()` when atomicity matters.
122
+ * - Two-step by default: resolves the query to its matching items with `getQuery()`, then sets each one concurrently with `setItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `UPDATE … WHERE`).
123
+ * - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters.
123
124
  *
124
125
  * @param collection Collection to write to.
125
126
  * @param query Query selecting the items to set.
@@ -127,10 +128,11 @@ export declare abstract class DBProvider<I extends Identifier = Identifier, T ex
127
128
  * @example await provider.setQuery(users, { age: 40 }, { active: true });
128
129
  * @see https://shelving.cc/db/DBProvider/setQuery
129
130
  */
130
- abstract setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
131
+ setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
131
132
  /**
132
133
  * Apply partial updates to every item matching a query.
133
- * - Not guaranteed atomic: an implementation may resolve the matching items first and then write per item (two-step — see `ThroughDBProvider`), so wrap the call in `transact()` when atomicity matters.
134
+ * - Two-step by default: resolves the query to its matching items with `getQuery()`, then updates each one concurrently with `updateItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `UPDATE … WHERE`).
135
+ * - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters.
134
136
  *
135
137
  * @param collection Collection to write to.
136
138
  * @param query Query selecting the items to update.
@@ -138,17 +140,18 @@ export declare abstract class DBProvider<I extends Identifier = Identifier, T ex
138
140
  * @example await provider.updateQuery(users, { age: 40 }, { active: true });
139
141
  * @see https://shelving.cc/db/DBProvider/updateQuery
140
142
  */
141
- abstract updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
143
+ updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
142
144
  /**
143
145
  * Delete every item matching a query.
144
- * - Not guaranteed atomic: an implementation may resolve the matching items first and then delete per item (two-step — see `ThroughDBProvider`), so wrap the call in `transact()` when atomicity matters.
146
+ * - Two-step by default: resolves the query to its matching items with `getQuery()`, then deletes each one concurrently with `deleteItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `DELETE … WHERE`).
147
+ * - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters.
145
148
  *
146
149
  * @param collection Collection to delete from.
147
150
  * @param query Query selecting the items to delete.
148
151
  * @example await provider.deleteQuery(users, { active: false });
149
152
  * @see https://shelving.cc/db/DBProvider/deleteQuery
150
153
  */
151
- abstract deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
154
+ deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
152
155
  /**
153
156
  * Get the first item matching a query, or `undefined` if there are none.
154
157
  *
@@ -1,6 +1,7 @@
1
1
  import { RequiredError } from "../../error/RequiredError.js";
2
2
  import { UnsupportedError } from "../../error/UnsupportedError.js";
3
3
  import { countArray, getFirst } from "../../util/array.js";
4
+ import { awaitValues } from "../../util/async.js";
4
5
  import { awaitDispose } from "../../util/dispose.js";
5
6
  /**
6
7
  * Provider with a fully asynchronous interface for database access.
@@ -49,6 +50,50 @@ export class DBProvider {
49
50
  async countQuery(collection, query) {
50
51
  return countArray(await this.getQuery(collection, query));
51
52
  }
53
+ /**
54
+ * Set (overwrite) the data for every item matching a query.
55
+ * - Two-step by default: resolves the query to its matching items with `getQuery()`, then sets each one concurrently with `setItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `UPDATE … WHERE`).
56
+ * - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters.
57
+ *
58
+ * @param collection Collection to write to.
59
+ * @param query Query selecting the items to set.
60
+ * @param data Full data to store for each matching item.
61
+ * @example await provider.setQuery(users, { age: 40 }, { active: true });
62
+ * @see https://shelving.cc/db/DBProvider/setQuery
63
+ */
64
+ async setQuery(collection, query, data) {
65
+ const items = await this.getQuery(collection, query);
66
+ await awaitValues(...items.map(({ id }) => this.setItem(collection, id, data)));
67
+ }
68
+ /**
69
+ * Apply partial updates to every item matching a query.
70
+ * - Two-step by default: resolves the query to its matching items with `getQuery()`, then updates each one concurrently with `updateItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `UPDATE … WHERE`).
71
+ * - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters.
72
+ *
73
+ * @param collection Collection to write to.
74
+ * @param query Query selecting the items to update.
75
+ * @param updates Updates to apply to each matching item.
76
+ * @example await provider.updateQuery(users, { age: 40 }, { active: true });
77
+ * @see https://shelving.cc/db/DBProvider/updateQuery
78
+ */
79
+ async updateQuery(collection, query, updates) {
80
+ const items = await this.getQuery(collection, query);
81
+ await awaitValues(...items.map(({ id }) => this.updateItem(collection, id, updates)));
82
+ }
83
+ /**
84
+ * Delete every item matching a query.
85
+ * - Two-step by default: resolves the query to its matching items with `getQuery()`, then deletes each one concurrently with `deleteItem()`. Engine providers override this with a native query write where one exists (e.g. SQL `DELETE … WHERE`).
86
+ * - Not guaranteed atomic: the resolve and the writes are separate steps, so wrap the call in `transact()` when atomicity matters.
87
+ *
88
+ * @param collection Collection to delete from.
89
+ * @param query Query selecting the items to delete.
90
+ * @example await provider.deleteQuery(users, { active: false });
91
+ * @see https://shelving.cc/db/DBProvider/deleteQuery
92
+ */
93
+ async deleteQuery(collection, query) {
94
+ const items = await this.getQuery(collection, query);
95
+ await awaitValues(...items.map(({ id }) => this.deleteItem(collection, id)));
96
+ }
52
97
  /**
53
98
  * Get the first item matching a query, or `undefined` if there are none.
54
99
  *
@@ -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
+ }
@@ -4,17 +4,17 @@ import type { Query } from "../../util/query.js";
4
4
  import type { Sourceable } from "../../util/source.js";
5
5
  import type { Updates } from "../../util/update.js";
6
6
  import type { Collection } from "../collection/Collection.js";
7
- import type { DBProvider } from "./DBProvider.js";
7
+ import { DBProvider } from "./DBProvider.js";
8
8
  /**
9
- * Database provider that passes every operation straight through to a wrapped `source` provider.
9
+ * Database provider that passes every core operation straight through to a wrapped `source` provider.
10
10
  *
11
11
  * - Base for the layered `Through*Provider` family (validation, caching, logging, change tracking); subclasses override individual methods to add behaviour and call `super` to delegate.
12
- * - Query writes (`setQuery()`, `updateQuery()`, `deleteQuery()`) are two-step by default — resolved to their matching items with `getQuery()`, then written per item through this provider's own item methods — so wrapper behaviour applies to every implied write. Subclasses that don't need per-item behaviour override them to pass through to `source` directly.
12
+ * - Only the core operations delegate to `source` — derived reads (`DBProvider.requireItem()`, `DBProvider.getFirst()`, `DBProvider.requireFirst()`) and two-step query writes are inherited from `DBProvider`, so they route through this provider's own overridden methods and wrapper behaviour applies to everything they do. Wrappers that don't need per-item behaviour override the query writes to pass through to `source` directly.
13
13
  * - Exposes `source` and implements `Sourceable`, so wrapped providers can be discovered with `getSource()` / `requireSource()`.
14
14
  *
15
15
  * @see https://shelving.cc/db/ThroughDBProvider
16
16
  */
17
- export declare class ThroughDBProvider<I extends Identifier, T extends Data> implements DBProvider<I, T>, Sourceable<DBProvider<I, T>> {
17
+ export declare class ThroughDBProvider<I extends Identifier, T extends Data> extends DBProvider<I, T> implements Sourceable<DBProvider<I, T>> {
18
18
  /**
19
19
  * The wrapped source provider that every operation is delegated to.
20
20
  *
@@ -23,28 +23,15 @@ export declare class ThroughDBProvider<I extends Identifier, T extends Data> imp
23
23
  readonly source: DBProvider<I, T>;
24
24
  constructor(source: DBProvider<I, T>);
25
25
  getItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<OptionalItem<II, TT>>;
26
- requireItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<Item<II, TT>>;
27
26
  getItemSequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): OptionalItemSequence<II, TT>;
28
27
  addItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, data: TT): Promise<II>;
29
28
  setItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, data: TT): Promise<void>;
30
29
  updateItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, updates: Updates<Item<II, TT>>): Promise<void>;
31
30
  deleteItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
31
+ /** Delegates to `source` so its native counting is kept (the base implementation would fetch the items and count them). */
32
32
  countQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<number>;
33
33
  getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
34
34
  getQuerySequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
35
- /**
36
- * Two-step: resolve the query to its matching items with `getQuery()`, then set each one with `setItem()`.
37
- * - Routes every implied write through this provider's own item methods, so wrapper behaviour applies to each item — the same theory as `transact()` re-wrapping the transaction provider.
38
- * - The per-item writes run concurrently (`awaitValues()`), so a batch over a remote source costs one round-trip of latency, not one per item.
39
- * - The resolve and the writes are separate steps, so this is only atomic inside `transact()`.
40
- */
41
- setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
42
- /** Two-step: resolve the query to its matching items with `getQuery()`, then update each one concurrently with `updateItem()` — see `ThroughDBProvider.setQuery()`. */
43
- updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
44
- /** Two-step: resolve the query to its matching items with `getQuery()`, then delete each one concurrently with `deleteItem()` — see `ThroughDBProvider.setQuery()`. */
45
- deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
46
- getFirst<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<OptionalItem<II, TT>>;
47
- requireFirst<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<Item<II, TT>>;
48
35
  transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
49
36
  /** Clone this provider with different `source`. */
50
37
  cloneWith(source: DBProvider<I, T>): this;
@@ -1,15 +1,15 @@
1
- import { awaitValues } from "../../util/async.js";
2
1
  import { awaitDispose } from "../../util/dispose.js";
2
+ import { DBProvider } from "./DBProvider.js";
3
3
  /**
4
- * Database provider that passes every operation straight through to a wrapped `source` provider.
4
+ * Database provider that passes every core operation straight through to a wrapped `source` provider.
5
5
  *
6
6
  * - Base for the layered `Through*Provider` family (validation, caching, logging, change tracking); subclasses override individual methods to add behaviour and call `super` to delegate.
7
- * - Query writes (`setQuery()`, `updateQuery()`, `deleteQuery()`) are two-step by default — resolved to their matching items with `getQuery()`, then written per item through this provider's own item methods — so wrapper behaviour applies to every implied write. Subclasses that don't need per-item behaviour override them to pass through to `source` directly.
7
+ * - Only the core operations delegate to `source` — derived reads (`DBProvider.requireItem()`, `DBProvider.getFirst()`, `DBProvider.requireFirst()`) and two-step query writes are inherited from `DBProvider`, so they route through this provider's own overridden methods and wrapper behaviour applies to everything they do. Wrappers that don't need per-item behaviour override the query writes to pass through to `source` directly.
8
8
  * - Exposes `source` and implements `Sourceable`, so wrapped providers can be discovered with `getSource()` / `requireSource()`.
9
9
  *
10
10
  * @see https://shelving.cc/db/ThroughDBProvider
11
11
  */
12
- export class ThroughDBProvider {
12
+ export class ThroughDBProvider extends DBProvider {
13
13
  /**
14
14
  * The wrapped source provider that every operation is delegated to.
15
15
  *
@@ -17,14 +17,12 @@ export class ThroughDBProvider {
17
17
  */
18
18
  source;
19
19
  constructor(source) {
20
+ super();
20
21
  this.source = source;
21
22
  }
22
23
  getItem(collection, id) {
23
24
  return this.source.getItem(collection, id);
24
25
  }
25
- requireItem(collection, id) {
26
- return this.source.requireItem(collection, id);
27
- }
28
26
  getItemSequence(collection, id) {
29
27
  return this.source.getItemSequence(collection, id);
30
28
  }
@@ -40,6 +38,7 @@ export class ThroughDBProvider {
40
38
  deleteItem(collection, id) {
41
39
  return this.source.deleteItem(collection, id);
42
40
  }
41
+ /** Delegates to `source` so its native counting is kept (the base implementation would fetch the items and count them). */
43
42
  countQuery(collection, query) {
44
43
  return this.source.countQuery(collection, query);
45
44
  }
@@ -49,32 +48,6 @@ export class ThroughDBProvider {
49
48
  getQuerySequence(collection, query) {
50
49
  return this.source.getQuerySequence(collection, query);
51
50
  }
52
- /**
53
- * Two-step: resolve the query to its matching items with `getQuery()`, then set each one with `setItem()`.
54
- * - Routes every implied write through this provider's own item methods, so wrapper behaviour applies to each item — the same theory as `transact()` re-wrapping the transaction provider.
55
- * - The per-item writes run concurrently (`awaitValues()`), so a batch over a remote source costs one round-trip of latency, not one per item.
56
- * - The resolve and the writes are separate steps, so this is only atomic inside `transact()`.
57
- */
58
- async setQuery(collection, query, data) {
59
- const items = await this.getQuery(collection, query);
60
- await awaitValues(...items.map(({ id }) => this.setItem(collection, id, data)));
61
- }
62
- /** Two-step: resolve the query to its matching items with `getQuery()`, then update each one concurrently with `updateItem()` — see `ThroughDBProvider.setQuery()`. */
63
- async updateQuery(collection, query, updates) {
64
- const items = await this.getQuery(collection, query);
65
- await awaitValues(...items.map(({ id }) => this.updateItem(collection, id, updates)));
66
- }
67
- /** Two-step: resolve the query to its matching items with `getQuery()`, then delete each one concurrently with `deleteItem()` — see `ThroughDBProvider.setQuery()`. */
68
- async deleteQuery(collection, query) {
69
- const items = await this.getQuery(collection, query);
70
- await awaitValues(...items.map(({ id }) => this.deleteItem(collection, id)));
71
- }
72
- getFirst(collection, query) {
73
- return this.source.getFirst(collection, query);
74
- }
75
- requireFirst(collection, query) {
76
- return this.source.requireFirst(collection, query);
77
- }
78
51
  // Run the transaction against the wrapped `source` provider, keeping this provider's behaviour inside the transaction.
79
52
  transact(callback) {
80
53
  return this.source.transact(transaction => callback(this.cloneWith(transaction)));
@@ -85,6 +58,7 @@ export class ThroughDBProvider {
85
58
  }
86
59
  // Implement `AsyncDisposable`
87
60
  async [Symbol.asyncDispose]() {
88
- await awaitDispose(this.source);
61
+ await awaitDispose(this.source, // Dispose the source API provider.
62
+ super[Symbol.asyncDispose]());
89
63
  }
90
64
  }
@@ -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.279.0",
3
+ "version": "1.281.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
- }