-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsync.ts
More file actions
299 lines (280 loc) · 11.7 KB
/
Copy pathsync.ts
File metadata and controls
299 lines (280 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
// `sync()` — the explicit synchronous view of a WIT-sync export (contracts/
// embedder-api.md §"Functions and async").
//
// Placement: application machinery exported from
// `@polyengine/runtime/embedder`, like `createStream` — only an instantiating
// application holds export functions, so this is deliberately NOT host-module
// vocabulary and does not touch `@polyengine/protocol`.
//
// Recognition is by brand (`polyengine.syncCallable/1`, a registry symbol per
// module identity) so views work across mixed runtime copies. Unlike the boolean brands in
// `@polyengine/protocol`'s `brands.ts` (whose payload is always `true`), this
// brand carries a PAYLOAD describing the callable's synchronous form — the
// dispatch shapes below are what `instantiate.ts` / `resources.ts` attach at
// wrap time and what this module reads back.
/** The registry symbol. `Symbol.for` per module identity: N runtime copies agree on it
* without sharing modules. */
export const SYNC_CALLABLE: unique symbol = Symbol.for(
"polyengine.syncCallable/1",
);
/**
* The brand payload, keyed by what the branded value is.
*
* - `"free"` — a lifted export function (plain export, interface member, or
* resource static): `fn` is the fully-wrapped synchronous form.
* - `"method"` — a guest-resource prototype method: `fn` takes the resource
* instance as its first argument (the `borrow<R>`/`own<R>` self param the
* lifted function already declares).
* - `"async"` — an async-typed export: carries no synchronous form, named so
* `sync()` can report the real reason.
*/
export type SyncPayload =
| { kind: "free"; fn: (...args: unknown[]) => unknown }
| { kind: "method"; fn: (self: unknown, ...args: unknown[]) => unknown }
| { kind: "async" };
/**
* Stamp `payload` on `target` under the brand: non-enumerable, non-writable,
* matching `@polyengine/protocol`'s `defineBrand` (protocol/src/brands.ts) —
* implemented locally since the runtime does not add application-tier
* vocabulary to the protocol package.
*
* @internal — written by `instantiate.ts` and `resources.ts` at wrap/
* class-build time; not part of the public `sync()` surface.
*/
export function markSyncCallable(target: object, payload: SyncPayload): void {
Object.defineProperty(target, SYNC_CALLABLE, {
value: payload,
enumerable: false,
writable: false,
configurable: false,
});
}
/**
* Read the brand payload off `target`, or `undefined` if unbranded.
* Structural, like `hasBrand`: accepts a payload minted by any copy.
* @internal
*/
export function syncPayloadOf(target: unknown): SyncPayload | undefined {
if (target === null) return undefined;
const t = typeof target;
if (t !== "object" && t !== "function") return undefined;
return (target as Record<symbol, SyncPayload | undefined>)[SYNC_CALLABLE];
}
/** Own, function-valued, branded members of `proto`'s prototype chain
* (stopping at `Object.prototype`), nearest wins. Used to recognize a
* guest-resource INSTANCE: its class's prototype carries `"method"`-branded
* data properties (`resources.ts` `buildGuestResourceClass`). */
function protoBrandedMembers(proto: object): Map<string, SyncPayload> {
const out = new Map<string, SyncPayload>();
for (
let o: object | null = proto;
o !== null && o !== Object.prototype;
o = Object.getPrototypeOf(o)
) {
for (const key of Object.getOwnPropertyNames(o)) {
if (out.has(key) || key === "constructor") continue;
const d = Object.getOwnPropertyDescriptor(o, key);
if (d === undefined || typeof d.value !== "function") continue;
const p = syncPayloadOf(d.value);
if (p !== undefined) out.set(key, p);
}
}
return out;
}
/** Own, function-valued, branded static members of a guest-resource class. */
function ownBrandedStatics(cls: object): Map<string, SyncPayload> {
const out = new Map<string, SyncPayload>();
for (const key of Object.getOwnPropertyNames(cls)) {
if (key === "prototype" || key === "name" || key === "length") continue;
const d = Object.getOwnPropertyDescriptor(cls, key);
if (d === undefined || typeof d.value !== "function") continue;
const p = syncPayloadOf(d.value);
if (p !== undefined) out.set(key, p);
}
return out;
}
function isResourceInstance(v: object): boolean {
if (typeof v === "function") return false; // a class, not an instance
const proto = Object.getPrototypeOf(v);
if (proto === null || proto === Object.prototype) return false;
return protoBrandedMembers(proto).size > 0;
}
// deno-lint-ignore ban-types
function isResourceClass(v: Function): boolean {
return ownBrandedStatics(v).size > 0;
}
function asyncMessage(name: string): string {
return `sync(): '${name}' is an async-typed WIT export; async exports ` +
`have no synchronous form`;
}
function methodMessage(name: string): string {
return `sync(): '${name}' is a resource method; call sync(instance) ` +
`instead of sync(fn) — a bare method function has no receiver to bind`;
}
/** Views are stable: `sync(x) === sync(x)` for the same target. */
const views = new WeakMap<object, unknown>();
function memoView(key: object, build: () => unknown): unknown {
const cached = views.get(key);
if (cached !== undefined) return cached;
const view = build();
views.set(key, view);
return view;
}
/** A view member that reports its real reason (async) only when accessed —
* so an unrelated sync member of the same record/class/instance stays usable
* (see the CONTRACT note on record recursion below). */
function throwingMember(view: object, key: string, message: string): void {
Object.defineProperty(view, key, {
enumerable: true,
configurable: true,
get(): never {
throw new TypeError(message);
},
});
}
function instanceView(instance: object): unknown {
return memoView(instance, () => {
const proto = Object.getPrototypeOf(instance) as object;
const members = protoBrandedMembers(proto);
const view: Record<string, unknown> = {};
for (const [key, p] of members) {
if (p.kind === "method") {
const fn = p.fn;
view[key] = (...a: unknown[]) => fn(instance, ...a);
} else if (p.kind === "async") {
throwingMember(view, key, asyncMessage(key));
}
// A "free"-kind branded proto member should not occur (methods are
// always branded "method" by `buildGuestResourceClass`); nothing to do
// if it somehow did — the instance view only ever exposes methods
// (statics are not reachable from an instance; §"Functions and async").
}
return view;
});
}
function classView(cls: object): unknown {
return memoView(cls, () => {
const statics = ownBrandedStatics(cls);
const view: Record<string, unknown> = {};
for (const [key, p] of statics) {
if (p.kind === "free") {
view[key] = p.fn;
} else if (p.kind === "async") {
throwingMember(view, key, asyncMessage(key));
}
}
return view;
});
}
/**
* Map one record MEMBER by the `sync(record)` recursion rule: a branded
* function or a nested resource class/instance/record maps recursively;
* anything else (including an unbranded function) passes through unchanged.
*
* Record views invoke this lazily, so an async member fails on access without
* preventing use of unrelated sync members in the same record.
*/
function mapMember(v: unknown): unknown {
if (typeof v === "function") {
const p = syncPayloadOf(v);
if (p !== undefined) {
if (p.kind === "free") return p.fn;
if (p.kind === "method") throw new TypeError(methodMessage(v.name));
throw new TypeError(asyncMessage(v.name));
}
if (isResourceClass(v)) return classView(v);
return v; // unbranded function: pass through unchanged
}
if (v !== null && typeof v === "object") {
if (isResourceInstance(v)) return instanceView(v);
return recordView(v); // a nested (interface) record
}
return v; // primitives, null: pass through unchanged
}
function recordView(rec: object): unknown {
return memoView(rec, () => {
const view: Record<string, unknown> = {};
for (const key of Object.keys(rec)) {
const d = Object.getOwnPropertyDescriptor(rec, key);
if (d === undefined) continue;
const value = d.value;
// Lazy: `mapMember` runs (and can throw, for an async member) only
// when the caller actually reads this key — see the CONTRACT note on
// `mapMember` above.
Object.defineProperty(view, key, {
enumerable: true,
configurable: true,
get: () => mapMember(value),
});
}
return view;
});
}
/** `Promise<R>`-returning functions synchronize to `R`; records map
* recursively; everything else passes through. Type-level refusal of an
* async export is not attempted (the contract only requires the runtime
* error) — `Sync<F>` stays structural.
*
* Test non-Promise functions before objects to preserve call signatures.
* `object`, unlike Record<string, unknown>, also accepts named interfaces
* and class instance types without requiring an index signature. */
export type Sync<F> = F extends (...a: infer A) => Promise<infer R>
? (...a: A) => R
: F extends (...a: never[]) => unknown ? F // non-Promise functions (e.g. `drop(): void`) pass through unchanged
: F extends object ? { [K in keyof F]: Sync<F[K]> } // interfaces, class instances, records
: F;
/**
* The synchronous form of a WIT-sync export (contracts/embedder-api.md
* §"Functions and async").
*
* - `sync(fn)` — a lifted export function (plain export, interface member,
* or resource static): returns the synchronous form `(...args) => T`.
* - `sync(instance)` — a guest-resource wrapper: a view whose members call
* the synchronous forms with `instance` as receiver.
* - `sync(cls)` — a guest-resource class: a view of synchronous statics
* (constructors are already synchronous; `new` the class itself).
* - `sync(record)` — an exports record or nested interface record: a view
* with every member mapped by these same rules, recursively; non-branded
* members pass through unchanged.
* - Views are stable: `sync(x) === sync(x)`.
* - An async-typed export, bare resource-method function, unbranded top-level
* function, or primitive throws TypeError. In views, unsupported branded
* members fail on access; unbranded functions pass through.
*/
export function sync<F extends (...a: never[]) => Promise<unknown>>(
target: F,
): Sync<F>;
export function sync<T extends object>(target: T): Sync<T>;
/** Fallback for a non-branded/primitive target — always throws at runtime
* (see the dispatch above); typed loosely so a caller passing an arbitrary
* value (as opposed to a known export/record/instance/class shape) still
* type-checks, matching the runtime's willingness to name the mistake. */
export function sync(target: unknown): unknown;
export function sync(target: unknown): unknown {
if (typeof target === "function") {
const p = syncPayloadOf(target);
if (p !== undefined) {
if (p.kind === "free") return p.fn;
if (p.kind === "method") {
throw new TypeError(methodMessage(target.name || "<anonymous>"));
}
throw new TypeError(asyncMessage(target.name || "<anonymous>"));
}
if (isResourceClass(target)) return classView(target);
throw new TypeError(
`sync(): '${
target.name || "<anonymous>"
}' is not a sync-callable export (unbranded function)`,
);
}
if (target === null || typeof target !== "object") {
throw new TypeError(
`sync(): expected a lifted export function, guest-resource instance/` +
`class, or exports record; got ${
target === null ? "null" : typeof target
}`,
);
}
if (isResourceInstance(target)) return instanceView(target);
return recordView(target);
}