@mcp-b/do-runtime 0.1.2 → 0.2.2
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 +25 -0
- package/README.md +9 -0
- package/dist/index.js +96 -4
- package/dist/index.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/io/io-context.d.ts +39 -0
- package/dist/src/io/worker.d.ts +2 -3
- package/dist/src/server/actor-container.d.ts +4 -4
- package/dist/src/server/actor-namespace.d.ts +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 31cb2a6: Gate the streams `pipeThrough` and `pipeTo` produce. Native pipe machinery reads a gated body through internal spec operations and hands back a brand-new uninstrumented stream, so `res.body.pipeThrough(new TextDecoderStream()).getReader().read()` — the MCP SDK's SSE path — resumed foreign on every chunk and the next storage call threw "no input lock available in this context". `pipeThrough` now re-gates the readable it returns (recursively, so chains stay covered) and `pipeTo`'s settlement resumes gated.
|
|
8
|
+
|
|
9
|
+
## 0.2.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 5c91da1: Name the last gated site in the "no input lock available in this context" error. The throw lands at the next storage call, which can be several layers past the foreign await that actually dropped the lock; the error now carries where the gate was last engaged — an `awaitIo` call site with its stack, an `entry` dispatch with its method name, a re-entry callback's registration site — and how many milliseconds before the throw, which brackets the offending await between two coordinates.
|
|
14
|
+
|
|
15
|
+
## 0.2.0 — 2026-08-21
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Expose `createDurableObjectNamespace()` and its placement-channel types for standard named Durable Object bindings.
|
|
20
|
+
- Run Agents SDK `routeAgentRequest()`, `getAgentByName()` direct stubs, decorated callables, streaming RPC, email routing, and sub-agents through the MV3 extension host.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Preserve named facet identities instead of re-hashing serialized Durable Object IDs.
|
|
25
|
+
- Re-enter the calling actor's input gate after outbound namespace-stub calls.
|
|
26
|
+
- Preserve browser WebSocket upgrade requests across PartyServer request clones.
|
|
27
|
+
|
|
3
28
|
## 0.1.2
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -186,6 +186,15 @@ The lifecycle:
|
|
|
186
186
|
5. Reach the platform through `container.globals` (or install it with `installActorScope`). For a host-provided promise an actor must await, wrap it once in `container.awaitIo()`.
|
|
187
187
|
6. Watch `container.onBroken`; dispose the placement; recreate it on the next event over the same storage.
|
|
188
188
|
|
|
189
|
+
For a standard Durable Object binding, call
|
|
190
|
+
`createDurableObjectNamespace(uniqueKey, channel)` and put the result in `env`
|
|
191
|
+
and `ctx.exports`. The channel maps each routed id to a placed `Fetcher`; that
|
|
192
|
+
binding works directly with Agents SDK `routeAgentRequest()` and
|
|
193
|
+
`getAgentByName()`. When an actor uses the binding to call another actor, wrap
|
|
194
|
+
the transport promise with the caller's `container.awaitIo()` so its continuation
|
|
195
|
+
re-enters the owning input gate. The extension example shows both the external
|
|
196
|
+
router binding and the per-container actor binding.
|
|
197
|
+
|
|
189
198
|
### Storage
|
|
190
199
|
|
|
191
200
|
`SqlDatabaseProvider.open(name)` is the runtime execution seam. The runtime owns database names, tables, transactions, reset behaviour, facet metadata, and streaming `sql.ingest()` statement boundaries; the host chooses the physical provider and prefix. Stored KV values use structured-clone semantics across workerd, Node, and the browser; existing JSON rows remain readable. `_cf_` names are reserved to the runtime.
|
package/dist/index.js
CHANGED
|
@@ -664,7 +664,28 @@ var INPUT_GATE_BROKEN_PREFIX = "broken.inputGateBroken; ";
|
|
|
664
664
|
*/
|
|
665
665
|
function requireInputLock(ctx, op) {
|
|
666
666
|
if (ctx.hasCurrent()) return;
|
|
667
|
-
throw new Error(`${op}: no input lock available in this context`);
|
|
667
|
+
throw new Error(`${op}: no input lock available in this context${ctx.describeLostLock()}`);
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* A stack for `noteGateUse`, captured where user frames are still on the stack.
|
|
671
|
+
*
|
|
672
|
+
* The invocation stack's own push and pop are scheduler moments — `#runImpl`
|
|
673
|
+
* runs from a gate resumption and `#exit` from a `MessageChannel` callback — so
|
|
674
|
+
* a trace taken there names only runtime internals. The moments that still see
|
|
675
|
+
* the caller are the synchronous entries into the gate machinery: an `awaitIo`
|
|
676
|
+
* call, an `entry` dispatch, a callback's registration. V8's zero-cost async
|
|
677
|
+
* traces extend those with the awaiting chain, which is usually the frame the
|
|
678
|
+
* reader actually wants.
|
|
679
|
+
*
|
|
680
|
+
* The first slice drops the `Error` header (absent on SpiderMonkey) and the two
|
|
681
|
+
* runtime frames: this helper and the gate entry point that called it.
|
|
682
|
+
*/
|
|
683
|
+
function captureGateStack() {
|
|
684
|
+
const stack = (/* @__PURE__ */ new Error()).stack;
|
|
685
|
+
if (stack === void 0) return void 0;
|
|
686
|
+
const frames = stack.split("\n");
|
|
687
|
+
const trimmed = frames.slice(frames[0]?.startsWith("Error") ? 3 : 2).join("\n");
|
|
688
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
668
689
|
}
|
|
669
690
|
/**
|
|
670
691
|
* The end of the microtask checkpoint — the moment `runImpl`'s `KJ_DEFER` fires.
|
|
@@ -1014,6 +1035,13 @@ var IoContext = class {
|
|
|
1014
1035
|
* the overlapping slices happen to leave in.
|
|
1015
1036
|
*/
|
|
1016
1037
|
#currentInputLocks = [];
|
|
1038
|
+
/**
|
|
1039
|
+
* Where this context's gate was last deliberately engaged, for
|
|
1040
|
+
* `describeLostLock`. One slot, overwritten on every engagement — the gate
|
|
1041
|
+
* serialises slices, so the latest note is the best available ancestor of
|
|
1042
|
+
* whatever continuation is running lockless now.
|
|
1043
|
+
*/
|
|
1044
|
+
#lastGateUse;
|
|
1017
1045
|
#abortException;
|
|
1018
1046
|
#abortPromise;
|
|
1019
1047
|
#rejectAbort;
|
|
@@ -1074,6 +1102,42 @@ var IoContext = class {
|
|
|
1074
1102
|
return currentSlice === this;
|
|
1075
1103
|
}
|
|
1076
1104
|
/**
|
|
1105
|
+
* Record that user code just engaged this context's gate — an `awaitIo`, an
|
|
1106
|
+
* `entry` dispatch, a re-entry callback firing. No upstream analogue, because
|
|
1107
|
+
* upstream cannot lose the lock; here a continuation that awaits a promise
|
|
1108
|
+
* the runtime does not own comes back lockless, the throw lands at the next
|
|
1109
|
+
* storage call three layers later, and the gap between "where the code last
|
|
1110
|
+
* verifiably ran gated" and the throw site is exactly where the foreign await
|
|
1111
|
+
* hides. This is that first coordinate. Always on: the capture rides calls
|
|
1112
|
+
* that already allocate promise machinery, and a stack costs microseconds
|
|
1113
|
+
* against the diagnosis it replaces.
|
|
1114
|
+
*/
|
|
1115
|
+
noteGateUse(what, stack) {
|
|
1116
|
+
this.#lastGateUse = {
|
|
1117
|
+
what,
|
|
1118
|
+
stack,
|
|
1119
|
+
at: this.now()
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* The suffix `requireInputLock` appends when the invocation stack is empty:
|
|
1124
|
+
* where this context's gate was last engaged, and how long before the throw.
|
|
1125
|
+
*
|
|
1126
|
+
* "Last engaged" is the honest claim, not "this continuation's ancestor" —
|
|
1127
|
+
* once the offending chain went lockless the gate reopened, so another slice
|
|
1128
|
+
* may have run in between and be the note this reports. In practice the loss
|
|
1129
|
+
* is discovered within the same event storm and the note is the parent; when
|
|
1130
|
+
* it is not, an engagement of this actor moments earlier is still the right
|
|
1131
|
+
* neighbourhood to search.
|
|
1132
|
+
*/
|
|
1133
|
+
describeLostLock() {
|
|
1134
|
+
const use = this.#lastGateUse;
|
|
1135
|
+
if (use === void 0) return " (this context has never held its gate: the call arrived from outside any actor invocation)";
|
|
1136
|
+
const age = Math.round(this.now() - use.at);
|
|
1137
|
+
const stackSuffix = use.stack === void 0 ? "" : `, at:\n${use.stack}`;
|
|
1138
|
+
return ` (an await after the last gated point resumed from a promise the runtime does not own; the gate was last engaged by ${use.what} ${age}ms before this call${stackSuffix})`;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1077
1141
|
* ← `IoContext::getActorOrThrow()`. Upstream's throws when the request is not
|
|
1078
1142
|
* an actor request; there is no such request here, so it is a plain accessor.
|
|
1079
1143
|
*/
|
|
@@ -1234,13 +1298,16 @@ var IoContext = class {
|
|
|
1234
1298
|
makeReentryCallback(func) {
|
|
1235
1299
|
this.#requireCurrent();
|
|
1236
1300
|
const criticalSection = this.getCriticalSection();
|
|
1301
|
+
const registrationStack = captureGateStack();
|
|
1237
1302
|
return async (...args) => {
|
|
1303
|
+
this.noteGateUse("a re-entry callback registered at the site below", registrationStack);
|
|
1238
1304
|
const call = this.run((lock) => func(lock, ...args), criticalSection);
|
|
1239
1305
|
this.addTask(call.then(() => {}, () => {}));
|
|
1240
1306
|
return await call;
|
|
1241
1307
|
};
|
|
1242
1308
|
}
|
|
1243
1309
|
awaitIo(promise, func = identity) {
|
|
1310
|
+
this.noteGateUse("awaitIo", captureGateStack());
|
|
1244
1311
|
return this.#awaitIoImpl(promise, this.getCriticalSection(), func);
|
|
1245
1312
|
}
|
|
1246
1313
|
awaitIoWithInputLock(promise, func = identity) {
|
|
@@ -1250,6 +1317,7 @@ var IoContext = class {
|
|
|
1250
1317
|
} catch (exception) {
|
|
1251
1318
|
return Promise.reject(exception);
|
|
1252
1319
|
}
|
|
1320
|
+
this.noteGateUse("awaitIoWithInputLock", captureGateStack());
|
|
1253
1321
|
return this.#awaitIoImpl(promise, inputLock, func);
|
|
1254
1322
|
}
|
|
1255
1323
|
/**
|
|
@@ -1263,6 +1331,7 @@ var IoContext = class {
|
|
|
1263
1331
|
*/
|
|
1264
1332
|
blockConcurrencyWhile(callback) {
|
|
1265
1333
|
const lock = this.getInputLock();
|
|
1334
|
+
this.noteGateUse("blockConcurrencyWhile", captureGateStack());
|
|
1266
1335
|
const criticalSection = lock.startCriticalSection();
|
|
1267
1336
|
const { promise: result, resolve } = Promise.withResolvers();
|
|
1268
1337
|
this.addTask((async () => {
|
|
@@ -1312,7 +1381,7 @@ var IoContext = class {
|
|
|
1312
1381
|
*/
|
|
1313
1382
|
#requireCurrent() {
|
|
1314
1383
|
const lock = this.#currentInputLocks.at(-1);
|
|
1315
|
-
if (lock === void 0) throw new Error(
|
|
1384
|
+
if (lock === void 0) throw new Error(`no input lock available in this context${this.describeLostLock()}`);
|
|
1316
1385
|
return lock;
|
|
1317
1386
|
}
|
|
1318
1387
|
/** ← the far side of `runInContextScope`'s `KJ_DEFER`. */
|
|
@@ -3419,7 +3488,7 @@ var DurableObjectFacets = class {
|
|
|
3419
3488
|
const id = options.id;
|
|
3420
3489
|
return {
|
|
3421
3490
|
actorClass: requireFacetClass(options.class).getChannel(),
|
|
3422
|
-
id: id === void 0 ? this.#parentId : typeof id === "string" ? id : id.toString()
|
|
3491
|
+
id: id === void 0 ? this.#parentId : typeof id === "string" ? id : id.name ?? id.toString()
|
|
3423
3492
|
};
|
|
3424
3493
|
});
|
|
3425
3494
|
return facetManager.getFacet(name, getStartInfo);
|
|
@@ -3681,6 +3750,8 @@ var BYOB_READER_UNGATABLE_MESSAGE = "getReader({ mode: 'byob' }): a BYOB reader
|
|
|
3681
3750
|
function gateReadableStream(ctx, stream) {
|
|
3682
3751
|
const getReader = stream.getReader.bind(stream);
|
|
3683
3752
|
const tee = stream.tee.bind(stream);
|
|
3753
|
+
const pipeThrough = stream.pipeThrough.bind(stream);
|
|
3754
|
+
const pipeTo = stream.pipeTo.bind(stream);
|
|
3684
3755
|
Object.defineProperties(stream, {
|
|
3685
3756
|
getReader: {
|
|
3686
3757
|
configurable: true,
|
|
@@ -3697,6 +3768,20 @@ function gateReadableStream(ctx, stream) {
|
|
|
3697
3768
|
const [a, b] = tee();
|
|
3698
3769
|
return [gateReadableStream(ctx, a), gateReadableStream(ctx, b)];
|
|
3699
3770
|
}
|
|
3771
|
+
},
|
|
3772
|
+
pipeThrough: {
|
|
3773
|
+
configurable: true,
|
|
3774
|
+
writable: true,
|
|
3775
|
+
value(transform, options) {
|
|
3776
|
+
return gateReadableStream(ctx, pipeThrough(transform, options));
|
|
3777
|
+
}
|
|
3778
|
+
},
|
|
3779
|
+
pipeTo: {
|
|
3780
|
+
configurable: true,
|
|
3781
|
+
writable: true,
|
|
3782
|
+
value(destination, options) {
|
|
3783
|
+
return ctx.awaitIo(pipeTo(destination, options));
|
|
3784
|
+
}
|
|
3700
3785
|
}
|
|
3701
3786
|
});
|
|
3702
3787
|
return stream;
|
|
@@ -6944,6 +7029,7 @@ var ActorContainerImpl = class {
|
|
|
6944
7029
|
const cached = bound.get(property);
|
|
6945
7030
|
if (cached !== void 0) return cached;
|
|
6946
7031
|
const gated = async (...args) => {
|
|
7032
|
+
this.#ctx.noteGateUse(`entry ${String(property)}()`, captureGateStack());
|
|
6947
7033
|
const result = await this.#ctx.run(() => this.#withExternalEntry(() => value.apply(subject, args)));
|
|
6948
7034
|
await this.#ctx.waitForOutputLocks();
|
|
6949
7035
|
return result;
|
|
@@ -7132,6 +7218,12 @@ async function createActorContainer(options) {
|
|
|
7132
7218
|
return new ActorContainerImpl(options, db, tree, tree);
|
|
7133
7219
|
}
|
|
7134
7220
|
//#endregion
|
|
7221
|
+
//#region src/server/actor-namespace.ts
|
|
7222
|
+
/** Assemble the configured namespace binding a host places in `env`. */
|
|
7223
|
+
function createDurableObjectNamespace(uniqueKey, channel) {
|
|
7224
|
+
return new DurableObjectNamespace(channel, new ActorIdFactoryImpl(uniqueKey));
|
|
7225
|
+
}
|
|
7226
|
+
//#endregion
|
|
7135
7227
|
//#region src/transport/rpc-session.ts
|
|
7136
7228
|
/**
|
|
7137
7229
|
* ← workerd `NO upstream correspondence (capnweb adaptation)`
|
|
@@ -7175,6 +7267,6 @@ function newRpcSession(port, localMain) {
|
|
|
7175
7267
|
return newMessagePortRpcSession(port, localMain);
|
|
7176
7268
|
}
|
|
7177
7269
|
//#endregion
|
|
7178
|
-
export { ACTOR_CLASS_SERIALIZATION_UNIMPLEMENTED_MESSAGE, ALARM_RETRY_MAX_TRIES, ALARM_RETRY_START_SECONDS, ALLOW_EXPERIMENTAL_MESSAGE, ALREADY_ACCEPTED_MESSAGE, AlarmInvocationInfo, AlarmScheduler, BYOB_READER_UNGATABLE_MESSAGE, DEAD_LOAD_CONTEXT_MESSAGE, DEFAULT_ALARM_OUTLET, FACET_ALARM_UNIMPLEMENTED_MESSAGE, FACET_NAME_MAX_LENGTH, FACET_TREE_MAX_DEPTH, FOREIGN_SLICE_MESSAGE, HIBERNATION_UNIMPLEMENTED_MESSAGE, LoopbackDurableObjectClass, NOT_BYTES_MESSAGE, NO_GLOBAL_OUTBOUND_MESSAGE, NO_MODULES_MESSAGE, PITR_UNIMPLEMENTED_MESSAGE, REPLICATION_UNIMPLEMENTED_MESSAGE, RETRY_BACKOFF_MAX, RETRY_JITTER_FACTOR, STREAMING_TAILS_EXPERIMENTAL_MESSAGE, WorkerLoader, WorkerStub, actorScopeBindings, alarmRetryDelayMs, asLoopbackDurableObjectClass, createActorContainer, gateRequestBody, installActorScope, jsModuleInPythonWorkerMessage, moduleFieldCountMessage, moduleNameMessage, newRpcSession, noFacets, notSerializableMessage, pythonModuleInJsWorkerMessage, typeScriptModuleNameMessage };
|
|
7270
|
+
export { ACTOR_CLASS_SERIALIZATION_UNIMPLEMENTED_MESSAGE, ALARM_RETRY_MAX_TRIES, ALARM_RETRY_START_SECONDS, ALLOW_EXPERIMENTAL_MESSAGE, ALREADY_ACCEPTED_MESSAGE, AlarmInvocationInfo, AlarmScheduler, BYOB_READER_UNGATABLE_MESSAGE, DEAD_LOAD_CONTEXT_MESSAGE, DEFAULT_ALARM_OUTLET, FACET_ALARM_UNIMPLEMENTED_MESSAGE, FACET_NAME_MAX_LENGTH, FACET_TREE_MAX_DEPTH, FOREIGN_SLICE_MESSAGE, HIBERNATION_UNIMPLEMENTED_MESSAGE, LoopbackDurableObjectClass, NOT_BYTES_MESSAGE, NO_GLOBAL_OUTBOUND_MESSAGE, NO_MODULES_MESSAGE, PITR_UNIMPLEMENTED_MESSAGE, REPLICATION_UNIMPLEMENTED_MESSAGE, RETRY_BACKOFF_MAX, RETRY_JITTER_FACTOR, STREAMING_TAILS_EXPERIMENTAL_MESSAGE, WorkerLoader, WorkerStub, actorScopeBindings, alarmRetryDelayMs, asLoopbackDurableObjectClass, createActorContainer, createDurableObjectNamespace, gateRequestBody, installActorScope, jsModuleInPythonWorkerMessage, moduleFieldCountMessage, moduleNameMessage, newRpcSession, noFacets, notSerializableMessage, pythonModuleInJsWorkerMessage, typeScriptModuleNameMessage };
|
|
7179
7271
|
|
|
7180
7272
|
//# sourceMappingURL=index.js.map
|