@earendil-works/pi-durable 1.0.4 → 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 +66 -0
- package/README.md +149 -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 +72 -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 +11 -8
- package/dist/harness/compaction.js.map +1 -1
- package/dist/harness/context.d.ts +29 -1
- package/dist/harness/context.d.ts.map +1 -1
- package/dist/harness/context.js +125 -24
- package/dist/harness/context.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 +34 -11
- 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 +56 -28
- 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 +794 -168
- 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 +388 -63
- package/dist/harness/tool.js.map +1 -1
- package/dist/harness/types.d.ts +171 -15
- 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 +71 -6
- package/dist/session/session.d.ts.map +1 -1
- package/dist/session/session.js +463 -50
- package/dist/session/session.js.map +1 -1
- package/dist/session/transaction.d.ts +2 -0
- package/dist/session/transaction.d.ts.map +1 -1
- package/dist/session/transaction.js +19 -1
- package/dist/session/transaction.js.map +1 -1
- package/dist/storage/jsonl/storage.d.ts +8 -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 +2 -0
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +79 -27
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/scan.d.ts +18 -0
- package/dist/storage/scan.d.ts.map +1 -0
- package/dist/storage/scan.js +29 -0
- package/dist/storage/scan.js.map +1 -0
- package/dist/storage/sqlite/cloudflare.d.ts +46 -0
- package/dist/storage/sqlite/cloudflare.d.ts.map +1 -0
- package/dist/storage/sqlite/cloudflare.js +106 -0
- package/dist/storage/sqlite/cloudflare.js.map +1 -0
- package/dist/storage/sqlite/storage.d.ts +2 -0
- package/dist/storage/sqlite/storage.d.ts.map +1 -1
- package/dist/storage/sqlite/storage.js +77 -27
- 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 +61 -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 +89 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +50 -5
package/dist/session/session.js
CHANGED
|
@@ -1,49 +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
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
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
|
+
*/
|
|
13
|
+
export function createSession(storage, options) {
|
|
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,
|
|
32
|
-
|
|
107
|
+
storage: this.#storage,
|
|
108
|
+
now,
|
|
109
|
+
cached: (id) => {
|
|
110
|
+
const cached = this.#documents.get(id);
|
|
111
|
+
if (cached !== undefined)
|
|
112
|
+
this.#touchDocument(id);
|
|
113
|
+
return cached;
|
|
114
|
+
},
|
|
33
115
|
load: (definition, addressId, address, context) => this.#loadDocument(definition, addressId, address, context),
|
|
34
116
|
install: (document) => {
|
|
35
117
|
this.#documents.set(document.addressId, document);
|
|
118
|
+
this.#touchDocument(document.addressId);
|
|
36
119
|
},
|
|
37
120
|
evict: (id, recordId) => {
|
|
38
121
|
if (this.#documents.get(id)?.record.id === recordId)
|
|
39
|
-
this.#
|
|
122
|
+
this.#dropDocument(id);
|
|
40
123
|
},
|
|
41
124
|
conversationCreated: (tx, record) => this.conversationCreated(tx, record),
|
|
42
125
|
};
|
|
43
126
|
}
|
|
127
|
+
/** The Storage behind the failure guard; every component reads and writes through it. */
|
|
128
|
+
get storage() {
|
|
129
|
+
return this.#storage;
|
|
130
|
+
}
|
|
44
131
|
commit(change, context) {
|
|
45
132
|
return this.commitWith(change, context);
|
|
46
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
|
+
}
|
|
47
149
|
/**
|
|
48
150
|
* Internal commit exposing the concrete transaction and its internal operations, such as the reserved-ID root
|
|
49
151
|
* bootstrap and task replacement. `scope` sets the default `tx.createTask()` conversation and the task attributed to
|
|
@@ -51,7 +153,7 @@ export class SessionImpl {
|
|
|
51
153
|
*/
|
|
52
154
|
commitWith(change, context, scope) {
|
|
53
155
|
try {
|
|
54
|
-
this
|
|
156
|
+
this.assertUsable();
|
|
55
157
|
}
|
|
56
158
|
catch (error) {
|
|
57
159
|
return Promise.reject(error);
|
|
@@ -61,7 +163,7 @@ export class SessionImpl {
|
|
|
61
163
|
/** Internal: run a read-only job on the mutation line so multi-read derivations observe one committed state. */
|
|
62
164
|
readOnLine(job) {
|
|
63
165
|
try {
|
|
64
|
-
this
|
|
166
|
+
this.assertUsable();
|
|
65
167
|
}
|
|
66
168
|
catch (error) {
|
|
67
169
|
return Promise.reject(error);
|
|
@@ -86,11 +188,13 @@ export class SessionImpl {
|
|
|
86
188
|
return { record: loaded.record, version: loaded.valueVersion, value: loaded.tracker.value };
|
|
87
189
|
}
|
|
88
190
|
async snapshot(token, ...args) {
|
|
89
|
-
this
|
|
191
|
+
this.assertUsable();
|
|
90
192
|
const definition = token.definition;
|
|
91
193
|
const resolved = resolveAddress(definition, args);
|
|
92
194
|
const context = args[resolved.nextArgument];
|
|
93
195
|
const cached = this.#documents.get(resolved.id);
|
|
196
|
+
if (cached?.valueVersion === definition.version)
|
|
197
|
+
this.#touchDocument(resolved.id);
|
|
94
198
|
const loaded = cached?.valueVersion === definition.version
|
|
95
199
|
? cached
|
|
96
200
|
: await this.#enqueue(async () => {
|
|
@@ -105,7 +209,7 @@ export class SessionImpl {
|
|
|
105
209
|
}
|
|
106
210
|
documentState(token, ...args) {
|
|
107
211
|
try {
|
|
108
|
-
this
|
|
212
|
+
this.assertUsable();
|
|
109
213
|
const definition = token.definition;
|
|
110
214
|
const resolved = resolveAddress(definition, args);
|
|
111
215
|
const context = args[resolved.nextArgument];
|
|
@@ -116,7 +220,7 @@ export class SessionImpl {
|
|
|
116
220
|
return undefined;
|
|
117
221
|
const { observer: source, detach } = this.#attachDocument(definition, loaded, (value, release) => new CommittedStateSource(value, release));
|
|
118
222
|
try {
|
|
119
|
-
return replicatedState(source);
|
|
223
|
+
return replicatedState(source, { onError: (error) => this.report(error) });
|
|
120
224
|
}
|
|
121
225
|
catch (error) {
|
|
122
226
|
detach();
|
|
@@ -129,7 +233,7 @@ export class SessionImpl {
|
|
|
129
233
|
}
|
|
130
234
|
}
|
|
131
235
|
async watchDoc(token, ...args) {
|
|
132
|
-
this
|
|
236
|
+
this.assertUsable();
|
|
133
237
|
const definition = token.definition;
|
|
134
238
|
const resolved = resolveAddress(definition, args);
|
|
135
239
|
const context = args[resolved.nextArgument];
|
|
@@ -149,7 +253,7 @@ export class SessionImpl {
|
|
|
149
253
|
throw cancellationError(signal);
|
|
150
254
|
if (loaded === undefined)
|
|
151
255
|
return undefined;
|
|
152
|
-
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;
|
|
153
257
|
});
|
|
154
258
|
if (watch === undefined)
|
|
155
259
|
return undefined;
|
|
@@ -166,7 +270,7 @@ export class SessionImpl {
|
|
|
166
270
|
}
|
|
167
271
|
}
|
|
168
272
|
async snapshotAsOf(token, ...args) {
|
|
169
|
-
this
|
|
273
|
+
this.assertUsable();
|
|
170
274
|
const definition = token.definition;
|
|
171
275
|
const resolved = resolveAddress(definition, args);
|
|
172
276
|
if (resolved.address.scope.kind !== "conversation") {
|
|
@@ -207,16 +311,42 @@ export class SessionImpl {
|
|
|
207
311
|
.then(() => this.beforeClose())
|
|
208
312
|
.then(() => this.#enqueue(async () => {
|
|
209
313
|
this.#commitListeners.clear();
|
|
314
|
+
this.#internalListeners.clear();
|
|
210
315
|
this.#documents.clear();
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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();
|
|
217
335
|
}
|
|
218
336
|
return awaitWithContext(this.#closing, context);
|
|
219
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) { }
|
|
220
350
|
/**
|
|
221
351
|
* Runs inside every transaction that creates or forks a conversation, after the conversation record is staged. A
|
|
222
352
|
* plain Session stages nothing; a Harness stages its built-in documents.
|
|
@@ -228,15 +358,27 @@ export class SessionImpl {
|
|
|
228
358
|
beforeClose() {
|
|
229
359
|
return Promise.resolve();
|
|
230
360
|
}
|
|
231
|
-
/** 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. */
|
|
232
362
|
subscribeCommits(listener) {
|
|
233
|
-
this
|
|
363
|
+
this.assertUsable();
|
|
234
364
|
this.#commitListeners.add(listener);
|
|
235
365
|
return () => this.#commitListeners.delete(listener);
|
|
236
366
|
}
|
|
237
|
-
/**
|
|
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
|
+
*/
|
|
238
380
|
subscribeClose(listener) {
|
|
239
|
-
this
|
|
381
|
+
this.assertUsable();
|
|
240
382
|
this.#closeListeners.add(listener);
|
|
241
383
|
return () => this.#closeListeners.delete(listener);
|
|
242
384
|
}
|
|
@@ -244,8 +386,120 @@ export class SessionImpl {
|
|
|
244
386
|
unloadDocuments() {
|
|
245
387
|
return this.#enqueue(async () => {
|
|
246
388
|
this.#documents.clear();
|
|
389
|
+
this.#idleDocuments.clear();
|
|
247
390
|
});
|
|
248
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
|
+
}
|
|
249
503
|
async #runCommit(change, context, scope) {
|
|
250
504
|
this.#assertHealthy();
|
|
251
505
|
context.abortSignal?.throwIfAborted();
|
|
@@ -253,14 +507,26 @@ export class SessionImpl {
|
|
|
253
507
|
let result;
|
|
254
508
|
try {
|
|
255
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();
|
|
256
512
|
}
|
|
257
513
|
catch (error) {
|
|
258
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();
|
|
259
525
|
throw error;
|
|
260
526
|
}
|
|
261
|
-
const writes = await tx.settleSuccess();
|
|
262
527
|
if (writes.length === 0) {
|
|
263
528
|
tx.discard();
|
|
529
|
+
this.#sweepDocuments();
|
|
264
530
|
return result;
|
|
265
531
|
}
|
|
266
532
|
let seq;
|
|
@@ -269,10 +535,8 @@ export class SessionImpl {
|
|
|
269
535
|
seq = await this.#storage.commit(writes, withoutAbortSignal(context));
|
|
270
536
|
}
|
|
271
537
|
catch (error) {
|
|
538
|
+
// The guard has failed the Session.
|
|
272
539
|
tx.discard();
|
|
273
|
-
// Callback errors never reach this branch; StorageRejected alone guarantees that no batch effect committed.
|
|
274
|
-
if (!(error instanceof StorageRejected))
|
|
275
|
-
this.#poison = { error };
|
|
276
540
|
throw error;
|
|
277
541
|
}
|
|
278
542
|
let documents;
|
|
@@ -281,14 +545,17 @@ export class SessionImpl {
|
|
|
281
545
|
}
|
|
282
546
|
catch (error) {
|
|
283
547
|
// Storage already committed; a failed adoption leaves memory behind durable state.
|
|
284
|
-
this
|
|
548
|
+
this.fail(error);
|
|
285
549
|
throw error;
|
|
286
550
|
}
|
|
551
|
+
for (const document of documents)
|
|
552
|
+
this.#touchDocument(addressId(document.record));
|
|
287
553
|
this.#publish(seq, writes, documents, context);
|
|
554
|
+
this.#sweepDocuments();
|
|
288
555
|
return result;
|
|
289
556
|
}
|
|
290
557
|
#publish(seq, writes, documents, context) {
|
|
291
|
-
if (this.#commitListeners.size === 0)
|
|
558
|
+
if (this.#commitListeners.size === 0 && this.#internalListeners.size === 0)
|
|
292
559
|
return;
|
|
293
560
|
const changes = [];
|
|
294
561
|
for (const write of writes) {
|
|
@@ -303,8 +570,13 @@ export class SessionImpl {
|
|
|
303
570
|
for (const document of documents)
|
|
304
571
|
changes.push(document);
|
|
305
572
|
const publication = { seq, changes };
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
}
|
|
308
580
|
}
|
|
309
581
|
/**
|
|
310
582
|
* Attach an observer to one committed incarnation: check the definition, then forward this incarnation's committed
|
|
@@ -315,13 +587,23 @@ export class SessionImpl {
|
|
|
315
587
|
checkRecordVersion(definition, loaded.record, loaded.storedVersion);
|
|
316
588
|
let unsubscribeCommit = () => { };
|
|
317
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);
|
|
318
592
|
const detach = () => {
|
|
319
593
|
unsubscribeCommit();
|
|
320
594
|
unsubscribeClose();
|
|
595
|
+
unpin();
|
|
321
596
|
};
|
|
322
|
-
|
|
597
|
+
let observer;
|
|
598
|
+
try {
|
|
599
|
+
observer = create(loaded.tracker.value, detach);
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
unpin();
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
323
605
|
const observed = { version: loaded.valueVersion };
|
|
324
|
-
unsubscribeCommit = this.
|
|
606
|
+
unsubscribeCommit = this.observeCommits((publication, context) => {
|
|
325
607
|
for (const change of publication.changes) {
|
|
326
608
|
if (change.type !== "document" || change.record.id !== loaded.record.id)
|
|
327
609
|
continue;
|
|
@@ -331,19 +613,24 @@ export class SessionImpl {
|
|
|
331
613
|
// A migration-only base changes nothing for an observer of the new version.
|
|
332
614
|
if (ops.length === 0)
|
|
333
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();
|
|
334
619
|
observer.advance(change.value, ops, frameContext);
|
|
335
620
|
}
|
|
336
621
|
});
|
|
337
|
-
unsubscribeClose = this.subscribeClose(() => observer.closeSession());
|
|
622
|
+
unsubscribeClose = this.subscribeClose(() => observer.closeSession(this.#failure));
|
|
338
623
|
return { observer, detach };
|
|
339
624
|
}
|
|
340
625
|
async #loadDocument(definition, addressId, address, context) {
|
|
341
626
|
const cached = this.#documents.get(addressId);
|
|
342
627
|
// A tracker serves only tokens of the version its value was materialized for; others reload from Storage.
|
|
343
|
-
if (cached?.valueVersion === definition.version)
|
|
628
|
+
if (cached?.valueVersion === definition.version) {
|
|
629
|
+
this.#touchDocument(addressId);
|
|
344
630
|
return cached;
|
|
631
|
+
}
|
|
345
632
|
if (cached !== undefined)
|
|
346
|
-
this.#
|
|
633
|
+
this.#dropDocument(addressId);
|
|
347
634
|
const record = await this.#storage.findDocument(address, "current", context);
|
|
348
635
|
if (record === undefined)
|
|
349
636
|
return undefined;
|
|
@@ -360,6 +647,7 @@ export class SessionImpl {
|
|
|
360
647
|
tracker: track(value),
|
|
361
648
|
};
|
|
362
649
|
this.#documents.set(addressId, loaded);
|
|
650
|
+
this.#touchDocument(addressId);
|
|
363
651
|
return loaded;
|
|
364
652
|
}
|
|
365
653
|
#enqueue(job) {
|
|
@@ -367,17 +655,30 @@ export class SessionImpl {
|
|
|
367
655
|
this.#tail = run.then(() => undefined, () => undefined);
|
|
368
656
|
return run;
|
|
369
657
|
}
|
|
370
|
-
|
|
658
|
+
/** Internal: throw `SessionFailed` when failed, else an error when closing. */
|
|
659
|
+
assertUsable() {
|
|
660
|
+
this.#assertHealthy();
|
|
371
661
|
if (this.#closing !== undefined)
|
|
372
662
|
throw new Error("Session is closed");
|
|
373
|
-
this.#assertHealthy();
|
|
374
663
|
}
|
|
375
664
|
#assertHealthy() {
|
|
376
|
-
if (this.#
|
|
377
|
-
throw new
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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);
|
|
381
682
|
}
|
|
382
683
|
}
|
|
383
684
|
/**
|
|
@@ -395,4 +696,116 @@ function observedOperations(observed, change) {
|
|
|
395
696
|
function cancellationError(signal) {
|
|
396
697
|
return signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
|
|
397
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
|
+
}
|
|
398
811
|
//# sourceMappingURL=session.js.map
|