@nimbus-sh/fabric 0.1.0 → 0.3.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/README.md +208 -293
- package/dist/bindings.js +5 -5
- package/dist/budgets.d.ts +132 -0
- package/dist/budgets.d.ts.map +1 -0
- package/dist/budgets.js +248 -0
- package/dist/composition.d.ts +3 -0
- package/dist/composition.d.ts.map +1 -0
- package/dist/composition.js +2 -0
- package/dist/connections.d.ts +81 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +114 -0
- package/dist/derived.d.ts +65 -0
- package/dist/derived.d.ts.map +1 -0
- package/dist/derived.js +95 -0
- package/dist/do-calls.d.ts +94 -0
- package/dist/do-calls.d.ts.map +1 -0
- package/dist/do-calls.js +111 -0
- package/dist/facet-pool.d.ts +90 -0
- package/dist/facet-pool.d.ts.map +1 -0
- package/dist/facet-pool.js +113 -0
- package/dist/{fanout-pool.d.ts → fanout.d.ts} +20 -20
- package/dist/fanout.d.ts.map +1 -0
- package/dist/{fanout-pool.js → fanout.js} +20 -20
- package/dist/{launch-journal.d.ts → fenced-work.d.ts} +58 -17
- package/dist/fenced-work.d.ts.map +1 -0
- package/dist/fenced-work.js +241 -0
- package/dist/generation.d.ts +69 -0
- package/dist/generation.d.ts.map +1 -0
- package/dist/generation.js +118 -0
- package/dist/{facet-image-store.d.ts → image-store.d.ts} +8 -8
- package/dist/image-store.d.ts.map +1 -0
- package/dist/{facet-image-store.js → image-store.js} +4 -4
- package/dist/index.d.ts +16 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -8
- package/dist/{loader-pool.d.ts → isolate-pool.d.ts} +19 -19
- package/dist/isolate-pool.d.ts.map +1 -0
- package/dist/{loader-pool.js → isolate-pool.js} +20 -20
- package/dist/journal.d.ts +111 -0
- package/dist/journal.d.ts.map +1 -0
- package/dist/journal.js +177 -0
- package/dist/outbox.d.ts +249 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +355 -0
- package/dist/process-fabric.d.ts +33 -15
- package/dist/process-fabric.d.ts.map +1 -1
- package/dist/process-fabric.js +25 -15
- package/dist/process-host.d.ts +1 -1
- package/dist/process-host.d.ts.map +1 -1
- package/dist/process-host.js +19 -11
- package/dist/sealed.d.ts +78 -0
- package/dist/sealed.d.ts.map +1 -0
- package/dist/sealed.js +145 -0
- package/dist/timers.d.ts +138 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +231 -0
- package/dist/{launch-pacer.d.ts → turn-budget.d.ts} +24 -21
- package/dist/turn-budget.d.ts.map +1 -0
- package/dist/{launch-pacer.js → turn-budget.js} +24 -12
- package/dist/workerd-facet-host.d.ts +67 -70
- package/dist/workerd-facet-host.d.ts.map +1 -1
- package/dist/workerd-facet-host.js +129 -181
- package/examples/agent-core-adapter.ts +191 -0
- package/package.json +4 -2
- package/src/bindings.ts +6 -6
- package/src/budgets.ts +308 -0
- package/src/composition.ts +16 -0
- package/src/connections.ts +140 -0
- package/src/derived.ts +135 -0
- package/src/do-calls.ts +156 -0
- package/src/facet-pool.ts +157 -0
- package/src/{fanout-pool.ts → fanout.ts} +35 -35
- package/src/{launch-journal.ts → fenced-work.ts} +129 -42
- package/src/generation.ts +144 -0
- package/src/{facet-image-store.ts → image-store.ts} +9 -9
- package/src/index.ts +16 -8
- package/src/{loader-pool.ts → isolate-pool.ts} +34 -34
- package/src/journal.ts +242 -0
- package/src/node-async-hooks.d.ts +14 -0
- package/src/outbox.ts +520 -0
- package/src/process-fabric.ts +43 -34
- package/src/process-host.ts +22 -20
- package/src/sealed.ts +150 -0
- package/src/timers.ts +294 -0
- package/src/{launch-pacer.ts → turn-budget.ts} +34 -27
- package/src/workerd-facet-host.ts +159 -208
- package/dist/alarms.d.ts +0 -134
- package/dist/alarms.d.ts.map +0 -1
- package/dist/alarms.js +0 -214
- package/dist/ctx-exports.d.ts +0 -47
- package/dist/ctx-exports.d.ts.map +0 -1
- package/dist/ctx-exports.js +0 -54
- package/dist/facet-image-store.d.ts.map +0 -1
- package/dist/fanout-pool.d.ts.map +0 -1
- package/dist/launch-journal.d.ts.map +0 -1
- package/dist/launch-journal.js +0 -154
- package/dist/launch-pacer.d.ts.map +0 -1
- package/dist/loader-ledger.d.ts +0 -57
- package/dist/loader-ledger.d.ts.map +0 -1
- package/dist/loader-ledger.js +0 -91
- package/dist/loader-pool.d.ts.map +0 -1
- package/src/alarms.ts +0 -275
- package/src/ctx-exports.ts +0 -77
- package/src/loader-ledger.ts +0 -112
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* derived.ts — a watermark memo: derive a cheap key, compare, rebuild only
|
|
3
|
+
* on change.
|
|
4
|
+
*
|
|
5
|
+
* Proteus's ActorAgent hand-rolls this pair four times — cached value plus
|
|
6
|
+
* cached key, compared and rebuilt inline: the system prompt (key composed
|
|
7
|
+
* from soul text, executors, model, tools, stance), the tool set (keyed
|
|
8
|
+
* partly on `_craftCacheKey()`, two synchronous SQLite aggregates), the MCP
|
|
9
|
+
* tool surface (keyed on UserDO's `mcp_updated_at`), and the SOUL text
|
|
10
|
+
* (no key at all — push-invalidated by `setSoul`). The consumer port moved
|
|
11
|
+
* the prompt and tool-set pairs onto `derived` cleanly; the MCP pair needs
|
|
12
|
+
* the per-call context and the hooks; the SOUL pair cannot move at all.
|
|
13
|
+
*
|
|
14
|
+
* The SOUL pair names this module's limit: a value LOADED asynchronously
|
|
15
|
+
* (an awaited file read at turn start) but READ synchronously (the prompt
|
|
16
|
+
* builder consults what is already in memory). Neither variant expresses
|
|
17
|
+
* that split — `derived` cannot await the load, `derivedAsync` cannot serve
|
|
18
|
+
* a synchronous read — so an async-load/sync-read snapshot stays a
|
|
19
|
+
* hand-rolled field with a push invalidation.
|
|
20
|
+
*
|
|
21
|
+
* `derived` is synchronous end to end, because its consumers are: Think
|
|
22
|
+
* calls `getSystemPrompt(): string` synchronously, and nothing synchronous
|
|
23
|
+
* may await — the init-gate rule. `derivedAsync` exists because ONE consumer
|
|
24
|
+
* call site needs it: the MCP surface awaits a cross-DO RPC for both the
|
|
25
|
+
* watermark and the build, and its proven failure policy is stale-on-error —
|
|
26
|
+
* a watermark or build failure serves the last good value without touching
|
|
27
|
+
* the stored key, and only surfaces when there is no value to serve.
|
|
28
|
+
*
|
|
29
|
+
* `invalidate()` is the push half the SOUL memo proved: an out-of-band write
|
|
30
|
+
* (`setSoul`) clears the memo so the next read rebuilds under an unchanged
|
|
31
|
+
* watermark.
|
|
32
|
+
*/
|
|
33
|
+
export interface Derived<T, C = void> {
|
|
34
|
+
/**
|
|
35
|
+
* `context` reaches the watermark and the build of THIS call — the seam
|
|
36
|
+
* for per-call state (a stub, a caller identity, a work mode). The memo
|
|
37
|
+
* stores one value; the watermark must cover everything the build reads,
|
|
38
|
+
* context included, or a context change serves another context's value.
|
|
39
|
+
*/
|
|
40
|
+
get(context: C): T;
|
|
41
|
+
/** Force the next get to rebuild, watermark unchanged. */
|
|
42
|
+
invalidate(): void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The consumer's logging seams. Both MCP logs the port could not express:
|
|
46
|
+
* `onRebuild` fires after a build stores (the "rebuilt @ wm=N" line), and
|
|
47
|
+
* `onStale` — async only — fires when a failure serves the stale value,
|
|
48
|
+
* the one path where the error is otherwise absorbed. A surfaced error
|
|
49
|
+
* (nothing stale to serve) reports itself.
|
|
50
|
+
*/
|
|
51
|
+
export interface DerivedHooks {
|
|
52
|
+
/** After a build stores. `previousKey` is undefined on the first build. */
|
|
53
|
+
onRebuild?(previousKey: string | number | undefined, nextKey: string | number): void;
|
|
54
|
+
}
|
|
55
|
+
export interface DerivedAsyncHooks extends DerivedHooks {
|
|
56
|
+
/** A watermark or build failure just served the stale value. */
|
|
57
|
+
onStale?(error: unknown): void;
|
|
58
|
+
}
|
|
59
|
+
export declare function derived<T, C = void>(watermark: (context: C) => string | number, build: (context: C, key: string | number) => T, hooks?: DerivedHooks): Derived<T, C>;
|
|
60
|
+
export interface DerivedAsync<T, C = void> {
|
|
61
|
+
get(context: C): Promise<T>;
|
|
62
|
+
invalidate(): void;
|
|
63
|
+
}
|
|
64
|
+
export declare function derivedAsync<T, C = void>(watermark: (context: C) => Promise<string | number>, build: (context: C, key: string | number) => Promise<T>, hooks?: DerivedAsyncHooks): DerivedAsync<T, C>;
|
|
65
|
+
//# sourceMappingURL=derived.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derived.d.ts","sourceRoot":"","sources":["../src/derived.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,WAAW,OAAO,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI;IAClC;;;;;OAKG;IACH,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;IACnB,0DAA0D;IAC1D,UAAU,IAAI,IAAI,CAAC;CACpB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,SAAS,CAAC,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CACtF;AAED,MAAM,WAAW,iBAAkB,SAAQ,YAAY;IACrD,gEAAgE;IAChE,OAAO,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CAChC;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,EACjC,SAAS,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,MAAM,GAAG,MAAM,EAC1C,KAAK,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,KAAK,CAAC,EAC9C,KAAK,GAAE,YAAiB,GACvB,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAmBf;AAED,MAAM,WAAW,YAAY,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI;IACvC,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5B,UAAU,IAAI,IAAI,CAAC;CACpB;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,EACtC,SAAS,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC,EACnD,KAAK,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,EACvD,KAAK,GAAE,iBAAsB,GAC5B,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAsCpB"}
|
package/dist/derived.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* derived.ts — a watermark memo: derive a cheap key, compare, rebuild only
|
|
3
|
+
* on change.
|
|
4
|
+
*
|
|
5
|
+
* Proteus's ActorAgent hand-rolls this pair four times — cached value plus
|
|
6
|
+
* cached key, compared and rebuilt inline: the system prompt (key composed
|
|
7
|
+
* from soul text, executors, model, tools, stance), the tool set (keyed
|
|
8
|
+
* partly on `_craftCacheKey()`, two synchronous SQLite aggregates), the MCP
|
|
9
|
+
* tool surface (keyed on UserDO's `mcp_updated_at`), and the SOUL text
|
|
10
|
+
* (no key at all — push-invalidated by `setSoul`). The consumer port moved
|
|
11
|
+
* the prompt and tool-set pairs onto `derived` cleanly; the MCP pair needs
|
|
12
|
+
* the per-call context and the hooks; the SOUL pair cannot move at all.
|
|
13
|
+
*
|
|
14
|
+
* The SOUL pair names this module's limit: a value LOADED asynchronously
|
|
15
|
+
* (an awaited file read at turn start) but READ synchronously (the prompt
|
|
16
|
+
* builder consults what is already in memory). Neither variant expresses
|
|
17
|
+
* that split — `derived` cannot await the load, `derivedAsync` cannot serve
|
|
18
|
+
* a synchronous read — so an async-load/sync-read snapshot stays a
|
|
19
|
+
* hand-rolled field with a push invalidation.
|
|
20
|
+
*
|
|
21
|
+
* `derived` is synchronous end to end, because its consumers are: Think
|
|
22
|
+
* calls `getSystemPrompt(): string` synchronously, and nothing synchronous
|
|
23
|
+
* may await — the init-gate rule. `derivedAsync` exists because ONE consumer
|
|
24
|
+
* call site needs it: the MCP surface awaits a cross-DO RPC for both the
|
|
25
|
+
* watermark and the build, and its proven failure policy is stale-on-error —
|
|
26
|
+
* a watermark or build failure serves the last good value without touching
|
|
27
|
+
* the stored key, and only surfaces when there is no value to serve.
|
|
28
|
+
*
|
|
29
|
+
* `invalidate()` is the push half the SOUL memo proved: an out-of-band write
|
|
30
|
+
* (`setSoul`) clears the memo so the next read rebuilds under an unchanged
|
|
31
|
+
* watermark.
|
|
32
|
+
*/
|
|
33
|
+
export function derived(watermark, build, hooks = {}) {
|
|
34
|
+
let key;
|
|
35
|
+
let value;
|
|
36
|
+
let has = false;
|
|
37
|
+
return {
|
|
38
|
+
get(context) {
|
|
39
|
+
const next = watermark(context);
|
|
40
|
+
if (has && next === key)
|
|
41
|
+
return value;
|
|
42
|
+
value = build(context, next);
|
|
43
|
+
hooks.onRebuild?.(key, next);
|
|
44
|
+
key = next;
|
|
45
|
+
has = true;
|
|
46
|
+
return value;
|
|
47
|
+
},
|
|
48
|
+
invalidate() {
|
|
49
|
+
has = false;
|
|
50
|
+
value = undefined;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function derivedAsync(watermark, build, hooks = {}) {
|
|
55
|
+
let key;
|
|
56
|
+
let value;
|
|
57
|
+
let has = false;
|
|
58
|
+
return {
|
|
59
|
+
async get(context) {
|
|
60
|
+
let next;
|
|
61
|
+
try {
|
|
62
|
+
next = await watermark(context);
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
if (has) {
|
|
66
|
+
hooks.onStale?.(e);
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
if (has && next === key)
|
|
72
|
+
return value;
|
|
73
|
+
let built;
|
|
74
|
+
try {
|
|
75
|
+
built = await build(context, next);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
if (has) {
|
|
79
|
+
hooks.onStale?.(e);
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
value = built;
|
|
85
|
+
hooks.onRebuild?.(key, next);
|
|
86
|
+
key = next;
|
|
87
|
+
has = true;
|
|
88
|
+
return built;
|
|
89
|
+
},
|
|
90
|
+
invalidate() {
|
|
91
|
+
has = false;
|
|
92
|
+
value = undefined;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* do-calls.ts — the two verbs for calling another Durable Object, split by
|
|
3
|
+
* the one property that decides whether a retry is safe.
|
|
4
|
+
*
|
|
5
|
+
* Both consumers asked for this. Proteus hand-wrote the retry
|
|
6
|
+
* (`cf-backend/src/lib/do-rpc.ts`) with the rule its header states:
|
|
7
|
+
* "An operation that appends, sends, charges or mints is never wrapped: a
|
|
8
|
+
* dropped call there may already have run, so a retry is a correctness bug
|
|
9
|
+
* wearing resilience as a costume." agent-core has no retry machinery at all
|
|
10
|
+
* and its backlog calls the gap "the most production-proven gap in the
|
|
11
|
+
* corpus". Here the rule is a type: `idempotent` retries, `mutating` cannot.
|
|
12
|
+
*
|
|
13
|
+
* What the platform contract requires, and this keeps:
|
|
14
|
+
* - a FRESH stub per attempt. Cloudflare documents that many exceptions
|
|
15
|
+
* leave a stub permanently broken, so both verbs take a stub RESOLVER,
|
|
16
|
+
* not a stub — which is also what lets placement pins and auth wrappers
|
|
17
|
+
* compose (agent-core's PlacementResolver pins an Actor to one
|
|
18
|
+
* jurisdiction for life; the resolver seam is where that lives).
|
|
19
|
+
* - `overloaded` is never retried, by either verb: retrying an overloaded
|
|
20
|
+
* object is what overloaded it.
|
|
21
|
+
* - attempts and backoff are the consumer-proven bounds: 3 attempts total,
|
|
22
|
+
* full-jitter delays in [0, 2**attempt * 60ms).
|
|
23
|
+
*
|
|
24
|
+
* The resolver MINTS a stub per call and the verb disposes each one it
|
|
25
|
+
* minted — that ownership is what makes the fresh-stub retry real.
|
|
26
|
+
*
|
|
27
|
+
* The stub method call itself happens inside the CALLER's closure
|
|
28
|
+
* (`(stub) => stub.method(args)`): nothing here proxies property resolution
|
|
29
|
+
* or dispatches by method name. Do NOT use these verbs around a dynamically
|
|
30
|
+
* loaded worker's entrypoint stub — those calls must stay direct property
|
|
31
|
+
* calls bracketed by `beginLoaderFetch` (budgets.ts records the 7/7 staging
|
|
32
|
+
* poisoning that rule comes from). These verbs are for Durable Object
|
|
33
|
+
* namespace stubs, where the thunk shape is production-proven in Proteus.
|
|
34
|
+
*/
|
|
35
|
+
import { type DoCallClass } from '@nimbus-sh/platform/oom-classify.js';
|
|
36
|
+
export interface DoCallRetryPolicy {
|
|
37
|
+
maxAttempts?: number;
|
|
38
|
+
baseDelayMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Called once per retry, before its backoff delay, with the failure the
|
|
41
|
+
* retry is answering. The consumer's logging seam: Proteus's hand-rolled
|
|
42
|
+
* predecessor logged every retry so a flaky object is visible in Workers
|
|
43
|
+
* Logs rather than silently absorbed, and `operation` names it there.
|
|
44
|
+
*/
|
|
45
|
+
onRetry?(info: DoCallRetryInfo): void;
|
|
46
|
+
}
|
|
47
|
+
/** What one retry is answering: which call, which platform class, which
|
|
48
|
+
* attempt just failed out of how many. */
|
|
49
|
+
export interface DoCallRetryInfo {
|
|
50
|
+
operation: string;
|
|
51
|
+
classification: DoCallClass;
|
|
52
|
+
/** The 1-based attempt that failed; the retry about to run is attempt+1. */
|
|
53
|
+
attempt: number;
|
|
54
|
+
maxAttempts: number;
|
|
55
|
+
error: unknown;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Mints one stub per call. `idempotent` calls it once per attempt.
|
|
59
|
+
*
|
|
60
|
+
* MINT means mint. Both verbs dispose the stub they were handed when the
|
|
61
|
+
* call settles — on success as much as on failure (`disposeRpcResource`).
|
|
62
|
+
* A resolver that returns a shared, long-lived stub hands its other users
|
|
63
|
+
* a disposed stub, and only in production: a test double is a plain object
|
|
64
|
+
* with nothing to dispose, so the test stays green while the deployed
|
|
65
|
+
* Worker breaks on the second call.
|
|
66
|
+
*/
|
|
67
|
+
export type DoStubResolver<S> = () => S | Promise<S>;
|
|
68
|
+
/**
|
|
69
|
+
* A failed `mutating` call, typed so the caller can act on WHAT failed:
|
|
70
|
+
* `classification` names the platform condition, and a transient class on a
|
|
71
|
+
* mutating call means the call may already have run — the indeterminacy the
|
|
72
|
+
* consumer's rule exists to surface rather than paper over.
|
|
73
|
+
*/
|
|
74
|
+
export declare class DoCallError extends Error {
|
|
75
|
+
readonly operation: string;
|
|
76
|
+
readonly verb: 'idempotent' | 'mutating';
|
|
77
|
+
readonly classification: DoCallClass;
|
|
78
|
+
constructor(operation: string, verb: 'idempotent' | 'mutating', classification: DoCallClass, cause: unknown);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Call another Durable Object with an operation that is safe to repeat: a
|
|
82
|
+
* read, or a converge-to-a-value write. Transient failures retry on a fresh
|
|
83
|
+
* stub with full-jitter backoff; overloaded and permanent failures surface
|
|
84
|
+
* unchanged, as does the last error at exhaustion.
|
|
85
|
+
*/
|
|
86
|
+
export declare function idempotent<S, T>(operation: string, stub: DoStubResolver<S>, call: (stub: S) => Promise<T>, policy?: DoCallRetryPolicy): Promise<T>;
|
|
87
|
+
/**
|
|
88
|
+
* Call another Durable Object with an operation that appends, sends, charges
|
|
89
|
+
* or mints. NEVER retried — a dropped call may already have run. Failure
|
|
90
|
+
* surfaces as a {@link DoCallError} carrying the classification, so the
|
|
91
|
+
* caller can tell a refusal from an indeterminate drop.
|
|
92
|
+
*/
|
|
93
|
+
export declare function mutating<S, T>(operation: string, stub: DoStubResolver<S>, call: (stub: S) => Promise<T>): Promise<T>;
|
|
94
|
+
//# sourceMappingURL=do-calls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"do-calls.d.ts","sourceRoot":"","sources":["../src/do-calls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAU1G,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,OAAO,CAAC,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAAC;CACvC;AAED;2CAC2C;AAC3C,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,WAAW,CAAC;IAC5B,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAErD;;;;;GAKG;AACH,qBAAa,WAAY,SAAQ,KAAK;IAElC,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,UAAU;IACxC,QAAQ,CAAC,cAAc,EAAE,WAAW;gBAF3B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,YAAY,GAAG,UAAU,EAC/B,cAAc,EAAE,WAAW,EACpC,KAAK,EAAE,OAAO;CASjB;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAAE,CAAC,EACnC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EACvB,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAC7B,MAAM,GAAE,iBAAsB,GAC7B,OAAO,CAAC,CAAC,CAAC,CAoBZ;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,CAAC,EAAE,CAAC,EACjC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EACvB,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5B,OAAO,CAAC,CAAC,CAAC,CAUZ"}
|
package/dist/do-calls.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* do-calls.ts — the two verbs for calling another Durable Object, split by
|
|
3
|
+
* the one property that decides whether a retry is safe.
|
|
4
|
+
*
|
|
5
|
+
* Both consumers asked for this. Proteus hand-wrote the retry
|
|
6
|
+
* (`cf-backend/src/lib/do-rpc.ts`) with the rule its header states:
|
|
7
|
+
* "An operation that appends, sends, charges or mints is never wrapped: a
|
|
8
|
+
* dropped call there may already have run, so a retry is a correctness bug
|
|
9
|
+
* wearing resilience as a costume." agent-core has no retry machinery at all
|
|
10
|
+
* and its backlog calls the gap "the most production-proven gap in the
|
|
11
|
+
* corpus". Here the rule is a type: `idempotent` retries, `mutating` cannot.
|
|
12
|
+
*
|
|
13
|
+
* What the platform contract requires, and this keeps:
|
|
14
|
+
* - a FRESH stub per attempt. Cloudflare documents that many exceptions
|
|
15
|
+
* leave a stub permanently broken, so both verbs take a stub RESOLVER,
|
|
16
|
+
* not a stub — which is also what lets placement pins and auth wrappers
|
|
17
|
+
* compose (agent-core's PlacementResolver pins an Actor to one
|
|
18
|
+
* jurisdiction for life; the resolver seam is where that lives).
|
|
19
|
+
* - `overloaded` is never retried, by either verb: retrying an overloaded
|
|
20
|
+
* object is what overloaded it.
|
|
21
|
+
* - attempts and backoff are the consumer-proven bounds: 3 attempts total,
|
|
22
|
+
* full-jitter delays in [0, 2**attempt * 60ms).
|
|
23
|
+
*
|
|
24
|
+
* The resolver MINTS a stub per call and the verb disposes each one it
|
|
25
|
+
* minted — that ownership is what makes the fresh-stub retry real.
|
|
26
|
+
*
|
|
27
|
+
* The stub method call itself happens inside the CALLER's closure
|
|
28
|
+
* (`(stub) => stub.method(args)`): nothing here proxies property resolution
|
|
29
|
+
* or dispatches by method name. Do NOT use these verbs around a dynamically
|
|
30
|
+
* loaded worker's entrypoint stub — those calls must stay direct property
|
|
31
|
+
* calls bracketed by `beginLoaderFetch` (budgets.ts records the 7/7 staging
|
|
32
|
+
* poisoning that rule comes from). These verbs are for Durable Object
|
|
33
|
+
* namespace stubs, where the thunk shape is production-proven in Proteus.
|
|
34
|
+
*/
|
|
35
|
+
import { classifyDoCall, isRetryableDoCall } from '@nimbus-sh/platform/oom-classify.js';
|
|
36
|
+
import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
37
|
+
/** Total attempts. Two retries is what a dropped connection or a deploy
|
|
38
|
+
* bounce needs; beyond that the object is not coming back inside this
|
|
39
|
+
* request (the consumer's measured bound). */
|
|
40
|
+
const MAX_ATTEMPTS = 3;
|
|
41
|
+
/** Full-jitter base, in the shape the Agents SDK itself uses. */
|
|
42
|
+
const BASE_DELAY_MS = 60;
|
|
43
|
+
/**
|
|
44
|
+
* A failed `mutating` call, typed so the caller can act on WHAT failed:
|
|
45
|
+
* `classification` names the platform condition, and a transient class on a
|
|
46
|
+
* mutating call means the call may already have run — the indeterminacy the
|
|
47
|
+
* consumer's rule exists to surface rather than paper over.
|
|
48
|
+
*/
|
|
49
|
+
export class DoCallError extends Error {
|
|
50
|
+
operation;
|
|
51
|
+
verb;
|
|
52
|
+
classification;
|
|
53
|
+
constructor(operation, verb, classification, cause) {
|
|
54
|
+
const text = cause instanceof Error ? cause.message : String(cause);
|
|
55
|
+
const indeterminate = isRetryableDoCall(classification)
|
|
56
|
+
? ' — a dropped mutating call may already have run, so it is not retried'
|
|
57
|
+
: '';
|
|
58
|
+
super(`${verb} call '${operation}' failed [${classification}]: ${text}${indeterminate}`, { cause });
|
|
59
|
+
this.operation = operation;
|
|
60
|
+
this.verb = verb;
|
|
61
|
+
this.classification = classification;
|
|
62
|
+
this.name = 'DoCallError';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Call another Durable Object with an operation that is safe to repeat: a
|
|
67
|
+
* read, or a converge-to-a-value write. Transient failures retry on a fresh
|
|
68
|
+
* stub with full-jitter backoff; overloaded and permanent failures surface
|
|
69
|
+
* unchanged, as does the last error at exhaustion.
|
|
70
|
+
*/
|
|
71
|
+
export async function idempotent(operation, stub, call, policy = {}) {
|
|
72
|
+
const maxAttempts = policy.maxAttempts ?? MAX_ATTEMPTS;
|
|
73
|
+
const baseDelayMs = policy.baseDelayMs ?? BASE_DELAY_MS;
|
|
74
|
+
for (let attempt = 1;; attempt++) {
|
|
75
|
+
const minted = await stub();
|
|
76
|
+
try {
|
|
77
|
+
const result = await call(minted);
|
|
78
|
+
disposeRpcResource(minted);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
// A stub that threw may be permanently broken; it is never reused.
|
|
83
|
+
disposeRpcResource(minted);
|
|
84
|
+
const classification = classifyDoCall(error);
|
|
85
|
+
if (!isRetryableDoCall(classification) || attempt >= maxAttempts)
|
|
86
|
+
throw error;
|
|
87
|
+
policy.onRetry?.({ operation, classification, attempt, maxAttempts, error });
|
|
88
|
+
await new Promise((resolve) => {
|
|
89
|
+
setTimeout(resolve, Math.floor(Math.random() * 2 ** attempt * baseDelayMs));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Call another Durable Object with an operation that appends, sends, charges
|
|
96
|
+
* or mints. NEVER retried — a dropped call may already have run. Failure
|
|
97
|
+
* surfaces as a {@link DoCallError} carrying the classification, so the
|
|
98
|
+
* caller can tell a refusal from an indeterminate drop.
|
|
99
|
+
*/
|
|
100
|
+
export async function mutating(operation, stub, call) {
|
|
101
|
+
const minted = await stub();
|
|
102
|
+
try {
|
|
103
|
+
const result = await call(minted);
|
|
104
|
+
disposeRpcResource(minted);
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
disposeRpcResource(minted);
|
|
109
|
+
throw new DoCallError(operation, 'mutating', classifyDoCall(error), error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* facet-pool.ts — leased facets, so reclaiming storage is the default and
|
|
3
|
+
* leaking it takes intent.
|
|
4
|
+
*
|
|
5
|
+
* Proteus's facet-spawn.ts (313 lines) exists because the platform's two
|
|
6
|
+
* teardown verbs are indistinguishable to a caller and only one gives
|
|
7
|
+
* storage back: `abort` is mid-flight eviction with storage KEPT, `delete`
|
|
8
|
+
* is terminal with storage WIPED. Its docstring records the cost of
|
|
9
|
+
* confusing them: "the leak this module previously had, in which every head
|
|
10
|
+
* and every MCTS branch abandoned a permanent database inside the
|
|
11
|
+
* orchestrator DO". The lease makes that leak unreachable: disposal retires
|
|
12
|
+
* the facet (evict, then wipe), and keeping storage is the explicit opt-in
|
|
13
|
+
* (`detach()`, today's abort).
|
|
14
|
+
*
|
|
15
|
+
* The constraints a caller must not be surprised by, from Proteus's platform
|
|
16
|
+
* catalog (all proven by probe or by source):
|
|
17
|
+
* - a facet cannot set alarms (`do.facet.no_alarms`) — a head cannot
|
|
18
|
+
* schedule its own resumption; everything time-driven routes through the
|
|
19
|
+
* root's single alarm (`timers`).
|
|
20
|
+
* - a facet stub is coordinator-local (`do.facet.stub_local`) — it cannot
|
|
21
|
+
* be transferred, stored, or re-invoked indirectly.
|
|
22
|
+
* - facet storage is charged to the ROOT's shared budget, and a clone that
|
|
23
|
+
* crosses it is an uncatchable reset, not an error (`do.storage.bytes`).
|
|
24
|
+
* - a parent and its facets are evicted JOINTLY after minutes idle, so
|
|
25
|
+
* in-memory facet state is never safe to assume between two RPCs.
|
|
26
|
+
* - 65,536 facet ids per DO lifetime (`do.facet.count`), append-only and
|
|
27
|
+
* never reclaimed — the binding constraint for the leak, reached an
|
|
28
|
+
* order of magnitude before the byte quota. The pool counts first-use
|
|
29
|
+
* names in the durable ledger (budgets.ts) and refuses a NEW name at the
|
|
30
|
+
* wall by name, instead of letting the platform fail opaquely. Refusal
|
|
31
|
+
* at the wall is exact, not a threshold: the ledger never overcounts.
|
|
32
|
+
*
|
|
33
|
+
* A failed reclaim stays loud (facet-spawn's `runOnceAndReclaim`): storage
|
|
34
|
+
* that was not given back is a permanent charge against the root's quota,
|
|
35
|
+
* and swallowing that is how the original leak stayed invisible.
|
|
36
|
+
*
|
|
37
|
+
* The pool drives the RAW `ctx.facets` container and assumes it is the only
|
|
38
|
+
* thing naming facets on this actor. The Agents SDK's sub-agent layer makes
|
|
39
|
+
* the same assumption from the other side — it owns facet naming and runs
|
|
40
|
+
* its own cleanup — so the two are mutually exclusive on one actor:
|
|
41
|
+
* whichever acts second aborts or retires facets the other still tracks,
|
|
42
|
+
* and the facet-id ledger here counts only the names this pool minted.
|
|
43
|
+
*/
|
|
44
|
+
/** `ctx.facets`, as the pool drives it — same surface the facet host uses. */
|
|
45
|
+
export interface FacetPoolContainer {
|
|
46
|
+
get(name: string, start: () => Promise<{
|
|
47
|
+
class: unknown;
|
|
48
|
+
}>): unknown;
|
|
49
|
+
abort(name: string, reason?: unknown): void;
|
|
50
|
+
delete(name: string): void;
|
|
51
|
+
}
|
|
52
|
+
/** The hosting actor's context: its facet container, and the storage the
|
|
53
|
+
* facet-id ledger persists through. */
|
|
54
|
+
export interface FacetPoolContext {
|
|
55
|
+
facets?: FacetPoolContainer;
|
|
56
|
+
storage: {
|
|
57
|
+
get(key: string): Promise<unknown> | unknown;
|
|
58
|
+
put(key: string, value: unknown): Promise<void>;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* One leased facet. Dispose (or `retire()`) evicts the instance and WIPES
|
|
63
|
+
* its storage; `detach()` first to keep the storage — after it, disposal
|
|
64
|
+
* only evicts. The stub is coordinator-local: do not store it past the turn
|
|
65
|
+
* or hand it to anything else.
|
|
66
|
+
*/
|
|
67
|
+
export interface FacetLease<S> {
|
|
68
|
+
readonly name: string;
|
|
69
|
+
readonly stub: S;
|
|
70
|
+
/** Keep the facet's storage: disposal becomes eviction only. */
|
|
71
|
+
detach(): void;
|
|
72
|
+
/** Idempotent. Throws, loudly, when the platform refuses the wipe. */
|
|
73
|
+
retire(): Promise<void>;
|
|
74
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
/** The facet pool of one hosting actor. Cheap accessor, like `timers()`. */
|
|
77
|
+
export declare function facetPool(ctx: FacetPoolContext): FacetPool;
|
|
78
|
+
export declare class FacetPool {
|
|
79
|
+
private readonly ctx;
|
|
80
|
+
constructor(ctx: FacetPoolContext);
|
|
81
|
+
/**
|
|
82
|
+
* Open (or re-enter) the named facet under a lease. A first-use name
|
|
83
|
+
* consumes one of the object's 65,536 lifetime facet ids and is refused at
|
|
84
|
+
* the wall; a reused name costs nothing, in this incarnation or any other.
|
|
85
|
+
*/
|
|
86
|
+
acquire<S = unknown>(name: string, start: () => Promise<{
|
|
87
|
+
class: unknown;
|
|
88
|
+
}>): Promise<FacetLease<S>>;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=facet-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"facet-pool.d.ts","sourceRoot":"","sources":["../src/facet-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AASH,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC;IACrE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAC5C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;wCACwC;AACxC,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,OAAO,EAAE;QACP,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;QAC7C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACjD,CAAC;CACH;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,gEAAgE;IAChE,MAAM,IAAI,IAAI,CAAC;IACf,sEAAsE;IACtE,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAKD,4EAA4E;AAC5E,wBAAgB,SAAS,CAAC,GAAG,EAAE,gBAAgB,GAAG,SAAS,CAE1D;AAED,qBAAa,SAAS;IACR,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,gBAAgB;IAElD;;;;OAIG;IACG,OAAO,CAAC,CAAC,GAAG,OAAO,EACvB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,GACvC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;CAiD1B"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* facet-pool.ts — leased facets, so reclaiming storage is the default and
|
|
3
|
+
* leaking it takes intent.
|
|
4
|
+
*
|
|
5
|
+
* Proteus's facet-spawn.ts (313 lines) exists because the platform's two
|
|
6
|
+
* teardown verbs are indistinguishable to a caller and only one gives
|
|
7
|
+
* storage back: `abort` is mid-flight eviction with storage KEPT, `delete`
|
|
8
|
+
* is terminal with storage WIPED. Its docstring records the cost of
|
|
9
|
+
* confusing them: "the leak this module previously had, in which every head
|
|
10
|
+
* and every MCTS branch abandoned a permanent database inside the
|
|
11
|
+
* orchestrator DO". The lease makes that leak unreachable: disposal retires
|
|
12
|
+
* the facet (evict, then wipe), and keeping storage is the explicit opt-in
|
|
13
|
+
* (`detach()`, today's abort).
|
|
14
|
+
*
|
|
15
|
+
* The constraints a caller must not be surprised by, from Proteus's platform
|
|
16
|
+
* catalog (all proven by probe or by source):
|
|
17
|
+
* - a facet cannot set alarms (`do.facet.no_alarms`) — a head cannot
|
|
18
|
+
* schedule its own resumption; everything time-driven routes through the
|
|
19
|
+
* root's single alarm (`timers`).
|
|
20
|
+
* - a facet stub is coordinator-local (`do.facet.stub_local`) — it cannot
|
|
21
|
+
* be transferred, stored, or re-invoked indirectly.
|
|
22
|
+
* - facet storage is charged to the ROOT's shared budget, and a clone that
|
|
23
|
+
* crosses it is an uncatchable reset, not an error (`do.storage.bytes`).
|
|
24
|
+
* - a parent and its facets are evicted JOINTLY after minutes idle, so
|
|
25
|
+
* in-memory facet state is never safe to assume between two RPCs.
|
|
26
|
+
* - 65,536 facet ids per DO lifetime (`do.facet.count`), append-only and
|
|
27
|
+
* never reclaimed — the binding constraint for the leak, reached an
|
|
28
|
+
* order of magnitude before the byte quota. The pool counts first-use
|
|
29
|
+
* names in the durable ledger (budgets.ts) and refuses a NEW name at the
|
|
30
|
+
* wall by name, instead of letting the platform fail opaquely. Refusal
|
|
31
|
+
* at the wall is exact, not a threshold: the ledger never overcounts.
|
|
32
|
+
*
|
|
33
|
+
* A failed reclaim stays loud (facet-spawn's `runOnceAndReclaim`): storage
|
|
34
|
+
* that was not given back is a permanent charge against the root's quota,
|
|
35
|
+
* and swallowing that is how the original leak stayed invisible.
|
|
36
|
+
*
|
|
37
|
+
* The pool drives the RAW `ctx.facets` container and assumes it is the only
|
|
38
|
+
* thing naming facets on this actor. The Agents SDK's sub-agent layer makes
|
|
39
|
+
* the same assumption from the other side — it owns facet naming and runs
|
|
40
|
+
* its own cleanup — so the two are mutually exclusive on one actor:
|
|
41
|
+
* whichever acts second aborts or retires facets the other still tracks,
|
|
42
|
+
* and the facet-id ledger here counts only the names this pool minted.
|
|
43
|
+
*/
|
|
44
|
+
import { FACET_ID_LIFETIME_BUDGET, facetNameCount, recordFacetNameMinted, withFacetBudgetNamed, } from './budgets.js';
|
|
45
|
+
/** Names this ctx's pool has already charged to the lifetime ledger. */
|
|
46
|
+
const chargedNames = new WeakMap();
|
|
47
|
+
/** The facet pool of one hosting actor. Cheap accessor, like `timers()`. */
|
|
48
|
+
export function facetPool(ctx) {
|
|
49
|
+
return new FacetPool(ctx);
|
|
50
|
+
}
|
|
51
|
+
export class FacetPool {
|
|
52
|
+
ctx;
|
|
53
|
+
constructor(ctx) {
|
|
54
|
+
this.ctx = ctx;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Open (or re-enter) the named facet under a lease. A first-use name
|
|
58
|
+
* consumes one of the object's 65,536 lifetime facet ids and is refused at
|
|
59
|
+
* the wall; a reused name costs nothing, in this incarnation or any other.
|
|
60
|
+
*/
|
|
61
|
+
async acquire(name, start) {
|
|
62
|
+
const facets = this.ctx.facets;
|
|
63
|
+
if (!facets || typeof facets.get !== 'function') {
|
|
64
|
+
throw new Error('fabric: ctx.facets is unavailable in this Durable Object; facets cannot be leased');
|
|
65
|
+
}
|
|
66
|
+
let charged = chargedNames.get(this.ctx);
|
|
67
|
+
if (!charged) {
|
|
68
|
+
charged = new Set();
|
|
69
|
+
chargedNames.set(this.ctx, charged);
|
|
70
|
+
}
|
|
71
|
+
if (!charged.has(name)) {
|
|
72
|
+
const consumed = facetNameCount(this.ctx);
|
|
73
|
+
if (consumed >= FACET_ID_LIFETIME_BUDGET) {
|
|
74
|
+
throw withFacetBudgetNamed(consumed, new Error(`facet '${name}' refused before creation: no lifetime ids remain`));
|
|
75
|
+
}
|
|
76
|
+
recordFacetNameMinted(this.ctx, consumed + 1);
|
|
77
|
+
charged.add(name);
|
|
78
|
+
}
|
|
79
|
+
const stub = facets.get(name, start);
|
|
80
|
+
let settled = false;
|
|
81
|
+
let keepStorage = false;
|
|
82
|
+
const retire = async () => {
|
|
83
|
+
if (settled)
|
|
84
|
+
return;
|
|
85
|
+
settled = true;
|
|
86
|
+
// Evict first so the wipe never lands under a live writer; a facet
|
|
87
|
+
// already gone makes the abort a no-op.
|
|
88
|
+
try {
|
|
89
|
+
facets.abort(name, new Error('fabric: facet lease retired'));
|
|
90
|
+
}
|
|
91
|
+
catch { /* already gone */ }
|
|
92
|
+
if (keepStorage)
|
|
93
|
+
return;
|
|
94
|
+
try {
|
|
95
|
+
facets.delete(name);
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
throw new Error(`fabric: facet '${name}' was evicted but its storage was not reclaimed — `
|
|
99
|
+
+ `it is leaked into the root Durable Object's shared quota: ${errorText(e)}`, { cause: e });
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
name,
|
|
104
|
+
stub,
|
|
105
|
+
detach() { keepStorage = true; },
|
|
106
|
+
retire,
|
|
107
|
+
[Symbol.asyncDispose]: retire,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function errorText(error) {
|
|
112
|
+
return error instanceof Error ? error.message : String(error);
|
|
113
|
+
}
|