@earendil-works/pi-durable 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +147 -11
- package/dist/codemode/codemode.d.ts +173 -0
- package/dist/codemode/codemode.d.ts.map +1 -0
- package/dist/codemode/codemode.js +886 -0
- package/dist/codemode/codemode.js.map +1 -0
- package/dist/codemode/describe.d.ts +38 -0
- package/dist/codemode/describe.d.ts.map +1 -0
- package/dist/codemode/describe.js +119 -0
- package/dist/codemode/describe.js.map +1 -0
- package/dist/codemode/index.d.ts +3 -0
- package/dist/codemode/index.d.ts.map +1 -0
- package/dist/codemode/index.js +3 -0
- package/dist/codemode/index.js.map +1 -0
- package/dist/codemode/models.d.ts +28 -0
- package/dist/codemode/models.d.ts.map +1 -0
- package/dist/codemode/models.js +231 -0
- package/dist/codemode/models.js.map +1 -0
- package/dist/codemode/output.d.ts +41 -0
- package/dist/codemode/output.d.ts.map +1 -0
- package/dist/codemode/output.js +167 -0
- package/dist/codemode/output.js.map +1 -0
- package/dist/env/index.d.ts +42 -0
- package/dist/env/index.d.ts.map +1 -1
- package/dist/env/index.js.map +1 -1
- package/dist/errors.d.ts +20 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +34 -5
- package/dist/errors.js.map +1 -1
- package/dist/harness/agent.d.ts +5 -2
- package/dist/harness/agent.d.ts.map +1 -1
- package/dist/harness/agent.js +69 -25
- package/dist/harness/agent.js.map +1 -1
- package/dist/harness/compaction.d.ts +6 -1
- package/dist/harness/compaction.d.ts.map +1 -1
- package/dist/harness/compaction.js +10 -7
- package/dist/harness/compaction.js.map +1 -1
- package/dist/harness/events.d.ts +35 -15
- package/dist/harness/events.d.ts.map +1 -1
- package/dist/harness/events.js +77 -23
- package/dist/harness/events.js.map +1 -1
- package/dist/harness/generation.d.ts +2 -0
- package/dist/harness/generation.d.ts.map +1 -1
- package/dist/harness/generation.js +31 -8
- package/dist/harness/generation.js.map +1 -1
- package/dist/harness/harness.d.ts +7 -0
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +53 -26
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/inbox.d.ts +16 -4
- package/dist/harness/inbox.d.ts.map +1 -1
- package/dist/harness/inbox.js +26 -3
- package/dist/harness/inbox.js.map +1 -1
- package/dist/harness/live.d.ts +74 -17
- package/dist/harness/live.d.ts.map +1 -1
- package/dist/harness/live.js +66 -14
- package/dist/harness/live.js.map +1 -1
- package/dist/harness/scheduler.d.ts +18 -5
- package/dist/harness/scheduler.d.ts.map +1 -1
- package/dist/harness/scheduler.js +696 -163
- package/dist/harness/scheduler.js.map +1 -1
- package/dist/harness/submissions.d.ts +7 -4
- package/dist/harness/submissions.d.ts.map +1 -1
- package/dist/harness/submissions.js +32 -13
- package/dist/harness/submissions.js.map +1 -1
- package/dist/harness/task-graph.d.ts +1 -1
- package/dist/harness/task-graph.d.ts.map +1 -1
- package/dist/harness/task-graph.js +15 -10
- package/dist/harness/task-graph.js.map +1 -1
- package/dist/harness/tool.d.ts +45 -5
- package/dist/harness/tool.d.ts.map +1 -1
- package/dist/harness/tool.js +377 -62
- package/dist/harness/tool.js.map +1 -1
- package/dist/harness/types.d.ts +153 -13
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/util.d.ts +6 -1
- package/dist/harness/util.d.ts.map +1 -1
- package/dist/harness/util.js +5 -2
- package/dist/harness/util.js.map +1 -1
- package/dist/harness/view.d.ts +6 -2
- package/dist/harness/view.d.ts.map +1 -1
- package/dist/harness/view.js +22 -12
- package/dist/harness/view.js.map +1 -1
- package/dist/images/cloudflare.d.ts +4 -0
- package/dist/images/cloudflare.d.ts.map +1 -0
- package/dist/images/cloudflare.js +9 -0
- package/dist/images/cloudflare.js.map +1 -0
- package/dist/images/exif.d.ts +3 -0
- package/dist/images/exif.d.ts.map +1 -0
- package/dist/images/exif.js +71 -0
- package/dist/images/exif.js.map +1 -0
- package/dist/images/index.d.ts +18 -0
- package/dist/images/index.d.ts.map +1 -0
- package/dist/images/index.js +125 -0
- package/dist/images/index.js.map +1 -0
- package/dist/images/node-worker.d.ts +17 -0
- package/dist/images/node-worker.d.ts.map +1 -0
- package/dist/images/node-worker.js +16 -0
- package/dist/images/node-worker.js.map +1 -0
- package/dist/images/node.d.ts +21 -0
- package/dist/images/node.d.ts.map +1 -0
- package/dist/images/node.js +121 -0
- package/dist/images/node.js.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/search.d.ts +13 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +85 -0
- package/dist/search.js.map +1 -0
- package/dist/session/observation.d.ts +10 -3
- package/dist/session/observation.d.ts.map +1 -1
- package/dist/session/observation.js +24 -34
- package/dist/session/observation.js.map +1 -1
- package/dist/session/session.d.ts +68 -5
- package/dist/session/session.d.ts.map +1 -1
- package/dist/session/session.js +461 -49
- package/dist/session/session.js.map +1 -1
- package/dist/session/transaction.d.ts.map +1 -1
- package/dist/session/transaction.js +1 -0
- package/dist/session/transaction.js.map +1 -1
- package/dist/storage/jsonl/storage.d.ts +4 -0
- package/dist/storage/jsonl/storage.d.ts.map +1 -1
- package/dist/storage/jsonl/storage.js +2 -1
- package/dist/storage/jsonl/storage.js.map +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +8 -10
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/scan.d.ts.map +1 -1
- package/dist/storage/scan.js +5 -4
- package/dist/storage/scan.js.map +1 -1
- package/dist/storage/sqlite/storage.d.ts.map +1 -1
- package/dist/storage/sqlite/storage.js +10 -12
- package/dist/storage/sqlite/storage.js.map +1 -1
- package/dist/testing/env-conformance.d.ts.map +1 -1
- package/dist/testing/env-conformance.js +100 -0
- package/dist/testing/env-conformance.js.map +1 -1
- package/dist/testing/runner.d.ts +1 -1
- package/dist/testing/runner.d.ts.map +1 -1
- package/dist/testing/runner.js.map +1 -1
- package/dist/testing/storage-conformance.d.ts.map +1 -1
- package/dist/testing/storage-conformance.js +3 -2
- package/dist/testing/storage-conformance.js.map +1 -1
- package/dist/testing/types.d.ts +2 -0
- package/dist/testing/types.d.ts.map +1 -1
- package/dist/testing/types.js.map +1 -1
- package/dist/tool-search/index.d.ts +2 -0
- package/dist/tool-search/index.d.ts.map +1 -0
- package/dist/tool-search/index.js +2 -0
- package/dist/tool-search/index.js.map +1 -0
- package/dist/tool-search/tool-search.d.ts +23 -0
- package/dist/tool-search/tool-search.d.ts.map +1 -0
- package/dist/tool-search/tool-search.js +84 -0
- package/dist/tool-search/tool-search.js.map +1 -0
- package/dist/tools/bash.d.ts +10 -2
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +27 -9
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.js +1 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/image-processor.d.ts +47 -0
- package/dist/tools/image-processor.d.ts.map +1 -0
- package/dist/tools/image-processor.js +30 -0
- package/dist/tools/image-processor.js.map +1 -0
- package/dist/tools/image.d.ts +8 -0
- package/dist/tools/image.d.ts.map +1 -1
- package/dist/tools/image.js +47 -0
- package/dist/tools/image.js.map +1 -1
- package/dist/tools/index.d.ts +25 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +13 -5
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read.d.ts +10 -2
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +87 -28
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.js +1 -1
- package/dist/tools/write.js.map +1 -1
- package/dist/types.d.ts +59 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +43 -4
package/dist/session/session.js
CHANGED
|
@@ -1,50 +1,151 @@
|
|
|
1
1
|
import { replicatedState } from "@earendil-works/chord";
|
|
2
|
-
import { awaitWithContext, withoutAbortSignal } from "@earendil-works/chord/context";
|
|
2
|
+
import { awaitWithContext, BACKGROUND_CONTEXT, withoutAbortSignal } from "@earendil-works/chord/context";
|
|
3
3
|
import { track } from "@earendil-works/chord/delta";
|
|
4
|
-
import { checkRecordScope, checkRecordVersion, materializeDocument, resolveAddress, } from "../documents.js";
|
|
5
|
-
import {
|
|
4
|
+
import { addressId, checkRecordScope, checkRecordVersion, materializeDocument, resolveAddress, } from "../documents.js";
|
|
5
|
+
import { SessionFailed, StorageRequestError } from "../errors.js";
|
|
6
6
|
import { idFromNumber } from "../ids.js";
|
|
7
7
|
import { CommittedStateSource, CommittedWatch, RETIREMENT_OPERATIONS, } from "./observation.js";
|
|
8
8
|
import { Transaction } from "./transaction.js";
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Open a Session kernel over one storage backend. `now` is the wall clock for task times; default `Date.now`. The first
|
|
11
|
+
* error a Storage method throws fails the Session (`SessionFailed`); see `SessionImpl`.
|
|
12
|
+
*/
|
|
10
13
|
export function createSession(storage, options) {
|
|
11
|
-
|
|
14
|
+
const retention = options?.documentRetentionMs;
|
|
15
|
+
return new SessionImpl(storage, options?.now, retention === undefined ? undefined : () => retention);
|
|
16
|
+
}
|
|
17
|
+
/** Default document retention: ten minutes, as for kept contexts. */
|
|
18
|
+
let defaultRetention = 600_000;
|
|
19
|
+
/** Internal, for tests: the retention of every Session that keeps the default, as a suite run with eviction at once. */
|
|
20
|
+
export function setDefaultDocumentRetention(ms) {
|
|
21
|
+
defaultRetention = ms;
|
|
12
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* A retention as given, when it is a number of milliseconds, `0` or more (`Infinity` keeps documents); otherwise the
|
|
25
|
+
* default.
|
|
26
|
+
*/
|
|
27
|
+
export function documentRetention(ms) {
|
|
28
|
+
return typeof ms === "number" && ms >= 0 ? ms : defaultRetention;
|
|
29
|
+
}
|
|
30
|
+
/** Longest delay `setTimeout` supports; a longer retention waits in several steps. */
|
|
31
|
+
const MAX_TIMER_DELAY = 2_147_483_647;
|
|
13
32
|
/**
|
|
14
33
|
* Session kernel: one mutation line, the loaded document tracker cache, and committed publication.
|
|
15
34
|
*
|
|
35
|
+
* The cache is bounded by use: a document with no watch or document state attached leaves it once it was not read or
|
|
36
|
+
* written for the retention period, and the next access loads it from Storage. Eviction runs as a job on the mutation
|
|
37
|
+
* line, so no transaction ever holds a tracker that leaves.
|
|
38
|
+
*
|
|
16
39
|
* Only committed state is observable. Every commit callback, preparation, Storage settlement, adoption, and
|
|
17
40
|
* publication enqueue runs while the line is held; listeners run later.
|
|
41
|
+
*
|
|
42
|
+
* Storage failures are final. Every Storage call goes through one guard: the first error a Storage method throws, other
|
|
43
|
+
* than for a cancelled caller, fails the Session, as does a commit it cannot adopt. That call gets the error; every later
|
|
44
|
+
* call and every Storage call still underway gets `SessionFailed`; close listeners run at once with the error, and the
|
|
45
|
+
* Session then closes itself, Storage included, and `closed` settles. Nothing retries: transient errors are the
|
|
46
|
+
* Storage's to retry. Reopening recovers from what was committed.
|
|
18
47
|
*/
|
|
19
48
|
export class SessionImpl {
|
|
20
49
|
#storage;
|
|
21
50
|
#documents = new Map();
|
|
51
|
+
/** Cached documents with no observer attached, by address, oldest use first, with the time of that use. */
|
|
52
|
+
#idleDocuments = new Map();
|
|
53
|
+
/** Observers attached per document incarnation (record ID); a cached incarnation with any is never evicted. */
|
|
54
|
+
#attachedDocuments = new Map();
|
|
55
|
+
#clock;
|
|
56
|
+
#documentRetentionMs;
|
|
57
|
+
/** The pending eviction: a timer for the oldest idle document, or a line job when the retention is 0. */
|
|
58
|
+
#documentSweep;
|
|
59
|
+
#sweepJobs = 0;
|
|
60
|
+
/** Set once a timer could not be unreferenced: then none is kept, and the end of each commit evicts what expired. */
|
|
61
|
+
#documentTimersUnavailable = false;
|
|
22
62
|
#commitListeners = new Set();
|
|
63
|
+
/** The Session's own listeners, such as its scheduler's: they keep memory in step with storage, so a throw fails it. */
|
|
64
|
+
#internalListeners = new Set();
|
|
23
65
|
#closeListeners = new Set();
|
|
24
66
|
#host;
|
|
25
67
|
#tail = Promise.resolve();
|
|
26
68
|
#closing;
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
69
|
+
#failure;
|
|
70
|
+
/** Set when close reaches the backend: calls that fail from then on fail because it closes. */
|
|
71
|
+
#failed = Promise.withResolvers();
|
|
72
|
+
#closed = Promise.withResolvers();
|
|
73
|
+
get closed() {
|
|
74
|
+
return this.#closed.promise;
|
|
75
|
+
}
|
|
76
|
+
/** Internal: the error that failed the Session, if one did. */
|
|
77
|
+
get failure() {
|
|
78
|
+
return this.#failure;
|
|
79
|
+
}
|
|
80
|
+
/** Internal: rejects with `SessionFailed` the moment the Session fails, before work underway has ended; never resolves. */
|
|
81
|
+
get failed() {
|
|
82
|
+
return this.#failed.promise;
|
|
83
|
+
}
|
|
84
|
+
constructor(storage, now = Date.now, documentRetentionMs = () => undefined) {
|
|
85
|
+
// Waits race it; none has to.
|
|
86
|
+
this.#failed.promise.catch(() => { });
|
|
87
|
+
this.#storage = new GuardedStorage(storage, this);
|
|
88
|
+
// Cache bookkeeping must never change the outcome of the operation it follows: a clock or a retention setting
|
|
89
|
+
// that throws counts as now, and as the default.
|
|
90
|
+
this.#clock = () => {
|
|
91
|
+
try {
|
|
92
|
+
return now();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return Date.now();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
this.#documentRetentionMs = () => {
|
|
99
|
+
try {
|
|
100
|
+
return documentRetention(documentRetentionMs());
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return documentRetention(undefined);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
30
106
|
this.#host = {
|
|
31
|
-
storage,
|
|
107
|
+
storage: this.#storage,
|
|
32
108
|
now,
|
|
33
|
-
cached: (id) =>
|
|
109
|
+
cached: (id) => {
|
|
110
|
+
const cached = this.#documents.get(id);
|
|
111
|
+
if (cached !== undefined)
|
|
112
|
+
this.#touchDocument(id);
|
|
113
|
+
return cached;
|
|
114
|
+
},
|
|
34
115
|
load: (definition, addressId, address, context) => this.#loadDocument(definition, addressId, address, context),
|
|
35
116
|
install: (document) => {
|
|
36
117
|
this.#documents.set(document.addressId, document);
|
|
118
|
+
this.#touchDocument(document.addressId);
|
|
37
119
|
},
|
|
38
120
|
evict: (id, recordId) => {
|
|
39
121
|
if (this.#documents.get(id)?.record.id === recordId)
|
|
40
|
-
this.#
|
|
122
|
+
this.#dropDocument(id);
|
|
41
123
|
},
|
|
42
124
|
conversationCreated: (tx, record) => this.conversationCreated(tx, record),
|
|
43
125
|
};
|
|
44
126
|
}
|
|
127
|
+
/** The Storage behind the failure guard; every component reads and writes through it. */
|
|
128
|
+
get storage() {
|
|
129
|
+
return this.#storage;
|
|
130
|
+
}
|
|
45
131
|
commit(change, context) {
|
|
46
132
|
return this.commitWith(change, context);
|
|
47
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Internal: fail the Session with `error`, the first wins, and report it once. Admission ends, and close listeners run
|
|
136
|
+
* now; they read the error from `failure`. The Storage guard calls this; the Harness also calls it for a throw in its
|
|
137
|
+
* scheduler's own commits.
|
|
138
|
+
*/
|
|
139
|
+
fail(error) {
|
|
140
|
+
if (this.#failure !== undefined)
|
|
141
|
+
return;
|
|
142
|
+
this.#failure = { error };
|
|
143
|
+
this.#failed.reject(new SessionFailed(error));
|
|
144
|
+
// Seal first, so a report handler that calls back finds the Session failed. Closing runs the close listeners
|
|
145
|
+
// synchronously; a failing backend close is the caller's to see, and here nobody waits.
|
|
146
|
+
this.close(BACKGROUND_CONTEXT).catch(() => { });
|
|
147
|
+
this.report(error);
|
|
148
|
+
}
|
|
48
149
|
/**
|
|
49
150
|
* Internal commit exposing the concrete transaction and its internal operations, such as the reserved-ID root
|
|
50
151
|
* bootstrap and task replacement. `scope` sets the default `tx.createTask()` conversation and the task attributed to
|
|
@@ -52,7 +153,7 @@ export class SessionImpl {
|
|
|
52
153
|
*/
|
|
53
154
|
commitWith(change, context, scope) {
|
|
54
155
|
try {
|
|
55
|
-
this
|
|
156
|
+
this.assertUsable();
|
|
56
157
|
}
|
|
57
158
|
catch (error) {
|
|
58
159
|
return Promise.reject(error);
|
|
@@ -62,7 +163,7 @@ export class SessionImpl {
|
|
|
62
163
|
/** Internal: run a read-only job on the mutation line so multi-read derivations observe one committed state. */
|
|
63
164
|
readOnLine(job) {
|
|
64
165
|
try {
|
|
65
|
-
this
|
|
166
|
+
this.assertUsable();
|
|
66
167
|
}
|
|
67
168
|
catch (error) {
|
|
68
169
|
return Promise.reject(error);
|
|
@@ -87,11 +188,13 @@ export class SessionImpl {
|
|
|
87
188
|
return { record: loaded.record, version: loaded.valueVersion, value: loaded.tracker.value };
|
|
88
189
|
}
|
|
89
190
|
async snapshot(token, ...args) {
|
|
90
|
-
this
|
|
191
|
+
this.assertUsable();
|
|
91
192
|
const definition = token.definition;
|
|
92
193
|
const resolved = resolveAddress(definition, args);
|
|
93
194
|
const context = args[resolved.nextArgument];
|
|
94
195
|
const cached = this.#documents.get(resolved.id);
|
|
196
|
+
if (cached?.valueVersion === definition.version)
|
|
197
|
+
this.#touchDocument(resolved.id);
|
|
95
198
|
const loaded = cached?.valueVersion === definition.version
|
|
96
199
|
? cached
|
|
97
200
|
: await this.#enqueue(async () => {
|
|
@@ -106,7 +209,7 @@ export class SessionImpl {
|
|
|
106
209
|
}
|
|
107
210
|
documentState(token, ...args) {
|
|
108
211
|
try {
|
|
109
|
-
this
|
|
212
|
+
this.assertUsable();
|
|
110
213
|
const definition = token.definition;
|
|
111
214
|
const resolved = resolveAddress(definition, args);
|
|
112
215
|
const context = args[resolved.nextArgument];
|
|
@@ -117,7 +220,7 @@ export class SessionImpl {
|
|
|
117
220
|
return undefined;
|
|
118
221
|
const { observer: source, detach } = this.#attachDocument(definition, loaded, (value, release) => new CommittedStateSource(value, release));
|
|
119
222
|
try {
|
|
120
|
-
return replicatedState(source);
|
|
223
|
+
return replicatedState(source, { onError: (error) => this.report(error) });
|
|
121
224
|
}
|
|
122
225
|
catch (error) {
|
|
123
226
|
detach();
|
|
@@ -130,7 +233,7 @@ export class SessionImpl {
|
|
|
130
233
|
}
|
|
131
234
|
}
|
|
132
235
|
async watchDoc(token, ...args) {
|
|
133
|
-
this
|
|
236
|
+
this.assertUsable();
|
|
134
237
|
const definition = token.definition;
|
|
135
238
|
const resolved = resolveAddress(definition, args);
|
|
136
239
|
const context = args[resolved.nextArgument];
|
|
@@ -150,7 +253,7 @@ export class SessionImpl {
|
|
|
150
253
|
throw cancellationError(signal);
|
|
151
254
|
if (loaded === undefined)
|
|
152
255
|
return undefined;
|
|
153
|
-
return this.#attachDocument(definition, loaded, (value, release) => new CommittedWatch(value, release)).observer;
|
|
256
|
+
return this.#attachDocument(definition, loaded, (value, release) => new CommittedWatch(value, release, (error) => this.report(error))).observer;
|
|
154
257
|
});
|
|
155
258
|
if (watch === undefined)
|
|
156
259
|
return undefined;
|
|
@@ -167,7 +270,7 @@ export class SessionImpl {
|
|
|
167
270
|
}
|
|
168
271
|
}
|
|
169
272
|
async snapshotAsOf(token, ...args) {
|
|
170
|
-
this
|
|
273
|
+
this.assertUsable();
|
|
171
274
|
const definition = token.definition;
|
|
172
275
|
const resolved = resolveAddress(definition, args);
|
|
173
276
|
if (resolved.address.scope.kind !== "conversation") {
|
|
@@ -208,16 +311,42 @@ export class SessionImpl {
|
|
|
208
311
|
.then(() => this.beforeClose())
|
|
209
312
|
.then(() => this.#enqueue(async () => {
|
|
210
313
|
this.#commitListeners.clear();
|
|
314
|
+
this.#internalListeners.clear();
|
|
211
315
|
this.#documents.clear();
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
316
|
+
this.#idleDocuments.clear();
|
|
317
|
+
this.#attachedDocuments.clear();
|
|
318
|
+
try {
|
|
319
|
+
await this.#storage.close(cleanup);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
// A Storage that cannot close is a failed one; an earlier failure stays the cause.
|
|
323
|
+
if (this.#failure === undefined) {
|
|
324
|
+
this.#failure = { error };
|
|
325
|
+
this.report(error);
|
|
326
|
+
}
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
}))
|
|
330
|
+
.finally(() => {
|
|
331
|
+
const failure = this.#failure;
|
|
332
|
+
this.#closed.resolve(failure === undefined ? { reason: "closed" } : { reason: "failed", error: failure.error });
|
|
333
|
+
});
|
|
334
|
+
this.#notifyClose();
|
|
218
335
|
}
|
|
219
336
|
return awaitWithContext(this.#closing, context);
|
|
220
337
|
}
|
|
338
|
+
/** Run the close listeners once, each on its own: one that fails is reported, and the rest still run. */
|
|
339
|
+
#notifyClose() {
|
|
340
|
+
if (this.#documentSweep?.timer !== undefined)
|
|
341
|
+
clearTimeout(this.#documentSweep.timer);
|
|
342
|
+
this.#documentSweep = undefined;
|
|
343
|
+
const listeners = [...this.#closeListeners];
|
|
344
|
+
this.#closeListeners.clear();
|
|
345
|
+
for (const listener of listeners)
|
|
346
|
+
contained(listener, (error) => this.report(error));
|
|
347
|
+
}
|
|
348
|
+
/** Internal: the Session's failure, and errors of listeners, which never fail what ran them. A plain Session drops them. */
|
|
349
|
+
report(_error) { }
|
|
221
350
|
/**
|
|
222
351
|
* Runs inside every transaction that creates or forks a conversation, after the conversation record is staged. A
|
|
223
352
|
* plain Session stages nothing; a Harness stages its built-in documents.
|
|
@@ -229,15 +358,27 @@ export class SessionImpl {
|
|
|
229
358
|
beforeClose() {
|
|
230
359
|
return Promise.resolve();
|
|
231
360
|
}
|
|
232
|
-
/** Register a synchronous post-adoption listener. It must not
|
|
361
|
+
/** Register a synchronous post-adoption listener. It must not block or call Session operations; a throw is reported. */
|
|
233
362
|
subscribeCommits(listener) {
|
|
234
|
-
this
|
|
363
|
+
this.assertUsable();
|
|
235
364
|
this.#commitListeners.add(listener);
|
|
236
365
|
return () => this.#commitListeners.delete(listener);
|
|
237
366
|
}
|
|
238
|
-
/**
|
|
367
|
+
/**
|
|
368
|
+
* Internal: `subscribeCommits()` for the Session's own components, whose state follows each commit. A throw leaves
|
|
369
|
+
* that state behind storage, so it fails the Session instead of being reported. They run before host listeners.
|
|
370
|
+
*/
|
|
371
|
+
observeCommits(listener) {
|
|
372
|
+
this.assertUsable();
|
|
373
|
+
this.#internalListeners.add(listener);
|
|
374
|
+
return () => this.#internalListeners.delete(listener);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Register a listener called synchronously when close begins, also when a failure closes the Session (`failure` is
|
|
378
|
+
* then set). It must not block or call Session operations; a throw is reported.
|
|
379
|
+
*/
|
|
239
380
|
subscribeClose(listener) {
|
|
240
|
-
this
|
|
381
|
+
this.assertUsable();
|
|
241
382
|
this.#closeListeners.add(listener);
|
|
242
383
|
return () => this.#closeListeners.delete(listener);
|
|
243
384
|
}
|
|
@@ -245,8 +386,120 @@ export class SessionImpl {
|
|
|
245
386
|
unloadDocuments() {
|
|
246
387
|
return this.#enqueue(async () => {
|
|
247
388
|
this.#documents.clear();
|
|
389
|
+
this.#idleDocuments.clear();
|
|
248
390
|
});
|
|
249
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Internal, for tests: how many documents the cache holds, how many are idle, how many incarnations are pinned, and how
|
|
394
|
+
* many sweep jobs were queued on the line so far.
|
|
395
|
+
*/
|
|
396
|
+
get documentCacheSizes() {
|
|
397
|
+
return {
|
|
398
|
+
cached: this.#documents.size,
|
|
399
|
+
idle: this.#idleDocuments.size,
|
|
400
|
+
attached: this.#attachedDocuments.size,
|
|
401
|
+
sweeps: this.#sweepJobs,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
/** Note a use of a cached document: an unattached one becomes the newest idle one, and an eviction is scheduled. */
|
|
405
|
+
#touchDocument(addressId) {
|
|
406
|
+
const cached = this.#documents.get(addressId);
|
|
407
|
+
if (cached === undefined || this.#attachedDocuments.has(cached.record.id))
|
|
408
|
+
return;
|
|
409
|
+
const now = this.#clock();
|
|
410
|
+
const since = this.#idleDocuments.get(addressId);
|
|
411
|
+
// A hot document moves to the back at most once per granule, a tenth of the retention up to 1 s: it may then leave
|
|
412
|
+
// up to a granule early, which costs a reload, while a commit-heavy workload skips the reordering.
|
|
413
|
+
if (since !== undefined && now - since < Math.min(1_000, this.#documentRetentionMs() / 10))
|
|
414
|
+
return;
|
|
415
|
+
this.#idleDocuments.delete(addressId);
|
|
416
|
+
this.#idleDocuments.set(addressId, now);
|
|
417
|
+
// A pending sweep for the oldest document comes first unless the retention was shortened since; then it moves up.
|
|
418
|
+
if (this.#documentSweep === undefined || this.#documentSweep.at > now + this.#documentRetentionMs()) {
|
|
419
|
+
this.#scheduleDocumentSweep();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
#dropDocument(addressId) {
|
|
423
|
+
this.#documents.delete(addressId);
|
|
424
|
+
this.#idleDocuments.delete(addressId);
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Schedule the eviction of the oldest idle document: on the line at once when the retention is 0, otherwise when it
|
|
428
|
+
* expires, by a timer that never keeps the process alive. Where timers cannot be unreferenced, as in Cloudflare
|
|
429
|
+
* Workers, none is kept: the end of each commit evicts what expired.
|
|
430
|
+
*/
|
|
431
|
+
#scheduleDocumentSweep() {
|
|
432
|
+
if (this.#closing !== undefined)
|
|
433
|
+
return;
|
|
434
|
+
const oldest = this.#idleDocuments.values().next();
|
|
435
|
+
if (oldest.done === true)
|
|
436
|
+
return;
|
|
437
|
+
const at = oldest.value + this.#documentRetentionMs();
|
|
438
|
+
if (this.#documentSweep !== undefined && this.#documentSweep.at <= at)
|
|
439
|
+
return;
|
|
440
|
+
if (this.#documentSweep?.timer !== undefined)
|
|
441
|
+
clearTimeout(this.#documentSweep.timer);
|
|
442
|
+
// The pending sweep stays set until its job runs, so touches while the line is busy queue no more.
|
|
443
|
+
const sweep = { at, timer: undefined };
|
|
444
|
+
const run = () => {
|
|
445
|
+
this.#sweepJobs++;
|
|
446
|
+
this.#enqueue(async () => {
|
|
447
|
+
if (this.#documentSweep === sweep)
|
|
448
|
+
this.#documentSweep = undefined;
|
|
449
|
+
if (this.#closing === undefined)
|
|
450
|
+
this.#sweepDocuments();
|
|
451
|
+
}).catch(() => { });
|
|
452
|
+
};
|
|
453
|
+
const delay = at - this.#clock();
|
|
454
|
+
this.#documentSweep = sweep;
|
|
455
|
+
if (delay <= 0) {
|
|
456
|
+
queueMicrotask(run);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (this.#documentTimersUnavailable) {
|
|
460
|
+
this.#documentSweep = undefined;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const timer = setTimeout(run, Math.min(delay, MAX_TIMER_DELAY));
|
|
464
|
+
const unref = timer.unref;
|
|
465
|
+
if (typeof unref !== "function") {
|
|
466
|
+
clearTimeout(timer);
|
|
467
|
+
this.#documentTimersUnavailable = true;
|
|
468
|
+
this.#documentSweep = undefined;
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
unref.call(timer);
|
|
472
|
+
sweep.timer = timer;
|
|
473
|
+
}
|
|
474
|
+
/** On the line: evict the idle documents whose retention passed, oldest first, then schedule the next eviction. */
|
|
475
|
+
#sweepDocuments() {
|
|
476
|
+
const expired = this.#clock() - this.#documentRetentionMs();
|
|
477
|
+
for (const [addressId, since] of this.#idleDocuments) {
|
|
478
|
+
if (since > expired)
|
|
479
|
+
break;
|
|
480
|
+
this.#dropDocument(addressId);
|
|
481
|
+
}
|
|
482
|
+
this.#scheduleDocumentSweep();
|
|
483
|
+
}
|
|
484
|
+
/** Pin a cached incarnation for one observer; the returned release unpins it once, idle again after the last. */
|
|
485
|
+
#pinDocument(addressId, recordId) {
|
|
486
|
+
this.#attachedDocuments.set(recordId, (this.#attachedDocuments.get(recordId) ?? 0) + 1);
|
|
487
|
+
this.#idleDocuments.delete(addressId);
|
|
488
|
+
let pinned = true;
|
|
489
|
+
return () => {
|
|
490
|
+
if (!pinned)
|
|
491
|
+
return;
|
|
492
|
+
pinned = false;
|
|
493
|
+
const count = (this.#attachedDocuments.get(recordId) ?? 1) - 1;
|
|
494
|
+
if (count > 0) {
|
|
495
|
+
this.#attachedDocuments.set(recordId, count);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
this.#attachedDocuments.delete(recordId);
|
|
499
|
+
if (this.#documents.get(addressId)?.record.id === recordId)
|
|
500
|
+
this.#touchDocument(addressId);
|
|
501
|
+
};
|
|
502
|
+
}
|
|
250
503
|
async #runCommit(change, context, scope) {
|
|
251
504
|
this.#assertHealthy();
|
|
252
505
|
context.abortSignal?.throwIfAborted();
|
|
@@ -254,14 +507,26 @@ export class SessionImpl {
|
|
|
254
507
|
let result;
|
|
255
508
|
try {
|
|
256
509
|
result = await change(tx);
|
|
510
|
+
// A callback that caught a failed read must not commit, nor succeed as if it had.
|
|
511
|
+
this.#assertHealthy();
|
|
257
512
|
}
|
|
258
513
|
catch (error) {
|
|
259
514
|
await tx.settleFailure();
|
|
515
|
+
this.#sweepDocuments();
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
518
|
+
let writes;
|
|
519
|
+
try {
|
|
520
|
+
writes = await tx.settleSuccess();
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
// Settlement aborted every change already.
|
|
524
|
+
this.#sweepDocuments();
|
|
260
525
|
throw error;
|
|
261
526
|
}
|
|
262
|
-
const writes = await tx.settleSuccess();
|
|
263
527
|
if (writes.length === 0) {
|
|
264
528
|
tx.discard();
|
|
529
|
+
this.#sweepDocuments();
|
|
265
530
|
return result;
|
|
266
531
|
}
|
|
267
532
|
let seq;
|
|
@@ -270,10 +535,8 @@ export class SessionImpl {
|
|
|
270
535
|
seq = await this.#storage.commit(writes, withoutAbortSignal(context));
|
|
271
536
|
}
|
|
272
537
|
catch (error) {
|
|
538
|
+
// The guard has failed the Session.
|
|
273
539
|
tx.discard();
|
|
274
|
-
// Callback errors never reach this branch; StorageRejected alone guarantees that no batch effect committed.
|
|
275
|
-
if (!(error instanceof StorageRejected))
|
|
276
|
-
this.#poison = { error };
|
|
277
540
|
throw error;
|
|
278
541
|
}
|
|
279
542
|
let documents;
|
|
@@ -282,14 +545,17 @@ export class SessionImpl {
|
|
|
282
545
|
}
|
|
283
546
|
catch (error) {
|
|
284
547
|
// Storage already committed; a failed adoption leaves memory behind durable state.
|
|
285
|
-
this
|
|
548
|
+
this.fail(error);
|
|
286
549
|
throw error;
|
|
287
550
|
}
|
|
551
|
+
for (const document of documents)
|
|
552
|
+
this.#touchDocument(addressId(document.record));
|
|
288
553
|
this.#publish(seq, writes, documents, context);
|
|
554
|
+
this.#sweepDocuments();
|
|
289
555
|
return result;
|
|
290
556
|
}
|
|
291
557
|
#publish(seq, writes, documents, context) {
|
|
292
|
-
if (this.#commitListeners.size === 0)
|
|
558
|
+
if (this.#commitListeners.size === 0 && this.#internalListeners.size === 0)
|
|
293
559
|
return;
|
|
294
560
|
const changes = [];
|
|
295
561
|
for (const write of writes) {
|
|
@@ -304,8 +570,13 @@ export class SessionImpl {
|
|
|
304
570
|
for (const document of documents)
|
|
305
571
|
changes.push(document);
|
|
306
572
|
const publication = { seq, changes };
|
|
307
|
-
|
|
308
|
-
|
|
573
|
+
// The commit is durable: a failing listener neither fails the commit nor skips the others.
|
|
574
|
+
for (const listener of [...this.#internalListeners]) {
|
|
575
|
+
contained(() => listener(publication, context), (error) => this.fail(error));
|
|
576
|
+
}
|
|
577
|
+
for (const listener of [...this.#commitListeners]) {
|
|
578
|
+
contained(() => listener(publication, context), (error) => this.report(error));
|
|
579
|
+
}
|
|
309
580
|
}
|
|
310
581
|
/**
|
|
311
582
|
* Attach an observer to one committed incarnation: check the definition, then forward this incarnation's committed
|
|
@@ -316,13 +587,23 @@ export class SessionImpl {
|
|
|
316
587
|
checkRecordVersion(definition, loaded.record, loaded.storedVersion);
|
|
317
588
|
let unsubscribeCommit = () => { };
|
|
318
589
|
let unsubscribeClose = () => { };
|
|
590
|
+
// Attached, the incarnation stays cached; once its last observer detaches, or it retires, it is idle again.
|
|
591
|
+
const unpin = this.#pinDocument(loaded.addressId, loaded.record.id);
|
|
319
592
|
const detach = () => {
|
|
320
593
|
unsubscribeCommit();
|
|
321
594
|
unsubscribeClose();
|
|
595
|
+
unpin();
|
|
322
596
|
};
|
|
323
|
-
|
|
597
|
+
let observer;
|
|
598
|
+
try {
|
|
599
|
+
observer = create(loaded.tracker.value, detach);
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
unpin();
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
324
605
|
const observed = { version: loaded.valueVersion };
|
|
325
|
-
unsubscribeCommit = this.
|
|
606
|
+
unsubscribeCommit = this.observeCommits((publication, context) => {
|
|
326
607
|
for (const change of publication.changes) {
|
|
327
608
|
if (change.type !== "document" || change.record.id !== loaded.record.id)
|
|
328
609
|
continue;
|
|
@@ -332,19 +613,24 @@ export class SessionImpl {
|
|
|
332
613
|
// A migration-only base changes nothing for an observer of the new version.
|
|
333
614
|
if (ops.length === 0)
|
|
334
615
|
continue;
|
|
616
|
+
// A retired incarnation never changes again: its observers keep their last value but pin nothing.
|
|
617
|
+
if (change.value === null)
|
|
618
|
+
unpin();
|
|
335
619
|
observer.advance(change.value, ops, frameContext);
|
|
336
620
|
}
|
|
337
621
|
});
|
|
338
|
-
unsubscribeClose = this.subscribeClose(() => observer.closeSession());
|
|
622
|
+
unsubscribeClose = this.subscribeClose(() => observer.closeSession(this.#failure));
|
|
339
623
|
return { observer, detach };
|
|
340
624
|
}
|
|
341
625
|
async #loadDocument(definition, addressId, address, context) {
|
|
342
626
|
const cached = this.#documents.get(addressId);
|
|
343
627
|
// A tracker serves only tokens of the version its value was materialized for; others reload from Storage.
|
|
344
|
-
if (cached?.valueVersion === definition.version)
|
|
628
|
+
if (cached?.valueVersion === definition.version) {
|
|
629
|
+
this.#touchDocument(addressId);
|
|
345
630
|
return cached;
|
|
631
|
+
}
|
|
346
632
|
if (cached !== undefined)
|
|
347
|
-
this.#
|
|
633
|
+
this.#dropDocument(addressId);
|
|
348
634
|
const record = await this.#storage.findDocument(address, "current", context);
|
|
349
635
|
if (record === undefined)
|
|
350
636
|
return undefined;
|
|
@@ -361,6 +647,7 @@ export class SessionImpl {
|
|
|
361
647
|
tracker: track(value),
|
|
362
648
|
};
|
|
363
649
|
this.#documents.set(addressId, loaded);
|
|
650
|
+
this.#touchDocument(addressId);
|
|
364
651
|
return loaded;
|
|
365
652
|
}
|
|
366
653
|
#enqueue(job) {
|
|
@@ -368,17 +655,30 @@ export class SessionImpl {
|
|
|
368
655
|
this.#tail = run.then(() => undefined, () => undefined);
|
|
369
656
|
return run;
|
|
370
657
|
}
|
|
371
|
-
|
|
658
|
+
/** Internal: throw `SessionFailed` when failed, else an error when closing. */
|
|
659
|
+
assertUsable() {
|
|
660
|
+
this.#assertHealthy();
|
|
372
661
|
if (this.#closing !== undefined)
|
|
373
662
|
throw new Error("Session is closed");
|
|
374
|
-
this.#assertHealthy();
|
|
375
663
|
}
|
|
376
664
|
#assertHealthy() {
|
|
377
|
-
if (this.#
|
|
378
|
-
throw new
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
665
|
+
if (this.#failure !== undefined)
|
|
666
|
+
throw new SessionFailed(this.#failure.error);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Run a host callback that must not fail its caller: a throw, or a promise it returns that rejects, goes to `onError`,
|
|
671
|
+
* which is not awaited.
|
|
672
|
+
*/
|
|
673
|
+
export function contained(callback, onError) {
|
|
674
|
+
try {
|
|
675
|
+
const result = callback();
|
|
676
|
+
// Any thenable, also a promise from another realm; `Promise.resolve` adopts it with handlers of its own.
|
|
677
|
+
if (typeof result?.then === "function")
|
|
678
|
+
Promise.resolve(result).catch(onError);
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
onError(error);
|
|
382
682
|
}
|
|
383
683
|
}
|
|
384
684
|
/**
|
|
@@ -396,4 +696,116 @@ function observedOperations(observed, change) {
|
|
|
396
696
|
function cancellationError(signal) {
|
|
397
697
|
return signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
|
|
398
698
|
}
|
|
699
|
+
/**
|
|
700
|
+
* The Session's Storage behind its failure guard; every component of the Session reads and writes through it. Before a
|
|
701
|
+
* call, a failed Session gets `SessionFailed` without reaching the backend. A call that throws fails the Session and
|
|
702
|
+
* rethrows the backend's error, unless it is a `StorageRequestError` or a read whose caller's context was aborted:
|
|
703
|
+
* neither says the Storage is broken.
|
|
704
|
+
* `close()` always reaches the backend, so a failed Session still closes it.
|
|
705
|
+
*/
|
|
706
|
+
class GuardedStorage {
|
|
707
|
+
#storage;
|
|
708
|
+
#latch;
|
|
709
|
+
/** Backend calls underway, also those off the Session line, which close waits for before it closes the backend. */
|
|
710
|
+
#underway = new Set();
|
|
711
|
+
#closing = false;
|
|
712
|
+
constructor(storage, latch) {
|
|
713
|
+
this.#storage = storage;
|
|
714
|
+
this.#latch = latch;
|
|
715
|
+
}
|
|
716
|
+
/** Never exempt, whatever it throws: once admitted, whether a failed batch committed is unknown. */
|
|
717
|
+
commit(writes, context) {
|
|
718
|
+
return this.#call(undefined, () => this.#storage.commit(writes, context));
|
|
719
|
+
}
|
|
720
|
+
mintId() {
|
|
721
|
+
return this.#call(undefined, () => this.#storage.mintId());
|
|
722
|
+
}
|
|
723
|
+
conversation(id, context) {
|
|
724
|
+
return this.#call(context, () => this.#storage.conversation(id, context));
|
|
725
|
+
}
|
|
726
|
+
scanConversations(query, limit, cursor, context) {
|
|
727
|
+
return this.#call(context, () => this.#storage.scanConversations(query, limit, cursor, context));
|
|
728
|
+
}
|
|
729
|
+
entry(first, second, third) {
|
|
730
|
+
if (third === undefined) {
|
|
731
|
+
const context = second;
|
|
732
|
+
return this.#call(context, () => this.#storage.entry(first, context));
|
|
733
|
+
}
|
|
734
|
+
return this.#call(third, () => this.#storage.entry(first, second, third));
|
|
735
|
+
}
|
|
736
|
+
findLatestHeadMarker(conversationId, atOrBeforeEntryId, context) {
|
|
737
|
+
return this.#call(context, () => this.#storage.findLatestHeadMarker(conversationId, atOrBeforeEntryId, context));
|
|
738
|
+
}
|
|
739
|
+
scanEntries(query, limit, cursor, context) {
|
|
740
|
+
return this.#call(context, () => this.#storage.scanEntries(query, limit, cursor, context));
|
|
741
|
+
}
|
|
742
|
+
task(id, context) {
|
|
743
|
+
return this.#call(context, () => this.#storage.task(id, context));
|
|
744
|
+
}
|
|
745
|
+
scanTasks(query, limit, cursor, context) {
|
|
746
|
+
return this.#call(context, () => this.#storage.scanTasks(query, limit, cursor, context));
|
|
747
|
+
}
|
|
748
|
+
submission(id, context) {
|
|
749
|
+
return this.#call(context, () => this.#storage.submission(id, context));
|
|
750
|
+
}
|
|
751
|
+
scanSubmissions(query, limit, cursor, context) {
|
|
752
|
+
return this.#call(context, () => this.#storage.scanSubmissions(query, limit, cursor, context));
|
|
753
|
+
}
|
|
754
|
+
submissionByRequest(conversationId, requestId, context) {
|
|
755
|
+
return this.#call(context, () => this.#storage.submissionByRequest(conversationId, requestId, context));
|
|
756
|
+
}
|
|
757
|
+
findDocument(address, at, context) {
|
|
758
|
+
return this.#call(context, () => this.#storage.findDocument(address, at, context));
|
|
759
|
+
}
|
|
760
|
+
document(id, at, context) {
|
|
761
|
+
return this.#call(context, () => this.#storage.document(id, at, context));
|
|
762
|
+
}
|
|
763
|
+
scanDocuments(query, limit, cursor, context) {
|
|
764
|
+
return this.#call(context, () => this.#storage.scanDocuments(query, limit, cursor, context));
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Close the backend once every call underway has settled, so none outlives it or fails after `closed` settles. New
|
|
768
|
+
* calls are refused from here on: a paged read must not start its next page while the backend closes.
|
|
769
|
+
*/
|
|
770
|
+
async close(context) {
|
|
771
|
+
this.#closing = true;
|
|
772
|
+
await Promise.allSettled([...this.#underway]);
|
|
773
|
+
return this.#storage.close(context);
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Run one backend call under the guard. `read` is a read's context; `commit()` and `mintId()` pass none, so nothing
|
|
777
|
+
* exempts their errors: once a batch is admitted, whether it committed is unknown. A call still underway when another
|
|
778
|
+
* fails the Session ends with `SessionFailed` too, whatever it returns: nothing it read or wrote is used.
|
|
779
|
+
*/
|
|
780
|
+
async #call(read, run) {
|
|
781
|
+
this.#assertHealthy();
|
|
782
|
+
if (this.#closing)
|
|
783
|
+
throw new Error("Session is closed");
|
|
784
|
+
let result;
|
|
785
|
+
// An async wrapper, so a backend that throws synchronously is guarded too.
|
|
786
|
+
const call = (async () => run())();
|
|
787
|
+
this.#underway.add(call);
|
|
788
|
+
try {
|
|
789
|
+
result = await call;
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
this.#assertHealthy();
|
|
793
|
+
// An invalid read, or one its caller cancelled, says nothing about the Storage; it fails that call only.
|
|
794
|
+
const exempt = read !== undefined && (error instanceof StorageRequestError || read.abortSignal?.aborted === true);
|
|
795
|
+
if (!exempt)
|
|
796
|
+
this.#latch.fail(error);
|
|
797
|
+
throw error;
|
|
798
|
+
}
|
|
799
|
+
finally {
|
|
800
|
+
this.#underway.delete(call);
|
|
801
|
+
}
|
|
802
|
+
this.#assertHealthy();
|
|
803
|
+
return result;
|
|
804
|
+
}
|
|
805
|
+
#assertHealthy() {
|
|
806
|
+
const failure = this.#latch.failure;
|
|
807
|
+
if (failure !== undefined)
|
|
808
|
+
throw new SessionFailed(failure.error);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
399
811
|
//# sourceMappingURL=session.js.map
|