shelving 1.276.1 → 1.278.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.
@@ -74,7 +74,7 @@ export declare class APICache<P, R> implements AsyncDisposable {
74
74
  * @example cache.refresh(getUser, { id: "abc" })
75
75
  * @see https://shelving.cc/api/APICache/refresh
76
76
  */
77
- refresh<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, maxAge?: number): void;
77
+ refresh<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, maxAge?: number): Promise<void>;
78
78
  /**
79
79
  * Trigger a refetch on all stores for an endpoint.
80
80
  *
@@ -83,6 +83,6 @@ export declare class APICache<P, R> implements AsyncDisposable {
83
83
  * @example cache.refreshAll(getUser)
84
84
  * @see https://shelving.cc/api/APICache/refreshAll
85
85
  */
86
- refreshAll<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, maxAge?: number): void;
86
+ refreshAll<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, maxAge?: number): Promise<void>;
87
87
  [Symbol.asyncDispose](): Promise<void>;
88
88
  }
@@ -78,8 +78,8 @@ export class APICache {
78
78
  * @example cache.refresh(getUser, { id: "abc" })
79
79
  * @see https://shelving.cc/api/APICache/refresh
80
80
  */
81
- refresh(endpoint, payload, maxAge) {
82
- this._get(endpoint)?.refresh(payload, maxAge);
81
+ async refresh(endpoint, payload, maxAge) {
82
+ await this._get(endpoint)?.refresh(payload, maxAge);
83
83
  }
84
84
  /**
85
85
  * Trigger a refetch on all stores for an endpoint.
@@ -89,8 +89,8 @@ export class APICache {
89
89
  * @example cache.refreshAll(getUser)
90
90
  * @see https://shelving.cc/api/APICache/refreshAll
91
91
  */
92
- refreshAll(endpoint, maxAge) {
93
- this._get(endpoint)?.refreshAll(maxAge);
92
+ async refreshAll(endpoint, maxAge) {
93
+ await this._get(endpoint)?.refreshAll(maxAge);
94
94
  }
95
95
  // Implement `AsyncDisposable`
96
96
  [Symbol.asyncDispose]() {
@@ -43,13 +43,13 @@ export declare class CachedAPIProvider<P, R> extends ThroughAPIProvider<P, R> im
43
43
  * @param payload The payload identifying the cached result.
44
44
  * @see https://shelving.cc/api/CachedAPIProvider/refresh
45
45
  */
46
- refresh<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP): void;
46
+ refresh<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP): Promise<void>;
47
47
  /**
48
48
  * Refresh every cached result for an endpoint, across all payloads.
49
49
  *
50
50
  * @param endpoint The endpoint whose cached results should be refreshed.
51
51
  * @see https://shelving.cc/api/CachedAPIProvider/refreshAll
52
52
  */
53
- refreshAll<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>): void;
53
+ refreshAll<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>): Promise<void>;
54
54
  [Symbol.asyncDispose](): Promise<void>;
55
55
  }
