@polyengine/runtime 0.1.0-pre.g633468a
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/LICENSE +202 -0
- package/README.md +23 -0
- package/esm/cabi/async_values.js +162 -0
- package/esm/cabi/bulk_lists.js +198 -0
- package/esm/cabi/context.js +42 -0
- package/esm/cabi/flatten.js +145 -0
- package/esm/cabi/float.js +67 -0
- package/esm/cabi/handles.js +253 -0
- package/esm/cabi/layout.js +149 -0
- package/esm/cabi/lift.js +196 -0
- package/esm/cabi/load.js +146 -0
- package/esm/cabi/lower.js +141 -0
- package/esm/cabi/memory.js +182 -0
- package/esm/cabi/mod.js +22 -0
- package/esm/cabi/store.js +186 -0
- package/esm/cabi/strings.js +336 -0
- package/esm/cabi/trap.js +38 -0
- package/esm/cabi/types.js +264 -0
- package/esm/cabi/values.js +64 -0
- package/esm/cache/core.js +156 -0
- package/esm/cache/dir.js +170 -0
- package/esm/cache/mod.js +4 -0
- package/esm/cache/web.js +136 -0
- package/esm/digest/digest.js +332 -0
- package/esm/digest/mod.js +3 -0
- package/esm/digest/verify.js +129 -0
- package/esm/embedder/casing.js +56 -0
- package/esm/embedder/copy.js +42 -0
- package/esm/embedder/errors.js +26 -0
- package/esm/embedder/imports.js +63 -0
- package/esm/embedder/instantiate.js +978 -0
- package/esm/embedder/mod.js +40 -0
- package/esm/embedder/resources.js +406 -0
- package/esm/embedder/streams.js +770 -0
- package/esm/embedder/values.js +450 -0
- package/esm/embedder/version.js +273 -0
- package/esm/exec/boundary.js +1902 -0
- package/esm/exec/executor.js +1004 -0
- package/esm/exec/host_streams.js +818 -0
- package/esm/exec/mod.js +4 -0
- package/esm/intrinsics/async_builtins.js +510 -0
- package/esm/intrinsics/context.js +90 -0
- package/esm/intrinsics/errors.js +13 -0
- package/esm/intrinsics/fact_calls.js +865 -0
- package/esm/intrinsics/mod.js +564 -0
- package/esm/intrinsics/stream_builtins.js +578 -0
- package/esm/intrinsics/transcode.js +443 -0
- package/esm/jspi/bridge.js +579 -0
- package/esm/jspi/mechanics.js +89 -0
- package/esm/jspi/mod.js +5 -0
- package/esm/jspi/suspending.js +15 -0
- package/esm/jspi/types.js +29 -0
- package/esm/package.json +3 -0
- package/esm/plan/format.js +5 -0
- package/esm/plan/loader.js +657 -0
- package/esm/plan/mod.js +3 -0
- package/esm/shim/mod.js +2 -0
- package/esm/shim/translator.js +94 -0
- package/esm/task/mod.js +483 -0
- package/esm/task/scheduler.js +1028 -0
- package/esm/task/streams.js +786 -0
- package/esm/task/subtask.js +135 -0
- package/esm/task/thread.js +255 -0
- package/esm/task/waitable.js +144 -0
- package/package.json +91 -0
- package/types/cabi/async_values.d.ts +35 -0
- package/types/cabi/bulk_lists.d.ts +18 -0
- package/types/cabi/context.d.ts +59 -0
- package/types/cabi/flatten.d.ts +14 -0
- package/types/cabi/float.d.ts +14 -0
- package/types/cabi/handles.d.ts +70 -0
- package/types/cabi/layout.d.ts +13 -0
- package/types/cabi/lift.d.ts +25 -0
- package/types/cabi/load.d.ts +14 -0
- package/types/cabi/lower.d.ts +10 -0
- package/types/cabi/memory.d.ts +58 -0
- package/types/cabi/mod.d.ts +15 -0
- package/types/cabi/store.d.ts +12 -0
- package/types/cabi/strings.d.ts +23 -0
- package/types/cabi/trap.d.ts +11 -0
- package/types/cabi/types.d.ts +206 -0
- package/types/cabi/values.d.ts +5 -0
- package/types/cache/core.d.ts +97 -0
- package/types/cache/dir.d.ts +6 -0
- package/types/cache/mod.d.ts +3 -0
- package/types/cache/web.d.ts +10 -0
- package/types/digest/digest.d.ts +17 -0
- package/types/digest/mod.d.ts +2 -0
- package/types/digest/verify.d.ts +48 -0
- package/types/embedder/casing.d.ts +40 -0
- package/types/embedder/copy.d.ts +24 -0
- package/types/embedder/errors.d.ts +11 -0
- package/types/embedder/imports.d.ts +47 -0
- package/types/embedder/instantiate.d.ts +88 -0
- package/types/embedder/mod.d.ts +11 -0
- package/types/embedder/resources.d.ts +158 -0
- package/types/embedder/streams.d.ts +202 -0
- package/types/embedder/values.d.ts +70 -0
- package/types/embedder/version.d.ts +85 -0
- package/types/exec/boundary.d.ts +360 -0
- package/types/exec/executor.d.ts +125 -0
- package/types/exec/host_streams.d.ts +165 -0
- package/types/exec/mod.d.ts +3 -0
- package/types/intrinsics/async_builtins.d.ts +69 -0
- package/types/intrinsics/context.d.ts +28 -0
- package/types/intrinsics/errors.d.ts +5 -0
- package/types/intrinsics/fact_calls.d.ts +120 -0
- package/types/intrinsics/mod.d.ts +187 -0
- package/types/intrinsics/stream_builtins.d.ts +113 -0
- package/types/intrinsics/transcode.d.ts +21 -0
- package/types/jspi/bridge.d.ts +227 -0
- package/types/jspi/mechanics.d.ts +50 -0
- package/types/jspi/mod.d.ts +3 -0
- package/types/jspi/suspending.d.ts +1 -0
- package/types/jspi/types.d.ts +26 -0
- package/types/plan/format.d.ts +369 -0
- package/types/plan/loader.d.ts +113 -0
- package/types/plan/mod.d.ts +2 -0
- package/types/shim/mod.d.ts +1 -0
- package/types/shim/translator.d.ts +55 -0
- package/types/task/mod.d.ts +257 -0
- package/types/task/scheduler.d.ts +421 -0
- package/types/task/streams.d.ts +370 -0
- package/types/task/subtask.d.ts +96 -0
- package/types/task/thread.d.ts +73 -0
- package/types/task/waitable.d.ts +67 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Embedder conventions layer (contracts/embedder-api.md; docs/milestones.md C2 / docs/consumers.md).
|
|
2
|
+
//
|
|
3
|
+
// The host-facing surface: camelCase facades, resource classes on both sides,
|
|
4
|
+
// stream/future handles, version-canonical import resolution and the branded
|
|
5
|
+
// error model — all built at instantiate time from the plan's type tables, so
|
|
6
|
+
// the layer works fully untyped. Bindgen (a separate track) emits compile-time
|
|
7
|
+
// types that cast this facade; no generated code participates.
|
|
8
|
+
// Copy registration (contracts/embedder-api.md §"Module identity and
|
|
9
|
+
// @polyengine/protocol", amendment A9; issue #83). Runs at module evaluation, so
|
|
10
|
+
// merely importing the embedder surface puts this copy on the census — which
|
|
11
|
+
// is what makes every cross-copy diagnostic below able to name both sides.
|
|
12
|
+
// Multiple copies are DIAGNOSED, NEVER REFUSED: two isolated bundles on one
|
|
13
|
+
// page that exchange no values are legal.
|
|
14
|
+
import { PROTOCOL_GENERATION, registerRuntimeCopy } from "@polyengine/protocol";
|
|
15
|
+
import { COPY_URL, RUNTIME_VERSION } from "./copy.js";
|
|
16
|
+
registerRuntimeCopy({
|
|
17
|
+
// `COPY_URL` (embedder/copy.ts) rather than this module's own
|
|
18
|
+
// `import.meta.url`, so the census and every cross-copy message name the
|
|
19
|
+
// copy identically — one module owns the identity.
|
|
20
|
+
url: COPY_URL,
|
|
21
|
+
runtimeVersion: RUNTIME_VERSION,
|
|
22
|
+
protocolGeneration: PROTOCOL_GENERATION,
|
|
23
|
+
});
|
|
24
|
+
export { COPY_URL, RUNTIME_VERSION } from "./copy.js";
|
|
25
|
+
// The A9 vocabulary, re-exported unchanged: embedder code needs no import
|
|
26
|
+
// change, and consumers that want the multi-copy-robust spellings get them
|
|
27
|
+
// from the same module they already import.
|
|
28
|
+
export { copyCensus, DROPPED, ERROR_CONTEXT, FUTURE, hasBrand, INVALID_HANDLE, isDroppedError, isInvalidHandleError, isPeerTrappedError, isStreamProducerError, isSuspending, isTrap, isComponentException, PEER_TRAPPED, PROTOCOL_GENERATION, registerRuntimeCopy, RESOURCE_STATE, runtimeCopies, STREAM, STREAM_PRODUCER, SUSPENDING, TRAP, COMPONENT_EXCEPTION, } from "@polyengine/protocol";
|
|
29
|
+
export { artifactsFromEnvelope, instantiate, instantiateEmbedder, resolveArtifacts, } from "./instantiate.js";
|
|
30
|
+
export { requiredImports } from "./imports.js";
|
|
31
|
+
export { DroppedError, InvalidHandleError, NameCollisionError, PeerTrappedError, Trap, ComponentException, } from "./errors.js";
|
|
32
|
+
export { ErrorContext, Future, Stream, StreamProducerError, StreamWriter, } from "./streams.js";
|
|
33
|
+
export { GuestResource, HostResourceRegistry } from "./resources.js";
|
|
34
|
+
export { camelCase, parseLeafName, pascalCase } from "./casing.js";
|
|
35
|
+
// Per-declaration suspendability (contracts/embedder-api.md §"Functions and
|
|
36
|
+
// async", amendment A1): declares that a sync-typed host import may return a
|
|
37
|
+
// Promise, parking the calling wasm frame (JSPI engines only).
|
|
38
|
+
export { suspending } from "../jspi/suspending.js";
|
|
39
|
+
export { asTrackKeySpelling, compareSemver, ImportRegistrationError, ImportResolutionError, ImportResolver, parseInterfaceId, parseSemver, trackKey, } from "./version.js";
|
|
40
|
+
export { BorrowScope, fromHost, toHost, } from "./values.js";
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// Resources as classes on both sides of the boundary
|
|
2
|
+
// (contracts/embedder-api.md §"Resources"; C2 checklist item 3).
|
|
3
|
+
//
|
|
4
|
+
// The raw boundary represents `own<R>` / `borrow<R>` as bare **reps**
|
|
5
|
+
// (cabi/handles.ts `liftOwn` returns `rh.rep`; the host never holds a table
|
|
6
|
+
// index). C0 findings 1-3 were embedders turning that into identity tables and
|
|
7
|
+
// hand-transcribed `[method]…` keys by hand. Both become runtime obligations
|
|
8
|
+
// here.
|
|
9
|
+
//
|
|
10
|
+
// Ownership, per the contract's 2x4 table:
|
|
11
|
+
//
|
|
12
|
+
// | position | guest-implemented R | host-implemented R |
|
|
13
|
+
// | host receives own<R> | new wrapper (host owns) | instance back, mapping released, NO dispose |
|
|
14
|
+
// | host receives borrow<R> | wrapper valid for the call | instance, mapping kept |
|
|
15
|
+
// | host passes own<R> | wrapper invalidated | instance registered |
|
|
16
|
+
// | host passes borrow<R> | wrapper stays valid | rep reused/allocated |
|
|
17
|
+
import { RESOURCE_STATE } from "@polyengine/protocol";
|
|
18
|
+
import { hostDtorCall } from "../exec/boundary.js";
|
|
19
|
+
import { COPY_URL, describeCrossCopy } from "./copy.js";
|
|
20
|
+
import { InvalidHandleError } from "./errors.js";
|
|
21
|
+
import { camelCase, pascalCase } from "./casing.js";
|
|
22
|
+
/**
|
|
23
|
+
* Internal state of a guest-resource wrapper.
|
|
24
|
+
*
|
|
25
|
+
* The KEY is the process-global `polyengine.resourceState/1` brand since amendment
|
|
26
|
+
* A9 (it used to be a module-local `Symbol(...)`, on the now-repealed
|
|
27
|
+
* assumption that bundle and source runtimes are never mixed in one process —
|
|
28
|
+
* issue #83 showed they routinely are). The state SHAPE stays strictly
|
|
29
|
+
* runtime-internal, exactly as the A9 brand table notes: another copy may
|
|
30
|
+
* RECOGNIZE a wrapper, and must never read or write this object. `copyUrl` is
|
|
31
|
+
* what lets this copy tell its own wrappers from a foreign copy's.
|
|
32
|
+
*/
|
|
33
|
+
const STATE = RESOURCE_STATE;
|
|
34
|
+
/** Base of every runtime-built guest-resource class. */
|
|
35
|
+
export class GuestResource {
|
|
36
|
+
/** Drop the handle (alias of `[Symbol.dispose]`, so TS `using` works). */
|
|
37
|
+
drop() {
|
|
38
|
+
dropWrapper(this);
|
|
39
|
+
}
|
|
40
|
+
[Symbol.dispose]() {
|
|
41
|
+
dropWrapper(this);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Backstop for leaked handles (docs/architecture.md §7). A wrapper that becomes unreachable
|
|
46
|
+
* without `drop()` still runs the guest destructor — late, but not never.
|
|
47
|
+
*/
|
|
48
|
+
const runBackstop = (s) => {
|
|
49
|
+
// Idempotence: `valid` is the single guard. A wrapper that was dropped,
|
|
50
|
+
// transferred, or invalidated already cleared it (and unregistered), so the
|
|
51
|
+
// backstop can neither double-run a dtor nor resurrect a dead rep.
|
|
52
|
+
if (!s.valid || !s.owns)
|
|
53
|
+
return;
|
|
54
|
+
s.valid = false;
|
|
55
|
+
if (s.lends > 0) {
|
|
56
|
+
// A live guest borrow of this rep is outstanding (#86). Running the dtor
|
|
57
|
+
// now is exactly the use-after-free the reference forbids
|
|
58
|
+
// (definitions.py line 2325, `trap_if(h.num_lends != 0)`); the last
|
|
59
|
+
// `releaseLend` runs it instead. The closure held by the lowering scope
|
|
60
|
+
// keeps `s` alive, so the deferred drop is not lost with the wrapper.
|
|
61
|
+
s.pendingDrop = true;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
runHostDrop(s);
|
|
65
|
+
};
|
|
66
|
+
const leaked = new FinalizationRegistry(runBackstop);
|
|
67
|
+
/**
|
|
68
|
+
* Simulate the GC backstop firing for `w` (the FinalizationRegistry callback,
|
|
69
|
+
* verbatim). Test seam: real GC finalization is unschedulable, and #86 is
|
|
70
|
+
* precisely about what the backstop does in a window a test must control.
|
|
71
|
+
*
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
export function simulateFinalizationForTest(w) {
|
|
75
|
+
const s = wrapperState(w);
|
|
76
|
+
if (s !== undefined)
|
|
77
|
+
runBackstop(s);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Run a host-initiated drop of a guest `own` handle.
|
|
81
|
+
*
|
|
82
|
+
* The host holds a rep, never a table index, so there is nothing to remove
|
|
83
|
+
* from a handle table: the observable remainder of definitions.py
|
|
84
|
+
* `canon_resource_drop` for an owning handle is the lifted dtor call
|
|
85
|
+
* (`hostDtorCall`, exec/boundary.ts), with `caller = None` — a host-initiated
|
|
86
|
+
* call, `Store.invoke`'s `caller = None`.
|
|
87
|
+
*
|
|
88
|
+
* Never throws: the two callers are `drop()`/`[Symbol.dispose]()` — where a
|
|
89
|
+
* trap *is* reportable, so it propagates — and the FinalizationRegistry
|
|
90
|
+
* callback, where a throw would be swallowed by the engine with no
|
|
91
|
+
* diagnostic. `runHostDrop` is the latter's form: a trapping dtor poisons the
|
|
92
|
+
* implementing instance (which the lift harness does) and is additionally
|
|
93
|
+
* recorded on the store's host-failure channel, so the next driven call
|
|
94
|
+
* surfaces it instead of silently continuing on a half-destroyed instance
|
|
95
|
+
* (#86, second defect: the former `catch {}`).
|
|
96
|
+
*/
|
|
97
|
+
function runHostDrop(s) {
|
|
98
|
+
try {
|
|
99
|
+
hostDtorCall(s.rt, s.rep);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
recordHostFailure(s.rt, e);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Park a failure that has no frame to propagate into on the store. */
|
|
106
|
+
function recordHostFailure(rt, e) {
|
|
107
|
+
const store = rt.impl?.store;
|
|
108
|
+
if (store !== undefined && store.hostFailure === undefined) {
|
|
109
|
+
store.hostFailure = e;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function initWrapper(w, state) {
|
|
113
|
+
state.copyUrl ??= COPY_URL;
|
|
114
|
+
w[STATE] = state;
|
|
115
|
+
if (state.owns)
|
|
116
|
+
leaked.register(w, state, w);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* This copy's state for a wrapper, or `undefined`.
|
|
120
|
+
*
|
|
121
|
+
* A wrapper minted by ANOTHER copy carries the same (process-global) brand key
|
|
122
|
+
* but its state belongs to that copy — reading it here would be reading a
|
|
123
|
+
* foreign copy's private shape (A9). So it is not a state: it is
|
|
124
|
+
* `undefined` here, and `requireLive` turns that into the named cross-copy
|
|
125
|
+
* error rather than a misleading "not a resource handle" / "not live".
|
|
126
|
+
*/
|
|
127
|
+
export function wrapperState(w) {
|
|
128
|
+
const s = w[STATE];
|
|
129
|
+
if (s === undefined)
|
|
130
|
+
return undefined;
|
|
131
|
+
return s.copyUrl === COPY_URL ? s : undefined;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* True iff `w` carries the A9 resource-state key but is not one of ours.
|
|
135
|
+
*
|
|
136
|
+
* Note the resource brand is the odd one out in the A9 table: its value is the
|
|
137
|
+
* state OBJECT, not `true`, so `hasBrand` does not apply — presence of the key
|
|
138
|
+
* is the recognition. Only meaningful once `wrapperState` has returned
|
|
139
|
+
* `undefined`, i.e. presence here means "another copy's wrapper".
|
|
140
|
+
*/
|
|
141
|
+
function isForeignWrapper(w) {
|
|
142
|
+
return w[STATE] !== undefined;
|
|
143
|
+
}
|
|
144
|
+
function requireLive(w, what) {
|
|
145
|
+
const s = wrapperState(w);
|
|
146
|
+
if (s === undefined) {
|
|
147
|
+
if (isForeignWrapper(w)) {
|
|
148
|
+
throw new InvalidHandleError(`${what}: ${describeCrossCopy("this resource handle", "Resource wrappers hold a rep in the minting copy's tables; there " +
|
|
149
|
+
"is no by-value form — call through the copy that created it.")}`);
|
|
150
|
+
}
|
|
151
|
+
throw new InvalidHandleError(`${what}: not a resource handle`);
|
|
152
|
+
}
|
|
153
|
+
if (!s.valid) {
|
|
154
|
+
throw new InvalidHandleError(`${what}: this ${s.className} handle is no longer valid (it was ` +
|
|
155
|
+
`transferred as own<…>, dropped, or was a borrow that outlived its ` +
|
|
156
|
+
`call)`);
|
|
157
|
+
}
|
|
158
|
+
return s;
|
|
159
|
+
}
|
|
160
|
+
function dropWrapper(w) {
|
|
161
|
+
const s = wrapperState(w);
|
|
162
|
+
if (s === undefined || !s.valid)
|
|
163
|
+
return;
|
|
164
|
+
s.valid = false;
|
|
165
|
+
leaked.unregister(w);
|
|
166
|
+
if (!s.owns)
|
|
167
|
+
return; // a borrow was never ours to drop
|
|
168
|
+
if (s.lends > 0) {
|
|
169
|
+
// Lent out to an in-flight guest call (#86): defer rather than destroy a
|
|
170
|
+
// rep the guest still holds a `borrow` of. `drop(): void` stays
|
|
171
|
+
// non-blocking either way — the deferred dtor runs from `releaseLend`.
|
|
172
|
+
s.pendingDrop = true;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// The dtor runs as an ordinary LIFTED sync call (`hostDtorCall`, #160):
|
|
176
|
+
// definitions.py `canon_resource_drop` (line 2319) lifts it with
|
|
177
|
+
// `CanonicalOptions(async_ = False)` rather than calling it bare, and that
|
|
178
|
+
// is what gives the activation a Task/Thread. A dtor that suspends (a
|
|
179
|
+
// `promising`-entered dtor calling a `Suspending` import,
|
|
180
|
+
// docs/architecture.md §7) therefore releases the implementing instance's
|
|
181
|
+
// entry bracket at its first park, so the scheduler can resume it — the
|
|
182
|
+
// old held-bracket form wedged exactly there (#160).
|
|
183
|
+
//
|
|
184
|
+
// `drop(): void` stays non-blocking: an unfinished dtor's tail is driven
|
|
185
|
+
// by the store like any other parked activation, and a failure that has no
|
|
186
|
+
// frame to return into is parked on `store.hostFailure`.
|
|
187
|
+
hostDtorCall(s.rt, s.rep);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Record that a host-held `own` wrapper was lowered as `borrow<R>` into a
|
|
191
|
+
* guest call, and return the (idempotent) release for the end of that call.
|
|
192
|
+
*
|
|
193
|
+
* definitions.py: `lift_borrow` -> `Subtask.add_lender` (line 890) on the way
|
|
194
|
+
* in, `Subtask.deliver_resolve` (line 902) on the way out.
|
|
195
|
+
*/
|
|
196
|
+
export function lendWrapper(w) {
|
|
197
|
+
const s = wrapperState(w);
|
|
198
|
+
if (s === undefined)
|
|
199
|
+
return () => { };
|
|
200
|
+
s.lends += 1;
|
|
201
|
+
let released = false;
|
|
202
|
+
return () => {
|
|
203
|
+
if (released)
|
|
204
|
+
return;
|
|
205
|
+
released = true;
|
|
206
|
+
releaseLend(s);
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function releaseLend(s) {
|
|
210
|
+
s.lends -= 1;
|
|
211
|
+
if (s.lends > 0 || !s.pendingDrop)
|
|
212
|
+
return;
|
|
213
|
+
s.pendingDrop = false;
|
|
214
|
+
// The drop that arrived while the handle was lent. `valid` is already
|
|
215
|
+
// false (both deferral sites clear it first), so nothing can race this.
|
|
216
|
+
runHostDrop(s);
|
|
217
|
+
}
|
|
218
|
+
/** Host-side `num_lends` — diagnostics and white-box tests. */
|
|
219
|
+
export function wrapperLends(w) {
|
|
220
|
+
return wrapperState(w)?.lends ?? 0;
|
|
221
|
+
}
|
|
222
|
+
/** Invalidate a wrapper without dropping (used to end a borrow's lifetime). */
|
|
223
|
+
export function invalidateWrapper(w) {
|
|
224
|
+
const s = wrapperState(w);
|
|
225
|
+
if (s === undefined)
|
|
226
|
+
return;
|
|
227
|
+
s.valid = false;
|
|
228
|
+
leaked.unregister(w);
|
|
229
|
+
}
|
|
230
|
+
/** Read a wrapper's rep for a lowering site, applying the ownership rule. */
|
|
231
|
+
export function takeRep(w, own, what) {
|
|
232
|
+
if (typeof w !== "object" || w === null) {
|
|
233
|
+
throw new InvalidHandleError(`${what}: expected a resource class instance, got ${typeof w}`);
|
|
234
|
+
}
|
|
235
|
+
const s = requireLive(w, what);
|
|
236
|
+
if (own) {
|
|
237
|
+
// definitions.py `lift_own` (line 1508): `trap_if(h.num_lends != 0)`. A
|
|
238
|
+
// handle currently lent to an in-flight call cannot be transferred away.
|
|
239
|
+
if (s.lends > 0) {
|
|
240
|
+
throw new InvalidHandleError(`${what}: this ${s.className} handle is still lent out as a borrow ` +
|
|
241
|
+
`to an in-flight call and cannot be transferred`);
|
|
242
|
+
}
|
|
243
|
+
// Transfer: the wrapper is invalidated, and must NOT run the destructor.
|
|
244
|
+
s.valid = false;
|
|
245
|
+
leaked.unregister(w);
|
|
246
|
+
}
|
|
247
|
+
return s.rep;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Build the class for a guest-implemented resource.
|
|
251
|
+
*
|
|
252
|
+
* The JS constructor is **synchronous**: a JS constructor cannot return a
|
|
253
|
+
* Promise, so the contract's "exports are uniformly Promise-shaped" rule has
|
|
254
|
+
* one unavoidable exception here. A guest constructor that does not complete
|
|
255
|
+
* synchronously is reported as such rather than silently returning a
|
|
256
|
+
* half-built object (see the report's contract-friction list).
|
|
257
|
+
*/
|
|
258
|
+
export function buildGuestResourceClass(spec, rt, adapt, lowerArgs) {
|
|
259
|
+
const className = pascalCase(spec.name);
|
|
260
|
+
const cls = class extends GuestResource {
|
|
261
|
+
constructor(...args) {
|
|
262
|
+
super();
|
|
263
|
+
if (spec.ctor === null) {
|
|
264
|
+
throw new TypeError(`${className} has no WIT constructor; use its static functions`);
|
|
265
|
+
}
|
|
266
|
+
const where = `${className} constructor`;
|
|
267
|
+
const lowered = lowerArgs(args, spec.ctorParams ?? [], where);
|
|
268
|
+
const rep = spec.ctor(...lowered);
|
|
269
|
+
if (rep !== null && typeof rep === "object" && "then" in rep) {
|
|
270
|
+
throw new TypeError(`${where}: the guest constructor did not complete synchronously. ` +
|
|
271
|
+
`A JS constructor cannot await; expose an async factory instead.`);
|
|
272
|
+
}
|
|
273
|
+
if (typeof rep !== "number") {
|
|
274
|
+
throw new TypeError(`${where}: expected an own handle rep, got ${typeof rep}`);
|
|
275
|
+
}
|
|
276
|
+
initWrapper(this, {
|
|
277
|
+
rep,
|
|
278
|
+
valid: true,
|
|
279
|
+
owns: true,
|
|
280
|
+
rt,
|
|
281
|
+
className,
|
|
282
|
+
lends: 0,
|
|
283
|
+
pendingDrop: false,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
Object.defineProperty(cls, "name", { value: className });
|
|
288
|
+
for (const m of spec.methods) {
|
|
289
|
+
const js = camelCase(m.member);
|
|
290
|
+
const where = `${className}.${js}`;
|
|
291
|
+
Object.defineProperty(cls.prototype, js, {
|
|
292
|
+
configurable: true,
|
|
293
|
+
writable: true,
|
|
294
|
+
value: function (...args) {
|
|
295
|
+
// params[0] is the `borrow<R>`/`own<R>` self.
|
|
296
|
+
return adapt(m.raw, m.params, m.results, where, [this, ...args]);
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
for (const s of spec.statics) {
|
|
301
|
+
const js = camelCase(s.member);
|
|
302
|
+
const where = `${className}.${js} (static)`;
|
|
303
|
+
Object.defineProperty(cls, js, {
|
|
304
|
+
configurable: true,
|
|
305
|
+
writable: true,
|
|
306
|
+
value: (...args) => adapt(s.raw, s.params, s.results, where, args),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return cls;
|
|
310
|
+
}
|
|
311
|
+
/** Materialize an `own`/`borrow` wrapper for a rep coming out of a guest. */
|
|
312
|
+
export function makeWrapper(
|
|
313
|
+
// deno-lint-ignore no-explicit-any
|
|
314
|
+
cls, rep, rt, owns) {
|
|
315
|
+
const w = Object.create(cls.prototype);
|
|
316
|
+
initWrapper(w, {
|
|
317
|
+
rep,
|
|
318
|
+
valid: true,
|
|
319
|
+
owns,
|
|
320
|
+
rt,
|
|
321
|
+
className: cls.name ?? "resource",
|
|
322
|
+
lends: 0,
|
|
323
|
+
pendingDrop: false,
|
|
324
|
+
});
|
|
325
|
+
return w;
|
|
326
|
+
}
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Host-implemented resources
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
/**
|
|
331
|
+
* Runtime-owned instance <-> rep mapping for a host-implemented resource.
|
|
332
|
+
*
|
|
333
|
+
* The rep->instance direction is a **strong** map for exactly as long as the
|
|
334
|
+
* guest holds handles: the guest's handle is the only reference keeping a
|
|
335
|
+
* host object alive across calls, and a weak map here would let it be
|
|
336
|
+
* collected under the guest's feet.
|
|
337
|
+
*/
|
|
338
|
+
export class HostResourceRegistry {
|
|
339
|
+
className;
|
|
340
|
+
#byRep = new Map();
|
|
341
|
+
#byInstance = new WeakMap();
|
|
342
|
+
#next = 1;
|
|
343
|
+
constructor(className) {
|
|
344
|
+
this.className = className;
|
|
345
|
+
}
|
|
346
|
+
/** The host is passing an instance to the guest: allocate (or reuse) a rep. */
|
|
347
|
+
repFor(instance) {
|
|
348
|
+
if (instance === null || typeof instance !== "object") {
|
|
349
|
+
throw new TypeError(`${this.className}: expected a class instance, got ${typeof instance}`);
|
|
350
|
+
}
|
|
351
|
+
const held = this.#byInstance.get(instance);
|
|
352
|
+
if (held !== undefined && this.#byRep.has(held))
|
|
353
|
+
return held;
|
|
354
|
+
const rep = this.#next++;
|
|
355
|
+
this.#byRep.set(rep, instance);
|
|
356
|
+
this.#byInstance.set(instance, rep);
|
|
357
|
+
return rep;
|
|
358
|
+
}
|
|
359
|
+
/** Is this instance already registered with a live rep? */
|
|
360
|
+
hasInstance(instance) {
|
|
361
|
+
if (instance === null || typeof instance !== "object")
|
|
362
|
+
return false;
|
|
363
|
+
const held = this.#byInstance.get(instance);
|
|
364
|
+
return held !== undefined && this.#byRep.has(held);
|
|
365
|
+
}
|
|
366
|
+
/** Is `rep` live? Diagnostics and white-box tests. */
|
|
367
|
+
hasRep(rep) {
|
|
368
|
+
return this.#byRep.has(rep);
|
|
369
|
+
}
|
|
370
|
+
/** Release a rep if it is still live; no dtor, no error when already gone. */
|
|
371
|
+
releaseIfPresent(rep) {
|
|
372
|
+
this.#byRep.delete(rep);
|
|
373
|
+
}
|
|
374
|
+
/** A `borrow<R>` arrived from the guest: the host's own instance, mapping kept. */
|
|
375
|
+
lookup(rep) {
|
|
376
|
+
const inst = this.#byRep.get(rep);
|
|
377
|
+
if (inst === undefined) {
|
|
378
|
+
throw new InvalidHandleError(`${this.className}: no live instance for rep ${rep}`);
|
|
379
|
+
}
|
|
380
|
+
return inst;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* An `own<R>` arrived from the guest: the host gets its instance back, the
|
|
384
|
+
* guest's handle is gone, and **no dispose runs** (the contract's 2x4 table).
|
|
385
|
+
*/
|
|
386
|
+
release(rep) {
|
|
387
|
+
const inst = this.lookup(rep);
|
|
388
|
+
this.#byRep.delete(rep);
|
|
389
|
+
return inst;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* The guest dropped its last own handle: run the destructor. This is the
|
|
393
|
+
* `HostResourceType` dtor the executor calls from `canon_resource_drop`.
|
|
394
|
+
*/
|
|
395
|
+
dtor(rep) {
|
|
396
|
+
const inst = this.#byRep.get(rep);
|
|
397
|
+
if (inst === undefined)
|
|
398
|
+
return;
|
|
399
|
+
this.#byRep.delete(rep);
|
|
400
|
+
inst[Symbol.dispose]?.();
|
|
401
|
+
}
|
|
402
|
+
/** Live handle count — diagnostics and tests. */
|
|
403
|
+
get liveCount() {
|
|
404
|
+
return this.#byRep.size;
|
|
405
|
+
}
|
|
406
|
+
}
|