@microverse.ts/host-surface 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 +53 -22
- package/dist/application/ports/ScriptReferenceResolverPort.d.ts +13 -0
- package/dist/application/ports/ScriptReferenceResolverPort.d.ts.map +1 -0
- package/dist/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.d.ts +4 -4
- package/dist/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.d.ts.map +1 -1
- package/dist/application/useCases/compileHostSurface.d.ts +7 -6
- package/dist/application/useCases/compileHostSurface.d.ts.map +1 -1
- package/dist/domain/componentSlotPrelude.d.ts +11 -0
- package/dist/domain/componentSlotPrelude.d.ts.map +1 -0
- package/dist/domain/componentTypeSpec.d.ts +12 -0
- package/dist/domain/componentTypeSpec.d.ts.map +1 -0
- package/dist/domain/hostSurfaceManifest.d.ts +3 -2
- package/dist/domain/hostSurfaceManifest.d.ts.map +1 -1
- package/dist/domain/hostSurfaceSpecTypes.d.ts +89 -0
- package/dist/domain/hostSurfaceSpecTypes.d.ts.map +1 -0
- package/dist/domain/hostSurfaceTypes.d.ts +16 -92
- package/dist/domain/hostSurfaceTypes.d.ts.map +1 -1
- package/dist/domain/luaGlobalHook.d.ts +1 -1
- package/dist/domain/safeObjectKey.d.ts +1 -1
- package/dist/domain/safeObjectKey.d.ts.map +1 -1
- package/dist/domain/scriptCatalogManifest.d.ts +14 -0
- package/dist/domain/scriptCatalogManifest.d.ts.map +1 -0
- package/dist/domain/scriptContextSymbol.d.ts +6 -0
- package/dist/domain/scriptContextSymbol.d.ts.map +1 -0
- package/dist/domain/scriptProfileSpec.d.ts +31 -0
- package/dist/domain/scriptProfileSpec.d.ts.map +1 -0
- package/dist/domain/scriptPropertyMergeEnv.d.ts +6 -0
- package/dist/domain/scriptPropertyMergeEnv.d.ts.map +1 -0
- package/dist/domain/surfaceCapabilities.d.ts +1 -1
- package/dist/domain/surfaceCapabilities.d.ts.map +1 -1
- package/dist/domain/surfaceMethodDef.d.ts +1 -1
- package/dist/domain/surfaceMethodDef.d.ts.map +1 -1
- package/dist/index.d.ts +30 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +846 -258
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/adapters/augmentHostWithCapabilityRegistry.d.ts +1 -1
- package/dist/infrastructure/adapters/augmentHostWithCapabilityRegistry.d.ts.map +1 -1
- package/dist/infrastructure/adapters/augmentHostWithScriptContext.d.ts +4 -0
- package/dist/infrastructure/adapters/augmentHostWithScriptContext.d.ts.map +1 -0
- package/dist/infrastructure/adapters/zodSchemaValidationAdapter.d.ts +1 -1
- package/dist/infrastructure/adapters/zodSchemaValidationAdapter.d.ts.map +1 -1
- package/dist/infrastructure/builders/bridgeMergeEnv.d.ts +6 -8
- package/dist/infrastructure/builders/bridgeMergeEnv.d.ts.map +1 -1
- package/dist/infrastructure/builders/defineHostSurfaceFacade.d.ts +5 -5
- package/dist/infrastructure/builders/defineHostSurfaceFacade.d.ts.map +1 -1
- package/dist/infrastructure/builders/filterBridgeDeclarations.d.ts +11 -0
- package/dist/infrastructure/builders/filterBridgeDeclarations.d.ts.map +1 -0
- package/dist/infrastructure/builders/surfaceBuilder.d.ts +16 -11
- package/dist/infrastructure/builders/surfaceBuilder.d.ts.map +1 -1
- package/dist/infrastructure/components/hostScriptSession.d.ts +42 -72
- package/dist/infrastructure/components/hostScriptSession.d.ts.map +1 -1
- package/package.json +10 -9
package/dist/index.js
CHANGED
|
@@ -1,18 +1,404 @@
|
|
|
1
1
|
import { buildDeclarativeBridgeTable } from "@microverse.ts/runtime-bridge";
|
|
2
|
-
import {
|
|
2
|
+
import { applyScriptPropertyChanges, assertValidScriptPropertyBag, cloneScriptPropertyBag, createMicroverseScript, createScriptInstanceContext, diffScriptProperties } from "@microverse.ts/runtime-core";
|
|
3
|
+
import { createCapabilityId } from "@microverse.ts/runtime-capabilities";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
5
|
-
|
|
5
|
+
import { validateWithZodSchema } from "@microverse.ts/runtime-zod";
|
|
6
|
+
//#region src/domain/scriptContextSymbol.ts
|
|
7
|
+
var MICROVERSE_SCRIPT_CONTEXT = Symbol("microverse.scriptContext");
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.ts
|
|
10
|
+
function isThenable(value) {
|
|
11
|
+
if (value === null || value === void 0) return false;
|
|
12
|
+
if (typeof value !== "object" && typeof value !== "function") return false;
|
|
13
|
+
return typeof value.then === "function";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Builds declarative bridge declarations from a host surface spec, using the schema validation port for Lua ↔ host payloads.
|
|
17
|
+
*/
|
|
18
|
+
function createBridgeDeclarationsFromHostSurfaceSpec(schemaValidation, spec) {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const bridgeName of Object.keys(spec)) {
|
|
21
|
+
const methods = spec[bridgeName];
|
|
22
|
+
out.push({
|
|
23
|
+
name: bridgeName,
|
|
24
|
+
perEntity: true,
|
|
25
|
+
createApi: (host, slotKey) => {
|
|
26
|
+
const api = {};
|
|
27
|
+
for (const methodName of Object.keys(methods)) {
|
|
28
|
+
const entry = methods[methodName];
|
|
29
|
+
api[methodName] = (...args) => {
|
|
30
|
+
const payload = args.length >= 2 ? args[1] : args[0];
|
|
31
|
+
const parsedIn = schemaValidation.validateWithZodSchema(entry.input, payload);
|
|
32
|
+
if (parsedIn._tag === "err") throw new Error(parsedIn.error);
|
|
33
|
+
const script = host[MICROVERSE_SCRIPT_CONTEXT] ?? createScriptInstanceContext({
|
|
34
|
+
instanceId: String(slotKey),
|
|
35
|
+
scriptId: "unknown",
|
|
36
|
+
slotKey: String(slotKey)
|
|
37
|
+
});
|
|
38
|
+
const raw = entry.handler({
|
|
39
|
+
host,
|
|
40
|
+
slotKey: String(slotKey),
|
|
41
|
+
script
|
|
42
|
+
}, parsedIn.value);
|
|
43
|
+
if (isThenable(raw)) return raw.then((resolved) => {
|
|
44
|
+
const parsedOut = schemaValidation.validateWithZodSchema(entry.output, resolved);
|
|
45
|
+
if (parsedOut._tag === "err") throw new Error(parsedOut.error);
|
|
46
|
+
return parsedOut.value;
|
|
47
|
+
});
|
|
48
|
+
const parsedOut = schemaValidation.validateWithZodSchema(entry.output, raw);
|
|
49
|
+
if (parsedOut._tag === "err") throw new Error(parsedOut.error);
|
|
50
|
+
return parsedOut.value;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return Object.freeze(api);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/infrastructure/builders/filterBridgeDeclarations.ts
|
|
61
|
+
/**
|
|
62
|
+
* Keeps bridge declarations (and methods) whose capability is in the allow set.
|
|
63
|
+
*/
|
|
64
|
+
function filterBridgeDeclarationsByCapabilities(declarations, spec, capabilities) {
|
|
65
|
+
const allowed = new Set(capabilities.map((c) => String(c)));
|
|
66
|
+
const bridgeNames = /* @__PURE__ */ new Set();
|
|
67
|
+
for (const bridgeName of Object.keys(spec)) {
|
|
68
|
+
const methods = spec[bridgeName];
|
|
69
|
+
for (const methodName of Object.keys(methods)) {
|
|
70
|
+
const entry = methods[methodName];
|
|
71
|
+
if (allowed.has(String(entry.capability))) {
|
|
72
|
+
bridgeNames.add(bridgeName);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return declarations.filter((d) => bridgeNames.has(d.name)).map((d) => ({
|
|
78
|
+
...d,
|
|
79
|
+
createApi: (host, slotKey) => {
|
|
80
|
+
const api = d.createApi(host, slotKey);
|
|
81
|
+
const methods = spec[d.name];
|
|
82
|
+
const filtered = {};
|
|
83
|
+
for (const methodName of Object.keys(api)) {
|
|
84
|
+
const entry = methods[methodName];
|
|
85
|
+
if (entry !== void 0 && allowed.has(String(entry.capability))) filtered[methodName] = api[methodName];
|
|
86
|
+
}
|
|
87
|
+
return Object.freeze(filtered);
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
function createFilteredBridgeDeclarations(schemaValidation, spec, capabilities) {
|
|
92
|
+
return filterBridgeDeclarationsByCapabilities(createBridgeDeclarationsFromHostSurfaceSpec(schemaValidation, spec), spec, capabilities);
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
6
95
|
//#region src/infrastructure/builders/bridgeMergeEnv.ts
|
|
7
96
|
/**
|
|
8
|
-
* Builds a frozen
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
97
|
+
* Builds a frozen bridge table for a component profile capability set.
|
|
98
|
+
*/
|
|
99
|
+
function buildBridgeMergeEnvForProfile(schemaValidation, host, slotKey, spec, capabilities) {
|
|
100
|
+
return buildDeclarativeBridgeTable(host, slotKey, [...createFilteredBridgeDeclarations(schemaValidation, spec, capabilities)]);
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/domain/safeObjectKey.ts
|
|
104
|
+
/** Keys that must not be used as dynamic property names on ordinary objects. */
|
|
105
|
+
var FORBIDDEN_OBJECT_KEYS = new Set([
|
|
106
|
+
"__proto__",
|
|
107
|
+
"constructor",
|
|
108
|
+
"prototype"
|
|
109
|
+
]);
|
|
110
|
+
/**
|
|
111
|
+
* Rejects bridge/method names that could trigger prototype pollution when used as object keys.
|
|
13
112
|
*/
|
|
14
|
-
function
|
|
15
|
-
|
|
113
|
+
function assertSafeObjectKey(kind, name) {
|
|
114
|
+
if (FORBIDDEN_OBJECT_KEYS.has(name)) throw new Error(`Invalid surface ${kind} name "${name}": reserved key`);
|
|
115
|
+
}
|
|
116
|
+
/** Record with no inherited prototype — safe for dynamic string keys at runtime. */
|
|
117
|
+
function createNullPrototypeRecord() {
|
|
118
|
+
return Object.create(null);
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/domain/surfaceCapabilities.ts
|
|
122
|
+
/** Runtime list of capability ids declared on a compiled surface spec. */
|
|
123
|
+
function collectCapabilitiesFromHostSurfaceSpec(spec) {
|
|
124
|
+
const out = [];
|
|
125
|
+
const seen = /* @__PURE__ */ new Set();
|
|
126
|
+
for (const bridgeName of Object.keys(spec)) {
|
|
127
|
+
const methods = spec[bridgeName];
|
|
128
|
+
for (const methodName of Object.keys(methods)) {
|
|
129
|
+
const id = methods[methodName].capability;
|
|
130
|
+
const key = String(id);
|
|
131
|
+
if (!seen.has(key)) {
|
|
132
|
+
seen.add(key);
|
|
133
|
+
out.push(id);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
/** Narrows `capabilities` to those declared on the surface (runtime check). */
|
|
140
|
+
function pickSurfaceCapabilities(surfaceCapabilities, ...capabilities) {
|
|
141
|
+
const allowed = new Set(surfaceCapabilities.map((c) => String(c)));
|
|
142
|
+
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(", ")})`);
|
|
143
|
+
return capabilities;
|
|
144
|
+
}
|
|
145
|
+
z.object({});
|
|
146
|
+
var EMPTY_STATE = z.object({});
|
|
147
|
+
function scriptProfileComponentClassName(profileName) {
|
|
148
|
+
return `${profileName}Component`;
|
|
149
|
+
}
|
|
150
|
+
function scriptProfilePropsAlias(profileName) {
|
|
151
|
+
return `${profileName}Props`;
|
|
152
|
+
}
|
|
153
|
+
function scriptProfileStateAlias(profileName) {
|
|
154
|
+
return `${profileName}State`;
|
|
155
|
+
}
|
|
156
|
+
function scriptProfileBridgesClassName(profileName) {
|
|
157
|
+
return `${profileName}Bridges`;
|
|
158
|
+
}
|
|
159
|
+
/** Bridge table names whose methods include at least one capability from the profile. */
|
|
160
|
+
function bridgeNamesForCapabilities(spec, capabilities) {
|
|
161
|
+
const allowed = new Set(capabilities.map((c) => String(c)));
|
|
162
|
+
const names = [];
|
|
163
|
+
for (const bridgeName of Object.keys(spec)) {
|
|
164
|
+
const methods = spec[bridgeName];
|
|
165
|
+
for (const methodName of Object.keys(methods)) {
|
|
166
|
+
const entry = methods[methodName];
|
|
167
|
+
if (allowed.has(String(entry.capability))) {
|
|
168
|
+
names.push(bridgeName);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return names.sort((a, b) => a.localeCompare(b));
|
|
174
|
+
}
|
|
175
|
+
function resolveScriptProfile(registry, name, spec) {
|
|
176
|
+
const def = registry[name];
|
|
177
|
+
if (def === void 0) throw new Error(`unknown script profile: ${name}`);
|
|
178
|
+
const capabilities = def.capabilities.map((c) => createCapabilityId(c));
|
|
179
|
+
let props = def.props;
|
|
180
|
+
let state = def.state ?? EMPTY_STATE;
|
|
181
|
+
const hooks = [...def.hooks ?? []];
|
|
182
|
+
let parentName = def.extends;
|
|
183
|
+
const visited = new Set([name]);
|
|
184
|
+
while (parentName !== void 0) {
|
|
185
|
+
if (visited.has(parentName)) throw new Error(`script profile inheritance cycle: ${name}`);
|
|
186
|
+
visited.add(parentName);
|
|
187
|
+
const parent = registry[parentName];
|
|
188
|
+
if (parent === void 0) throw new Error(`script profile "${name}" extends unknown profile "${parentName}"`);
|
|
189
|
+
const parentCaps = parent.capabilities.map((c) => createCapabilityId(c));
|
|
190
|
+
for (const cap of parentCaps) if (!capabilities.some((c) => String(c) === String(cap))) capabilities.push(cap);
|
|
191
|
+
props = parent.props.merge(props);
|
|
192
|
+
state = (parent.state ?? EMPTY_STATE).merge(state);
|
|
193
|
+
const parentHooks = parent.hooks ?? [];
|
|
194
|
+
for (const h of parentHooks) if (!hooks.includes(h)) hooks.push(h);
|
|
195
|
+
parentName = parent.extends;
|
|
196
|
+
}
|
|
197
|
+
hooks.sort((a, b) => a.localeCompare(b));
|
|
198
|
+
const bridgeNames = bridgeNamesForCapabilities(spec, capabilities);
|
|
199
|
+
return {
|
|
200
|
+
name,
|
|
201
|
+
extends: def.extends,
|
|
202
|
+
capabilities,
|
|
203
|
+
props,
|
|
204
|
+
state,
|
|
205
|
+
hooks,
|
|
206
|
+
bridgeNames,
|
|
207
|
+
references: def.references
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function buildResolvedScriptProfileRegistry(registry, spec) {
|
|
211
|
+
const out = {};
|
|
212
|
+
for (const name of Object.keys(registry)) out[name] = resolveScriptProfile(registry, name, spec);
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
function validateScriptProfileRegistry(registry, spec, componentHooks, opts) {
|
|
216
|
+
if ((opts?.requireAtLeastOne ?? false) && Object.keys(registry).length === 0) throw new Error("host surface: at least one script profile or .componentType() is required");
|
|
217
|
+
const surfaceCaps = new Set(collectCapabilitiesFromHostSurfaceSpec(spec).map((c) => String(c)));
|
|
218
|
+
const hookKinds = componentHooks !== void 0 ? new Set(Object.keys(componentHooks)) : /* @__PURE__ */ new Set();
|
|
219
|
+
for (const name of Object.keys(registry)) {
|
|
220
|
+
assertSafeObjectKey("componentType", name);
|
|
221
|
+
const def = registry[name];
|
|
222
|
+
if (def.extends !== void 0) assertSafeObjectKey("componentType", def.extends);
|
|
223
|
+
for (const cap of def.capabilities) {
|
|
224
|
+
const id = createCapabilityId(cap);
|
|
225
|
+
if (!surfaceCaps.has(String(id))) throw new Error(`script profile "${name}": capability not declared on surface: ${String(cap)}`);
|
|
226
|
+
}
|
|
227
|
+
for (const hook of def.hooks ?? []) if (!hookKinds.has(hook)) throw new Error(`script profile "${name}": hook "${hook}" not declared in .componentHooks()`);
|
|
228
|
+
if (def.extends !== void 0 && registry[def.extends] === void 0) throw new Error(`script profile "${name}" extends unknown profile "${def.extends}"`);
|
|
229
|
+
}
|
|
230
|
+
for (const name of Object.keys(registry)) resolveScriptProfile(registry, name, spec);
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/domain/scriptCatalogManifest.ts
|
|
234
|
+
/** LuaCATS aliases per catalog scriptId → resolved component class (for `---@type` in `.lua` files). */
|
|
235
|
+
function buildScriptCatalogLuaDefManifest(entries) {
|
|
236
|
+
return {
|
|
237
|
+
schemaVersion: 1,
|
|
238
|
+
output: "generated/scriptCatalog.d.lua",
|
|
239
|
+
classes: [],
|
|
240
|
+
aliases: entries.filter((entry) => entry.localComponentClass !== true).slice().sort((a, b) => a.scriptId.localeCompare(b.scriptId)).map((entry) => {
|
|
241
|
+
const componentClass = scriptProfileComponentClassName(entry.profileId);
|
|
242
|
+
return {
|
|
243
|
+
name: scriptCatalogComponentAlias(entry.scriptId),
|
|
244
|
+
definition: componentClass
|
|
245
|
+
};
|
|
246
|
+
}),
|
|
247
|
+
globals: [],
|
|
248
|
+
luaHooks: []
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function scriptCatalogComponentAlias(scriptId) {
|
|
252
|
+
return `${scriptId.replace(/[^A-Za-z0-9_]/g, "_")}ScriptComponent`;
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/domain/componentSlotPrelude.ts
|
|
256
|
+
/** @see MICROVERSE_LUA_COMPONENT_SLOT_PRELUDE */
|
|
257
|
+
var MICROVERSE_LUA_COMPONENT_SLOT_PRELUDE = `
|
|
258
|
+
rawset(_ENV, "__microverse_component_rawProps", {})
|
|
259
|
+
rawset(_ENV, "__microverse_component_dirty", {})
|
|
260
|
+
|
|
261
|
+
local function rawProps()
|
|
262
|
+
return rawget(_ENV, "__microverse_component_rawProps")
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
local function dirty()
|
|
266
|
+
return rawget(_ENV, "__microverse_component_dirty")
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
local PropertiesMT = {
|
|
270
|
+
__index = function(t, k)
|
|
271
|
+
return rawget(t, "__raw")[k]
|
|
272
|
+
end,
|
|
273
|
+
__newindex = function(t, k, v)
|
|
274
|
+
rawget(t, "__raw")[k] = v
|
|
275
|
+
local d = dirty()
|
|
276
|
+
d[k] = v
|
|
277
|
+
end,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
local ReferencesMT = {
|
|
281
|
+
__index = function(t, k)
|
|
282
|
+
local wrap = rawget(_ENV, "__microverse_reference_wrap")
|
|
283
|
+
local raw = rawget(t, "__raw")
|
|
284
|
+
local val = raw[k]
|
|
285
|
+
if type(wrap) == "function" then
|
|
286
|
+
return wrap(k, val)
|
|
287
|
+
end
|
|
288
|
+
return val
|
|
289
|
+
end,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function __microverse_lua_build_component_impl(bridges)
|
|
293
|
+
local impl = { state = {}, bridges = type(bridges) == "table" and bridges or {} }
|
|
294
|
+
local proxy = { __raw = rawProps() }
|
|
295
|
+
setmetatable(proxy, PropertiesMT)
|
|
296
|
+
impl.properties = proxy
|
|
297
|
+
local refProxy = { __raw = rawProps() }
|
|
298
|
+
setmetatable(refProxy, ReferencesMT)
|
|
299
|
+
impl.references = refProxy
|
|
300
|
+
local base = rawget(_ENV, "__microverse_component_hook_base")
|
|
301
|
+
if type(base) == "table" then
|
|
302
|
+
setmetatable(impl, { __index = base })
|
|
303
|
+
end
|
|
304
|
+
rawset(_ENV, "__microverse_lua_ComponentImpl", impl)
|
|
305
|
+
return impl
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
rawset(_ENV, "__microverse_lua_build_component_impl", __microverse_lua_build_component_impl)
|
|
309
|
+
|
|
310
|
+
rawset(_ENV, "__microverse_lua_component_apply_incoming", function()
|
|
311
|
+
local incoming = rawget(_ENV, "__microverseIncomingProps")
|
|
312
|
+
if type(incoming) ~= "table" then
|
|
313
|
+
return
|
|
314
|
+
end
|
|
315
|
+
local rp = rawProps()
|
|
316
|
+
local impl = rawget(_ENV, "__microverse_lua_ComponentImpl")
|
|
317
|
+
if type(impl) ~= "table" or type(impl.properties) ~= "table" then
|
|
318
|
+
for k, v in pairs(incoming) do
|
|
319
|
+
rp[k] = v
|
|
320
|
+
end
|
|
321
|
+
return
|
|
322
|
+
end
|
|
323
|
+
for k, v in pairs(incoming) do
|
|
324
|
+
impl.properties[k] = v
|
|
325
|
+
end
|
|
326
|
+
end)
|
|
327
|
+
|
|
328
|
+
rawset(_ENV, "__microverse_lua_component_flush_to_sink", function()
|
|
329
|
+
local push = rawget(_ENV, "__microverseFlushPush")
|
|
330
|
+
local d = dirty()
|
|
331
|
+
for k, v in pairs(d) do
|
|
332
|
+
if type(push) == "function" then
|
|
333
|
+
push(k, v)
|
|
334
|
+
end
|
|
335
|
+
d[k] = nil
|
|
336
|
+
end
|
|
337
|
+
end)
|
|
338
|
+
`.trim();
|
|
339
|
+
/** Lua prelude that registers `TypeName:extend()` singletons for each component type. */
|
|
340
|
+
function profileBridgeSlotKey(typeName, bridgeName) {
|
|
341
|
+
return `__mv_${typeName}_${bridgeName}`;
|
|
342
|
+
}
|
|
343
|
+
function profileBridgeNamesMergeEnvKey(typeName) {
|
|
344
|
+
return `__microverse_profile_bridge_names_${typeName}`;
|
|
345
|
+
}
|
|
346
|
+
function buildComponentTypeBridgeNamesPreludeLua(componentTypes) {
|
|
347
|
+
const lines = [];
|
|
348
|
+
for (const typeName of Object.keys(componentTypes).sort((a, b) => a.localeCompare(b))) {
|
|
349
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(typeName)) throw new Error(`unsafe component type name for bridge names prelude: ${typeName}`);
|
|
350
|
+
const entries = componentTypes[typeName].bridgeNames.map((n) => JSON.stringify(n)).join(", ");
|
|
351
|
+
lines.push(`rawset(_ENV, ${JSON.stringify(profileBridgeNamesMergeEnvKey(typeName))}, { ${entries} })`);
|
|
352
|
+
}
|
|
353
|
+
return lines.join("\n");
|
|
354
|
+
}
|
|
355
|
+
function buildComponentTypeSingletonsPreludeLua(typeNames) {
|
|
356
|
+
const lines = [
|
|
357
|
+
"local function __microverse_collect_profile_bridges(typeName)",
|
|
358
|
+
" local names = rawget(_ENV, \"__microverse_profile_bridge_names_\" .. typeName)",
|
|
359
|
+
" local bridges = {}",
|
|
360
|
+
" if type(names) ~= \"table\" then",
|
|
361
|
+
" return bridges",
|
|
362
|
+
" end",
|
|
363
|
+
" for _, name in ipairs(names) do",
|
|
364
|
+
" if type(name) == \"string\" then",
|
|
365
|
+
" local b = rawget(_ENV, \"__mv_\" .. typeName .. \"_\" .. name)",
|
|
366
|
+
" if type(b) == \"table\" then",
|
|
367
|
+
" bridges[name] = b",
|
|
368
|
+
" end",
|
|
369
|
+
" end",
|
|
370
|
+
" end",
|
|
371
|
+
" return bridges",
|
|
372
|
+
"end",
|
|
373
|
+
"local function __microverse_make_type_extend(typeName)",
|
|
374
|
+
" return function(self)",
|
|
375
|
+
" local ext = rawget(_ENV, \"__microverse_lua_extend_component\")",
|
|
376
|
+
" if type(ext) == \"function\" then",
|
|
377
|
+
" ext(typeName)",
|
|
378
|
+
" end",
|
|
379
|
+
" local build = rawget(_ENV, \"__microverse_lua_build_component_impl\")",
|
|
380
|
+
" if type(build) ~= \"function\" then",
|
|
381
|
+
" error(\"component type extend: build impl missing\")",
|
|
382
|
+
" end",
|
|
383
|
+
" return build(__microverse_collect_profile_bridges(typeName))",
|
|
384
|
+
" end",
|
|
385
|
+
"end"
|
|
386
|
+
];
|
|
387
|
+
for (const name of typeNames) {
|
|
388
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`unsafe component type name for Lua singleton: ${name}`);
|
|
389
|
+
lines.push(`rawset(_ENV, ${JSON.stringify(name)}, { extend = __microverse_make_type_extend(${JSON.stringify(name)}) })`);
|
|
390
|
+
}
|
|
391
|
+
return lines.join("\n");
|
|
392
|
+
}
|
|
393
|
+
/** Applies host-selected profile (same as `Type:extend()` without requiring Lua to call it). */
|
|
394
|
+
function buildApplyHostScriptProfileChunkLua(profileName) {
|
|
395
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(profileName)) throw new Error(`unsafe script profile name: ${profileName}`);
|
|
396
|
+
return [
|
|
397
|
+
`local T = ${profileName}`,
|
|
398
|
+
"if type(T) == \"table\" and type(T.extend) == \"function\" then",
|
|
399
|
+
" T:extend()",
|
|
400
|
+
"end"
|
|
401
|
+
].join("\n");
|
|
16
402
|
}
|
|
17
403
|
//#endregion
|
|
18
404
|
//#region src/infrastructure/adapters/zodSchemaValidationAdapter.ts
|
|
@@ -159,29 +545,33 @@ function asyncHandleClassName(bridgeName, methodName) {
|
|
|
159
545
|
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
160
546
|
return `${cap(bridgeName)}${cap(methodName)}Handle`;
|
|
161
547
|
}
|
|
162
|
-
|
|
548
|
+
/** LuaCATS class for runtime bridge key `audit` → `Audit` (distinct from field name for LuaLS). */
|
|
549
|
+
function bridgeLuaClassName(bridgeTableName) {
|
|
550
|
+
return bridgeTableName.charAt(0).toUpperCase() + bridgeTableName.slice(1);
|
|
551
|
+
}
|
|
552
|
+
function buildComponentEventManifestFields(kinds, componentClassName, componentHooks) {
|
|
163
553
|
const out = [];
|
|
164
554
|
for (const kind of kinds) {
|
|
165
|
-
if (!(
|
|
166
|
-
const payloadName = `
|
|
555
|
+
if (!(componentHooks[kind] instanceof z.ZodObject)) throw new Error(`defineHostSurface componentHooks: "${kind}" must be a z.object(...)`);
|
|
556
|
+
const payloadName = `MicroverseEvt_${kind}`;
|
|
167
557
|
const hookName = luaGlobalHookName(kind);
|
|
168
558
|
out.push({
|
|
169
559
|
name: hookName,
|
|
170
|
-
description:
|
|
171
|
-
luaType: `fun(self: ${
|
|
560
|
+
description: `Host invokes when \`${kind}\` is emitted. Payload: \`${payloadName}\`.`,
|
|
561
|
+
luaType: `fun(self: ${componentClassName}, evt: ${payloadName})`
|
|
172
562
|
});
|
|
173
563
|
}
|
|
174
564
|
return out;
|
|
175
565
|
}
|
|
176
|
-
function
|
|
566
|
+
function pushComponentEventPayloadClasses(kinds, componentHooks, classes) {
|
|
177
567
|
for (const kind of kinds) {
|
|
178
|
-
const schema =
|
|
179
|
-
if (!(schema instanceof z.ZodObject)) throw new Error(`defineHostSurface
|
|
180
|
-
const name = `
|
|
568
|
+
const schema = componentHooks[kind];
|
|
569
|
+
if (!(schema instanceof z.ZodObject)) throw new Error(`defineHostSurface componentHooks: "${kind}" must be a z.object(...)`);
|
|
570
|
+
const name = `MicroverseEvt_${kind}`;
|
|
181
571
|
const shape = schema.shape;
|
|
182
572
|
classes.push({
|
|
183
573
|
name,
|
|
184
|
-
description: `
|
|
574
|
+
description: `Domain event payload for \`${luaGlobalHookName(kind)}\` (Zod → LuaCATS fields).`,
|
|
185
575
|
fields: Object.keys(shape).map((k) => ({
|
|
186
576
|
name: k,
|
|
187
577
|
luaType: zodToLuaTypeRef(shape[k])
|
|
@@ -190,9 +580,115 @@ function pushWorkflowPayloadManifestClasses(kinds, workflowHooks, classes) {
|
|
|
190
580
|
});
|
|
191
581
|
}
|
|
192
582
|
}
|
|
193
|
-
function
|
|
583
|
+
function pushBaseComponentClass(classes) {
|
|
584
|
+
classes.push({
|
|
585
|
+
name: "Component",
|
|
586
|
+
description: "Base component lifecycle (extended by typed component profiles).",
|
|
587
|
+
fields: [
|
|
588
|
+
{
|
|
589
|
+
name: "init",
|
|
590
|
+
luaType: "fun(self: Component)",
|
|
591
|
+
description: "Called once after mount and initial props are applied."
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
name: "onPropsChanged",
|
|
595
|
+
luaType: "fun(self: Component, key: string, newValue: any)",
|
|
596
|
+
description: "Called when the host patches a property key."
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: "onDestroy",
|
|
600
|
+
luaType: "fun(self: Component)",
|
|
601
|
+
description: "Called before the script instance slot is disposed."
|
|
602
|
+
}
|
|
603
|
+
],
|
|
604
|
+
emitSingleton: false
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
function pushProfileBridgesClass(typeName, bridgeNames, classes) {
|
|
608
|
+
if (bridgeNames.length === 0) return;
|
|
609
|
+
const bridgesName = scriptProfileBridgesClassName(typeName);
|
|
610
|
+
classes.push({
|
|
611
|
+
name: bridgesName,
|
|
612
|
+
description: `Host bridges for \`${typeName}\` components (\`self.bridges\` after \`${typeName}:extend()\`).`,
|
|
613
|
+
fields: bridgeNames.map((name) => ({
|
|
614
|
+
name,
|
|
615
|
+
luaType: bridgeLuaClassName(name)
|
|
616
|
+
})),
|
|
617
|
+
emitSingleton: false
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
function pushComponentTypeManifest(classes, aliases, typeName, profile, componentHooks) {
|
|
621
|
+
const propsAlias = scriptProfilePropsAlias(typeName);
|
|
622
|
+
const stateAlias = scriptProfileStateAlias(typeName);
|
|
623
|
+
aliases.set(propsAlias, zodToLuaTypeRef(profile.props));
|
|
624
|
+
aliases.set(stateAlias, zodToLuaTypeRef(profile.state));
|
|
625
|
+
pushProfileBridgesClass(typeName, profile.bridgeNames, classes);
|
|
626
|
+
const componentClass = scriptProfileComponentClassName(typeName);
|
|
627
|
+
const bridgesClass = profile.bridgeNames.length > 0 ? scriptProfileBridgesClassName(typeName) : "table";
|
|
628
|
+
const extendsClass = profile.extends !== void 0 ? scriptProfileComponentClassName(profile.extends) : "Component";
|
|
629
|
+
const fields = [
|
|
630
|
+
{
|
|
631
|
+
name: "properties",
|
|
632
|
+
luaType: propsAlias,
|
|
633
|
+
description: "Host-synced props (proxy)."
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: "state",
|
|
637
|
+
luaType: stateAlias,
|
|
638
|
+
description: "Lua-local state."
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: "bridges",
|
|
642
|
+
luaType: bridgesClass,
|
|
643
|
+
description: "Host bridges allowed for this component type."
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
name: "init",
|
|
647
|
+
luaType: `fun(self: ${componentClass})`,
|
|
648
|
+
description: "Called once after mount and initial props are applied."
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
name: "onPropsChanged",
|
|
652
|
+
luaType: `fun(self: ${componentClass}, key: string, newValue: any)`,
|
|
653
|
+
description: "Called when the host patches a property key."
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
name: "onDestroy",
|
|
657
|
+
luaType: `fun(self: ${componentClass})`,
|
|
658
|
+
description: "Called before the script instance slot is disposed."
|
|
659
|
+
}
|
|
660
|
+
];
|
|
661
|
+
if (componentHooks !== void 0 && profile.hooks.length > 0) fields.push(...buildComponentEventManifestFields(profile.hooks, componentClass, componentHooks));
|
|
662
|
+
classes.push({
|
|
663
|
+
name: componentClass,
|
|
664
|
+
extendsClass,
|
|
665
|
+
description: `Component instance from \`local C = ${typeName}:extend()\`.`,
|
|
666
|
+
fields,
|
|
667
|
+
emitSingleton: false
|
|
668
|
+
});
|
|
669
|
+
classes.push({
|
|
670
|
+
name: typeName,
|
|
671
|
+
description: `Factory for \`${componentClass}\` in this slot.`,
|
|
672
|
+
methods: [{
|
|
673
|
+
name: "extend",
|
|
674
|
+
description: "Creates the active component with profile-scoped bridges.",
|
|
675
|
+
params: [],
|
|
676
|
+
returns: componentClass
|
|
677
|
+
}],
|
|
678
|
+
emitSingleton: true
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
function pushComponentManifestClasses(classes, aliases, componentTypes, componentHooks) {
|
|
682
|
+
const eventKinds = componentHooks !== void 0 ? Object.keys(componentHooks).sort((a, b) => a.localeCompare(b)) : [];
|
|
683
|
+
if (componentHooks !== void 0) pushComponentEventPayloadClasses(eventKinds, componentHooks, classes);
|
|
684
|
+
pushBaseComponentClass(classes);
|
|
685
|
+
const typeNames = Object.keys(componentTypes).sort((a, b) => a.localeCompare(b));
|
|
686
|
+
for (const typeName of typeNames) pushComponentTypeManifest(classes, aliases, typeName, componentTypes[typeName], componentHooks);
|
|
687
|
+
}
|
|
688
|
+
function buildLuaDefManifestFromHostSurfaceSpec(spec, opts, componentHooks, componentTypes) {
|
|
194
689
|
const classes = [];
|
|
195
|
-
|
|
690
|
+
const bridgeNames = Object.keys(spec).sort((a, b) => a.localeCompare(b));
|
|
691
|
+
for (const bridgeName of bridgeNames) {
|
|
196
692
|
const methods = spec[bridgeName];
|
|
197
693
|
const manifestMethods = [];
|
|
198
694
|
for (const methodName of Object.keys(methods)) {
|
|
@@ -228,35 +724,16 @@ function buildLuaDefManifestFromHostSurfaceSpec(spec, opts, workflowHooks) {
|
|
|
228
724
|
});
|
|
229
725
|
}
|
|
230
726
|
classes.push({
|
|
231
|
-
name: bridgeName,
|
|
232
|
-
methods: manifestMethods
|
|
727
|
+
name: bridgeLuaClassName(bridgeName),
|
|
728
|
+
methods: manifestMethods,
|
|
729
|
+
emitSingleton: false
|
|
233
730
|
});
|
|
234
731
|
}
|
|
235
732
|
const fromLuaType = collectLuaTypeAliasesFromHostSpec(spec);
|
|
236
733
|
const fromOverrides = inferLuaTypeAliasesFromHostSpec(spec);
|
|
237
734
|
const merged = new Map([...fromLuaType.map((a) => [a.name, a.definition]), ...fromOverrides.map((a) => [a.name, a.definition])]);
|
|
238
735
|
if (opts.luaTypeAliases !== void 0) for (const [k, v] of Object.entries(opts.luaTypeAliases)) merged.set(k, v);
|
|
239
|
-
if (
|
|
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
|
-
}
|
|
736
|
+
if (componentTypes !== void 0) pushComponentManifestClasses(classes, merged, componentTypes, componentHooks);
|
|
260
737
|
const aliases = merged.size === 0 ? void 0 : [...merged.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, definition]) => ({
|
|
261
738
|
name,
|
|
262
739
|
definition
|
|
@@ -396,111 +873,39 @@ function unwrapInputSchema(schema) {
|
|
|
396
873
|
return cur;
|
|
397
874
|
}
|
|
398
875
|
//#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
876
|
//#region src/application/useCases/compileHostSurface.ts
|
|
481
|
-
function buildHostSurfaceCore(schemaValidation, spec,
|
|
877
|
+
function buildHostSurfaceCore(schemaValidation, spec, componentTypeRegistry, componentHooks) {
|
|
878
|
+
validateScriptProfileRegistry(componentTypeRegistry, spec, componentHooks, { requireAtLeastOne: false });
|
|
879
|
+
const componentTypes = buildResolvedScriptProfileRegistry(componentTypeRegistry, spec);
|
|
482
880
|
const capabilities = collectCapabilitiesFromHostSurfaceSpec(spec);
|
|
483
881
|
return {
|
|
882
|
+
getHostSurfaceSpec: () => spec,
|
|
484
883
|
toBridgeDeclarations: () => createBridgeDeclarationsFromHostSurfaceSpec(schemaValidation, spec),
|
|
485
|
-
toLuaDefManifest: (opts) => buildLuaDefManifestFromHostSurfaceSpec(spec, opts,
|
|
884
|
+
toLuaDefManifest: (opts) => buildLuaDefManifestFromHostSurfaceSpec(spec, opts, componentHooks, componentTypes),
|
|
486
885
|
capabilities,
|
|
886
|
+
componentTypes,
|
|
887
|
+
getComponentType: (name) => {
|
|
888
|
+
const profile = componentTypes[name];
|
|
889
|
+
if (profile === void 0) throw new Error(`unknown component type: ${name}`);
|
|
890
|
+
return profile;
|
|
891
|
+
},
|
|
487
892
|
pickCapabilities: (...picked) => pickSurfaceCapabilities(capabilities, ...picked)
|
|
488
893
|
};
|
|
489
894
|
}
|
|
490
|
-
function compileHostSurface(ports, spec,
|
|
895
|
+
function compileHostSurface(ports, spec, componentTypeRegistry, componentHooks) {
|
|
491
896
|
const [schemaValidation] = ports;
|
|
492
|
-
const core = buildHostSurfaceCore(schemaValidation, spec,
|
|
493
|
-
if (
|
|
897
|
+
const core = buildHostSurfaceCore(schemaValidation, spec, componentTypeRegistry, componentHooks);
|
|
898
|
+
if (componentHooks === void 0) return core;
|
|
494
899
|
return {
|
|
495
900
|
...core,
|
|
496
|
-
|
|
901
|
+
componentHooks
|
|
497
902
|
};
|
|
498
903
|
}
|
|
499
904
|
/**
|
|
500
905
|
* Same as {@link compileHostSurface}, but requires every bridge method to be typed with the same `THost`.
|
|
501
906
|
*/
|
|
502
|
-
function compileHostSurfaceFor(ports, spec,
|
|
503
|
-
return
|
|
907
|
+
function compileHostSurfaceFor(ports, spec, componentTypeRegistry, componentHooks) {
|
|
908
|
+
return componentHooks === void 0 ? compileHostSurface(ports, spec, componentTypeRegistry) : compileHostSurface(ports, spec, componentTypeRegistry, componentHooks);
|
|
504
909
|
}
|
|
505
910
|
//#endregion
|
|
506
911
|
//#region src/domain/inferMethodAsync.ts
|
|
@@ -527,35 +932,18 @@ function normalizeMethodDef(def) {
|
|
|
527
932
|
};
|
|
528
933
|
}
|
|
529
934
|
//#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
935
|
//#region src/infrastructure/builders/surfaceBuilder.ts
|
|
549
936
|
/**
|
|
550
937
|
* Fluent builder for a host surface. Created via {@link defineHostSurfaceFor} / {@link defineHostSurface} factory overloads.
|
|
551
938
|
*/
|
|
552
939
|
var SurfaceBuilder = class SurfaceBuilder {
|
|
553
940
|
spec = createNullPrototypeRecord();
|
|
554
|
-
|
|
941
|
+
componentTypeRegistry = createNullPrototypeRecord();
|
|
942
|
+
componentHooksSpec;
|
|
555
943
|
ports;
|
|
556
|
-
constructor(ports,
|
|
944
|
+
constructor(ports, componentHooks, initialSpec, initialComponentTypes) {
|
|
557
945
|
this.ports = ports;
|
|
558
|
-
this.
|
|
946
|
+
this.componentHooksSpec = componentHooks;
|
|
559
947
|
if (initialSpec !== void 0) for (const bridgeName of Object.keys(initialSpec)) {
|
|
560
948
|
assertSafeObjectKey("bridge", bridgeName);
|
|
561
949
|
const srcBridge = initialSpec[bridgeName];
|
|
@@ -566,19 +954,27 @@ var SurfaceBuilder = class SurfaceBuilder {
|
|
|
566
954
|
}
|
|
567
955
|
this.spec[bridgeName] = bridge;
|
|
568
956
|
}
|
|
957
|
+
if (initialComponentTypes !== void 0) for (const name of Object.keys(initialComponentTypes)) this.componentTypeRegistry[name] = initialComponentTypes[name];
|
|
958
|
+
}
|
|
959
|
+
/** Declares a typed Lua component profile (props, state, capabilities, hooks). */
|
|
960
|
+
componentType(name, def) {
|
|
961
|
+
assertSafeObjectKey("componentType", name);
|
|
962
|
+
this.componentTypeRegistry[name] = def;
|
|
963
|
+
return this;
|
|
569
964
|
}
|
|
570
965
|
/** Opens a Lua bridge table (e.g. `orders`, `greet`). */
|
|
571
966
|
bridge(name) {
|
|
572
967
|
return new BridgeBuilder(this, name);
|
|
573
968
|
}
|
|
574
|
-
/** Attaches
|
|
575
|
-
|
|
576
|
-
return new SurfaceBuilder(this.ports, hooks, this.spec);
|
|
969
|
+
/** Attaches component domain-event Zod schemas (emitted into `.d.lua` as `on*` methods on component types). */
|
|
970
|
+
componentHooks(hooks) {
|
|
971
|
+
return new SurfaceBuilder(this.ports, hooks, this.spec, this.componentTypeRegistry);
|
|
577
972
|
}
|
|
578
973
|
/** Compiles the accumulated spec into a {@link HostSurface}. */
|
|
579
974
|
build() {
|
|
580
975
|
const spec = this.spec;
|
|
581
|
-
|
|
976
|
+
const registry = this.componentTypeRegistry;
|
|
977
|
+
return compileHostSurfaceFor(this.ports, spec, registry, this.componentHooksSpec);
|
|
582
978
|
}
|
|
583
979
|
/** @internal */
|
|
584
980
|
addMethod(bridgeName, methodName, entry) {
|
|
@@ -654,73 +1050,244 @@ function defineHostSurfaceFor() {
|
|
|
654
1050
|
return createSurfaceBuilder();
|
|
655
1051
|
}
|
|
656
1052
|
//#endregion
|
|
657
|
-
//#region src/
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1053
|
+
//#region src/domain/scriptPropertyMergeEnv.ts
|
|
1054
|
+
function scriptPropertyBagToMergeEnv(bag) {
|
|
1055
|
+
const out = {};
|
|
1056
|
+
for (const [k, v] of Object.entries(bag)) out[k] = scriptPropertyValueToPlain(v);
|
|
1057
|
+
return out;
|
|
1058
|
+
}
|
|
1059
|
+
function scriptPropertyValueToPlain(value) {
|
|
1060
|
+
if (value === null || typeof value !== "object") return value;
|
|
1061
|
+
if (Array.isArray(value)) return value.map((item) => scriptPropertyValueToPlain(item));
|
|
1062
|
+
const out = {};
|
|
1063
|
+
for (const [k, v] of Object.entries(value)) out[k] = scriptPropertyValueToPlain(v);
|
|
1064
|
+
return out;
|
|
1065
|
+
}
|
|
1066
|
+
function mergeEnvSinkToScriptPropertyBag(sink) {
|
|
1067
|
+
const out = {};
|
|
1068
|
+
for (const [k, v] of Object.entries(sink)) {
|
|
1069
|
+
const converted = plainToScriptPropertyValue(v);
|
|
1070
|
+
if (converted !== void 0) out[k] = converted;
|
|
1071
|
+
}
|
|
1072
|
+
return out;
|
|
1073
|
+
}
|
|
1074
|
+
function plainToScriptPropertyValue(value) {
|
|
1075
|
+
if (value === null) return null;
|
|
1076
|
+
const t = typeof value;
|
|
1077
|
+
if (t === "string" || t === "boolean") return value;
|
|
1078
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1079
|
+
if (Array.isArray(value)) {
|
|
1080
|
+
const arr = [];
|
|
1081
|
+
for (const item of value) {
|
|
1082
|
+
const c = plainToScriptPropertyValue(item);
|
|
1083
|
+
if (c !== void 0) arr.push(c);
|
|
1084
|
+
}
|
|
1085
|
+
return arr;
|
|
1086
|
+
}
|
|
1087
|
+
if (t === "object" && value !== null) {
|
|
1088
|
+
const obj = {};
|
|
1089
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1090
|
+
const c = plainToScriptPropertyValue(v);
|
|
1091
|
+
if (c !== void 0) obj[k] = c;
|
|
1092
|
+
}
|
|
1093
|
+
return obj;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
//#endregion
|
|
1097
|
+
//#region src/infrastructure/adapters/augmentHostWithScriptContext.ts
|
|
1098
|
+
function augmentHostWithScriptContext(host, script) {
|
|
1099
|
+
return Object.assign(host, { [MICROVERSE_SCRIPT_CONTEXT]: script });
|
|
667
1100
|
}
|
|
668
1101
|
//#endregion
|
|
669
1102
|
//#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
|
-
*/
|
|
1103
|
+
var defaultSchemaValidation = createZodSchemaValidationPort();
|
|
685
1104
|
var HostScriptSession = class {
|
|
686
1105
|
opts;
|
|
687
1106
|
sandbox;
|
|
688
|
-
|
|
1107
|
+
hostProps = {};
|
|
1108
|
+
schemaValidation;
|
|
1109
|
+
activeComponentType;
|
|
1110
|
+
hostProfileApplied = false;
|
|
1111
|
+
context;
|
|
689
1112
|
constructor(opts) {
|
|
690
1113
|
this.opts = opts;
|
|
691
|
-
this.
|
|
1114
|
+
this.schemaValidation = opts.schemaValidation ?? defaultSchemaValidation;
|
|
1115
|
+
this.context = opts.script;
|
|
1116
|
+
if (opts.resolvedProfile !== void 0) this.activeComponentType = opts.resolvedProfile.name;
|
|
1117
|
+
else if (opts.profileId !== void 0) this.activeComponentType = opts.profileId;
|
|
692
1118
|
}
|
|
693
|
-
/**
|
|
694
|
-
* Allocates the underlying {@link MicroverseSlot} for this `slotKey` on the shared runtime.
|
|
695
|
-
*/
|
|
696
1119
|
openSession = async () => {
|
|
697
1120
|
this.sandbox = await this.opts.runtime.createMicroverse({ slotKey: this.opts.slotKey });
|
|
698
|
-
const
|
|
699
|
-
if (
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
script: createMicroverseScript(prelude),
|
|
1121
|
+
const sb = this.requireMicroverseSlot();
|
|
1122
|
+
if (this.opts.enableComponentRuntime !== false) {
|
|
1123
|
+
await sb.run({
|
|
1124
|
+
script: createMicroverseScript(MICROVERSE_LUA_COMPONENT_SLOT_PRELUDE),
|
|
703
1125
|
mergeEnv: this.mergeEnv(),
|
|
704
1126
|
timeout: this.opts.defaultTimeout
|
|
705
1127
|
});
|
|
1128
|
+
const activeProfile = this.tryGetActiveProfile();
|
|
1129
|
+
const singletonName = this.profileSingletonName();
|
|
1130
|
+
const typeNames = this.singletonTypeNames();
|
|
1131
|
+
if (typeNames.length > 0) {
|
|
1132
|
+
const bridgeRegistry = activeProfile !== void 0 ? { [activeProfile.name]: activeProfile } : this.opts.surface.componentTypes;
|
|
1133
|
+
await sb.run({
|
|
1134
|
+
script: createMicroverseScript([buildComponentTypeBridgeNamesPreludeLua(bridgeRegistry), buildComponentTypeSingletonsPreludeLua(typeNames)].join("\n")),
|
|
1135
|
+
mergeEnv: this.mergeEnv(),
|
|
1136
|
+
timeout: this.opts.defaultTimeout
|
|
1137
|
+
});
|
|
1138
|
+
if (singletonName !== void 0) {
|
|
1139
|
+
await sb.run({
|
|
1140
|
+
script: createMicroverseScript(buildApplyHostScriptProfileChunkLua(singletonName)),
|
|
1141
|
+
mergeEnv: this.mergeEnv(),
|
|
1142
|
+
timeout: this.opts.defaultTimeout
|
|
1143
|
+
});
|
|
1144
|
+
this.hostProfileApplied = true;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
const hooks = readComponentHooks(this.opts.surface);
|
|
1148
|
+
if (hooks !== void 0) {
|
|
1149
|
+
const prelude = buildComponentEventStubPreludeLua(hooks);
|
|
1150
|
+
await sb.run({
|
|
1151
|
+
script: createMicroverseScript(prelude),
|
|
1152
|
+
mergeEnv: this.mergeEnv(),
|
|
1153
|
+
timeout: this.opts.defaultTimeout
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
706
1156
|
}
|
|
707
1157
|
};
|
|
708
|
-
/**
|
|
709
|
-
* Exposes the in-memory registry (e.g. to mutate allowlists in advanced tests).
|
|
710
|
-
*/
|
|
711
|
-
getCapabilityRegistry = () => this.registry;
|
|
712
1158
|
requireMicroverseSlot() {
|
|
713
1159
|
if (this.sandbox === void 0) throw new Error("HostScriptSession: openSession() was not called");
|
|
714
1160
|
return this.sandbox;
|
|
715
1161
|
}
|
|
716
1162
|
mergeEnv() {
|
|
717
|
-
|
|
1163
|
+
const host = augmentHostWithScriptContext(this.opts.host, this.opts.script);
|
|
1164
|
+
const spec = this.opts.surface.getHostSurfaceSpec();
|
|
1165
|
+
const env = { __microverse_lua_extend_component: (typeName) => {
|
|
1166
|
+
if (this.opts.resolvedProfile !== void 0 && typeName === this.profileSingletonName()) {
|
|
1167
|
+
this.activeComponentType = this.opts.resolvedProfile.name;
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
this.opts.surface.getComponentType(typeName);
|
|
1171
|
+
this.activeComponentType = typeName;
|
|
1172
|
+
} };
|
|
1173
|
+
const activeProfile = this.tryGetActiveProfile();
|
|
1174
|
+
const typeNames = activeProfile !== void 0 ? [activeProfile.name] : Object.keys(this.opts.surface.componentTypes);
|
|
1175
|
+
for (const typeName of typeNames) {
|
|
1176
|
+
const profile = activeProfile !== void 0 && typeName === activeProfile.name ? activeProfile : this.opts.surface.getComponentType(typeName);
|
|
1177
|
+
const bridgeTable = buildBridgeMergeEnvForProfile(this.schemaValidation, host, String(this.opts.slotKey), spec, profile.capabilities);
|
|
1178
|
+
for (const bridgeName of profile.bridgeNames) env[profileBridgeSlotKey(typeName, bridgeName)] = bridgeTable[bridgeName];
|
|
1179
|
+
}
|
|
1180
|
+
const profileForRefs = this.tryGetActiveProfile();
|
|
1181
|
+
if (profileForRefs?.references !== void 0 && this.opts.referenceResolver !== void 0) {
|
|
1182
|
+
const profile = profileForRefs;
|
|
1183
|
+
env.__microverse_reference_wrap = (field, raw) => this.wrapReference(field, raw, profile);
|
|
1184
|
+
}
|
|
1185
|
+
return env;
|
|
718
1186
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1187
|
+
wrapReference(field, raw, profile) {
|
|
1188
|
+
const resolver = this.opts.referenceResolver;
|
|
1189
|
+
const refs = profile.references;
|
|
1190
|
+
if (resolver === void 0 || refs === void 0) return raw;
|
|
1191
|
+
const def = refs[field];
|
|
1192
|
+
if (def === void 0) return raw;
|
|
1193
|
+
const rawId = raw === null || raw === void 0 ? null : typeof raw === "string" ? raw : typeof raw === "number" || typeof raw === "boolean" ? String(raw) : null;
|
|
1194
|
+
return resolver.wrap({
|
|
1195
|
+
slotKey: String(this.opts.slotKey),
|
|
1196
|
+
field,
|
|
1197
|
+
raw: rawId,
|
|
1198
|
+
kind: def.kind,
|
|
1199
|
+
componentType: referenceComponentType(def)
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
emitAudit(event) {
|
|
1203
|
+
this.opts.onScriptAudit?.(event);
|
|
1204
|
+
}
|
|
1205
|
+
getActiveProfileName() {
|
|
1206
|
+
return this.activeComponentType ?? this.opts.profileId;
|
|
1207
|
+
}
|
|
1208
|
+
profileSingletonName() {
|
|
1209
|
+
return this.opts.profileSingleton ?? this.opts.profileId ?? this.opts.resolvedProfile?.name;
|
|
1210
|
+
}
|
|
1211
|
+
singletonTypeNames() {
|
|
1212
|
+
const singleton = this.profileSingletonName();
|
|
1213
|
+
const fromSurface = Object.keys(this.opts.surface.componentTypes);
|
|
1214
|
+
const names = new Set(fromSurface);
|
|
1215
|
+
if (singleton !== void 0) names.add(singleton);
|
|
1216
|
+
return [...names].sort((a, b) => a.localeCompare(b));
|
|
1217
|
+
}
|
|
1218
|
+
tryGetActiveProfile() {
|
|
1219
|
+
if (this.opts.resolvedProfile !== void 0) return this.opts.resolvedProfile;
|
|
1220
|
+
const name = this.getActiveProfileName();
|
|
1221
|
+
if (name === void 0) return;
|
|
1222
|
+
try {
|
|
1223
|
+
return this.opts.surface.getComponentType(name);
|
|
1224
|
+
} catch {
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
validatePropsBag(bag) {
|
|
1229
|
+
const profile = this.tryGetActiveProfile();
|
|
1230
|
+
if (profile === void 0) throw new Error("HostScriptSession: set profileId or resolvedProfile before props or host sync");
|
|
1231
|
+
const parsed = profile.props.parse(bag);
|
|
1232
|
+
assertValidScriptPropertyBag(parsed);
|
|
1233
|
+
return parsed;
|
|
1234
|
+
}
|
|
1235
|
+
getProps = () => ({ ...this.hostProps });
|
|
1236
|
+
setProps = async (bag) => {
|
|
1237
|
+
const next = this.validatePropsBag(bag);
|
|
1238
|
+
const changed = diffScriptProperties(this.hostProps, next);
|
|
1239
|
+
applyScriptPropertyChanges(this.hostProps, next, changed);
|
|
1240
|
+
await this.requireMicroverseSlot().run({
|
|
1241
|
+
script: createMicroverseScript("local f = rawget(_ENV, \"__microverse_lua_component_apply_incoming\")\nif type(f) == \"function\" then f() end"),
|
|
1242
|
+
mergeEnv: {
|
|
1243
|
+
...this.mergeEnv(),
|
|
1244
|
+
__microverseIncomingProps: scriptPropertyBagToMergeEnv(this.hostProps)
|
|
1245
|
+
},
|
|
1246
|
+
timeout: this.opts.defaultTimeout
|
|
1247
|
+
});
|
|
1248
|
+
if (changed.length > 0) {
|
|
1249
|
+
this.emitAudit({
|
|
1250
|
+
kind: "propsPatched",
|
|
1251
|
+
context: this.opts.script,
|
|
1252
|
+
changedKeys: changed
|
|
1253
|
+
});
|
|
1254
|
+
for (const key of changed) {
|
|
1255
|
+
const value = next[key];
|
|
1256
|
+
if (value !== void 0) await this.invokeComponentHook("onPropsChanged", key, value);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
patchProps = async (partial) => {
|
|
1261
|
+
await this.setProps({
|
|
1262
|
+
...this.hostProps,
|
|
1263
|
+
...partial
|
|
1264
|
+
});
|
|
1265
|
+
};
|
|
1266
|
+
flushDirtyProps = async () => {
|
|
1267
|
+
if (this.opts.enableComponentRuntime === false) return null;
|
|
1268
|
+
const collected = {};
|
|
1269
|
+
await this.requireMicroverseSlot().run({
|
|
1270
|
+
script: createMicroverseScript("local f = rawget(_ENV, \"__microverse_lua_component_flush_to_sink\")\nif type(f) == \"function\" then f() end"),
|
|
1271
|
+
mergeEnv: {
|
|
1272
|
+
...this.mergeEnv(),
|
|
1273
|
+
__microverseFlushPush: (key, value) => {
|
|
1274
|
+
const converted = plainToScriptPropertyValue(value);
|
|
1275
|
+
if (converted !== void 0) collected[key] = converted;
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1278
|
+
timeout: this.opts.defaultTimeout
|
|
1279
|
+
});
|
|
1280
|
+
const keys = Object.keys(collected);
|
|
1281
|
+
if (keys.length === 0) return null;
|
|
1282
|
+
const dirty = collected;
|
|
1283
|
+
for (const key of keys) this.hostProps[key] = dirty[key];
|
|
1284
|
+
this.emitAudit({
|
|
1285
|
+
kind: "propsFlushed",
|
|
1286
|
+
context: this.opts.script,
|
|
1287
|
+
dirtyKeys: keys
|
|
1288
|
+
});
|
|
1289
|
+
return dirty;
|
|
1290
|
+
};
|
|
724
1291
|
runChunk = async (source) => {
|
|
725
1292
|
return this.requireMicroverseSlot().run({
|
|
726
1293
|
script: createMicroverseScript(source),
|
|
@@ -728,37 +1295,44 @@ var HostScriptSession = class {
|
|
|
728
1295
|
timeout: this.opts.defaultTimeout
|
|
729
1296
|
});
|
|
730
1297
|
};
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
invokeGlobalHookIfPresent = (async (globalName, payload) => {
|
|
739
|
-
assertSafeLuaGlobalName(globalName);
|
|
1298
|
+
invokeComponentHook = async (hookName, ...args) => {
|
|
1299
|
+
assertSafeLuaGlobalName(hookName);
|
|
1300
|
+
this.emitAudit({
|
|
1301
|
+
kind: "hookInvoked",
|
|
1302
|
+
context: this.opts.script,
|
|
1303
|
+
hookName
|
|
1304
|
+
});
|
|
740
1305
|
const sb = this.requireMicroverseSlot();
|
|
741
|
-
const
|
|
742
|
-
|
|
1306
|
+
const argLiterals = args.map((a) => {
|
|
1307
|
+
if (typeof a === "object" && a !== null && !Array.isArray(a)) return luaTableLiteralFromPlainRecord(a);
|
|
1308
|
+
return luaValueLiteral(a);
|
|
1309
|
+
}).join(", ");
|
|
1310
|
+
const src = [
|
|
1311
|
+
`local impl = rawget(_ENV, "__microverse_lua_ComponentImpl")`,
|
|
1312
|
+
`if type(impl) == "table" then`,
|
|
1313
|
+
` local m = rawget(impl, ${JSON.stringify(hookName)})`,
|
|
1314
|
+
` if type(m) == "function" then`,
|
|
1315
|
+
argLiterals.length > 0 ? ` m(impl, ${argLiterals})` : ` m(impl)`,
|
|
1316
|
+
` end`,
|
|
1317
|
+
`end`
|
|
1318
|
+
].join("\n");
|
|
743
1319
|
return sb.run({
|
|
744
1320
|
script: createMicroverseScript(src),
|
|
745
1321
|
mergeEnv: this.mergeEnv(),
|
|
746
1322
|
timeout: this.opts.defaultTimeout
|
|
747
1323
|
});
|
|
1324
|
+
};
|
|
1325
|
+
invokeComponentEventHook = (async (hookName, payload) => {
|
|
1326
|
+
assertSafeLuaGlobalName(hookName);
|
|
1327
|
+
return this.invokeComponentHook(hookName, payload);
|
|
748
1328
|
});
|
|
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
1329
|
call = async (tableName, methodName, payload) => {
|
|
758
1330
|
const sb = this.requireMicroverseSlot();
|
|
759
1331
|
const tbl = luaTableLiteralFromUnknownRecord(payload);
|
|
760
1332
|
const src = [
|
|
761
|
-
`local
|
|
1333
|
+
`local impl = rawget(_ENV, "__microverse_lua_ComponentImpl")`,
|
|
1334
|
+
`local bridges = type(impl) == "table" and impl.bridges or nil`,
|
|
1335
|
+
`local t = type(bridges) == "table" and bridges[${JSON.stringify(tableName)}] or nil`,
|
|
762
1336
|
`local f = type(t) == "table" and t[${JSON.stringify(methodName)}] or nil`,
|
|
763
1337
|
`if type(f) == "function" then`,
|
|
764
1338
|
` f(${tbl})`,
|
|
@@ -770,61 +1344,55 @@ var HostScriptSession = class {
|
|
|
770
1344
|
timeout: this.opts.defaultTimeout
|
|
771
1345
|
});
|
|
772
1346
|
};
|
|
773
|
-
/**
|
|
774
|
-
* Releases the slot in the runtime adapter and clears the session handle.
|
|
775
|
-
*/
|
|
776
1347
|
dispose = async () => {
|
|
777
1348
|
if (this.sandbox !== void 0) {
|
|
1349
|
+
if (this.opts.enableComponentRuntime !== false) await this.invokeComponentHook("onDestroy");
|
|
778
1350
|
await this.sandbox.dispose();
|
|
779
1351
|
this.sandbox = void 0;
|
|
780
1352
|
}
|
|
781
1353
|
};
|
|
1354
|
+
/** Seeds host props bag without Lua sync (call before setProps after mount). */
|
|
1355
|
+
seedHostProps = (bag) => {
|
|
1356
|
+
const cloned = cloneScriptPropertyBag(bag);
|
|
1357
|
+
for (const k of Object.keys(this.hostProps)) delete this.hostProps[k];
|
|
1358
|
+
Object.assign(this.hostProps, cloned);
|
|
1359
|
+
};
|
|
1360
|
+
getHostProfileApplied = () => this.hostProfileApplied;
|
|
782
1361
|
};
|
|
1362
|
+
function referenceComponentType(def) {
|
|
1363
|
+
if (def.kind === "entityComponentRef" || def.kind === "entityComponentRefArray") return def.componentType;
|
|
1364
|
+
}
|
|
783
1365
|
function assertSafeLuaGlobalName(name) {
|
|
784
1366
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`unsafe Lua global name: ${name}`);
|
|
785
1367
|
}
|
|
786
|
-
function
|
|
787
|
-
if (!("
|
|
788
|
-
return surface.
|
|
1368
|
+
function readComponentHooks(surface) {
|
|
1369
|
+
if (!("componentHooks" in surface)) return;
|
|
1370
|
+
return surface.componentHooks;
|
|
789
1371
|
}
|
|
790
|
-
function
|
|
1372
|
+
function buildComponentEventStubPreludeLua(hooks) {
|
|
791
1373
|
return [
|
|
792
1374
|
"local Base = {}",
|
|
793
1375
|
"for _, name in ipairs({",
|
|
794
1376
|
...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
|
|
1377
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(kind)) throw new Error(`unsafe component hook kind: ${kind}`);
|
|
796
1378
|
return JSON.stringify(luaGlobalHookName(kind));
|
|
797
1379
|
}).map((h) => ` ${h},`),
|
|
798
1380
|
"}) do",
|
|
799
1381
|
" rawset(Base, name, function() end)",
|
|
800
1382
|
"end",
|
|
801
|
-
"rawset(_ENV, \"
|
|
802
|
-
" extend = function(_)",
|
|
803
|
-
" local w = setmetatable({}, { __index = Base })",
|
|
804
|
-
" rawset(_ENV, \"__microverse_lua_WorkflowImpl\", w)",
|
|
805
|
-
" return w",
|
|
806
|
-
" end,",
|
|
807
|
-
"})"
|
|
1383
|
+
"rawset(_ENV, \"__microverse_component_hook_base\", Base)"
|
|
808
1384
|
].join("\n");
|
|
809
1385
|
}
|
|
810
|
-
function
|
|
811
|
-
return
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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");
|
|
1386
|
+
function luaValueLiteral(value) {
|
|
1387
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
1388
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
1389
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
1390
|
+
if (value === null) return "nil";
|
|
1391
|
+
if (Array.isArray(value)) return `{ ${value.map((v) => luaValueLiteral(v)).join(", ")} }`;
|
|
1392
|
+
const record = value;
|
|
1393
|
+
const parts = [];
|
|
1394
|
+
for (const k of Object.keys(record)) parts.push(`${k} = ${luaValueLiteral(record[k])}`);
|
|
1395
|
+
return `{ ${parts.join(", ")} }`;
|
|
828
1396
|
}
|
|
829
1397
|
function luaTableLiteralFromPlainRecord(o) {
|
|
830
1398
|
const parts = [];
|
|
@@ -843,6 +1411,26 @@ function luaTableLiteralFromUnknownRecord(o) {
|
|
|
843
1411
|
return `{ ${parts.join(", ")} }`;
|
|
844
1412
|
}
|
|
845
1413
|
//#endregion
|
|
846
|
-
|
|
1414
|
+
//#region src/domain/capabilityRegistrySymbol.ts
|
|
1415
|
+
/**
|
|
1416
|
+
* Well-known symbol key used to attach a {@link CapabilityRegistryPort} on the host object
|
|
1417
|
+
* while surface bridge methods run. Populated by {@link augmentHostWithCapabilityRegistry} or
|
|
1418
|
+
* internally by {@link HostScriptSession}.
|
|
1419
|
+
*/
|
|
1420
|
+
var MICROVERSE_CAPABILITY_REGISTRY = Symbol.for("microverse:capabilityRegistry");
|
|
1421
|
+
//#endregion
|
|
1422
|
+
//#region src/infrastructure/adapters/augmentHostWithCapabilityRegistry.ts
|
|
1423
|
+
/**
|
|
1424
|
+
* Returns a shallow copy of `host` with {@link MICROVERSE_CAPABILITY_REGISTRY} set to `registry`.
|
|
1425
|
+
* Bridge handlers read the registry to enforce per-session capability allowlists.
|
|
1426
|
+
*
|
|
1427
|
+
* @param host - Your engine / service context passed into `buildDeclarativeBridgeTable`.
|
|
1428
|
+
* @param registry - Typically an {@link InMemoryCapabilityRegistry} from `@microverse.ts/runtime-capabilities`.
|
|
1429
|
+
*/
|
|
1430
|
+
function augmentHostWithCapabilityRegistry(host, registry) {
|
|
1431
|
+
return Object.assign(host, { [MICROVERSE_CAPABILITY_REGISTRY]: registry });
|
|
1432
|
+
}
|
|
1433
|
+
//#endregion
|
|
1434
|
+
export { BridgeBuilder, HostScriptSession, MICROVERSE_CAPABILITY_REGISTRY, MICROVERSE_LUA_COMPONENT_SLOT_PRELUDE, MICROVERSE_SCRIPT_CONTEXT, SurfaceBuilder, augmentHostWithCapabilityRegistry, augmentHostWithScriptContext, buildBridgeMergeEnvForProfile, buildComponentTypeSingletonsPreludeLua, buildResolvedScriptProfileRegistry, buildScriptCatalogLuaDefManifest, collectCapabilitiesFromHostSurfaceSpec, compileHostSurface, compileHostSurfaceFor, createBridgeDeclarationsFromHostSurfaceSpec, defineHostSurface, defineHostSurfaceFor, luaGlobalHookName, luaType, mergeEnvSinkToScriptPropertyBag, pickSurfaceCapabilities, resolveScriptProfile, scriptCatalogComponentAlias, scriptProfileBridgesClassName, scriptProfileComponentClassName, scriptProfilePropsAlias, scriptProfileStateAlias, scriptPropertyBagToMergeEnv, scriptPropertyValueToPlain, zodToLuaTypeRef };
|
|
847
1435
|
|
|
848
1436
|
//# sourceMappingURL=index.js.map
|