@@ -52,8 +52,8 @@ export class CachedAPIProvider extends ThroughAPIProvider {
52
52
  * @param payload The payload identifying the cached result.
53
53
  * @see https://shelving.cc/api/CachedAPIProvider/refresh
54
54
  */
55
- refresh(endpoint, payload) {
56
- this._cache.refresh(endpoint, payload, this.maxAge);
55
+ async refresh(endpoint, payload) {
56
+ await this._cache.refresh(endpoint, payload, this.maxAge);
57
57
  }
58
58
  /**
59
59
  * Refresh every cached result for an endpoint, across all payloads.
@@ -61,8 +61,8 @@ export class CachedAPIProvider extends ThroughAPIProvider {
61
61
  * @param endpoint The endpoint whose cached results should be refreshed.
62
62
  * @see https://shelving.cc/api/CachedAPIProvider/refreshAll
63
63
  */
64
- refreshAll(endpoint) {
65
- this._cache.refreshAll(endpoint, this.maxAge);
64
+ async refreshAll(endpoint) {
65
+ await this._cache.refreshAll(endpoint, this.maxAge);
66
66
  }
67
67
  // Implement `AsyncDisposable`
68
68
  async [Symbol.asyncDispose]() {
@@ -1,7 +1,6 @@
1
1
  import type { MutableArray } from "../../util/array.js";
2
2
  import type { Data } from "../../util/data.js";
3
3
  import type { Identifier, Item } from "../../util/item.js";
4
- import type { Query } from "../../util/query.js";
5
4
  import type { Updates } from "../../util/update.js";
6
5
  import type { Collection } from "../collection/Collection.js";
7
6
  import type { DBProvider } from "./DBProvider.js";
@@ -9,15 +8,14 @@ import { ThroughDBProvider } from "./ThroughDBProvider.js";
9
8
  /**
10
9
  * Structured log entry recording a single database write performed through a `ChangesDBProvider`.
11
10
  *
12
- * - `action` is the kind of write; `collection` is the collection name; `id`, `query`, `data`, and `updates` carry whichever fields apply to that write.
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.
13
12
  *
14
13
  * @see https://shelving.cc/db/DBChange
15
14
  */
16
- export type DBChange<I extends Identifier> = {
15
+ export type DBChange<I extends Identifier, T extends Data = Data> = {
17
16
  readonly action: "add" | "set" | "update" | "delete";
18
- readonly collection: string;
19
- readonly id?: I | undefined;
20
- readonly query?: unknown;
17
+ readonly collection: Collection<string, I, T>;
18
+ readonly id: I;
21
19
  readonly data?: unknown;
22
20
  readonly updates?: unknown;
23
21
  };
@@ -25,6 +23,8 @@ export type DBChange<I extends Identifier> = {
25
23
  * Database provider that records every write it performs to its `changes` log.
26
24
  *
27
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
28
  * - Useful for building audit logging, change feeds, or assertions in tests; reads are passed straight through and not logged.
29
29
  *
30
30
  * @see https://shelving.cc/db/ChangesDBProvider
@@ -35,8 +35,8 @@ export declare class ChangesDBProvider<I extends Identifier, T extends Data> ext
35
35
  *
36
36
  * @see https://shelving.cc/db/ChangesDBProvider/changes
37
37
  */
38
- get changes(): ReadonlyArray<DBChange<I>>;
39
- readonly _changes: MutableArray<DBChange<I>>;
38
+ get changes(): ReadonlyArray<DBChange<I, T>>;
39
+ readonly _changes: MutableArray<DBChange<I, T>>;
40
40
  /** Log an `"add"` change after writing. */
41
41
  addItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, data: TT): Promise<II>;
42
42
  /** Log a `"set"` change after writing. */
@@ -45,12 +45,18 @@ export declare class ChangesDBProvider<I extends Identifier, T extends Data> ext
45
45
  updateItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, updates: Updates<Item<II, TT>>): Promise<void>;
46
46
  /** Log a `"delete"` change after writing. */
47
47
  deleteItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
48
- /** Log a `"set"` change after writing. */
49
- setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
50
- /** Log an `"update"` change after writing. */
51
- updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
52
- /** Log a `"delete"` change after writing. */
53
- deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): 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>;
54
60
  cloneWith(source: DBProvider<I, T>): this;
55
61
  transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
56
62
  }
@@ -3,6 +3,8 @@ import { ThroughDBProvider } from "./ThroughDBProvider.js";
3
3
  * Database provider that records every write it performs to its `changes` log.
4
4
  *
5
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()`.
6
8
  * - Useful for building audit logging, change feeds, or assertions in tests; reads are passed straight through and not logged.
7
9
  *
8
10
  * @see https://shelving.cc/db/ChangesDBProvider
@@ -20,38 +22,45 @@ export class ChangesDBProvider extends ThroughDBProvider {
20
22
  /** Log an `"add"` change after writing. */
21
23
  async addItem(collection, data) {
22
24
  const id = await super.addItem(collection, data);
23
- this._changes.push({ action: "add", collection: collection.name, id, data });
25
+ this._changes.push({ action: "add", collection, id, data });
24
26
  return id;
25
27
  }
26
28
  /** Log a `"set"` change after writing. */
27
29
  async setItem(collection, id, data) {
28
30
  await super.setItem(collection, id, data);
29
- this._changes.push({ action: "set", collection: collection.name, id, data });
31
+ this._changes.push({ action: "set", collection, id, data });
30
32
  }
31
33
  /** Log an `"update"` change after writing. */
32
34
  async updateItem(collection, id, updates) {
33
35
  await super.updateItem(collection, id, updates);
34
- this._changes.push({ action: "update", collection: collection.name, id, updates });
36
+ this._changes.push({ action: "update", collection, id, updates });
35
37
  }
36
38
  /** Log a `"delete"` change after writing. */
37
39
  async deleteItem(collection, id) {
38
40
  await super.deleteItem(collection, id);
39
- this._changes.push({ action: "delete", collection: collection.name, id });
41
+ this._changes.push({ action: "delete", collection, id });
40
42
  }
41
- /** Log a `"set"` change after writing. */
42
- async setQuery(collection, query, data) {
43
- await super.setQuery(collection, query, data);
44
- this._changes.push({ action: "set", collection: collection.name, query, data });
45
- }
46
- /** Log an `"update"` change after writing. */
47
- async updateQuery(collection, query, updates) {
48
- await super.updateQuery(collection, query, updates);
49
- this._changes.push({ action: "update", collection: collection.name, query, updates });
50
- }
51
- /** Log a `"delete"` change after writing. */
52
- async deleteQuery(collection, query) {
53
- await super.deleteQuery(collection, query);
54
- this._changes.push({ action: "delete", collection: collection.name, query });
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
+ }
55
64
  }
56
65
  // Override so that transaction copies get their own log.
57
66
  cloneWith(source) {
@@ -119,6 +119,7 @@ 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
123
  *
123
124
  * @param collection Collection to write to.
124
125
  * @param query Query selecting the items to set.
@@ -129,6 +130,7 @@ export declare abstract class DBProvider<I extends Identifier = Identifier, T ex
129
130
  abstract setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
130
131
  /**
131
132
  * 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.
132
134
  *
133
135
  * @param collection Collection to write to.
134
136
  * @param query Query selecting the items to update.
@@ -139,6 +141,7 @@ export declare abstract class DBProvider<I extends Identifier = Identifier, T ex
139
141
  abstract updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
140
142
  /**
141
143
  * 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.
142
145
  *
143
146
  * @param collection Collection to delete from.
144
147
  * @param query Query selecting the items to delete.
@@ -174,7 +177,7 @@ export declare abstract class DBProvider<I extends Identifier = Identifier, T ex
174
177
  * - Reads see a consistent snapshot of the data from before the transaction, and do not see the transaction's own uncommitted writes.
175
178
  * - If the callback throws, nothing is committed and the error is rethrown.
176
179
  * - The callback may run more than once if the backend retries on contention, so it must have no side effects other than through its provider.
177
- * - Inside a transaction, realtime sequences and nested `transact()` calls throw `UnsupportedError`.
180
+ * - Portable code must not use realtime sequences or nested `transact()` calls inside a transaction — most backends throw `UnsupportedError`, though some (e.g. `MemoryDBProvider`) support them scoped to the transaction.
178
181
  * - Not every provider supports transactions — the base implementation throws `UnsupportedError`.
179
182
  *
180
183
  * @param callback Function that performs the transaction's reads and writes through the provider it receives.
@@ -89,7 +89,7 @@ export class DBProvider {
89
89
  * - Reads see a consistent snapshot of the data from before the transaction, and do not see the transaction's own uncommitted writes.
90
90
  * - If the callback throws, nothing is committed and the error is rethrown.
91
91
  * - The callback may run more than once if the backend retries on contention, so it must have no side effects other than through its provider.
92
- * - Inside a transaction, realtime sequences and nested `transact()` calls throw `UnsupportedError`.
92
+ * - Portable code must not use realtime sequences or nested `transact()` calls inside a transaction — most backends throw `UnsupportedError`, though some (e.g. `MemoryDBProvider`) support them scoped to the transaction.
93
93
  * - Not every provider supports transactions — the base implementation throws `UnsupportedError`.
94
94
  *
95
95
  * @param callback Function that performs the transaction's reads and writes through the provider it receives.
@@ -27,8 +27,11 @@ export declare class DebugDBProvider<I extends Identifier, T extends Data> exten
27
27
  countQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<number>;
28
28
  getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
29
29
  getQuerySequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
30
+ /** Passthrough, not two-step: log the query write the caller made, not the per-item writes it implies. */
30
31
  setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
32
+ /** Passthrough, not two-step: log the query write the caller made, not the per-item writes it implies. */
31
33
  updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
34
+ /** Passthrough, not two-step: log the query write the caller made, not the per-item writes it implies. */
32
35
  deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
33
36
  /** Adds TRANSACT logging around the base behaviour, which re-wraps so operations inside the transaction are logged too. */
34
37
  transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
@@ -120,10 +120,11 @@ export class DebugDBProvider extends ThroughDBProvider {
120
120
  console.error(`${ANSI_FAILURE} SEQUENCE QUERY`, collection.name, query, thrown);
121
121
  }
122
122
  }
123
+ /** Passthrough, not two-step: log the query write the caller made, not the per-item writes it implies. */
123
124
  async setQuery(collection, query, data) {
124
125
  try {
125
126
  console.debug(`${ANSI_RIGHT} SET QUERY`, collection.name, query, data);
126
- await super.setQuery(collection, query, data);
127
+ await this.source.setQuery(collection, query, data);
127
128
  console.debug(`${ANSI_SUCCESS} SET QUERY`, collection.name, query, data);
128
129
  }
129
130
  catch (reason) {
@@ -131,10 +132,11 @@ export class DebugDBProvider extends ThroughDBProvider {
131
132
  throw reason;
132
133
  }
133
134
  }
135
+ /** Passthrough, not two-step: log the query write the caller made, not the per-item writes it implies. */
134
136
  async updateQuery(collection, query, updates) {
135
137
  try {
136
138
  console.debug(`${ANSI_RIGHT} UPDATE QUERY`, collection.name, query, updates);
137
- await super.updateQuery(collection, query, updates);
139
+ await this.source.updateQuery(collection, query, updates);
138
140
  console.debug(`${ANSI_SUCCESS} UPDATE QUERY`, collection.name, query, updates);
139
141
  }
140
142
  catch (reason) {
@@ -142,10 +144,11 @@ export class DebugDBProvider extends ThroughDBProvider {
142
144
  throw reason;
143
145
  }
144
146
  }
147
+ /** Passthrough, not two-step: log the query write the caller made, not the per-item writes it implies. */
145
148
  async deleteQuery(collection, query) {
146
149
  try {
147
150
  console.debug(`${ANSI_RIGHT} DELETE QUERY`, collection.name, query);
148
- await super.deleteQuery(collection, query);
151
+ await this.source.deleteQuery(collection, query);
149
152
  console.debug(`${ANSI_SUCCESS} DELETE QUERY`, collection.name, query);
150
153
  }
151
154
  catch (reason) {
@@ -11,6 +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
15
  *
15
16
  * @see https://shelving.cc/db/MemoryDBProvider
16
17
  */
@@ -57,13 +58,32 @@ export declare class MemoryDBProvider<I extends Identifier = Identifier, T exten
57
58
  * @see https://shelving.cc/db/MemoryDBProvider/setItems
58
59
  */
59
60
  setItems<II extends I, TT extends T>(collection: Collection<string, II, TT>, items: Items<II, TT>): void;
61
+ /**
62
+ * Clone this provider into a new plain `MemoryDBProvider` containing the same items.
63
+ *
64
+ * - Shallow: new tables and maps sharing the same (immutable) item instances, so cloning is cheap and unchanged items keep their identity.
65
+ * - The clone is always a plain `MemoryDBProvider` with plain `MemoryTable`s, even when called on a subclass — writes to the clone only touch its own memory, never a subclass's backing store (e.g. `StorageDBProvider` persistence).
66
+ *
67
+ * @example provider.clone() // MemoryDBProvider
68
+ * @see https://shelving.cc/db/MemoryDBProvider/clone
69
+ */
70
+ clone(): MemoryDBProvider<I, T>;
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.
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
+ * - 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).
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
+ */
79
+ transact<X>(callback: (provider: DBProvider<I, T>) => Promise<X>): Promise<X>;
60
80
  [Symbol.asyncDispose](): Promise<void>;
61
81
  }
62
82
  /**
63
83
  * In-memory table holding the items of a single collection for a `MemoryDBProvider`.
64
84
  *
65
85
  * - Keys items by id in a `Map`, preserving the exact object instance passed in.
66
- * - Exposes a `next` `DeferredSequence` that resolves on every change, powering the live `*Sequence` subscriptions.
86
+ * - An internal `DeferredSequence` resolves on every change, powering the live `*Sequence` subscriptions.
67
87
  *
68
88
  * @example
69
89
  * const table = provider.getTable(users);
@@ -74,19 +94,16 @@ export declare class MemoryDBProvider<I extends Identifier = Identifier, T exten
74
94
  export declare class MemoryTable<I extends Identifier, T extends Data> implements AsyncDisposable {
75
95
  /** Actual data in this table. */
76
96
  protected readonly _data: Map<I, Item<I, T>>;
77
- /**
78
- * Deferred sequence that resolves on every change to this table.
79
- *
80
- * @see https://shelving.cc/db/MemoryTable/next
81
- */
82
- readonly next: DeferredSequence<void, void, void>;
97
+ /** Deferred sequence that resolves on every change to this table — `false` for a change, or `true` once when the table is disposed and its sequences should end. */
98
+ protected readonly _next: DeferredSequence<boolean, void, void>;
83
99
  /**
84
100
  * Collection this table stores the items of.
85
101
  *
86
102
  * @see https://shelving.cc/db/MemoryTable/collection
87
103
  */
88
104
  readonly collection: Collection<string, I, T>;
89
- constructor(collection: Collection<string, I, T>);
105
+ /** @param items Optional initial `[id, item]` entries to seed the table with. */
106
+ constructor(collection: Collection<string, I, T>, items?: Iterable<readonly [I, Item<I, T>]>);
90
107
  /**
91
108
  * Get an item by its id, or `undefined` if it doesn't exist.
92
109
  *
@@ -215,5 +232,15 @@ export declare class MemoryTable<I extends Identifier, T extends Data> implement
215
232
  deleteQuery(query: Query<Item<I, T>>): void;
216
233
  setItems(items: Items<I, T>): void;
217
234
  setItemsSequence(sequence: AsyncIterable<Items<I, T>>): AsyncIterable<Items<I, T>>;
235
+ /**
236
+ * Clone this table into a new plain `MemoryTable` containing the same items.
237
+ *
238
+ * - Shallow: a new map sharing the same (immutable) item instances, so cloning is cheap.
239
+ * - The clone is always a plain `MemoryTable`, even when called on a subclass — writes to the clone have no side effects (e.g. `StorageTable` persistence).
240
+ *
241
+ * @example table.clone() // MemoryTable
242
+ * @see https://shelving.cc/db/MemoryTable/clone
243
+ */
244
+ clone(): MemoryTable<I, T>;
218
245
  [Symbol.asyncDispose](): Promise<void>;
219
246
  }
@@ -8,6 +8,7 @@ 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";
11
12
  import { DBProvider } from "./DBProvider.js";
12
13
  /**
13
14
  * Synchronous in-memory database provider, storing each collection in a `MemoryTable`.
@@ -15,6 +16,7 @@ import { DBProvider } from "./DBProvider.js";
15
16
  * - Extremely fast (ideal as the cache behind `CacheDBProvider`!), but does not persist data after the process or browser window closes.
16
17
  * - Identity-preserving: `getItem()` etc. return the exact same object instance that was passed into `setItem()`.
17
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.
18
20
  *
19
21
  * @see https://shelving.cc/db/MemoryDBProvider
20
22
  */
@@ -89,6 +91,42 @@ export class MemoryDBProvider extends DBProvider {
89
91
  setItems(collection, items) {
90
92
  this.getTable(collection).setItems(items);
91
93
  }
94
+ /**
95
+ * Clone this provider into a new plain `MemoryDBProvider` containing the same items.
96
+ *
97
+ * - Shallow: new tables and maps sharing the same (immutable) item instances, so cloning is cheap and unchanged items keep their identity.
98
+ * - The clone is always a plain `MemoryDBProvider` with plain `MemoryTable`s, even when called on a subclass — writes to the clone only touch its own memory, never a subclass's backing store (e.g. `StorageDBProvider` persistence).
99
+ *
100
+ * @example provider.clone() // MemoryDBProvider
101
+ * @see https://shelving.cc/db/MemoryDBProvider/clone
102
+ */
103
+ clone() {
104
+ const clone = new MemoryDBProvider();
105
+ for (const [name, table] of Object.entries(this._tables))
106
+ if (table)
107
+ clone._tables[name] = table.clone();
108
+ return clone;
109
+ }
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.
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
+ * - 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).
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
+ */
118
+ async transact(callback) {
119
+ const clone = this.clone();
120
+ try {
121
+ const transaction = new ChangesDBProvider(clone);
122
+ const result = await callback(transaction);
123
+ await transaction.replay(this); // Commit the captured changes.
124
+ return result;
125
+ }
126
+ finally {
127
+ await clone[Symbol.asyncDispose](); // End any sequences opened inside the callback.
128
+ }
129
+ }
92
130
  // Implement `AsyncDisposable`
93
131
  async [Symbol.asyncDispose]() {
94
132
  await awaitDispose(...Object.values(this._tables), // Dispose all tables.
@@ -99,7 +137,7 @@ export class MemoryDBProvider extends DBProvider {
99
137
  * In-memory table holding the items of a single collection for a `MemoryDBProvider`.
100
138
  *
101
139
  * - Keys items by id in a `Map`, preserving the exact object instance passed in.
102
- * - Exposes a `next` `DeferredSequence` that resolves on every change, powering the live `*Sequence` subscriptions.
140
+ * - An internal `DeferredSequence` resolves on every change, powering the live `*Sequence` subscriptions.
103
141
  *
104
142
  * @example
105
143
  * const table = provider.getTable(users);
@@ -109,21 +147,19 @@ export class MemoryDBProvider extends DBProvider {
109
147
  */
110
148
  export class MemoryTable {
111
149
  /** Actual data in this table. */
112
- _data = new Map();
113
- /**
114
- * Deferred sequence that resolves on every change to this table.
115
- *
116
- * @see https://shelving.cc/db/MemoryTable/next
117
- */
118
- next = new DeferredSequence();
150
+ _data;
151
+ /** Deferred sequence that resolves on every change to this table — `false` for a change, or `true` once when the table is disposed and its sequences should end. */
152
+ _next = new DeferredSequence();
119
153
  /**
120
154
  * Collection this table stores the items of.
121
155
  *
122
156
  * @see https://shelving.cc/db/MemoryTable/collection
123
157
  */
124
158
  collection;
125
- constructor(collection) {
159
+ /** @param items Optional initial `[id, item]` entries to seed the table with. */
160
+ constructor(collection, items) {
126
161
  this.collection = collection;
162
+ this._data = new Map(items);
127
163
  }
128
164
  /**
129
165
  * Get an item by its id, or `undefined` if it doesn't exist.
@@ -150,7 +186,9 @@ export class MemoryTable {
150
186
  let lastValue = this.getItem(id);
151
187
  yield lastValue;
152
188
  while (true) {
153
- await this.next;
189
+ const done = await this._next;
190
+ if (done)
191
+ return;
154
192
  const nextValue = this.getItem(id);
155
193
  if (nextValue !== lastValue) {
156
194
  yield nextValue;
@@ -199,7 +237,7 @@ export class MemoryTable {
199
237
  const item = getItem(id, data);
200
238
  if (this._data.get(id) !== item) {
201
239
  this._data.set(id, item);
202
- this.next.resolve();
240
+ this._next.resolve(false);
203
241
  }
204
242
  }
205
243
  /**
@@ -244,7 +282,7 @@ export class MemoryTable {
244
282
  deleteItem(id) {
245
283
  if (this._data.has(id)) {
246
284
  this._data.delete(id);
247
- this.next.resolve();
285
+ this._next.resolve(false);
248
286
  }
249
287
  }
250
288
  /**
@@ -283,7 +321,9 @@ export class MemoryTable {
283
321
  let lastItems = this.getQuery(query);
284
322
  yield lastItems;
285
323
  while (true) {
286
- await this.next;
324
+ const done = await this._next;
325
+ if (done)
326
+ return;
287
327
  const nextItems = this.getQuery(query);
288
328
  if (!isArrayEqual(lastItems, nextItems)) {
289
329
  yield nextItems;
@@ -329,10 +369,20 @@ export class MemoryTable {
329
369
  yield items;
330
370
  }
331
371
  }
372
+ /**
373
+ * Clone this table into a new plain `MemoryTable` containing the same items.
374
+ *
375
+ * - Shallow: a new map sharing the same (immutable) item instances, so cloning is cheap.
376
+ * - The clone is always a plain `MemoryTable`, even when called on a subclass — writes to the clone have no side effects (e.g. `StorageTable` persistence).
377
+ *
378
+ * @example table.clone() // MemoryTable
379
+ * @see https://shelving.cc/db/MemoryTable/clone
380
+ */
381
+ clone() {
382
+ return new MemoryTable(this.collection, this._data);
383
+ }
332
384
  // Implement `AsyncDisposable`
333
385
  async [Symbol.asyncDispose]() {
334
- await awaitDispose(
335
- // Empty by default.
336
- );
386
+ await awaitDispose(() => this._next.resolve(true));
337
387
  }
338
388
  }
@@ -9,6 +9,7 @@ import type { DBProvider } from "./DBProvider.js";
9
9
  * Database provider that passes every 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
13
  * - Exposes `source` and implements `Sourceable`, so wrapped providers can be discovered with `getSource()` / `requireSource()`.
13
14
  *
14
15
  * @see https://shelving.cc/db/ThroughDBProvider
@@ -31,8 +32,16 @@ export declare class ThroughDBProvider<I extends Identifier, T extends Data> imp
31
32
  countQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<number>;
32
33
  getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
33
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
+ */
34
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()`. */
35
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()`. */
36
45
  deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
37
46
  getFirst<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<OptionalItem<II, TT>>;
38
47
  requireFirst<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<Item<II, TT>>;
@@ -1,8 +1,10 @@
1
+ import { awaitValues } from "../../util/async.js";
1
2
  import { awaitDispose } from "../../util/dispose.js";
2
3
  /**
3
4
  * Database provider that passes every operation straight through to a wrapped `source` provider.
4
5
  *
5
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.
6
8
  * - Exposes `source` and implements `Sourceable`, so wrapped providers can be discovered with `getSource()` / `requireSource()`.
7
9
  *
8
10
  * @see https://shelving.cc/db/ThroughDBProvider
@@ -47,14 +49,25 @@ export class ThroughDBProvider {
47
49
  getQuerySequence(collection, query) {
48
50
  return this.source.getQuerySequence(collection, query);
49
51
  }
50
- setQuery(collection, query, data) {
51
- return this.source.setQuery(collection, query, data);
52
- }
53
- updateQuery(collection, query, updates) {
54
- return this.source.updateQuery(collection, query, updates);
55
- }
56
- deleteQuery(collection, query) {
57
- return this.source.deleteQuery(collection, query);
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)));
58
71
  }
59
72
  getFirst(collection, query) {
60
73
  return this.source.getFirst(collection, query);
@@ -30,9 +30,16 @@ export declare class ValidationDBProvider<I extends Identifier, T extends Data>
30
30
  getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
31
31
  /** Validate each emitted result; throws `ValueError` if any items fail the collection schema. */
32
32
  getQuerySequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
33
- /** Validate the data before writing; throws `ValueError` if it fails the collection schema. */
33
+ /**
34
+ * Validate the data before writing; throws `ValueError` if it fails the collection schema.
35
+ * - Passthrough, not two-step: validation applies to the query write's own inputs up front, so per-item routing adds nothing — pass through to keep the source's native query write.
36
+ */
34
37
  setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
35
- /** Validate the updates before writing; throws `ValueError` if they fail the collection schema. */
38
+ /**
39
+ * Validate the updates before writing; throws `ValueError` if they fail the collection schema.
40
+ * - Passthrough, not two-step: validation applies to the query write's own inputs up front, so per-item routing adds nothing — pass through to keep the source's native query write.
41
+ */
36
42
  updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
43
+ /** Passthrough, not two-step: there are no inputs to validate per item, so keep the source's native query write. */
37
44
  deleteQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): Promise<void>;
38
45
  }
@@ -46,16 +46,23 @@ export class ValidationDBProvider extends ThroughDBProvider {
46
46
  for await (const items of super.getQuerySequence(collection, query))
47
47
  yield _validateItems(collection, items, this.getQuerySequence);
48
48
  }
49
- /** Validate the data before writing; throws `ValueError` if it fails the collection schema. */
49
+ /**
50
+ * Validate the data before writing; throws `ValueError` if it fails the collection schema.
51
+ * - Passthrough, not two-step: validation applies to the query write's own inputs up front, so per-item routing adds nothing — pass through to keep the source's native query write.
52
+ */
50
53
  setQuery(collection, query, data) {
51
- return super.setQuery(collection, query, collection.validate(data));
54
+ return this.source.setQuery(collection, query, collection.validate(data));
52
55
  }
53
- /** Validate the updates before writing; throws `ValueError` if they fail the collection schema. */
56
+ /**
57
+ * Validate the updates before writing; throws `ValueError` if they fail the collection schema.
58
+ * - Passthrough, not two-step: validation applies to the query write's own inputs up front, so per-item routing adds nothing — pass through to keep the source's native query write.
59
+ */
54
60
  updateQuery(collection, query, updates) {
55
- return super.updateQuery(collection, query, _validateUpdates(collection, updates, this.updateQuery));
61
+ return this.source.updateQuery(collection, query, _validateUpdates(collection, updates, this.updateQuery));
56
62
  }
63
+ /** Passthrough, not two-step: there are no inputs to validate per item, so keep the source's native query write. */
57
64
  deleteQuery(collection, query) {
58
- return super.deleteQuery(collection, query);
65
+ return this.source.deleteQuery(collection, query);
59
66
  }
60
67
  }
61
68
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.276.1",
3
+ "version": "1.278.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,14 +9,14 @@
9
9
  "main": "./index.js",
10
10
  "module": "./index.js",
11
11
  "devDependencies": {
12
- "@biomejs/biome": "^2.5.4",
12
+ "@biomejs/biome": "^2.5.7",
13
13
  "@heroicons/react": "^2.2.0",
14
14
  "@types/bun": "^1.3.14",
15
- "@types/react": "^19.2.17",
16
- "@types/react-dom": "^19.2.3",
15
+ "@types/react": "^19.2.18",
16
+ "@types/react-dom": "^19.2.4",
17
17
  "react": "^19.3.0-canary-fef12a01-20260413",
18
18
  "react-dom": "^19.3.0-canary-fef12a01-20260413",
19
- "stylelint": "^17.14.0",
19
+ "stylelint": "^17.14.1",
20
20
  "stylelint-config-standard": "^40.0.0",
21
21
  "typescript": "^7.0.2"
22
22
  },
@@ -2,10 +2,12 @@ import type { DBProvider } from "../db/provider/DBProvider.js";
2
2
  import type { Data } from "../util/data.js";
3
3
  /** Options for `testDBProvider()`, declaring the capabilities of the provider under test. */
4
4
  export interface TestDBProviderOptions {
5
- /** Whether the provider supports realtime sequences — when `false`, sequences are asserted to throw `UnsupportedError`. @default true */
5
+ /** Whether the provider supports realtime sequences — when `false`, sequences are asserted to throw `UnsupportedError` (including inside `transact()`); when `true` combined with `transactions`, sequences inside a transaction are asserted to observe the transaction and end with it. @default true */
6
6
  readonly realtime?: boolean;
7
7
  /** Whether the provider supports `transact()` — when `false`, it is asserted to throw `UnsupportedError`. @default false */
8
8
  readonly transactions?: boolean;
9
+ /** Whether `transact()` can be nested, committing the inner transaction into the outer — when `false`, nested calls are asserted to throw `UnsupportedError`. @default false */
10
+ readonly nestedTransactions?: boolean;
9
11
  }
10
12
  /**
11
13
  * Register the universal `DBProvider` contract test suite against a provider, so every backend proves the same behaviour.
@@ -19,4 +21,4 @@ export interface TestDBProviderOptions {
19
21
  * @example testDBProvider("MemoryDBProvider", () => new MemoryDBProvider<string>());
20
22
  * @see https://shelving.cc/test/testDBProvider
21
23
  */
22
- export declare function testDBProvider(name: string, createProvider: () => DBProvider<string, Data> | PromiseLike<DBProvider<string, Data>>, { realtime, transactions }?: TestDBProviderOptions): void;
24
+ export declare function testDBProvider(name: string, createProvider: () => DBProvider<string, Data> | PromiseLike<DBProvider<string, Data>>, { realtime, transactions, nestedTransactions }?: TestDBProviderOptions): void;
@@ -18,7 +18,7 @@ import { expectOrderedItems, expectUnorderedItems } from "./util.js";
18
18
  * @example testDBProvider("MemoryDBProvider", () => new MemoryDBProvider<string>());
19
19
  * @see https://shelving.cc/test/testDBProvider
20
20
  */
21
- export function testDBProvider(name, createProvider, { realtime = true, transactions = false } = {}) {
21
+ export function testDBProvider(name, createProvider, { realtime = true, transactions = false, nestedTransactions = false } = {}) {
22
22
  // Create the provider and wipe both fixture collections so each test starts clean.
23
23
  async function init() {
24
24
  const provider = await createProvider();
@@ -204,6 +204,7 @@ export function testDBProvider(name, createProvider, { realtime = true, transact
204
204
  if (transactions) {
205
205
  test("transact(): commits reads and writes atomically", async () => {
206
206
  const db = await init();
207
+ expect(await db.transact(async () => 123)).toBe(123); // The callback's value is returned.
207
208
  await db.setItem(BASICS_COLLECTION, "basic1", basic1);
208
209
  await db.setItem(BASICS_COLLECTION, "basic2", basic2);
209
210
  const id = await db.transact(async (tx) => {
@@ -252,15 +253,58 @@ export function testDBProvider(name, createProvider, { realtime = true, transact
252
253
  expectUnorderedItems(await db.getQuery(BASICS_COLLECTION, { str: "TX" }), ["basic1", "basic2", "basic3"]);
253
254
  expect(await db.countQuery(BASICS_COLLECTION, {})).toBe(6);
254
255
  });
255
- test("transact(): sequences and nested transactions are unsupported inside a transaction", async () => {
256
- const db = await init();
257
- expect(await db.transact(async () => 123)).toBe(123);
258
- await db.transact(async (tx) => {
259
- expect(() => tx.getItemSequence(BASICS_COLLECTION, "basic1")).toThrow(UnsupportedError);
260
- expect(() => tx.getQuerySequence(BASICS_COLLECTION, {})).toThrow(UnsupportedError);
261
- expect(() => tx.transact(async () => undefined)).toThrow(UnsupportedError);
256
+ // Sequence support inside a transaction follows the `realtime` flag — a provider that supports both capabilities supports them together.
257
+ if (realtime) {
258
+ test("transact(): sequences inside a transaction observe the transaction and end with it", async () => {
259
+ const db = await init();
260
+ await db.setItem(BASICS_COLLECTION, "basic1", basic1);
261
+ const emissions = [];
262
+ let sequence;
263
+ await db.transact(async (tx) => {
264
+ sequence = (async () => {
265
+ for await (const item of tx.getItemSequence(BASICS_COLLECTION, "basic1"))
266
+ emissions.push(item);
267
+ })();
268
+ await runMicrotasks();
269
+ await tx.updateItem(BASICS_COLLECTION, "basic1", { str: "TX" });
270
+ await runMicrotasks();
271
+ });
272
+ await sequence; // The sequence ends when the transaction completes (this would hang otherwise).
273
+ expect(emissions[0]).toMatchObject(basic1);
274
+ expect(emissions[1]).toMatchObject({ ...basic1, str: "TX" });
275
+ expect(await db.getItem(BASICS_COLLECTION, "basic1")).toMatchObject({ ...basic1, str: "TX" });
262
276
  });
263
- });
277
+ }
278
+ else {
279
+ test("transact(): sequences are unsupported inside a transaction", async () => {
280
+ const db = await init();
281
+ await db.transact(async (tx) => {
282
+ expect(() => tx.getItemSequence(BASICS_COLLECTION, "basic1")).toThrow(UnsupportedError);
283
+ expect(() => tx.getQuerySequence(BASICS_COLLECTION, {})).toThrow(UnsupportedError);
284
+ });
285
+ });
286
+ }
287
+ if (nestedTransactions) {
288
+ test("transact(): nested transactions commit into the outer transaction", async () => {
289
+ const db = await init();
290
+ await db.transact(async (tx) => {
291
+ await tx.transact(async (nested) => {
292
+ await nested.setItem(BASICS_COLLECTION, "basic1", basic1);
293
+ });
294
+ expect(await tx.getItem(BASICS_COLLECTION, "basic1")).toMatchObject(basic1); // The inner commit is visible to the outer transaction…
295
+ expect(await db.getItem(BASICS_COLLECTION, "basic1")).toBe(undefined); // …but not yet committed to the provider.
296
+ });
297
+ expect(await db.getItem(BASICS_COLLECTION, "basic1")).toMatchObject(basic1);
298
+ });
299
+ }
300
+ else {
301
+ test("transact(): nested transactions are unsupported", async () => {
302
+ const db = await init();
303
+ await db.transact(async (tx) => {
304
+ expect(() => tx.transact(async () => undefined)).toThrow(UnsupportedError);
305
+ });
306
+ });
307
+ }
264
308
  }
265
309
  else {
266
310
  test("transactions are not supported", async () => {