shelving 1.277.0 → 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.
- package/db/provider/ChangesDBProvider.d.ts +1 -1
- package/db/provider/ChangesDBProvider.js +1 -1
- package/db/provider/DBProvider.d.ts +1 -1
- package/db/provider/DBProvider.js +1 -1
- package/db/provider/MemoryDBProvider.d.ts +35 -8
- package/db/provider/MemoryDBProvider.js +66 -16
- package/package.json +1 -1
- package/test/testDBProvider.d.ts +4 -2
- package/test/testDBProvider.js +53 -9
|
@@ -48,7 +48,7 @@ export declare class ChangesDBProvider<I extends Identifier, T extends Data> ext
|
|
|
48
48
|
/**
|
|
49
49
|
* Replay the `changes` log onto another provider, re-issuing each write in order.
|
|
50
50
|
*
|
|
51
|
-
* - Useful for audit replay or syncing a secondary store.
|
|
51
|
+
* - Useful for audit replay or syncing a secondary store — and the commit mechanism for `MemoryDBProvider.transact()`.
|
|
52
52
|
* - `"add"` changes replay as `DBProvider.setItem()` with the logged id, so the target keeps the same generated ids.
|
|
53
53
|
* - The changes replay as a sequence of awaited writes — the replay itself is not atomic.
|
|
54
54
|
*
|
|
@@ -43,7 +43,7 @@ export class ChangesDBProvider extends ThroughDBProvider {
|
|
|
43
43
|
/**
|
|
44
44
|
* Replay the `changes` log onto another provider, re-issuing each write in order.
|
|
45
45
|
*
|
|
46
|
-
* - Useful for audit replay or syncing a secondary store.
|
|
46
|
+
* - Useful for audit replay or syncing a secondary store — and the commit mechanism for `MemoryDBProvider.transact()`.
|
|
47
47
|
* - `"add"` changes replay as `DBProvider.setItem()` with the logged id, so the target keeps the same generated ids.
|
|
48
48
|
* - The changes replay as a sequence of awaited writes — the replay itself is not atomic.
|
|
49
49
|
*
|
|
@@ -177,7 +177,7 @@ export declare abstract class DBProvider<I extends Identifier = Identifier, T ex
|
|
|
177
177
|
* - Reads see a consistent snapshot of the data from before the transaction, and do not see the transaction's own uncommitted writes.
|
|
178
178
|
* - If the callback throws, nothing is committed and the error is rethrown.
|
|
179
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.
|
|
180
|
-
* -
|
|
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.
|
|
181
181
|
* - Not every provider supports transactions — the base implementation throws `UnsupportedError`.
|
|
182
182
|
*
|
|
183
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
|
-
* -
|
|
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.
|
|
@@ -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
|
-
* -
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
* -
|
|
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
|
|
113
|
-
/**
|
|
114
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
}
|
package/package.json
CHANGED
package/test/testDBProvider.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/test/testDBProvider.js
CHANGED
|
@@ -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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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 () => {
|