@mcp-b/do-runtime 0.1.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 +14 -0
- package/LICENSE +110 -0
- package/LICENSE.workerd +176 -0
- package/NOTICE +7 -0
- package/README.md +282 -0
- package/dist/backends/node-sqlite.d.ts +38 -0
- package/dist/backends/node-sqlite.js +335 -0
- package/dist/backends/node-sqlite.js.map +1 -0
- package/dist/backends/sqlite-wasm.d.ts +130 -0
- package/dist/backends/sqlite-wasm.js +259 -0
- package/dist/backends/sqlite-wasm.js.map +1 -0
- package/dist/chunks/sqlite-DFg92Tgt.js +498 -0
- package/dist/chunks/sqlite-DFg92Tgt.js.map +1 -0
- package/dist/cloudflare-workers.js +351 -0
- package/dist/cloudflare-workers.js.map +1 -0
- package/dist/conformance/host.d.ts +58 -0
- package/dist/conformance.js +18 -0
- package/dist/conformance.js.map +1 -0
- package/dist/index.js +7184 -0
- package/dist/index.js.map +1 -0
- package/dist/server/alarm-scheduler.js +513 -0
- package/dist/server/alarm-scheduler.js.map +1 -0
- package/dist/src/api/actor-state.d.ts +396 -0
- package/dist/src/api/actor.d.ts +306 -0
- package/dist/src/api/cloudflare-workers.d.ts +259 -0
- package/dist/src/api/export-loopback.d.ts +264 -0
- package/dist/src/api/global-scope.d.ts +262 -0
- package/dist/src/api/http.d.ts +52 -0
- package/dist/src/api/sql.d.ts +188 -0
- package/dist/src/api/sync-kv.d.ts +51 -0
- package/dist/src/api/web-socket.d.ts +93 -0
- package/dist/src/api/worker-loader.d.ts +354 -0
- package/dist/src/index.d.ts +130 -0
- package/dist/src/io/actor-cache.d.ts +203 -0
- package/dist/src/io/actor-id.d.ts +74 -0
- package/dist/src/io/actor-sqlite.d.ts +298 -0
- package/dist/src/io/io-channels.d.ts +191 -0
- package/dist/src/io/io-context.d.ts +451 -0
- package/dist/src/io/io-gate.d.ts +298 -0
- package/dist/src/io/worker-source.d.ts +108 -0
- package/dist/src/io/worker.d.ts +88 -0
- package/dist/src/server/actor-container.d.ts +525 -0
- package/dist/src/server/actor-id-impl.d.ts +118 -0
- package/dist/src/server/alarm-scheduler.d.ts +201 -0
- package/dist/src/server/facet-deletion.d.ts +156 -0
- package/dist/src/server/facet-tree-index.d.ts +94 -0
- package/dist/src/server/sha256.d.ts +39 -0
- package/dist/src/transport/rpc-session.d.ts +34 -0
- package/dist/src/util/sqlite-kv.d.ts +98 -0
- package/dist/src/util/sqlite-metadata.d.ts +46 -0
- package/dist/src/util/sqlite.d.ts +291 -0
- package/package.json +111 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ← workerd `src/workerd/io/io-channels.h` — `IoChannelFactory::ActorClassChannel`,
|
|
3
|
+
* `ResourceLimits`, `WorkerStubChannel` and `DynamicWorkerSource`.
|
|
4
|
+
*
|
|
5
|
+
* **A partial port, and deliberately so.** `io-channels.h` is 492 lines and
|
|
6
|
+
* almost all of it is outgoing I/O: `SubrequestChannel`, `CacheClient`,
|
|
7
|
+
* `TimerChannel`, logfwdr and channel tokens. Every one of those is either a
|
|
8
|
+
* substrate boundary or a transport concern, and none of them is reachable from
|
|
9
|
+
* the files this module exists to serve. What IS reachable is four types. One is
|
|
10
|
+
* the token `DurableObjectClass` holds and
|
|
11
|
+
* `Worker::Actor::FacetManager::StartInfo` carries, which has to live in `io/`
|
|
12
|
+
* because `io/worker.ts` names it and `io/` may not see `api/`. The other three
|
|
13
|
+
* are the dynamic-worker half `api/worker-loader.ts` consumes; an earlier
|
|
14
|
+
* revision of this header deferred them to Section 7, which is where they landed.
|
|
15
|
+
*
|
|
16
|
+
* Two things upstream has here that this package does not, both for the same
|
|
17
|
+
* reason. `IoChannelFactory` itself is absent because **there is no numbered
|
|
18
|
+
* channel table**: upstream resolves a binding to a `uint` at configuration time
|
|
19
|
+
* and every channel method takes that number, while here a binding is a property
|
|
20
|
+
* of the `env` object the consumer supplies. So every `kj::OneOf<uint,
|
|
21
|
+
* IoOwn<...>>` in `api/actor.ts` keeps only its object arm — which is upstream's
|
|
22
|
+
* own alternative, offered on `DurableObjectNamespace` for exactly the case
|
|
23
|
+
* where a namespace "is constructed dynamically within an execution context,
|
|
24
|
+
* rather than being a long-lived binding". And `ActorChannel` is absent because
|
|
25
|
+
* its one method returns a `WorkerInterface`, capnp dispatch with no port; the
|
|
26
|
+
* JS-visible product of an actor channel is a `Fetcher`, so the two collapse —
|
|
27
|
+
* the same collapse `io/worker.ts`'s `FacetManager.getFacet` and
|
|
28
|
+
* `server/actor-container.ts`'s `FacetHandle.stub` already make. That collapse
|
|
29
|
+
* reaches `SubrequestChannel` too, which is why `DynamicWorkerSource`'s outbound
|
|
30
|
+
* and tail fields below are `Fetcher`s.
|
|
31
|
+
*
|
|
32
|
+
* Spec: §1.10, §1.11, decisions 14 and 15 in
|
|
33
|
+
* docs/decisions.md.
|
|
34
|
+
*/
|
|
35
|
+
import type { WorkerSource } from "./worker-source.js";
|
|
36
|
+
/**
|
|
37
|
+
* ← `IoChannelFactory::ActorClassChannel`. Upstream: "a reference to an actor
|
|
38
|
+
* class in another worker. This class acts as a token which can be passed into
|
|
39
|
+
* other interfaces that might use the actor class, particularly
|
|
40
|
+
* `Worker::Actor::FacetManager`" and "This class has no functional methods, since
|
|
41
|
+
* it serves as a token to be passed to other interfaces (namely the facets API)."
|
|
42
|
+
*
|
|
43
|
+
* Ours is the same token with the contents workerd keeps opaque made visible,
|
|
44
|
+
* because they have to be: upstream's token is opaque precisely because the class
|
|
45
|
+
* is in another isolate and only the supervisor can resolve it, whereas here the
|
|
46
|
+
* container resolves it against the `ctx.exports` record the consumer handed in.
|
|
47
|
+
* The class name is the host-visible value the container needs to resolve the
|
|
48
|
+
* otherwise opaque token against `ctx.exports`.
|
|
49
|
+
*
|
|
50
|
+
* `requireAllowsTransfer()` is upstream's and is kept, because it is the one
|
|
51
|
+
* behaviour on the token rather than data: it decides whether a stub over this
|
|
52
|
+
* channel may be serialized, and `DurableObjectClass.serialize` calls it before
|
|
53
|
+
* anything else.
|
|
54
|
+
*/
|
|
55
|
+
export interface ActorClassChannel {
|
|
56
|
+
/** The `ctx.exports` key the container resolves this class under. */
|
|
57
|
+
readonly className: string;
|
|
58
|
+
/**
|
|
59
|
+
* ← `requireAllowsTransfer()`. "Throws a JSG error if a Fetcher backed by this
|
|
60
|
+
* channel should not be serialized and passed to other workers."
|
|
61
|
+
*/
|
|
62
|
+
requireAllowsTransfer(): void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* ← `ResourceLimits` (`io-channels.h:375-386`). "provides a means to control the
|
|
66
|
+
* resource allocation for a worker stage via a set of optionally overridden
|
|
67
|
+
* parameters."
|
|
68
|
+
*
|
|
69
|
+
* **It is a no-op in workerd and it is a no-op here.** The bag is accepted by
|
|
70
|
+
* `WorkerLoader.load`, by `WorkerStub.getEntrypoint`/`getDurableObjectClass`, and
|
|
71
|
+
* by both `WorkerStubChannel` methods; the one implementation that receives it —
|
|
72
|
+
* `Server::WorkerLoaderNamespace::WorkerStubImpl::getEntrypointResolved` and
|
|
73
|
+
* `::getActorClassResolved` (`server.c++:4359`, `:4364`) — names the parameter
|
|
74
|
+
* and never reads it. `DynamicWorkerSource.limits` is likewise written by
|
|
75
|
+
* `toDynamicWorkerSource` (`worker-loader.c++:165`) and read by nothing in the
|
|
76
|
+
* open-source tree. So local development never reproduces limit enforcement on
|
|
77
|
+
* either runtime, and a reader who assumes otherwise is reading a field that is
|
|
78
|
+
* carried to production and dropped in workerd. `clone()` is absent for the
|
|
79
|
+
* reason `io/worker-source.ts`'s header gives.
|
|
80
|
+
*/
|
|
81
|
+
export type ResourceLimits = {
|
|
82
|
+
readonly cpuMs?: number;
|
|
83
|
+
readonly subRequests?: number;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* ← `CompatibilityDateValidation` (`io/compatibility-date.h:12-32`). How far into
|
|
87
|
+
* the future a dynamic Worker's `compatibilityDate` may be, which upstream says
|
|
88
|
+
* "will differ between workerd vs. production".
|
|
89
|
+
*/
|
|
90
|
+
export type CompatibilityDateValidation =
|
|
91
|
+
/** "Allow dates up through the date specified by `supportedCompatibilityDate`." */
|
|
92
|
+
"codeVersion"
|
|
93
|
+
/** "Allow dates up to through the current date. This should ONLY be used by Cloudflare." */
|
|
94
|
+
| "currentDateForCloudflare"
|
|
95
|
+
/** "Allow any future date. This should only be used to test `compileCompatibilityFlags` itself." */
|
|
96
|
+
| "futureForTest";
|
|
97
|
+
/**
|
|
98
|
+
* ← the arguments `compileCompatibilityFlags` takes
|
|
99
|
+
* (`io/compatibility-date.h:38-44`), where upstream's `DynamicWorkerSource` holds
|
|
100
|
+
* the compiled `CompatibilityFlags::Reader` it returns.
|
|
101
|
+
*
|
|
102
|
+
* **The compilation is a substrate boundary and this is the request that replaces
|
|
103
|
+
* it.** `compileCompatibilityFlags` walks the capnp *schema* of
|
|
104
|
+
* `CompatibilityFlags` reflectively — every flag's enable/disable names, its
|
|
105
|
+
* default-on date and its experimental annotation are schema annotations
|
|
106
|
+
* (`io/compatibility-date.c++:102-260`) — and this package has no
|
|
107
|
+
* `compatibility-date.capnp`, no schema reflection and no `FeatureFlags`. It is
|
|
108
|
+
* the same absence the four flags `api/actor.ts` meets already records. So the
|
|
109
|
+
* inputs travel to whatever is on the far side of `loadIsolate`, which is the
|
|
110
|
+
* layer that has an isolate to configure and therefore the only layer that could
|
|
111
|
+
* validate them.
|
|
112
|
+
*/
|
|
113
|
+
export type CompatibilityFlagsRequest = {
|
|
114
|
+
readonly compatibilityDate: string;
|
|
115
|
+
readonly compatibilityFlags: readonly string[];
|
|
116
|
+
readonly allowExperimental: boolean;
|
|
117
|
+
readonly dateValidation: CompatibilityDateValidation;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* ← the two arguments `WorkerStubChannel`'s methods take besides the entrypoint
|
|
121
|
+
* name (`io-channels.h:397-406`).
|
|
122
|
+
*
|
|
123
|
+
* `props` is upstream's `Frankenvalue`, which is a JS value plus a cap table; here
|
|
124
|
+
* it is the JS value, because there is no numbered channel table for a cap table
|
|
125
|
+
* to index.
|
|
126
|
+
*/
|
|
127
|
+
export type EntrypointRequest = {
|
|
128
|
+
/** `kj::none` selects the default entrypoint, as `"default"` does. */
|
|
129
|
+
readonly name: string | undefined;
|
|
130
|
+
readonly props: unknown;
|
|
131
|
+
readonly limits: ResourceLimits | undefined;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* ← `WorkerStubChannel` (`io-channels.h:390-408`). "Represents a dynamically-loaded
|
|
135
|
+
* Worker to which requests can be sent. This object is returned before the Worker
|
|
136
|
+
* actually loads, so if any errors occur while loading, any requests sent to the
|
|
137
|
+
* Worker will fail, propagating the exception."
|
|
138
|
+
*
|
|
139
|
+
* That last sentence is the contract `api/worker-loader.ts` depends on twice over:
|
|
140
|
+
* it is why `WorkerLoader.get` can return a stub synchronously while its code
|
|
141
|
+
* callback has not run, and it is why upstream's own tests assert a bad module
|
|
142
|
+
* list from `worker.getEntrypoint().greet(...)` rather than from `get()`
|
|
143
|
+
* (`api/tests/worker-loader-test.js:812`).
|
|
144
|
+
*
|
|
145
|
+
* Upstream splits each method in two — a non-virtual half that "waits for `props`
|
|
146
|
+
* to resolve first" and a virtual `…Resolved` half. The waiting half resolves
|
|
147
|
+
* `Frankenvalue` cap-table entries that are still promises; a JS value has no
|
|
148
|
+
* unresolved arm, so the pair collapses to one method.
|
|
149
|
+
*/
|
|
150
|
+
export interface WorkerStubChannel {
|
|
151
|
+
/** ← `getEntrypoint` / `getEntrypointResolved`, with `SubrequestChannel` collapsed. */
|
|
152
|
+
getEntrypoint(request: EntrypointRequest): Fetcher;
|
|
153
|
+
/** ← `getActorClass` / `getActorClassResolved`. */
|
|
154
|
+
getActorClass(request: EntrypointRequest): ActorClassChannel;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* ← `DynamicWorkerSource` (`io-channels.h:411-455`). "Source code needed to
|
|
158
|
+
* dynamically load a Worker."
|
|
159
|
+
*
|
|
160
|
+
* Field for field, with three departures, each recorded on the type it touches:
|
|
161
|
+
* `compatibilityFlags` carries the request rather than the compiled reader
|
|
162
|
+
* (`CompatibilityFlagsRequest` above), the three `SubrequestChannel` fields are
|
|
163
|
+
* `Fetcher`s (this file's header), and `ownContent` / `ownContentIsRpcResponse` /
|
|
164
|
+
* `clone()` / `ensureAllResolved()` are kj lifetime bookkeeping with nothing to
|
|
165
|
+
* bookkeep (`io/worker-source.ts`'s header).
|
|
166
|
+
*/
|
|
167
|
+
export type DynamicWorkerSource = {
|
|
168
|
+
readonly source: WorkerSource;
|
|
169
|
+
readonly compatibilityFlags: CompatibilityFlagsRequest;
|
|
170
|
+
readonly limits: ResourceLimits | undefined;
|
|
171
|
+
/**
|
|
172
|
+
* "`env` object to pass to the loaded worker. Can contain anything that can be
|
|
173
|
+
* serialized to a `Frankenvalue`."
|
|
174
|
+
*/
|
|
175
|
+
readonly env: unknown;
|
|
176
|
+
/**
|
|
177
|
+
* "Where should global fetch() (and connect()) be sent?"
|
|
178
|
+
*
|
|
179
|
+
* **`undefined` means blocked, and that is upstream's encoding rather than a
|
|
180
|
+
* shorthand.** The JS-facing `globalOutbound` has three states — omitted, `null`,
|
|
181
|
+
* a `Fetcher` — and `toDynamicWorkerSource` collapses them to two here:
|
|
182
|
+
* `kj::none` is written only for the explicit `null`, while the omitted case is
|
|
183
|
+
* resolved at that layer into the calling worker's own outbound
|
|
184
|
+
* (`worker-loader.c++:122-139`). By the time a source exists, "inherit" is no
|
|
185
|
+
* longer a state.
|
|
186
|
+
*/
|
|
187
|
+
readonly globalOutbound: Fetcher | undefined;
|
|
188
|
+
/** "Tail workers that should receive tail events for invocations of the dynamic worker." */
|
|
189
|
+
readonly tails: readonly Fetcher[];
|
|
190
|
+
readonly streamingTails: readonly Fetcher[];
|
|
191
|
+
};
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ← workerd `src/workerd/io/io-context.{h,c++}`
|
|
3
|
+
*
|
|
4
|
+
* IoContext: the one door, plus the two await forms.
|
|
5
|
+
*
|
|
6
|
+
* This is the file with no true upstream correspondence for its *enforcement*.
|
|
7
|
+
* Workerd acquires the input gate at isolate entry, so acquisition is
|
|
8
|
+
* structural. We have no isolate hook, so a lock is taken at our own dispatch
|
|
9
|
+
* boundary instead. See "The enforcement point is the one thing we cannot port"
|
|
10
|
+
* in the design record.
|
|
11
|
+
*
|
|
12
|
+
* What this file does, stated before anything about how it got here:
|
|
13
|
+
*
|
|
14
|
+
* 1. `#currentInputLocks` is a STACK of the locks held by the slices that are
|
|
15
|
+
* running. It is upstream's single `kj::Maybe<InputGate::Lock>
|
|
16
|
+
* currentInputLock` member (`io-context.h:993`) with the `Maybe` widened,
|
|
17
|
+
* and a "frame" is an entry in it. A frame carries nothing else: the only
|
|
18
|
+
* two things ever read from one are the lock (`getInputLock`) and the
|
|
19
|
+
* critical section that lock belongs to (`getCriticalSection`), and the
|
|
20
|
+
* lock answers both.
|
|
21
|
+
* 2. A lock is released, and leaves the stack, at the END OF THE MICROTASK
|
|
22
|
+
* CHECKPOINT of the slice holding it — not when that slice's synchronous
|
|
23
|
+
* body returns. That is upstream's own boundary, not a relaxation of it:
|
|
24
|
+
* `runImpl`'s inner `KJ_DEFER` calls `js.runMicrotasks()`
|
|
25
|
+
* (`io-context.c++:1262`) and `runInContextScope`'s outer one then clears
|
|
26
|
+
* `currentInputLock` (`:1214`), inner scope first, so the whole checkpoint
|
|
27
|
+
* drains under the lock. `currentInputLock` holds the `Lock` by value, so
|
|
28
|
+
* clearing it is the release. `atCheckpointEnd` below is that point.
|
|
29
|
+
* 3. NEITHER await form releases anything at the moment of the await.
|
|
30
|
+
* `awaitIo` reads `getCriticalSection()` and `awaitIoWithInputLock` takes
|
|
31
|
+
* an `addRef`; both then return a promise and let the slice end normally.
|
|
32
|
+
* So an invocation that calls `awaitIo` keeps the gate until its own
|
|
33
|
+
* checkpoint-end exit and the gate opens there — which is still before any
|
|
34
|
+
* real I/O can complete, so "a bare timer releases the input gate" holds.
|
|
35
|
+
* Upstream is the same: `getCriticalSection()` (`io-context.c++:362`) does
|
|
36
|
+
* not touch `currentInputLock`, and `:1214` is the only place that clears
|
|
37
|
+
* it. The difference between the two forms is entirely on the far side —
|
|
38
|
+
* `awaitIo` re-enters through `run(func, criticalSection)` and queues for a
|
|
39
|
+
* fresh lock, `awaitIoWithInputLock` re-enters holding the ref it took.
|
|
40
|
+
* 4. Removal from the stack is by identity, not by popping, because entries do
|
|
41
|
+
* overlap — three deep in the unit tests. One invocation can have several
|
|
42
|
+
* held awaits outstanding (`Promise.all` over two storage reads), and a
|
|
43
|
+
* section's last body slice overlaps the slice that resolves it.
|
|
44
|
+
*
|
|
45
|
+
* Two mechanics were validated against real workerd before this scaffolding
|
|
46
|
+
* landed, and both are easy to get wrong:
|
|
47
|
+
*
|
|
48
|
+
* 1. The ambient "which lock am I under" must be a STACK of invocation frames,
|
|
49
|
+
* not a single slot, so that `current` always names the running invocation.
|
|
50
|
+
* The prototype that found this stated it as "`awaitIo` splices its frame
|
|
51
|
+
* out and re-pushes the SAME frame on resume". That is not what happens
|
|
52
|
+
* here and the difference is worth knowing: nothing splices at the point of
|
|
53
|
+
* the await, because a lock already leaves the stack when its slice ends,
|
|
54
|
+
* which is the same event that releases it (`#exit` does both). A
|
|
55
|
+
* resumption then pushes a fresh lock. The prototype needed the splice
|
|
56
|
+
* because its door held one lock for a whole invocation; the identity that
|
|
57
|
+
* has to survive an await here is the critical section, and that is
|
|
58
|
+
* captured at the call rather than looked up later.
|
|
59
|
+
* 2. That ambient is safe here for a reason that does NOT generalise to the
|
|
60
|
+
* §2.3 ambient-field hazard: the gate guarantees pushes and pops nest
|
|
61
|
+
* properly in time, because only one holder chain is inside at once.
|
|
62
|
+
* `_cf_currentSubAgentBridge` has no such guarantee, which is why it is a
|
|
63
|
+
* live bug and this is not.
|
|
64
|
+
*
|
|
65
|
+
* Consequence: no async context is required. Do not add a dependency on
|
|
66
|
+
* decision 8 here without re-running the conformance gate suite first.
|
|
67
|
+
*
|
|
68
|
+
* The invariant a future simplifier has to re-check: a single slot would pass
|
|
69
|
+
* every test in `io-context.test.ts`, and that was established by trying it,
|
|
70
|
+
* not by argument. `current()` is only read from inside a slice, every slice
|
|
71
|
+
* pushes its own lock last, and no second slice can begin while an earlier
|
|
72
|
+
* frame is still waiting for its checkpoint-end exit — that frame is holding
|
|
73
|
+
* the gate. So the top of a stack and the last write to a slot always agree.
|
|
74
|
+
* The stack is still what is here, because a slot would be holding a stale
|
|
75
|
+
* value at every one of the overlaps in (4), and that is unobservable for
|
|
76
|
+
* exactly as long as the invariant holds. Tightening (2) back to the end of the
|
|
77
|
+
* synchronous body breaks it immediately, which is the regime the mechanic was
|
|
78
|
+
* found in.
|
|
79
|
+
*
|
|
80
|
+
* Spec: §1.2, §1.3, §1.5, §1.6, §1.7.1, §1.9, decisions 1, 2, 4 and 13 in
|
|
81
|
+
* docs/decisions.md.
|
|
82
|
+
*
|
|
83
|
+
* `TimeoutManager` is the only part of this file a consumer's own code reaches
|
|
84
|
+
* directly: every host-provided async primitive a
|
|
85
|
+
* Durable Object can await has to route through here, or the continuation after
|
|
86
|
+
* it resumes with an empty invocation stack. `TimeoutManager` below is the
|
|
87
|
+
* timer half; `api/global-scope.ts` and `api/web-socket.ts` are the rest.
|
|
88
|
+
*
|
|
89
|
+
* Not ported, because the substrate has no equivalent to port onto: isolate and
|
|
90
|
+
* async locks (`Worker::Lock`, `jsg::Lock`, `takeAsyncLock`) and everything
|
|
91
|
+
* that exists to enter or leave an isolate — which is precisely the thing this
|
|
92
|
+
* file substitutes for; the limit enforcer and `afterLimitTimeout` (the
|
|
93
|
+
* deadline takes the `Timer` port instead); trace spans, already a recorded
|
|
94
|
+
* divergence documented in §1.12; subrequest channels and HTTP, which are
|
|
95
|
+
* `api/http.ts`'s gating over the substrate's own `fetch`;
|
|
96
|
+
* `IoOwn`/`IoPtr`/`DeleteQueue`, which guard cross-context dereferences that GC
|
|
97
|
+
* makes impossible; hang detection and `registerPendingEvent`, which need the
|
|
98
|
+
* isolate's own idea of pending work; and the thread-local
|
|
99
|
+
* `IoContext::current()` static, whose lock-resolving half the invocation stack
|
|
100
|
+
* replaces — its *identity* half is `currentSlice` below, narrowed to the
|
|
101
|
+
* synchronous slice, with one consumer and no resolver. `EventOutcome` and
|
|
102
|
+
* `RequestObserver` are metrics types with no port, so `waitUntilStatus()`
|
|
103
|
+
* returns the first exception instead.
|
|
104
|
+
*/
|
|
105
|
+
import { CriticalSection, type InputGate, Lock, type OutputGate } from "./io-gate.js";
|
|
106
|
+
/**
|
|
107
|
+
* ← `kj::Timer`, threaded into IoContext upstream.
|
|
108
|
+
*
|
|
109
|
+
* A port with one production implementation would be an invented seam; this
|
|
110
|
+
* one has two (real clock in browser and workerd) plus a fake the conformance
|
|
111
|
+
* suite cannot exist without — the 30-second critical-section deadline and the
|
|
112
|
+
* alarm retry ladder are not assertable on wall-clock time in CI.
|
|
113
|
+
*/
|
|
114
|
+
export interface Timer {
|
|
115
|
+
now(): number;
|
|
116
|
+
/** `kj::Timer::afterDelay`. The signal replaces kj's cancel-by-drop. */
|
|
117
|
+
afterDelay(ms: number, signal?: AbortSignal): Promise<void>;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* ← the `Worker::Actor` surface reached through an `IoContext`:
|
|
121
|
+
* `a.getInputGate()`, `a.getOutputGate()` and `a.shutdownActorCache()` from
|
|
122
|
+
* `io-context.{h,c++}` itself, plus `assertCanSetAlarm()`, which
|
|
123
|
+
* `api/actor-state.c++:486` reaches through
|
|
124
|
+
* `IoContext::current().getActorOrThrow()`.
|
|
125
|
+
*
|
|
126
|
+
* `Worker::Actor` lives in `io/worker.h`, the same Bazel target as this file, so
|
|
127
|
+
* naming the members its consumers use is upstream's own layering rather than a
|
|
128
|
+
* new seam. Every context here is an actor context — there is no non-actor
|
|
129
|
+
* request in a Durable Object runtime — so upstream's
|
|
130
|
+
* `kj::Maybe<Worker::Actor&>` and every branch that tests it collapse to this
|
|
131
|
+
* being required, and `getActorOrThrow()` cannot actually throw.
|
|
132
|
+
*/
|
|
133
|
+
export interface Actor {
|
|
134
|
+
getInputGate(): InputGate;
|
|
135
|
+
getOutputGate(): OutputGate;
|
|
136
|
+
/** Abort abandons scheduled writes rather than flushing them (§1.6). */
|
|
137
|
+
shutdownActorCache(reason: unknown): void;
|
|
138
|
+
/**
|
|
139
|
+
* ← `Worker::Actor::assertCanSetAlarm()` (`io/worker.c++:4090`). Every branch
|
|
140
|
+
* of it reads the actor's class-instance lifecycle, which `server/` owns, so
|
|
141
|
+
* `api/actor-state.ts`'s obligation is to call it and the container's is to
|
|
142
|
+
* answer it.
|
|
143
|
+
*/
|
|
144
|
+
assertCanSetAlarm(): void;
|
|
145
|
+
}
|
|
146
|
+
/** ← `afterLimitTimeout(30 * kj::SECONDS)` in `IoContext::blockConcurrencyWhile`. */
|
|
147
|
+
export declare const BLOCK_CONCURRENCY_WHILE_TIMEOUT_MS = 30000;
|
|
148
|
+
/** Copied verbatim: users and upstream tests match on it. */
|
|
149
|
+
export declare const BLOCK_CONCURRENCY_WHILE_TIMEOUT_MESSAGE: string;
|
|
150
|
+
/**
|
|
151
|
+
* THE check every storage entry point in `api/` makes before touching the
|
|
152
|
+
* database, and the only place this package throws for a missing input lock.
|
|
153
|
+
*
|
|
154
|
+
* Upstream never faces the question. `IoContext::current()` is a thread-local
|
|
155
|
+
* read with a `KJ_REQUIRE` behind it, and it cannot fail for a storage call
|
|
156
|
+
* because isolate entry is the only way into an actor and it always took a
|
|
157
|
+
* lock. We have no isolate hook (see this file's header), so a continuation
|
|
158
|
+
* that resumed from a raw `setTimeout`, a raw `fetch`, or any other promise the
|
|
159
|
+
* runtime does not own comes back with an empty invocation stack, and its next
|
|
160
|
+
* storage call reaches `ActorSqlite` — which is synchronous, touches no gate,
|
|
161
|
+
* and would happily serve it outside any transaction boundary.
|
|
162
|
+
*
|
|
163
|
+
* So this throws, matching the assert. It is deliberately ONE function called
|
|
164
|
+
* from every entry point rather than a check written at each of them: the
|
|
165
|
+
* policy is a design decision that belongs to the design record, and when it
|
|
166
|
+
* changes it has to change in one place rather than across a surface the
|
|
167
|
+
* vendored consumer reaches from hundreds of call sites. There is no lenient
|
|
168
|
+
* mode, no flag, and no implicit acquire — a lost invocation is loud, and a
|
|
169
|
+
* lost transaction boundary is not.
|
|
170
|
+
*/
|
|
171
|
+
export declare function requireInputLock(ctx: IoContext, op: string): void;
|
|
172
|
+
/**
|
|
173
|
+
* The end of the microtask checkpoint — the moment `runImpl`'s `KJ_DEFER` fires.
|
|
174
|
+
*
|
|
175
|
+
* Upstream drains the whole checkpoint INSIDE the isolate run: the defer calls
|
|
176
|
+
* `js.runMicrotasks()` and only the outer defer in `runInContextScope` then
|
|
177
|
+
* clears `currentInputLock`. So a continuation that awaits nothing but
|
|
178
|
+
* already-resolved promises stays under the same lock, and a continuation that
|
|
179
|
+
* waits on real I/O does not. In JS the only observable end of a microtask drain
|
|
180
|
+
* is the next macrotask, so that is where a lock leaves the invocation stack.
|
|
181
|
+
*
|
|
182
|
+
* Measured, because the obvious alternatives are wrong in ways nothing catches:
|
|
183
|
+
* releasing synchronously when the invoked function returns, or on the next
|
|
184
|
+
* microtask, both hand the lock back BEFORE the code awaiting the I/O resumes as
|
|
185
|
+
* soon as one promise sits between the two — one `async` wrapper, a `.then`, a
|
|
186
|
+
* `Promise.all` — and `actor-state.ts` is exactly such a wrapper. The gate would
|
|
187
|
+
* then open in the middle of a storage await with no test failing, which is the
|
|
188
|
+
* silent loss of atomicity §1.7.1 names.
|
|
189
|
+
*
|
|
190
|
+
* `MessageChannel` and not `setTimeout`, decided by benchmark rather than by
|
|
191
|
+
* argument, because the two are three orders of magnitude apart on the shapes
|
|
192
|
+
* that wait for a release. A chain of these schedules each hand-off from inside
|
|
193
|
+
* the previous one's callback, so a `setTimeout` chain's nesting level climbs
|
|
194
|
+
* past five and stays there, where browsers clamp it to 4ms. Median per hand-off
|
|
195
|
+
* over 50 chained hops:
|
|
196
|
+
*
|
|
197
|
+
* | | setTimeout | MessageChannel |
|
|
198
|
+
* | node | 1.273 ms | 0.018 ms |
|
|
199
|
+
* | chromium page | 4.96 ms | 0.024 ms |
|
|
200
|
+
* | chromium Worker | 5.542 ms | 0.022 ms |
|
|
201
|
+
*
|
|
202
|
+
* Only two shapes pay it: an `awaitIo` chain, which has to re-acquire the lock
|
|
203
|
+
* its slice gave up, and a queue of events waiting on the holder. A chain of
|
|
204
|
+
* held storage awaits does not — the lock passes from `addRef` to `addRef` and
|
|
205
|
+
* the release is off the critical path. So 50 sequential facet RPCs cost 277ms
|
|
206
|
+
* of pure clamp in a Worker under `setTimeout` and 1.1ms under this.
|
|
207
|
+
*
|
|
208
|
+
* One channel per BATCH, and a batch is an explicit array: `atCheckpointEnd`
|
|
209
|
+
* pushes onto `pendingCheckpointEnds` and only the push that finds it empty opens
|
|
210
|
+
* a channel. Ordering then comes from the array, which the language guarantees,
|
|
211
|
+
* rather than from delivery order across separate channels, which no
|
|
212
|
+
* specification does. That mattered because the storage engine below depends on
|
|
213
|
+
* a commit scheduled inside a slice running before the release scheduled at the
|
|
214
|
+
* end of that slice; separate channels do deliver in post order in Node,
|
|
215
|
+
* measured across 200 of them including one scheduled from inside another's
|
|
216
|
+
* callback, but a browser that chose otherwise would open the gate onto a
|
|
217
|
+
* transaction a previous event left open, and nothing would say so.
|
|
218
|
+
*
|
|
219
|
+
* A callback scheduled DURING a drain lands in the next batch, which is the
|
|
220
|
+
* semantics to want: one hand-off is one checkpoint end, and a slice that begins
|
|
221
|
+
* inside this drain gets its own. The drain runs every callback even if one
|
|
222
|
+
* throws, and rethrows the first exception afterwards, because abandoning the
|
|
223
|
+
* rest of a batch is how a gate wedges with nothing to see.
|
|
224
|
+
*
|
|
225
|
+
* A long-lived shared port was rejected for the reason it always is: it has to be
|
|
226
|
+
* closed on abort and `unref`'d so it cannot hold a test runner's event loop
|
|
227
|
+
* open, and an `unref`'d port can drop a release at exit — a wedged gate. A
|
|
228
|
+
* channel that lives for exactly one message cannot. Batching gets most of what
|
|
229
|
+
* a shared port was worth anyway: a slice that schedules a commit and a release
|
|
230
|
+
* now allocates one channel where it used to allocate two.
|
|
231
|
+
*
|
|
232
|
+
* **Exported because `kj::evalLater()` is this same point.** `ActorSqlite` opens
|
|
233
|
+
* its implicit transaction on the first write and commits it "on the next turn of
|
|
234
|
+
* the event loop" (`actor-sqlite.c++:352-357`); upstream's next turn is after the
|
|
235
|
+
* isolate run, which is after `js.runMicrotasks()`, which is after
|
|
236
|
+
* `currentInputLock` is cleared. Upstream's two boundaries are one boundary, and
|
|
237
|
+
* they stay one here only if the commit rides the same primitive as the release.
|
|
238
|
+
* Two consequences the storage engine depends on, both properties of this
|
|
239
|
+
* function rather than of `ActorSqlite`:
|
|
240
|
+
*
|
|
241
|
+
* 1. **Everything that holds the input lock across an await is a microtask
|
|
242
|
+
* chain.** `awaitIoWithInputLock` resumes through `#awaitIoImpl`'s `.then`
|
|
243
|
+
* into `run(func, lock)`, which never waits on the gate. So a whole run of
|
|
244
|
+
* held storage awaits finishes before the next hand-off and its writes are
|
|
245
|
+
* one transaction (§1.7.1 row 1).
|
|
246
|
+
* 2. **Everything that releases it needs at least one hand-off.** `awaitIo`
|
|
247
|
+
* resumes through `gate.wait()`, which cannot resolve until `#exit` runs
|
|
248
|
+
* here. So a timer or outbound await puts the commit between the two writes
|
|
249
|
+
* (§1.7.1 row 2).
|
|
250
|
+
*
|
|
251
|
+
*/
|
|
252
|
+
export declare function atCheckpointEnd(run: () => void): void;
|
|
253
|
+
/** ← `IoContext::tryCurrent()` (`io-context.c++:1416-1422`), over the narrowed scope above. */
|
|
254
|
+
export declare function tryCurrentSlice(): IoContext | undefined;
|
|
255
|
+
/**
|
|
256
|
+
* ← `jsg::isExceptionFromInputGateBroken` (`jsg/exception.c++:168-172`):
|
|
257
|
+
* "annotateBroken() produces 'broken.inputGateBroken; {message}', optionally
|
|
258
|
+
* prefixed with 'remote.' when crossing RPC boundaries. Strip the remote prefix
|
|
259
|
+
* first, then check the tag."
|
|
260
|
+
*
|
|
261
|
+
* Its writer is `annotateInputGateBroken` directly above, which is why the two
|
|
262
|
+
* live together rather than the reader moving to its consumer: `jsg/` has no
|
|
263
|
+
* module here, and a prefix known in two places is a prefix that drifts.
|
|
264
|
+
*/
|
|
265
|
+
export declare function isExceptionFromInputGateBroken(exception: unknown): boolean;
|
|
266
|
+
/**
|
|
267
|
+
* ← `jsg::EXCEPTION_IS_USER_ERROR` (`jsg/exception.h:160`), a
|
|
268
|
+
* `kj::Exception::DetailTypeId` attached to an arbitrary exception rather than a
|
|
269
|
+
* type of exception.
|
|
270
|
+
*
|
|
271
|
+
* A symbol-keyed property is the closest JS has: it rides any thrown object,
|
|
272
|
+
* survives a rethrow, and cannot collide with anything an application writes.
|
|
273
|
+
* `Symbol.for` rather than `Symbol()` so the detail is still legible after the
|
|
274
|
+
* exception crosses a realm — the mistake decision 18 records capnweb making.
|
|
275
|
+
*/
|
|
276
|
+
export declare const EXCEPTION_IS_USER_ERROR: unique symbol;
|
|
277
|
+
/** ← `error.setDetail(jsg::EXCEPTION_IS_USER_ERROR, kj::heapArray<byte>(0))`. */
|
|
278
|
+
export declare function setUserErrorDetail(exception: unknown): void;
|
|
279
|
+
/** ← `e.getDetail(jsg::EXCEPTION_IS_USER_ERROR) != kj::none`. */
|
|
280
|
+
export declare function hasUserErrorDetail(exception: unknown): boolean;
|
|
281
|
+
export declare class IoContext {
|
|
282
|
+
#private;
|
|
283
|
+
constructor(actor: Actor, timer: Timer);
|
|
284
|
+
/**
|
|
285
|
+
* Get the current input lock. Throws an exception if no input lock is held (e.g. because
|
|
286
|
+
* this is not an actor request).
|
|
287
|
+
*
|
|
288
|
+
* ← `KJ_ASSERT_NONNULL(currentInputLock, ...).addRef()`. The `addRef` IS the §1.2
|
|
289
|
+
* distinction: it is the only way to hold the gate past the end of this slice.
|
|
290
|
+
*/
|
|
291
|
+
getInputLock(): Lock;
|
|
292
|
+
/** Get the current CriticalSection, if there is one, or returns null if not. */
|
|
293
|
+
getCriticalSection(): CriticalSection | undefined;
|
|
294
|
+
/** Is a gated slice running? The question `IoContext::hasCurrent()` answers upstream. */
|
|
295
|
+
hasCurrent(): boolean;
|
|
296
|
+
/**
|
|
297
|
+
* ← `IoContext::isCurrent()` (`io-context.c++:1428-1430`), over the narrowed
|
|
298
|
+
* scope `currentSlice` documents: true only while a synchronous body of THIS
|
|
299
|
+
* context is on the JS stack.
|
|
300
|
+
*
|
|
301
|
+
* Distinct from `hasCurrent()` above, which asks whether this context holds a
|
|
302
|
+
* lock at all — true throughout an outstanding held await, and true for a
|
|
303
|
+
* parent whose slice is awaiting a facet while the facet's body runs. This one
|
|
304
|
+
* is the question a shared global has to answer: is the code calling me this
|
|
305
|
+
* actor's?
|
|
306
|
+
*/
|
|
307
|
+
isCurrentSlice(): boolean;
|
|
308
|
+
/**
|
|
309
|
+
* ← `IoContext::getActorOrThrow()`. Upstream's throws when the request is not
|
|
310
|
+
* an actor request; there is no such request here, so it is a plain accessor.
|
|
311
|
+
*/
|
|
312
|
+
getActorOrThrow(): Actor;
|
|
313
|
+
/** ← `IoContext::now()` (`io-context.h:703`), which reads the same timer. */
|
|
314
|
+
now(): number;
|
|
315
|
+
/**
|
|
316
|
+
* ← `IoContext::setTimeoutImpl` (`io-context.c++:885-899`), clamp included.
|
|
317
|
+
*
|
|
318
|
+
* The generator parameter is gone with `TimeoutId::Generator` — see
|
|
319
|
+
* `TimeoutManager`'s header — so the signature is upstream's minus that one
|
|
320
|
+
* argument.
|
|
321
|
+
*/
|
|
322
|
+
setTimeoutImpl(repeat: boolean, callback: () => void, msDelay: number): number;
|
|
323
|
+
/** ← `IoContext::clearTimeoutImpl` (`io-context.c++:901-903`). */
|
|
324
|
+
clearTimeoutImpl(id: number): void;
|
|
325
|
+
/** ← `IoContext::getTimeoutCount` (`io-context.c++:905-907`). */
|
|
326
|
+
getTimeoutCount(): number;
|
|
327
|
+
/**
|
|
328
|
+
* Wait until all outstanding output locks have been unlocked. Does not wait for future
|
|
329
|
+
* output locks, even if they are created before past locks are unlocked.
|
|
330
|
+
*/
|
|
331
|
+
waitForOutputLocks(): Promise<void>;
|
|
332
|
+
/**
|
|
333
|
+
* Check if the output gate is currently broken. This indicates that there was a problem
|
|
334
|
+
* with committing storage writes.
|
|
335
|
+
*/
|
|
336
|
+
isOutputGateBroken(): boolean;
|
|
337
|
+
/** Lock output until the given promise completes. */
|
|
338
|
+
lockOutputWhile<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
339
|
+
/**
|
|
340
|
+
* Rejects if and when the context should be aborted, e.g. because a gate broke. This
|
|
341
|
+
* promise never resolves, only rejects.
|
|
342
|
+
*/
|
|
343
|
+
onAbort(): Promise<never>;
|
|
344
|
+
/** Force context abort now. */
|
|
345
|
+
abort(exception: unknown): void;
|
|
346
|
+
/**
|
|
347
|
+
* Await the given promise and, if it throws, call `abort()` with the exception. The promise
|
|
348
|
+
* given here should just be a monitoring promise, it should not represent any sort of
|
|
349
|
+
* background work beyond monitoring.
|
|
350
|
+
*/
|
|
351
|
+
abortWhen(promise: Promise<unknown>): void;
|
|
352
|
+
/**
|
|
353
|
+
* Arrange for the given promise to execute as part of this request.
|
|
354
|
+
*
|
|
355
|
+
* "In Actors, we treat all tasks as wait-until tasks, because it's perfectly legit to start
|
|
356
|
+
* a task under one request and then expect some other request to handle it later." Every
|
|
357
|
+
* context here is an actor context, so that branch is the only branch.
|
|
358
|
+
*/
|
|
359
|
+
addTask(promise: Promise<void>): void;
|
|
360
|
+
/**
|
|
361
|
+
* Indicates that the script has requested that it stay active until the given promise
|
|
362
|
+
* resolves. `drainWaitUntil()` waits until all such promises have completed. Touches
|
|
363
|
+
* neither gate (§1.9).
|
|
364
|
+
*/
|
|
365
|
+
addWaitUntil(promise: Promise<void>): void;
|
|
366
|
+
/** Returns the number of times addTask() has been called (even if the tasks have completed). */
|
|
367
|
+
taskCount(): number;
|
|
368
|
+
/**
|
|
369
|
+
* The first exception a background task failed with, if any.
|
|
370
|
+
*
|
|
371
|
+
* ← `waitUntilStatus()`, which returns an `EventOutcome` derived from the exception by
|
|
372
|
+
* `RequestObserver`. There is no observer here, so the exception itself is the status —
|
|
373
|
+
* and keeping it is what stops a failed background task from being swallowed, since
|
|
374
|
+
* upstream's other half of `taskFailed()` is a log this package has no port for.
|
|
375
|
+
*/
|
|
376
|
+
waitUntilStatus(): unknown;
|
|
377
|
+
/**
|
|
378
|
+
* ← `IncomingRequest::drain()`, actor branch. "For actors, all promises are canceled on
|
|
379
|
+
* actor shutdown, not on a fixed timeout, because work doesn't necessarily happen on a
|
|
380
|
+
* per-request basis in actors."
|
|
381
|
+
*/
|
|
382
|
+
drainWaitUntil(): Promise<void>;
|
|
383
|
+
/**
|
|
384
|
+
* Run the given callback within this context, holding an input lock.
|
|
385
|
+
*
|
|
386
|
+
* ← the two `IoContext::run()` overloads: given a CriticalSection it waits on that, given
|
|
387
|
+
* an already-held Lock it runs under it, and given neither it takes a fresh lock from the
|
|
388
|
+
* gate. The third case is what a new external event does, and it is the reason inheritance
|
|
389
|
+
* cannot be read from gate state — see `makeReentryCallback`.
|
|
390
|
+
*/
|
|
391
|
+
run<T>(func: (lock: Lock) => T | PromiseLike<T>, ilOrCs?: Lock | CriticalSection): Promise<T>;
|
|
392
|
+
/**
|
|
393
|
+
* Make a function which, when called, re-enters this IoContext to run some code.
|
|
394
|
+
*
|
|
395
|
+
* Upstream, on why the critical section travels with the callback at all:
|
|
396
|
+
*
|
|
397
|
+
* > "What if the call was made within blockConcurrencyWhile()? The callback will be blocked
|
|
398
|
+
* > until the critical section ends, which could lead to deadlock if the critical section
|
|
399
|
+
* > code is waiting on it? ... The callback is allowed to run within the critical section
|
|
400
|
+
* > (blockConcurrencyWhile()) from which it was called."
|
|
401
|
+
*
|
|
402
|
+
* The section is read here, at the point of capture, and never on invocation: a new
|
|
403
|
+
* external event that inherited the running section would skip the queue and
|
|
404
|
+
* `blockConcurrencyWhile` would silently block nothing (Part 4, mechanic 2).
|
|
405
|
+
*
|
|
406
|
+
* The returned function can be called multiple times.
|
|
407
|
+
*
|
|
408
|
+
* It does not route through `io-gate.ts`'s `makeReentryCallback`, which is the same idea
|
|
409
|
+
* expressed at the gate. Upstream's `IoContext::makeReentryCallback` is literally
|
|
410
|
+
* `ctx.run(func, cs)`, and going through the gate helper instead would take a lock this
|
|
411
|
+
* file then has to make current a second time. The gate copy stays: it is the shape a
|
|
412
|
+
* consumer holding only a gate needs, and Section 1's tests cover it.
|
|
413
|
+
*/
|
|
414
|
+
makeReentryCallback<Args extends unknown[], Result>(func: (lock: Lock, ...args: Args) => Result | PromiseLike<Result>): (...args: Args) => Promise<Result>;
|
|
415
|
+
/**
|
|
416
|
+
* Waits for some background I/O to complete, then executes `func` on the result.
|
|
417
|
+
*
|
|
418
|
+
* The input lock is NOT held across the wait: the resumption re-enters through
|
|
419
|
+
* `run(func, criticalSection)` and takes a fresh lock, so it queues behind whatever
|
|
420
|
+
* arrived in the meantime. This is what makes a Durable Object awaiting another Durable
|
|
421
|
+
* Object fully re-entrant (§1.3).
|
|
422
|
+
*
|
|
423
|
+
* `func` is a parameter rather than something the caller chains, for upstream's reason:
|
|
424
|
+
* chaining "required returning to the KJ event loop between running func() and running
|
|
425
|
+
* whatever JavaScript code was waiting on it". Here the equivalent cost is a promise hop
|
|
426
|
+
* outside the lock.
|
|
427
|
+
*/
|
|
428
|
+
awaitIo<T>(promise: Promise<T>): Promise<T>;
|
|
429
|
+
awaitIo<T, R>(promise: Promise<T>, func: (value: T) => R | PromiseLike<R>): Promise<R>;
|
|
430
|
+
/**
|
|
431
|
+
* Waits for the given I/O while holding the input lock, so that all other I/O is blocked
|
|
432
|
+
* from completing in the meantime (unless it is also holding the same input lock).
|
|
433
|
+
*
|
|
434
|
+
* This is the whole of §1.2's asymmetry, and per §1.7.1 it is also the implicit-transaction
|
|
435
|
+
* boundary: the four async storage calls take this form, everything else takes `awaitIo`.
|
|
436
|
+
* Calling it outside a gated slice throws rather than inventing a lock — a lost invocation
|
|
437
|
+
* is loud, a lost transaction boundary is not.
|
|
438
|
+
*/
|
|
439
|
+
awaitIoWithInputLock<T>(promise: Promise<T>): Promise<T>;
|
|
440
|
+
awaitIoWithInputLock<T, R>(promise: Promise<T>, func: (value: T) => R | PromiseLike<R>): Promise<R>;
|
|
441
|
+
/**
|
|
442
|
+
* Runs `callback` within its own critical section, returning its final result. If
|
|
443
|
+
* `callback` throws, the input lock will break, resetting the actor.
|
|
444
|
+
*
|
|
445
|
+
* Three behaviours live here rather than in `io-gate.ts`, which has no timer, and rather
|
|
446
|
+
* than in `api/actor-state.ts`, whose own `blockConcurrencyWhile` is a one-line forward:
|
|
447
|
+
* the 30-second deadline, the brokenness annotation, and the fact that on failure the
|
|
448
|
+
* returned promise is never settled at all.
|
|
449
|
+
*/
|
|
450
|
+
blockConcurrencyWhile<T>(callback: (lock: Lock) => T | PromiseLike<T>): Promise<T>;
|
|
451
|
+
}
|