@microverse.ts/host-surface 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/application/ports/SchemaValidationPort.d.ts +9 -0
- package/dist/application/ports/SchemaValidationPort.d.ts.map +1 -0
- package/dist/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.d.ts +9 -0
- package/dist/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.d.ts.map +1 -0
- package/dist/application/useCases/compileHostSurface.d.ts +13 -0
- package/dist/application/useCases/compileHostSurface.d.ts.map +1 -0
- package/dist/domain/capabilityRegistrySymbol.d.ts +14 -0
- package/dist/domain/capabilityRegistrySymbol.d.ts.map +1 -0
- package/dist/domain/hostSurfaceManifest.d.ts +8 -0
- package/dist/domain/hostSurfaceManifest.d.ts.map +1 -0
- package/dist/domain/hostSurfaceTypes.d.ts +135 -0
- package/dist/domain/hostSurfaceTypes.d.ts.map +1 -0
- package/dist/domain/inferMethodAsync.d.ts +10 -0
- package/dist/domain/inferMethodAsync.d.ts.map +1 -0
- package/dist/domain/luaGlobalHook.d.ts +12 -0
- package/dist/domain/luaGlobalHook.d.ts.map +1 -0
- package/dist/domain/luaTypeAtoms.d.ts +4 -0
- package/dist/domain/luaTypeAtoms.d.ts.map +1 -0
- package/dist/domain/safeObjectKey.d.ts +7 -0
- package/dist/domain/safeObjectKey.d.ts.map +1 -0
- package/dist/domain/surfaceCapabilities.d.ts +21 -0
- package/dist/domain/surfaceCapabilities.d.ts.map +1 -0
- package/dist/domain/surfaceCapabilityString.d.ts +6 -0
- package/dist/domain/surfaceCapabilityString.d.ts.map +1 -0
- package/dist/domain/surfaceMethodDef.d.ts +25 -0
- package/dist/domain/surfaceMethodDef.d.ts.map +1 -0
- package/dist/domain/zodLuaType.d.ts +18 -0
- package/dist/domain/zodLuaType.d.ts.map +1 -0
- package/dist/domain/zodToLuaTypeRef.d.ts +17 -0
- package/dist/domain/zodToLuaTypeRef.d.ts.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +848 -0
- package/dist/index.js.map +1 -0
- package/dist/infrastructure/adapters/augmentHostWithCapabilityRegistry.d.ts +11 -0
- package/dist/infrastructure/adapters/augmentHostWithCapabilityRegistry.d.ts.map +1 -0
- package/dist/infrastructure/adapters/zodSchemaValidationAdapter.d.ts +3 -0
- package/dist/infrastructure/adapters/zodSchemaValidationAdapter.d.ts.map +1 -0
- package/dist/infrastructure/builders/bridgeMergeEnv.d.ts +11 -0
- package/dist/infrastructure/builders/bridgeMergeEnv.d.ts.map +1 -0
- package/dist/infrastructure/builders/defineHostSurfaceFacade.d.ts +41 -0
- package/dist/infrastructure/builders/defineHostSurfaceFacade.d.ts.map +1 -0
- package/dist/infrastructure/builders/surfaceBuilder.d.ts +34 -0
- package/dist/infrastructure/builders/surfaceBuilder.d.ts.map +1 -0
- package/dist/infrastructure/components/hostScriptSession.d.ts +94 -0
- package/dist/infrastructure/components/hostScriptSession.d.ts.map +1 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
import { buildDeclarativeBridgeTable } from "@microverse.ts/runtime-bridge";
|
|
2
|
+
import { validateWithZodSchema } from "@microverse.ts/runtime-zod";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { InMemoryCapabilityRegistry, createAllowlist, createCapabilityId } from "@microverse.ts/runtime-capabilities";
|
|
5
|
+
import { createMicroverseScript } from "@microverse.ts/runtime-core";
|
|
6
|
+
//#region src/infrastructure/builders/bridgeMergeEnv.ts
|
|
7
|
+
/**
|
|
8
|
+
* Builds a frozen `mergeEnv` table: bridge name → API object, ready for `MicroverseSlot.run({ mergeEnv })`.
|
|
9
|
+
*
|
|
10
|
+
* @param host - Your host context, already extended with the capability registry symbol from `@microverse.ts/host-surface`.
|
|
11
|
+
* @param slotKey - Same slot key passed to `buildDeclarativeBridgeTable` (string form of `MicroverseId` is fine).
|
|
12
|
+
* @param surface - Result of {@link defineHostSurface} (implements {@link HostSurfaceCore}).
|
|
13
|
+
*/
|
|
14
|
+
function buildBridgeMergeEnvForHost(host, slotKey, surface) {
|
|
15
|
+
return buildDeclarativeBridgeTable(host, slotKey, [...surface.toBridgeDeclarations()]);
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/infrastructure/adapters/zodSchemaValidationAdapter.ts
|
|
19
|
+
function createZodSchemaValidationPort() {
|
|
20
|
+
return { validateWithZodSchema };
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/domain/luaGlobalHook.ts
|
|
24
|
+
/**
|
|
25
|
+
* Builds the `on{Kind}` method name dispatched on the slot’s workflow handler for a PascalCase event `kind`
|
|
26
|
+
* (e.g. `InventoryLow` → `onInventoryLow`).
|
|
27
|
+
* Throws if `kind` is not a safe Lua identifier fragment.
|
|
28
|
+
*/
|
|
29
|
+
function luaGlobalHookName(kind) {
|
|
30
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(kind)) throw new Error(`unsafe Lua identifier for event kind: ${kind}`);
|
|
31
|
+
return `on${kind}`;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/domain/luaTypeAtoms.ts
|
|
35
|
+
/** Lua / LuaCATS type atoms: never emit `---@alias` for these names from `lua.paramTypes` / `lua.returns` tokens. */
|
|
36
|
+
var LUA_TYPE_ATOMS = new Set([
|
|
37
|
+
"any",
|
|
38
|
+
"boolean",
|
|
39
|
+
"false",
|
|
40
|
+
"integer",
|
|
41
|
+
"nil",
|
|
42
|
+
"never",
|
|
43
|
+
"number",
|
|
44
|
+
"self",
|
|
45
|
+
"string",
|
|
46
|
+
"table",
|
|
47
|
+
"true",
|
|
48
|
+
"unknown"
|
|
49
|
+
]);
|
|
50
|
+
function isLuaTypeAtom(name) {
|
|
51
|
+
return LUA_TYPE_ATOMS.has(name);
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/domain/zodLuaType.ts
|
|
55
|
+
var aliasBySchema = /* @__PURE__ */ new WeakMap();
|
|
56
|
+
/**
|
|
57
|
+
* Registers a nominal LuaCATS name for a Zod schema. {@link zodToLuaTypeRef} emits the name;
|
|
58
|
+
* `.d.lua` generation adds a matching `---@alias` with the structural shape.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* export const orderDto = luaType('OrderDto', z.object({ id: z.string() }));
|
|
63
|
+
* // bridge: output: orderDto.optional() → returns `OrderDto|nil` in manifest
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
function luaType(name, schema) {
|
|
67
|
+
if (!/^[A-Za-z_]\w*$/.test(name)) throw new Error(`luaType: invalid alias name: ${name}`);
|
|
68
|
+
aliasBySchema.set(schema, name);
|
|
69
|
+
return schema;
|
|
70
|
+
}
|
|
71
|
+
/** @internal Root schema that carries a {@link luaType} registration (after unwrapping optional/nullable/…). */
|
|
72
|
+
function getLuaTypeRegistrationRoot(schema) {
|
|
73
|
+
let cur = schema;
|
|
74
|
+
for (;;) {
|
|
75
|
+
if (aliasBySchema.get(cur) !== void 0) return cur;
|
|
76
|
+
const next = unwrapOneLayer(cur);
|
|
77
|
+
if (next === void 0) return;
|
|
78
|
+
cur = next;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** @internal Nominal LuaCATS name for this schema, if registered via {@link luaType}. */
|
|
82
|
+
function resolveZodLuaTypeAlias(schema) {
|
|
83
|
+
const root = getLuaTypeRegistrationRoot(schema);
|
|
84
|
+
return root === void 0 ? void 0 : aliasBySchema.get(root);
|
|
85
|
+
}
|
|
86
|
+
function getRegisteredLuaTypeName(root) {
|
|
87
|
+
return aliasBySchema.get(root);
|
|
88
|
+
}
|
|
89
|
+
function unwrapOneLayer(schema) {
|
|
90
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) return schema.unwrap();
|
|
91
|
+
if (schema instanceof z.ZodDefault) return schema.removeDefault();
|
|
92
|
+
if (schema instanceof z.ZodReadonly) return schema.unwrap();
|
|
93
|
+
if (schema instanceof z.ZodEffects) return schema.innerType();
|
|
94
|
+
if (schema instanceof z.ZodPipeline) return schema._def.in;
|
|
95
|
+
if (schema instanceof z.ZodLazy) return schema.schema;
|
|
96
|
+
if (schema instanceof z.ZodBranded) return schema.unwrap();
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/domain/zodToLuaTypeRef.ts
|
|
100
|
+
/**
|
|
101
|
+
* Maps a Zod schema to a LuaCATS type reference string for manifest emission.
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* Register nominal types with {@link luaType} on shared Zod schemas (e.g. `orderDto`, `orderId`).
|
|
105
|
+
* Prefer that over per-method `lua.paramTypes` / `lua.returns` escape hatches on bridge methods.
|
|
106
|
+
*/
|
|
107
|
+
function zodToLuaTypeRef(schema, options) {
|
|
108
|
+
const emitAliasNames = options?.emitAliasNames !== false;
|
|
109
|
+
if (schema instanceof z.ZodOptional) return `${zodToLuaTypeRef(schema.unwrap(), options)}|nil`;
|
|
110
|
+
if (schema instanceof z.ZodNullable) return `${zodToLuaTypeRef(schema.unwrap(), options)}|nil`;
|
|
111
|
+
if (schema instanceof z.ZodDefault) return zodToLuaTypeRef(schema.removeDefault(), options);
|
|
112
|
+
if (schema instanceof z.ZodReadonly) return zodToLuaTypeRef(schema.unwrap(), options);
|
|
113
|
+
if (schema instanceof z.ZodCatch) return zodToLuaTypeRef(schema._def.innerType, options);
|
|
114
|
+
if (schema instanceof z.ZodPipeline) return zodToLuaTypeRef(schema._def.out, options);
|
|
115
|
+
if (schema instanceof z.ZodEffects) return zodToLuaTypeRef(schema.innerType(), options);
|
|
116
|
+
if (schema instanceof z.ZodLazy) return zodToLuaTypeRef(schema.schema, options);
|
|
117
|
+
if (schema instanceof z.ZodBranded) return zodToLuaTypeRef(schema.unwrap(), options);
|
|
118
|
+
if (emitAliasNames) {
|
|
119
|
+
const alias = resolveZodLuaTypeAlias(schema);
|
|
120
|
+
if (alias !== void 0) return alias;
|
|
121
|
+
}
|
|
122
|
+
if (schema instanceof z.ZodString) return "string";
|
|
123
|
+
if (schema instanceof z.ZodNumber) return zodNumberToLua(schema);
|
|
124
|
+
if (schema instanceof z.ZodBoolean) return "boolean";
|
|
125
|
+
if (schema instanceof z.ZodBigInt) return "integer";
|
|
126
|
+
if (schema instanceof z.ZodUndefined) return "nil";
|
|
127
|
+
if (schema instanceof z.ZodNull) return "nil";
|
|
128
|
+
if (schema instanceof z.ZodVoid) return "nil";
|
|
129
|
+
if (schema instanceof z.ZodUnknown || schema instanceof z.ZodAny) return "unknown";
|
|
130
|
+
if (schema instanceof z.ZodLiteral) {
|
|
131
|
+
const v = schema.value;
|
|
132
|
+
if (typeof v === "string") return `"${v}"`;
|
|
133
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
134
|
+
return "unknown";
|
|
135
|
+
}
|
|
136
|
+
if (schema instanceof z.ZodEnum) return schema.options.map((o) => `"${String(o)}"`).join("|");
|
|
137
|
+
if (schema instanceof z.ZodNativeEnum) return "string|number";
|
|
138
|
+
if (schema instanceof z.ZodArray) return "table";
|
|
139
|
+
if (schema instanceof z.ZodRecord) return "table";
|
|
140
|
+
if (schema instanceof z.ZodObject) {
|
|
141
|
+
const shape = schema.shape;
|
|
142
|
+
const keys = Object.keys(shape);
|
|
143
|
+
if (keys.length === 0) return "{}";
|
|
144
|
+
return `{ ${keys.map((k) => `${k}: ${zodToLuaTypeRef(shape[k], options)}`).join("; ")} }`;
|
|
145
|
+
}
|
|
146
|
+
if (schema instanceof z.ZodUnion) return schema.options.map((o) => zodToLuaTypeRef(o, options)).join("|");
|
|
147
|
+
if (schema instanceof z.ZodDiscriminatedUnion) return schema.options.map((o) => zodToLuaTypeRef(o, options)).join("|");
|
|
148
|
+
if (schema instanceof z.ZodIntersection) return "table";
|
|
149
|
+
if (schema instanceof z.ZodTuple) return "table";
|
|
150
|
+
return "unknown";
|
|
151
|
+
}
|
|
152
|
+
function zodNumberToLua(schema) {
|
|
153
|
+
if (schema._def.checks.some((c) => c.kind === "int")) return "integer";
|
|
154
|
+
return "number";
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/domain/hostSurfaceManifest.ts
|
|
158
|
+
function asyncHandleClassName(bridgeName, methodName) {
|
|
159
|
+
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
160
|
+
return `${cap(bridgeName)}${cap(methodName)}Handle`;
|
|
161
|
+
}
|
|
162
|
+
function buildWorkflowHookManifestFields(kinds, workflowHooks, selfType, fieldDescriptionSuffix) {
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const kind of kinds) {
|
|
165
|
+
if (!(workflowHooks[kind] instanceof z.ZodObject)) throw new Error(`defineHostSurface workflowHooks: "${kind}" must be a z.object(...)`);
|
|
166
|
+
const payloadName = `MicroverseWorkflowEvt_${kind}`;
|
|
167
|
+
const hookName = luaGlobalHookName(kind);
|
|
168
|
+
out.push({
|
|
169
|
+
name: hookName,
|
|
170
|
+
description: `${fieldDescriptionSuffix} Payload: \`${payloadName}\`.`,
|
|
171
|
+
luaType: `fun(self: ${selfType}, evt: ${payloadName})`
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
function pushWorkflowPayloadManifestClasses(kinds, workflowHooks, classes) {
|
|
177
|
+
for (const kind of kinds) {
|
|
178
|
+
const schema = workflowHooks[kind];
|
|
179
|
+
if (!(schema instanceof z.ZodObject)) throw new Error(`defineHostSurface workflowHooks: "${kind}" must be a z.object(...)`);
|
|
180
|
+
const name = `MicroverseWorkflowEvt_${kind}`;
|
|
181
|
+
const shape = schema.shape;
|
|
182
|
+
classes.push({
|
|
183
|
+
name,
|
|
184
|
+
description: `Workflow hook payload for \`${luaGlobalHookName(kind)}\` (Zod → LuaCATS fields).`,
|
|
185
|
+
fields: Object.keys(shape).map((k) => ({
|
|
186
|
+
name: k,
|
|
187
|
+
luaType: zodToLuaTypeRef(shape[k])
|
|
188
|
+
})),
|
|
189
|
+
emitSingleton: false
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function buildLuaDefManifestFromHostSurfaceSpec(spec, opts, workflowHooks) {
|
|
194
|
+
const classes = [];
|
|
195
|
+
for (const bridgeName of Object.keys(spec)) {
|
|
196
|
+
const methods = spec[bridgeName];
|
|
197
|
+
const manifestMethods = [];
|
|
198
|
+
for (const methodName of Object.keys(methods)) {
|
|
199
|
+
const entry = methods[methodName];
|
|
200
|
+
const resultLua = entry.lua?.returns ?? zodToLuaTypeRef(entry.output);
|
|
201
|
+
if (entry.async === true) {
|
|
202
|
+
const handleName = asyncHandleClassName(bridgeName, methodName);
|
|
203
|
+
const payloadParams = zodInputToManifestParams(entry.input, entry.lua?.paramTypes) ?? [];
|
|
204
|
+
manifestMethods.push({
|
|
205
|
+
name: methodName,
|
|
206
|
+
description: entry.description,
|
|
207
|
+
callStyle: "asyncBridge",
|
|
208
|
+
params: [...payloadParams, {
|
|
209
|
+
name: "onComplete",
|
|
210
|
+
luaType: `fun(result: ${resultLua})|nil`
|
|
211
|
+
}],
|
|
212
|
+
returns: handleName
|
|
213
|
+
});
|
|
214
|
+
classes.push({
|
|
215
|
+
name: handleName,
|
|
216
|
+
description: `Async handle for \`${bridgeName}:${methodName}\`. Call \`:await()\` for the resolved value.`,
|
|
217
|
+
fields: [{
|
|
218
|
+
name: "await",
|
|
219
|
+
luaType: `fun(self: ${handleName}): ${resultLua}`
|
|
220
|
+
}],
|
|
221
|
+
emitSingleton: false
|
|
222
|
+
});
|
|
223
|
+
} else manifestMethods.push({
|
|
224
|
+
name: methodName,
|
|
225
|
+
description: entry.description,
|
|
226
|
+
params: zodInputToManifestParams(entry.input, entry.lua?.paramTypes),
|
|
227
|
+
returns: resultLua
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
classes.push({
|
|
231
|
+
name: bridgeName,
|
|
232
|
+
methods: manifestMethods
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const fromLuaType = collectLuaTypeAliasesFromHostSpec(spec);
|
|
236
|
+
const fromOverrides = inferLuaTypeAliasesFromHostSpec(spec);
|
|
237
|
+
const merged = new Map([...fromLuaType.map((a) => [a.name, a.definition]), ...fromOverrides.map((a) => [a.name, a.definition])]);
|
|
238
|
+
if (opts.luaTypeAliases !== void 0) for (const [k, v] of Object.entries(opts.luaTypeAliases)) merged.set(k, v);
|
|
239
|
+
if (workflowHooks !== void 0) {
|
|
240
|
+
const kinds = Object.keys(workflowHooks).sort((a, b) => a.localeCompare(b));
|
|
241
|
+
pushWorkflowPayloadManifestClasses(kinds, workflowHooks, classes);
|
|
242
|
+
const abstractFields = buildWorkflowHookManifestFields(kinds, workflowHooks, "Workflow", "Host invokes this method on your table (from `workflow:extend()`) when the matching domain event fires.");
|
|
243
|
+
classes.push({
|
|
244
|
+
name: "Workflow",
|
|
245
|
+
description: "Abstract workflow handler type. Call `local w = workflow:extend()` then define `function w:onOrderPlaced(evt) … end` (etc.). Each Lua slot has its own `workflow` helper and handler table.",
|
|
246
|
+
fields: abstractFields,
|
|
247
|
+
emitSingleton: false
|
|
248
|
+
});
|
|
249
|
+
classes.push({
|
|
250
|
+
name: "workflow",
|
|
251
|
+
description: "Per-slot helper injected by the host (not a TS bridge). Creates the active handler table for this sandbox slot.",
|
|
252
|
+
methods: [{
|
|
253
|
+
name: "extend",
|
|
254
|
+
description: "Returns a new handler table with default no-op hooks; registers it for host → Lua dispatch in this slot.",
|
|
255
|
+
params: [],
|
|
256
|
+
returns: "Workflow"
|
|
257
|
+
}]
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const aliases = merged.size === 0 ? void 0 : [...merged.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, definition]) => ({
|
|
261
|
+
name,
|
|
262
|
+
definition
|
|
263
|
+
}));
|
|
264
|
+
return {
|
|
265
|
+
schemaVersion: 1,
|
|
266
|
+
output: opts.output,
|
|
267
|
+
headerNote: opts.headerNote,
|
|
268
|
+
aliases,
|
|
269
|
+
classes
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function nominalTokensFromLuaReturnString(retLua) {
|
|
273
|
+
const out = [];
|
|
274
|
+
const seen = /* @__PURE__ */ new Set();
|
|
275
|
+
for (const segment of retLua.split("|")) {
|
|
276
|
+
const s = segment.trim();
|
|
277
|
+
if (!/^[A-Za-z_]\w*$/.test(s)) continue;
|
|
278
|
+
if (isLuaTypeAtom(s)) continue;
|
|
279
|
+
if (seen.has(s)) continue;
|
|
280
|
+
seen.add(s);
|
|
281
|
+
out.push(s);
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
function unwrapOutputBaseForAlias(schema) {
|
|
286
|
+
let cur = schema;
|
|
287
|
+
for (;;) {
|
|
288
|
+
if (cur instanceof z.ZodOptional || cur instanceof z.ZodNullable) {
|
|
289
|
+
cur = cur.unwrap();
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (cur instanceof z.ZodDefault) {
|
|
293
|
+
cur = cur.removeDefault();
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (cur instanceof z.ZodReadonly) {
|
|
297
|
+
cur = cur.unwrap();
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (cur instanceof z.ZodEffects) {
|
|
301
|
+
cur = cur.innerType();
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (cur instanceof z.ZodPipeline) {
|
|
305
|
+
cur = cur._def.out;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
return cur;
|
|
311
|
+
}
|
|
312
|
+
function collectLuaTypeAliasesFromHostSpec(spec) {
|
|
313
|
+
const byName = /* @__PURE__ */ new Map();
|
|
314
|
+
const seenRoots = /* @__PURE__ */ new Set();
|
|
315
|
+
const consider = (schema) => {
|
|
316
|
+
const root = getLuaTypeRegistrationRoot(schema);
|
|
317
|
+
if (root === void 0 || seenRoots.has(root)) return;
|
|
318
|
+
const name = getRegisteredLuaTypeName(root);
|
|
319
|
+
if (name === void 0) return;
|
|
320
|
+
seenRoots.add(root);
|
|
321
|
+
const definition = zodToLuaTypeRef(root, { emitAliasNames: false });
|
|
322
|
+
if (definition !== name) byName.set(name, definition);
|
|
323
|
+
};
|
|
324
|
+
const walk = (schema) => {
|
|
325
|
+
consider(schema);
|
|
326
|
+
const base = unwrapInputSchema(schema);
|
|
327
|
+
if (base instanceof z.ZodObject) {
|
|
328
|
+
const shape = base.shape;
|
|
329
|
+
for (const field of Object.values(shape)) consider(field);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
for (const bridgeName of Object.keys(spec)) {
|
|
333
|
+
const methods = spec[bridgeName];
|
|
334
|
+
for (const methodName of Object.keys(methods)) {
|
|
335
|
+
const entry = methods[methodName];
|
|
336
|
+
walk(entry.input);
|
|
337
|
+
walk(entry.output);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return [...byName.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, definition]) => ({
|
|
341
|
+
name,
|
|
342
|
+
definition
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
function inferLuaTypeAliasesFromHostSpec(spec) {
|
|
346
|
+
const byName = /* @__PURE__ */ new Map();
|
|
347
|
+
for (const bridgeName of Object.keys(spec)) {
|
|
348
|
+
const methods = spec[bridgeName];
|
|
349
|
+
for (const methodName of Object.keys(methods)) {
|
|
350
|
+
const entry = methods[methodName];
|
|
351
|
+
const luaParams = entry.lua?.paramTypes;
|
|
352
|
+
if (luaParams !== void 0) {
|
|
353
|
+
const baseInput = unwrapInputSchema(entry.input);
|
|
354
|
+
if (baseInput instanceof z.ZodObject) {
|
|
355
|
+
const shape = baseInput.shape;
|
|
356
|
+
for (const [key, L] of Object.entries(luaParams)) {
|
|
357
|
+
if (typeof L !== "string") continue;
|
|
358
|
+
if (!/^[A-Za-z_]\w*$/.test(L) || isLuaTypeAtom(L)) continue;
|
|
359
|
+
const field = shape[key];
|
|
360
|
+
if (field === void 0) continue;
|
|
361
|
+
const def = zodToLuaTypeRef(field);
|
|
362
|
+
if (L !== def) byName.set(L, def);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const retLua = entry.lua?.returns;
|
|
367
|
+
if (typeof retLua === "string" && retLua.length > 0) for (const T of nominalTokensFromLuaReturnString(retLua)) {
|
|
368
|
+
const def = zodToLuaTypeRef(unwrapOutputBaseForAlias(entry.output));
|
|
369
|
+
if (T !== def) byName.set(T, def);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return [...byName.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, definition]) => ({
|
|
374
|
+
name,
|
|
375
|
+
definition
|
|
376
|
+
}));
|
|
377
|
+
}
|
|
378
|
+
function zodInputToManifestParams(input, luaParamTypes) {
|
|
379
|
+
const base = unwrapInputSchema(input);
|
|
380
|
+
if (base instanceof z.ZodObject) {
|
|
381
|
+
const shape = base.shape;
|
|
382
|
+
return Object.keys(shape).map((name) => ({
|
|
383
|
+
name,
|
|
384
|
+
luaType: luaParamTypes?.[name] ?? zodToLuaTypeRef(shape[name])
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
return [{
|
|
388
|
+
name: "value",
|
|
389
|
+
luaType: luaParamTypes?.value ?? zodToLuaTypeRef(base)
|
|
390
|
+
}];
|
|
391
|
+
}
|
|
392
|
+
function unwrapInputSchema(schema) {
|
|
393
|
+
let cur = schema;
|
|
394
|
+
if (cur instanceof z.ZodEffects) cur = cur.innerType();
|
|
395
|
+
if (cur instanceof z.ZodPipeline) cur = cur._def.in;
|
|
396
|
+
return cur;
|
|
397
|
+
}
|
|
398
|
+
//#endregion
|
|
399
|
+
//#region src/domain/surfaceCapabilities.ts
|
|
400
|
+
/** Runtime list of capability ids declared on a compiled surface spec. */
|
|
401
|
+
function collectCapabilitiesFromHostSurfaceSpec(spec) {
|
|
402
|
+
const out = [];
|
|
403
|
+
const seen = /* @__PURE__ */ new Set();
|
|
404
|
+
for (const bridgeName of Object.keys(spec)) {
|
|
405
|
+
const methods = spec[bridgeName];
|
|
406
|
+
for (const methodName of Object.keys(methods)) {
|
|
407
|
+
const id = methods[methodName].capability;
|
|
408
|
+
const key = String(id);
|
|
409
|
+
if (!seen.has(key)) {
|
|
410
|
+
seen.add(key);
|
|
411
|
+
out.push(id);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
/** Narrows `capabilities` to those declared on the surface (runtime check). */
|
|
418
|
+
function pickSurfaceCapabilities(surfaceCapabilities, ...capabilities) {
|
|
419
|
+
const allowed = new Set(surfaceCapabilities.map((c) => String(c)));
|
|
420
|
+
for (const id of capabilities) if (!allowed.has(String(id))) throw new Error(`capability not declared on host surface: ${String(id)} (surface has: ${[...allowed].join(", ")})`);
|
|
421
|
+
return capabilities;
|
|
422
|
+
}
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/domain/capabilityRegistrySymbol.ts
|
|
425
|
+
/**
|
|
426
|
+
* Well-known symbol key used to attach a {@link CapabilityRegistryPort} on the host object
|
|
427
|
+
* while surface bridge methods run. Populated by {@link augmentHostWithCapabilityRegistry} or
|
|
428
|
+
* internally by {@link HostScriptSession}.
|
|
429
|
+
*/
|
|
430
|
+
var MICROVERSE_CAPABILITY_REGISTRY = Symbol.for("microverse:capabilityRegistry");
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.ts
|
|
433
|
+
function isThenable(value) {
|
|
434
|
+
if (value === null || value === void 0) return false;
|
|
435
|
+
if (typeof value !== "object" && typeof value !== "function") return false;
|
|
436
|
+
return typeof value.then === "function";
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Builds declarative bridge declarations from a host surface spec, using the schema validation port for Lua ↔ host payloads.
|
|
440
|
+
*/
|
|
441
|
+
function createBridgeDeclarationsFromHostSurfaceSpec(schemaValidation, spec) {
|
|
442
|
+
const out = [];
|
|
443
|
+
for (const bridgeName of Object.keys(spec)) {
|
|
444
|
+
const methods = spec[bridgeName];
|
|
445
|
+
out.push({
|
|
446
|
+
name: bridgeName,
|
|
447
|
+
perEntity: true,
|
|
448
|
+
createApi: (host, slotKey) => {
|
|
449
|
+
const api = {};
|
|
450
|
+
for (const methodName of Object.keys(methods)) {
|
|
451
|
+
const entry = methods[methodName];
|
|
452
|
+
api[methodName] = (...args) => {
|
|
453
|
+
const payload = args.length >= 2 ? args[1] : args[0];
|
|
454
|
+
const registry = host[MICROVERSE_CAPABILITY_REGISTRY];
|
|
455
|
+
const capability = entry.capability;
|
|
456
|
+
if (!registry.isAllowed(capability)) throw new Error(`capability denied: ${String(capability)}`);
|
|
457
|
+
const parsedIn = schemaValidation.validateWithZodSchema(entry.input, payload);
|
|
458
|
+
if (parsedIn._tag === "err") throw new Error(parsedIn.error);
|
|
459
|
+
const raw = entry.handler({
|
|
460
|
+
host,
|
|
461
|
+
slotKey: String(slotKey)
|
|
462
|
+
}, parsedIn.value);
|
|
463
|
+
if (isThenable(raw)) return raw.then((resolved) => {
|
|
464
|
+
const parsedOut = schemaValidation.validateWithZodSchema(entry.output, resolved);
|
|
465
|
+
if (parsedOut._tag === "err") throw new Error(parsedOut.error);
|
|
466
|
+
return parsedOut.value;
|
|
467
|
+
});
|
|
468
|
+
const parsedOut = schemaValidation.validateWithZodSchema(entry.output, raw);
|
|
469
|
+
if (parsedOut._tag === "err") throw new Error(parsedOut.error);
|
|
470
|
+
return parsedOut.value;
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return Object.freeze(api);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
479
|
+
//#endregion
|
|
480
|
+
//#region src/application/useCases/compileHostSurface.ts
|
|
481
|
+
function buildHostSurfaceCore(schemaValidation, spec, workflowHooks) {
|
|
482
|
+
const capabilities = collectCapabilitiesFromHostSurfaceSpec(spec);
|
|
483
|
+
return {
|
|
484
|
+
toBridgeDeclarations: () => createBridgeDeclarationsFromHostSurfaceSpec(schemaValidation, spec),
|
|
485
|
+
toLuaDefManifest: (opts) => buildLuaDefManifestFromHostSurfaceSpec(spec, opts, workflowHooks),
|
|
486
|
+
capabilities,
|
|
487
|
+
pickCapabilities: (...picked) => pickSurfaceCapabilities(capabilities, ...picked)
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function compileHostSurface(ports, spec, workflowHooks) {
|
|
491
|
+
const [schemaValidation] = ports;
|
|
492
|
+
const core = buildHostSurfaceCore(schemaValidation, spec, workflowHooks);
|
|
493
|
+
if (workflowHooks === void 0) return core;
|
|
494
|
+
return {
|
|
495
|
+
...core,
|
|
496
|
+
workflowHooks
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Same as {@link compileHostSurface}, but requires every bridge method to be typed with the same `THost`.
|
|
501
|
+
*/
|
|
502
|
+
function compileHostSurfaceFor(ports, spec, workflowHooks) {
|
|
503
|
+
return workflowHooks === void 0 ? compileHostSurface(ports, spec) : compileHostSurface(ports, spec, workflowHooks);
|
|
504
|
+
}
|
|
505
|
+
//#endregion
|
|
506
|
+
//#region src/domain/inferMethodAsync.ts
|
|
507
|
+
/** Infers {@link HostSurfaceMethodEntry.async} from handler shape unless overridden explicitly. */
|
|
508
|
+
function inferMethodAsync(def) {
|
|
509
|
+
if (def.async === true) return true;
|
|
510
|
+
if (def.async === false) return false;
|
|
511
|
+
return def.handler.constructor.name === "AsyncFunction";
|
|
512
|
+
}
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region src/domain/surfaceMethodDef.ts
|
|
515
|
+
/**
|
|
516
|
+
* Converts a {@link SurfaceMethodDef} into a compiled {@link HostSurfaceMethodEntry}.
|
|
517
|
+
*/
|
|
518
|
+
function normalizeMethodDef(def) {
|
|
519
|
+
return {
|
|
520
|
+
capability: createCapabilityId(def.requires),
|
|
521
|
+
input: def.input,
|
|
522
|
+
output: def.output,
|
|
523
|
+
handler: def.handler,
|
|
524
|
+
async: inferMethodAsync(def),
|
|
525
|
+
...def.description !== void 0 ? { description: def.description } : {},
|
|
526
|
+
...def.lua !== void 0 ? { lua: def.lua } : {}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/domain/safeObjectKey.ts
|
|
531
|
+
/** Keys that must not be used as dynamic property names on ordinary objects. */
|
|
532
|
+
var FORBIDDEN_OBJECT_KEYS = new Set([
|
|
533
|
+
"__proto__",
|
|
534
|
+
"constructor",
|
|
535
|
+
"prototype"
|
|
536
|
+
]);
|
|
537
|
+
/**
|
|
538
|
+
* Rejects bridge/method names that could trigger prototype pollution when used as object keys.
|
|
539
|
+
*/
|
|
540
|
+
function assertSafeObjectKey(kind, name) {
|
|
541
|
+
if (FORBIDDEN_OBJECT_KEYS.has(name)) throw new Error(`Invalid surface ${kind} name "${name}": reserved key`);
|
|
542
|
+
}
|
|
543
|
+
/** Record with no inherited prototype — safe for dynamic string keys at runtime. */
|
|
544
|
+
function createNullPrototypeRecord() {
|
|
545
|
+
return Object.create(null);
|
|
546
|
+
}
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region src/infrastructure/builders/surfaceBuilder.ts
|
|
549
|
+
/**
|
|
550
|
+
* Fluent builder for a host surface. Created via {@link defineHostSurfaceFor} / {@link defineHostSurface} factory overloads.
|
|
551
|
+
*/
|
|
552
|
+
var SurfaceBuilder = class SurfaceBuilder {
|
|
553
|
+
spec = createNullPrototypeRecord();
|
|
554
|
+
workflowHooksSpec;
|
|
555
|
+
ports;
|
|
556
|
+
constructor(ports, workflowHooks, initialSpec) {
|
|
557
|
+
this.ports = ports;
|
|
558
|
+
this.workflowHooksSpec = workflowHooks;
|
|
559
|
+
if (initialSpec !== void 0) for (const bridgeName of Object.keys(initialSpec)) {
|
|
560
|
+
assertSafeObjectKey("bridge", bridgeName);
|
|
561
|
+
const srcBridge = initialSpec[bridgeName];
|
|
562
|
+
const bridge = createNullPrototypeRecord();
|
|
563
|
+
for (const methodName of Object.keys(srcBridge)) {
|
|
564
|
+
assertSafeObjectKey("method", methodName);
|
|
565
|
+
bridge[methodName] = srcBridge[methodName];
|
|
566
|
+
}
|
|
567
|
+
this.spec[bridgeName] = bridge;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/** Opens a Lua bridge table (e.g. `orders`, `greet`). */
|
|
571
|
+
bridge(name) {
|
|
572
|
+
return new BridgeBuilder(this, name);
|
|
573
|
+
}
|
|
574
|
+
/** Attaches workflow hook Zod schemas (emitted into `.d.lua` as `on*` methods). */
|
|
575
|
+
workflowHooks(hooks) {
|
|
576
|
+
return new SurfaceBuilder(this.ports, hooks, this.spec);
|
|
577
|
+
}
|
|
578
|
+
/** Compiles the accumulated spec into a {@link HostSurface}. */
|
|
579
|
+
build() {
|
|
580
|
+
const spec = this.spec;
|
|
581
|
+
return compileHostSurfaceFor(this.ports, spec, this.workflowHooksSpec);
|
|
582
|
+
}
|
|
583
|
+
/** @internal */
|
|
584
|
+
addMethod(bridgeName, methodName, entry) {
|
|
585
|
+
assertSafeObjectKey("bridge", bridgeName);
|
|
586
|
+
assertSafeObjectKey("method", methodName);
|
|
587
|
+
let bridge = this.spec[bridgeName];
|
|
588
|
+
if (bridge === void 0) {
|
|
589
|
+
bridge = createNullPrototypeRecord();
|
|
590
|
+
this.spec[bridgeName] = bridge;
|
|
591
|
+
}
|
|
592
|
+
bridge[methodName] = entry;
|
|
593
|
+
return this;
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
/**
|
|
597
|
+
* Per-bridge step in the fluent surface DSL. Return to {@link SurfaceBuilder} via `.method(…)`.
|
|
598
|
+
*/
|
|
599
|
+
var BridgeBuilder = class {
|
|
600
|
+
parent;
|
|
601
|
+
bridgeName;
|
|
602
|
+
constructor(parent, bridgeName) {
|
|
603
|
+
this.parent = parent;
|
|
604
|
+
this.bridgeName = bridgeName;
|
|
605
|
+
}
|
|
606
|
+
/** Registers one method on the current bridge and returns the root builder for chaining. */
|
|
607
|
+
method(name, def) {
|
|
608
|
+
return this.parent.addMethod(this.bridgeName, name, normalizeMethodDef(def));
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/infrastructure/builders/defineHostSurfaceFacade.ts
|
|
613
|
+
var defaultPorts = [createZodSchemaValidationPort()];
|
|
614
|
+
function createSurfaceBuilder() {
|
|
615
|
+
return new SurfaceBuilder(defaultPorts);
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Declares a **host surface** via the fluent builder (`bridge` → `method` → `build`).
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* ```ts
|
|
622
|
+
* const surface = defineHostSurface()
|
|
623
|
+
* .bridge('time')
|
|
624
|
+
* .method('delta', {
|
|
625
|
+
* requires: 'engine:time',
|
|
626
|
+
* input: z.object({}),
|
|
627
|
+
* output: z.number(),
|
|
628
|
+
* handler: ({ host }) => host.clock.dt,
|
|
629
|
+
* })
|
|
630
|
+
* .build();
|
|
631
|
+
* ```
|
|
632
|
+
*/
|
|
633
|
+
function defineHostSurface() {
|
|
634
|
+
return createSurfaceBuilder();
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Same as {@link defineHostSurface}, but every `handler` is typed against a single `THost`
|
|
638
|
+
* (the engine context passed to {@link HostScriptSession} / {@link MicroverseLua.create}).
|
|
639
|
+
*
|
|
640
|
+
* @example
|
|
641
|
+
* ```ts
|
|
642
|
+
* const surface = defineHostSurfaceFor<MyHost>()
|
|
643
|
+
* .bridge('greet')
|
|
644
|
+
* .method('hello', {
|
|
645
|
+
* requires: 'demo:greet',
|
|
646
|
+
* input: z.object({ name: z.string() }),
|
|
647
|
+
* output: z.string(),
|
|
648
|
+
* handler: ({ host }, { name }) => `Hello, ${name}`,
|
|
649
|
+
* })
|
|
650
|
+
* .build();
|
|
651
|
+
* ```
|
|
652
|
+
*/
|
|
653
|
+
function defineHostSurfaceFor() {
|
|
654
|
+
return createSurfaceBuilder();
|
|
655
|
+
}
|
|
656
|
+
//#endregion
|
|
657
|
+
//#region src/infrastructure/adapters/augmentHostWithCapabilityRegistry.ts
|
|
658
|
+
/**
|
|
659
|
+
* Returns a shallow copy of `host` with {@link MICROVERSE_CAPABILITY_REGISTRY} set to `registry`.
|
|
660
|
+
* Bridge handlers read the registry to enforce per-session capability allowlists.
|
|
661
|
+
*
|
|
662
|
+
* @param host - Your engine / service context passed into `buildDeclarativeBridgeTable`.
|
|
663
|
+
* @param registry - Typically an {@link InMemoryCapabilityRegistry} from `@microverse.ts/runtime-capabilities`.
|
|
664
|
+
*/
|
|
665
|
+
function augmentHostWithCapabilityRegistry(host, registry) {
|
|
666
|
+
return Object.assign(host, { [MICROVERSE_CAPABILITY_REGISTRY]: registry });
|
|
667
|
+
}
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/infrastructure/components/hostScriptSession.ts
|
|
670
|
+
/**
|
|
671
|
+
* Binds one **Lua slot** to a {@link HostSurface}: capability allowlist, Zod validation, and `mergeEnv` wiring.
|
|
672
|
+
*
|
|
673
|
+
* @remarks
|
|
674
|
+
* - **Lua → host (bridges):** tables/methods from {@link defineHostSurface} on `_ENV` call into TypeScript with Zod validation.
|
|
675
|
+
* - **Host → Lua (hooks):** when the surface defines `workflowHooks`, {@link HostScriptSession.openSession} installs
|
|
676
|
+
* a small `workflow` helper (`workflow:extend()` → handler table + slot registration) and
|
|
677
|
+
* {@link HostScriptSession.invokeGlobalHookIfPresent} dispatches `on…` methods on that table. Each session has its own
|
|
678
|
+
* slot env, so many workflows run concurrently without sharing Lua globals. Without `workflowHooks`, hooks are optional
|
|
679
|
+
* globals (`onSmoke`, …) invoked as plain functions.
|
|
680
|
+
* - Call {@link HostScriptSession.openSession} once before running chunks or invocations; {@link HostScriptSession.dispose} when the slot is torn down.
|
|
681
|
+
*
|
|
682
|
+
* @typeParam THost - Same host type as your surface handlers.
|
|
683
|
+
* @typeParam THooks - Align with {@link HostSurface} `workflowHooks` on the surface you pass in (or `undefined` when the surface has no workflow hooks).
|
|
684
|
+
*/
|
|
685
|
+
var HostScriptSession = class {
|
|
686
|
+
opts;
|
|
687
|
+
sandbox;
|
|
688
|
+
registry;
|
|
689
|
+
constructor(opts) {
|
|
690
|
+
this.opts = opts;
|
|
691
|
+
this.registry = new InMemoryCapabilityRegistry(createAllowlist([...opts.allowedCapabilities]));
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Allocates the underlying {@link MicroverseSlot} for this `slotKey` on the shared runtime.
|
|
695
|
+
*/
|
|
696
|
+
openSession = async () => {
|
|
697
|
+
this.sandbox = await this.opts.runtime.createMicroverse({ slotKey: this.opts.slotKey });
|
|
698
|
+
const hooks = readWorkflowHooks(this.opts.surface);
|
|
699
|
+
if (hooks !== void 0) {
|
|
700
|
+
const prelude = buildWorkflowStubPreludeLua(hooks);
|
|
701
|
+
await this.requireMicroverseSlot().run({
|
|
702
|
+
script: createMicroverseScript(prelude),
|
|
703
|
+
mergeEnv: this.mergeEnv(),
|
|
704
|
+
timeout: this.opts.defaultTimeout
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
/**
|
|
709
|
+
* Exposes the in-memory registry (e.g. to mutate allowlists in advanced tests).
|
|
710
|
+
*/
|
|
711
|
+
getCapabilityRegistry = () => this.registry;
|
|
712
|
+
requireMicroverseSlot() {
|
|
713
|
+
if (this.sandbox === void 0) throw new Error("HostScriptSession: openSession() was not called");
|
|
714
|
+
return this.sandbox;
|
|
715
|
+
}
|
|
716
|
+
mergeEnv() {
|
|
717
|
+
return buildBridgeMergeEnvForHost(augmentHostWithCapabilityRegistry(this.opts.host, this.registry), String(this.opts.slotKey), this.opts.surface);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Executes Lua source in the slot environment with surface bridges on `_ENV`.
|
|
721
|
+
*
|
|
722
|
+
* @param source - Full Lua chunk (compiled with `load(..., "t", env)` in the Wasm adapter).
|
|
723
|
+
*/
|
|
724
|
+
runChunk = async (source) => {
|
|
725
|
+
return this.requireMicroverseSlot().run({
|
|
726
|
+
script: createMicroverseScript(source),
|
|
727
|
+
mergeEnv: this.mergeEnv(),
|
|
728
|
+
timeout: this.opts.defaultTimeout
|
|
729
|
+
});
|
|
730
|
+
};
|
|
731
|
+
/**
|
|
732
|
+
* Host → Lua: when the surface has `workflowHooks`, invokes `impl:onHook(evt)` on the handler table registered by
|
|
733
|
+
* `workflow:extend()` in this slot. Otherwise invokes `_ENV[globalName](evt)` when that entry is a function.
|
|
734
|
+
*
|
|
735
|
+
* @param globalName - Hook method name, e.g. `onOrderPlaced` (same as {@link luaGlobalHookName}).
|
|
736
|
+
* @param payload - Table fields: only string, finite number, or boolean (Lua literals).
|
|
737
|
+
*/
|
|
738
|
+
invokeGlobalHookIfPresent = (async (globalName, payload) => {
|
|
739
|
+
assertSafeLuaGlobalName(globalName);
|
|
740
|
+
const sb = this.requireMicroverseSlot();
|
|
741
|
+
const tbl = luaTableLiteralFromPlainRecord(payload);
|
|
742
|
+
const src = readWorkflowHooks(this.opts.surface) !== void 0 ? buildWorkflowHookInvokeLuaSource(globalName, tbl) : buildGlobalHookInvokeLuaSource(globalName, tbl);
|
|
743
|
+
return sb.run({
|
|
744
|
+
script: createMicroverseScript(src),
|
|
745
|
+
mergeEnv: this.mergeEnv(),
|
|
746
|
+
timeout: this.opts.defaultTimeout
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
/**
|
|
750
|
+
* Host → Lua: invokes `_ENV[tableName][methodName](literalTable)` where `literalTable` is built only from
|
|
751
|
+
* string, finite number, or boolean fields in `payload`.
|
|
752
|
+
*
|
|
753
|
+
* @param tableName - Global table name in the slot env.
|
|
754
|
+
* @param methodName - Function field on that table.
|
|
755
|
+
* @param payload - Plain serializable fields for the Lua table literal.
|
|
756
|
+
*/
|
|
757
|
+
call = async (tableName, methodName, payload) => {
|
|
758
|
+
const sb = this.requireMicroverseSlot();
|
|
759
|
+
const tbl = luaTableLiteralFromUnknownRecord(payload);
|
|
760
|
+
const src = [
|
|
761
|
+
`local t = _ENV[${JSON.stringify(tableName)}]`,
|
|
762
|
+
`local f = type(t) == "table" and t[${JSON.stringify(methodName)}] or nil`,
|
|
763
|
+
`if type(f) == "function" then`,
|
|
764
|
+
` f(${tbl})`,
|
|
765
|
+
`end`
|
|
766
|
+
].join("\n");
|
|
767
|
+
return sb.run({
|
|
768
|
+
script: createMicroverseScript(src),
|
|
769
|
+
mergeEnv: this.mergeEnv(),
|
|
770
|
+
timeout: this.opts.defaultTimeout
|
|
771
|
+
});
|
|
772
|
+
};
|
|
773
|
+
/**
|
|
774
|
+
* Releases the slot in the runtime adapter and clears the session handle.
|
|
775
|
+
*/
|
|
776
|
+
dispose = async () => {
|
|
777
|
+
if (this.sandbox !== void 0) {
|
|
778
|
+
await this.sandbox.dispose();
|
|
779
|
+
this.sandbox = void 0;
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
};
|
|
783
|
+
function assertSafeLuaGlobalName(name) {
|
|
784
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`unsafe Lua global name: ${name}`);
|
|
785
|
+
}
|
|
786
|
+
function readWorkflowHooks(surface) {
|
|
787
|
+
if (!("workflowHooks" in surface)) return;
|
|
788
|
+
return surface.workflowHooks;
|
|
789
|
+
}
|
|
790
|
+
function buildWorkflowStubPreludeLua(hooks) {
|
|
791
|
+
return [
|
|
792
|
+
"local Base = {}",
|
|
793
|
+
"for _, name in ipairs({",
|
|
794
|
+
...Object.keys(hooks).sort((a, b) => a.localeCompare(b)).map((kind) => {
|
|
795
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(kind)) throw new Error(`unsafe workflow hook kind: ${kind}`);
|
|
796
|
+
return JSON.stringify(luaGlobalHookName(kind));
|
|
797
|
+
}).map((h) => ` ${h},`),
|
|
798
|
+
"}) do",
|
|
799
|
+
" rawset(Base, name, function() end)",
|
|
800
|
+
"end",
|
|
801
|
+
"rawset(_ENV, \"workflow\", {",
|
|
802
|
+
" extend = function(_)",
|
|
803
|
+
" local w = setmetatable({}, { __index = Base })",
|
|
804
|
+
" rawset(_ENV, \"__microverse_lua_WorkflowImpl\", w)",
|
|
805
|
+
" return w",
|
|
806
|
+
" end,",
|
|
807
|
+
"})"
|
|
808
|
+
].join("\n");
|
|
809
|
+
}
|
|
810
|
+
function buildWorkflowHookInvokeLuaSource(methodName, evtLiteral) {
|
|
811
|
+
return [
|
|
812
|
+
`local impl = rawget(_ENV, "__microverse_lua_WorkflowImpl")`,
|
|
813
|
+
`if type(impl) == "table" then`,
|
|
814
|
+
` local m = rawget(impl, ${JSON.stringify(methodName)})`,
|
|
815
|
+
` if type(m) == "function" then`,
|
|
816
|
+
` m(impl, ${evtLiteral})`,
|
|
817
|
+
` end`,
|
|
818
|
+
`end`
|
|
819
|
+
].join("\n");
|
|
820
|
+
}
|
|
821
|
+
function buildGlobalHookInvokeLuaSource(globalName, evtLiteral) {
|
|
822
|
+
return [
|
|
823
|
+
`local f = rawget(_ENV, ${JSON.stringify(globalName)})`,
|
|
824
|
+
`if type(f) == "function" then`,
|
|
825
|
+
` f(${evtLiteral})`,
|
|
826
|
+
`end`
|
|
827
|
+
].join("\n");
|
|
828
|
+
}
|
|
829
|
+
function luaTableLiteralFromPlainRecord(o) {
|
|
830
|
+
const parts = [];
|
|
831
|
+
for (const [k, v] of Object.entries(o)) if (typeof v === "string") parts.push(`${k} = ${JSON.stringify(v)}`);
|
|
832
|
+
else if (typeof v === "number" && Number.isFinite(v)) parts.push(`${k} = ${v}`);
|
|
833
|
+
else if (typeof v === "boolean") parts.push(`${k} = ${v ? "true" : "false"}`);
|
|
834
|
+
else throw new Error(`HostScriptSession: unsupported value type for key ${k}`);
|
|
835
|
+
return `{ ${parts.join(", ")} }`;
|
|
836
|
+
}
|
|
837
|
+
function luaTableLiteralFromUnknownRecord(o) {
|
|
838
|
+
const parts = [];
|
|
839
|
+
for (const [k, v] of Object.entries(o)) if (typeof v === "string") parts.push(`${k} = ${JSON.stringify(v)}`);
|
|
840
|
+
else if (typeof v === "number" && Number.isFinite(v)) parts.push(`${k} = ${v}`);
|
|
841
|
+
else if (typeof v === "boolean") parts.push(`${k} = ${v ? "true" : "false"}`);
|
|
842
|
+
else throw new Error(`HostScriptSession.call: unsupported value type for key ${k}`);
|
|
843
|
+
return `{ ${parts.join(", ")} }`;
|
|
844
|
+
}
|
|
845
|
+
//#endregion
|
|
846
|
+
export { BridgeBuilder, HostScriptSession, MICROVERSE_CAPABILITY_REGISTRY, SurfaceBuilder, augmentHostWithCapabilityRegistry, buildBridgeMergeEnvForHost, collectCapabilitiesFromHostSurfaceSpec, compileHostSurface, compileHostSurfaceFor, createBridgeDeclarationsFromHostSurfaceSpec, defineHostSurface, defineHostSurfaceFor, luaGlobalHookName, luaType, pickSurfaceCapabilities, zodToLuaTypeRef };
|
|
847
|
+
|
|
848
|
+
//# sourceMappingURL=index.js.map
|