@microverse.ts/host-surface 0.1.0 → 0.2.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.
Files changed (39) hide show
  1. package/README.md +14 -13
  2. package/dist/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.d.ts +3 -3
  3. package/dist/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.d.ts.map +1 -1
  4. package/dist/application/useCases/compileHostSurface.d.ts +5 -5
  5. package/dist/application/useCases/compileHostSurface.d.ts.map +1 -1
  6. package/dist/domain/componentSlotPrelude.d.ts +3 -0
  7. package/dist/domain/componentSlotPrelude.d.ts.map +1 -0
  8. package/dist/domain/hostSurfaceManifest.d.ts +2 -2
  9. package/dist/domain/hostSurfaceManifest.d.ts.map +1 -1
  10. package/dist/domain/hostSurfaceTypes.d.ts +12 -9
  11. package/dist/domain/hostSurfaceTypes.d.ts.map +1 -1
  12. package/dist/domain/luaGlobalHook.d.ts +1 -1
  13. package/dist/domain/scriptContextSymbol.d.ts +6 -0
  14. package/dist/domain/scriptContextSymbol.d.ts.map +1 -0
  15. package/dist/domain/scriptPropertyMergeEnv.d.ts +6 -0
  16. package/dist/domain/scriptPropertyMergeEnv.d.ts.map +1 -0
  17. package/dist/domain/surfaceCapabilities.d.ts +1 -1
  18. package/dist/domain/surfaceCapabilities.d.ts.map +1 -1
  19. package/dist/domain/surfaceMethodDef.d.ts +1 -1
  20. package/dist/domain/surfaceMethodDef.d.ts.map +1 -1
  21. package/dist/index.d.ts +18 -14
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +407 -139
  24. package/dist/index.js.map +1 -1
  25. package/dist/infrastructure/adapters/augmentHostWithCapabilityRegistry.d.ts +1 -1
  26. package/dist/infrastructure/adapters/augmentHostWithCapabilityRegistry.d.ts.map +1 -1
  27. package/dist/infrastructure/adapters/augmentHostWithScriptContext.d.ts +4 -0
  28. package/dist/infrastructure/adapters/augmentHostWithScriptContext.d.ts.map +1 -0
  29. package/dist/infrastructure/adapters/zodSchemaValidationAdapter.d.ts +1 -1
  30. package/dist/infrastructure/adapters/zodSchemaValidationAdapter.d.ts.map +1 -1
  31. package/dist/infrastructure/builders/bridgeMergeEnv.d.ts +6 -3
  32. package/dist/infrastructure/builders/bridgeMergeEnv.d.ts.map +1 -1
  33. package/dist/infrastructure/builders/defineHostSurfaceFacade.d.ts +5 -5
  34. package/dist/infrastructure/builders/defineHostSurfaceFacade.d.ts.map +1 -1
  35. package/dist/infrastructure/builders/surfaceBuilder.d.ts +11 -11
  36. package/dist/infrastructure/builders/surfaceBuilder.d.ts.map +1 -1
  37. package/dist/infrastructure/components/hostScriptSession.d.ts +23 -68
  38. package/dist/infrastructure/components/hostScriptSession.d.ts.map +1 -1
  39. package/package.json +10 -9
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { buildDeclarativeBridgeTable } from "@microverse.ts/runtime-bridge";
2
2
  import { validateWithZodSchema } from "@microverse.ts/runtime-zod";
3
3
  import { z } from "zod";
4
+ import { applyScriptPropertyChanges, assertValidScriptPropertyBag, cloneScriptPropertyBag, createMicroverseScript, createScriptInstanceContext, diffScriptProperties } from "@microverse.ts/runtime-core";
4
5
  import { InMemoryCapabilityRegistry, createAllowlist, createCapabilityId } from "@microverse.ts/runtime-capabilities";
5
- import { createMicroverseScript } from "@microverse.ts/runtime-core";
6
6
  //#region src/infrastructure/builders/bridgeMergeEnv.ts
7
7
  /**
8
8
  * Builds a frozen `mergeEnv` table: bridge name → API object, ready for `MicroverseSlot.run({ mergeEnv })`.
@@ -14,6 +14,10 @@ import { createMicroverseScript } from "@microverse.ts/runtime-core";
14
14
  function buildBridgeMergeEnvForHost(host, slotKey, surface) {
15
15
  return buildDeclarativeBridgeTable(host, slotKey, [...surface.toBridgeDeclarations()]);
16
16
  }
17
+ /** Bridge table names for `__microverse_bridge_names` in the slot env. */
18
+ function bridgeNamesFromSurface(surface) {
19
+ return surface.toBridgeDeclarations().map((d) => d.name);
20
+ }
17
21
  //#endregion
18
22
  //#region src/infrastructure/adapters/zodSchemaValidationAdapter.ts
19
23
  function createZodSchemaValidationPort() {
@@ -159,29 +163,45 @@ function asyncHandleClassName(bridgeName, methodName) {
159
163
  const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
160
164
  return `${cap(bridgeName)}${cap(methodName)}Handle`;
161
165
  }
162
- function buildWorkflowHookManifestFields(kinds, workflowHooks, selfType, fieldDescriptionSuffix) {
166
+ /** LuaCATS class for runtime bridge key `audit` → `Audit` (distinct from field name for LuaLS). */
167
+ function bridgeLuaClassName(bridgeTableName) {
168
+ return bridgeTableName.charAt(0).toUpperCase() + bridgeTableName.slice(1);
169
+ }
170
+ function pushMicroverseBridgesClass(bridgeNames, classes) {
171
+ if (bridgeNames.length === 0) return;
172
+ classes.push({
173
+ name: "MicroverseBridges",
174
+ description: "Capability-scoped host bridges for this component (`self.bridges` after `component:extend()`).",
175
+ fields: bridgeNames.map((name) => ({
176
+ name,
177
+ luaType: bridgeLuaClassName(name)
178
+ })),
179
+ emitSingleton: false
180
+ });
181
+ }
182
+ function buildComponentEventManifestFields(kinds, componentHooks) {
163
183
  const out = [];
164
184
  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}`;
185
+ if (!(componentHooks[kind] instanceof z.ZodObject)) throw new Error(`defineHostSurface componentHooks: "${kind}" must be a z.object(...)`);
186
+ const payloadName = `MicroverseEvt_${kind}`;
167
187
  const hookName = luaGlobalHookName(kind);
168
188
  out.push({
169
189
  name: hookName,
170
- description: `${fieldDescriptionSuffix} Payload: \`${payloadName}\`.`,
171
- luaType: `fun(self: ${selfType}, evt: ${payloadName})`
190
+ description: `Host invokes when \`${kind}\` is emitted. Payload: \`${payloadName}\`.`,
191
+ luaType: `fun(self: Component, evt: ${payloadName})`
172
192
  });
173
193
  }
174
194
  return out;
175
195
  }
176
- function pushWorkflowPayloadManifestClasses(kinds, workflowHooks, classes) {
196
+ function pushComponentEventPayloadClasses(kinds, componentHooks, classes) {
177
197
  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}`;
198
+ const schema = componentHooks[kind];
199
+ if (!(schema instanceof z.ZodObject)) throw new Error(`defineHostSurface componentHooks: "${kind}" must be a z.object(...)`);
200
+ const name = `MicroverseEvt_${kind}`;
181
201
  const shape = schema.shape;
182
202
  classes.push({
183
203
  name,
184
- description: `Workflow hook payload for \`${luaGlobalHookName(kind)}\` (Zod → LuaCATS fields).`,
204
+ description: `Domain event payload for \`${luaGlobalHookName(kind)}\` (Zod → LuaCATS fields).`,
185
205
  fields: Object.keys(shape).map((k) => ({
186
206
  name: k,
187
207
  luaType: zodToLuaTypeRef(shape[k])
@@ -190,9 +210,63 @@ function pushWorkflowPayloadManifestClasses(kinds, workflowHooks, classes) {
190
210
  });
191
211
  }
192
212
  }
193
- function buildLuaDefManifestFromHostSurfaceSpec(spec, opts, workflowHooks) {
213
+ function pushComponentManifestClasses(classes, bridgeNames, componentHooks) {
214
+ const eventKinds = componentHooks !== void 0 ? Object.keys(componentHooks).sort((a, b) => a.localeCompare(b)) : [];
215
+ if (componentHooks !== void 0) pushComponentEventPayloadClasses(eventKinds, componentHooks, classes);
216
+ const lifecycleFields = [
217
+ {
218
+ name: "properties",
219
+ luaType: "table",
220
+ description: "Host-synced props (proxy)."
221
+ },
222
+ {
223
+ name: "state",
224
+ luaType: "table",
225
+ description: "Lua-local state."
226
+ },
227
+ {
228
+ name: "bridges",
229
+ luaType: bridgeNames.length > 0 ? "MicroverseBridges" : "table",
230
+ description: "Host bridges allowed for this instance (not global in the slot)."
231
+ },
232
+ {
233
+ name: "init",
234
+ luaType: "fun(self: Component)",
235
+ description: "Called once after mount and initial props are applied."
236
+ },
237
+ {
238
+ name: "onPropsChanged",
239
+ luaType: "fun(self: Component, key: string, newValue: any)",
240
+ description: "Called when the host patches a property key."
241
+ },
242
+ {
243
+ name: "onDestroy",
244
+ luaType: "fun(self: Component)",
245
+ description: "Called before the script instance slot is disposed."
246
+ }
247
+ ];
248
+ if (componentHooks !== void 0) lifecycleFields.push(...buildComponentEventManifestFields(eventKinds, componentHooks));
249
+ classes.push({
250
+ name: "Component",
251
+ description: "Component instance from `local C = component:extend()`. Use `self.bridges` for host APIs; define `on*` methods for domain events.",
252
+ fields: lifecycleFields,
253
+ emitSingleton: false
254
+ });
255
+ classes.push({
256
+ name: "component",
257
+ description: "Per-slot helper injected by the host. Use `component:extend()` to create the active component.",
258
+ methods: [{
259
+ name: "extend",
260
+ description: "Returns the component table with `properties`, `state`, and `bridges` wired for this slot.",
261
+ params: [],
262
+ returns: "Component"
263
+ }]
264
+ });
265
+ }
266
+ function buildLuaDefManifestFromHostSurfaceSpec(spec, opts, componentHooks) {
194
267
  const classes = [];
195
- for (const bridgeName of Object.keys(spec)) {
268
+ const bridgeNames = Object.keys(spec).sort((a, b) => a.localeCompare(b));
269
+ for (const bridgeName of bridgeNames) {
196
270
  const methods = spec[bridgeName];
197
271
  const manifestMethods = [];
198
272
  for (const methodName of Object.keys(methods)) {
@@ -228,35 +302,17 @@ function buildLuaDefManifestFromHostSurfaceSpec(spec, opts, workflowHooks) {
228
302
  });
229
303
  }
230
304
  classes.push({
231
- name: bridgeName,
232
- methods: manifestMethods
305
+ name: bridgeLuaClassName(bridgeName),
306
+ methods: manifestMethods,
307
+ emitSingleton: false
233
308
  });
234
309
  }
310
+ pushMicroverseBridgesClass(bridgeNames, classes);
235
311
  const fromLuaType = collectLuaTypeAliasesFromHostSpec(spec);
236
312
  const fromOverrides = inferLuaTypeAliasesFromHostSpec(spec);
237
313
  const merged = new Map([...fromLuaType.map((a) => [a.name, a.definition]), ...fromOverrides.map((a) => [a.name, a.definition])]);
238
314
  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
- }
315
+ pushComponentManifestClasses(classes, bridgeNames, componentHooks);
260
316
  const aliases = merged.size === 0 ? void 0 : [...merged.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, definition]) => ({
261
317
  name,
262
318
  definition
@@ -429,6 +485,9 @@ function pickSurfaceCapabilities(surfaceCapabilities, ...capabilities) {
429
485
  */
430
486
  var MICROVERSE_CAPABILITY_REGISTRY = Symbol.for("microverse:capabilityRegistry");
431
487
  //#endregion
488
+ //#region src/domain/scriptContextSymbol.ts
489
+ var MICROVERSE_SCRIPT_CONTEXT = Symbol("microverse.scriptContext");
490
+ //#endregion
432
491
  //#region src/application/useCases/compileBridgeDeclarationsFromHostSurfaceSpec.ts
433
492
  function isThenable(value) {
434
493
  if (value === null || value === void 0) return false;
@@ -446,19 +505,26 @@ function createBridgeDeclarationsFromHostSurfaceSpec(schemaValidation, spec) {
446
505
  name: bridgeName,
447
506
  perEntity: true,
448
507
  createApi: (host, slotKey) => {
508
+ const hostWithScript = host;
449
509
  const api = {};
450
510
  for (const methodName of Object.keys(methods)) {
451
511
  const entry = methods[methodName];
452
512
  api[methodName] = (...args) => {
453
513
  const payload = args.length >= 2 ? args[1] : args[0];
454
- const registry = host[MICROVERSE_CAPABILITY_REGISTRY];
514
+ const registry = hostWithScript[MICROVERSE_CAPABILITY_REGISTRY];
455
515
  const capability = entry.capability;
456
516
  if (!registry.isAllowed(capability)) throw new Error(`capability denied: ${String(capability)}`);
457
517
  const parsedIn = schemaValidation.validateWithZodSchema(entry.input, payload);
458
518
  if (parsedIn._tag === "err") throw new Error(parsedIn.error);
459
- const raw = entry.handler({
460
- host,
519
+ const script = hostWithScript[MICROVERSE_SCRIPT_CONTEXT] ?? createScriptInstanceContext({
520
+ instanceId: String(slotKey),
521
+ scriptId: "unknown",
461
522
  slotKey: String(slotKey)
523
+ });
524
+ const raw = entry.handler({
525
+ host: hostWithScript,
526
+ slotKey: String(slotKey),
527
+ script
462
528
  }, parsedIn.value);
463
529
  if (isThenable(raw)) return raw.then((resolved) => {
464
530
  const parsedOut = schemaValidation.validateWithZodSchema(entry.output, resolved);
@@ -478,29 +544,29 @@ function createBridgeDeclarationsFromHostSurfaceSpec(schemaValidation, spec) {
478
544
  }
479
545
  //#endregion
480
546
  //#region src/application/useCases/compileHostSurface.ts
481
- function buildHostSurfaceCore(schemaValidation, spec, workflowHooks) {
547
+ function buildHostSurfaceCore(schemaValidation, spec, componentHooks) {
482
548
  const capabilities = collectCapabilitiesFromHostSurfaceSpec(spec);
483
549
  return {
484
550
  toBridgeDeclarations: () => createBridgeDeclarationsFromHostSurfaceSpec(schemaValidation, spec),
485
- toLuaDefManifest: (opts) => buildLuaDefManifestFromHostSurfaceSpec(spec, opts, workflowHooks),
551
+ toLuaDefManifest: (opts) => buildLuaDefManifestFromHostSurfaceSpec(spec, opts, componentHooks),
486
552
  capabilities,
487
553
  pickCapabilities: (...picked) => pickSurfaceCapabilities(capabilities, ...picked)
488
554
  };
489
555
  }
490
- function compileHostSurface(ports, spec, workflowHooks) {
556
+ function compileHostSurface(ports, spec, componentHooks) {
491
557
  const [schemaValidation] = ports;
492
- const core = buildHostSurfaceCore(schemaValidation, spec, workflowHooks);
493
- if (workflowHooks === void 0) return core;
558
+ const core = buildHostSurfaceCore(schemaValidation, spec, componentHooks);
559
+ if (componentHooks === void 0) return core;
494
560
  return {
495
561
  ...core,
496
- workflowHooks
562
+ componentHooks
497
563
  };
498
564
  }
499
565
  /**
500
566
  * Same as {@link compileHostSurface}, but requires every bridge method to be typed with the same `THost`.
501
567
  */
502
- function compileHostSurfaceFor(ports, spec, workflowHooks) {
503
- return workflowHooks === void 0 ? compileHostSurface(ports, spec) : compileHostSurface(ports, spec, workflowHooks);
568
+ function compileHostSurfaceFor(ports, spec, componentHooks) {
569
+ return componentHooks === void 0 ? compileHostSurface(ports, spec) : compileHostSurface(ports, spec, componentHooks);
504
570
  }
505
571
  //#endregion
506
572
  //#region src/domain/inferMethodAsync.ts
@@ -551,11 +617,11 @@ function createNullPrototypeRecord() {
551
617
  */
552
618
  var SurfaceBuilder = class SurfaceBuilder {
553
619
  spec = createNullPrototypeRecord();
554
- workflowHooksSpec;
620
+ componentHooksSpec;
555
621
  ports;
556
- constructor(ports, workflowHooks, initialSpec) {
622
+ constructor(ports, componentHooks, initialSpec) {
557
623
  this.ports = ports;
558
- this.workflowHooksSpec = workflowHooks;
624
+ this.componentHooksSpec = componentHooks;
559
625
  if (initialSpec !== void 0) for (const bridgeName of Object.keys(initialSpec)) {
560
626
  assertSafeObjectKey("bridge", bridgeName);
561
627
  const srcBridge = initialSpec[bridgeName];
@@ -571,14 +637,14 @@ var SurfaceBuilder = class SurfaceBuilder {
571
637
  bridge(name) {
572
638
  return new BridgeBuilder(this, name);
573
639
  }
574
- /** Attaches workflow hook Zod schemas (emitted into `.d.lua` as `on*` methods). */
575
- workflowHooks(hooks) {
640
+ /** Attaches component domain-event Zod schemas (emitted into `.d.lua` as `on*` methods on `Component`). */
641
+ componentHooks(hooks) {
576
642
  return new SurfaceBuilder(this.ports, hooks, this.spec);
577
643
  }
578
644
  /** Compiles the accumulated spec into a {@link HostSurface}. */
579
645
  build() {
580
646
  const spec = this.spec;
581
- return compileHostSurfaceFor(this.ports, spec, this.workflowHooksSpec);
647
+ return compileHostSurfaceFor(this.ports, spec, this.componentHooksSpec);
582
648
  }
583
649
  /** @internal */
584
650
  addMethod(bridgeName, methodName, entry) {
@@ -654,6 +720,143 @@ function defineHostSurfaceFor() {
654
720
  return createSurfaceBuilder();
655
721
  }
656
722
  //#endregion
723
+ //#region src/domain/componentSlotPrelude.ts
724
+ /** @see MICROVERSE_LUA_COMPONENT_SLOT_PRELUDE */
725
+ var MICROVERSE_LUA_COMPONENT_SLOT_PRELUDE = `
726
+ rawset(_ENV, "__microverse_component_rawProps", {})
727
+ rawset(_ENV, "__microverse_component_dirty", {})
728
+
729
+ local function rawProps()
730
+ return rawget(_ENV, "__microverse_component_rawProps")
731
+ end
732
+
733
+ local function dirty()
734
+ return rawget(_ENV, "__microverse_component_dirty")
735
+ end
736
+
737
+ local PropertiesMT = {
738
+ __index = function(t, k)
739
+ return rawget(t, "__raw")[k]
740
+ end,
741
+ __newindex = function(t, k, v)
742
+ rawget(t, "__raw")[k] = v
743
+ local d = dirty()
744
+ d[k] = v
745
+ end,
746
+ }
747
+
748
+ function __microverse_lua_attach_bridges(impl)
749
+ if type(impl) ~= "table" then
750
+ return
751
+ end
752
+ impl.bridges = impl.bridges or {}
753
+ local names = rawget(_ENV, "__microverse_bridge_names")
754
+ if type(names) ~= "table" then
755
+ return
756
+ end
757
+ for _, name in ipairs(names) do
758
+ if type(name) == "string" then
759
+ local g = rawget(_ENV, name)
760
+ if type(g) == "table" then
761
+ impl.bridges[name] = g
762
+ rawset(_ENV, name, nil)
763
+ end
764
+ end
765
+ end
766
+ end
767
+
768
+ rawset(_ENV, "__microverse_lua_attach_bridges", __microverse_lua_attach_bridges)
769
+
770
+ rawset(_ENV, "component", {
771
+ extend = function()
772
+ local impl = { state = {}, bridges = {} }
773
+ local proxy = { __raw = rawProps() }
774
+ setmetatable(proxy, PropertiesMT)
775
+ impl.properties = proxy
776
+ local base = rawget(_ENV, "__microverse_component_hook_base")
777
+ if type(base) == "table" then
778
+ setmetatable(impl, { __index = base })
779
+ end
780
+ __microverse_lua_attach_bridges(impl)
781
+ rawset(_ENV, "__microverse_lua_ComponentImpl", impl)
782
+ return impl
783
+ end,
784
+ })
785
+
786
+ rawset(_ENV, "__microverse_lua_component_apply_incoming", function()
787
+ local incoming = rawget(_ENV, "__microverseIncomingProps")
788
+ if type(incoming) ~= "table" then
789
+ return
790
+ end
791
+ local rp = rawProps()
792
+ local impl = rawget(_ENV, "__microverse_lua_ComponentImpl")
793
+ if type(impl) ~= "table" or type(impl.properties) ~= "table" then
794
+ for k, v in pairs(incoming) do
795
+ rp[k] = v
796
+ end
797
+ return
798
+ end
799
+ for k, v in pairs(incoming) do
800
+ impl.properties[k] = v
801
+ end
802
+ end)
803
+
804
+ rawset(_ENV, "__microverse_lua_component_flush_to_sink", function()
805
+ local push = rawget(_ENV, "__microverseFlushPush")
806
+ local d = dirty()
807
+ for k, v in pairs(d) do
808
+ if type(push) == "function" then
809
+ push(k, v)
810
+ end
811
+ d[k] = nil
812
+ end
813
+ end)
814
+ `.trim();
815
+ //#endregion
816
+ //#region src/domain/scriptPropertyMergeEnv.ts
817
+ function scriptPropertyBagToMergeEnv(bag) {
818
+ const out = {};
819
+ for (const [k, v] of Object.entries(bag)) out[k] = scriptPropertyValueToPlain(v);
820
+ return out;
821
+ }
822
+ function scriptPropertyValueToPlain(value) {
823
+ if (value === null || typeof value !== "object") return value;
824
+ if (Array.isArray(value)) return value.map((item) => scriptPropertyValueToPlain(item));
825
+ const out = {};
826
+ for (const [k, v] of Object.entries(value)) out[k] = scriptPropertyValueToPlain(v);
827
+ return out;
828
+ }
829
+ function mergeEnvSinkToScriptPropertyBag(sink) {
830
+ const out = {};
831
+ for (const [k, v] of Object.entries(sink)) {
832
+ const converted = plainToScriptPropertyValue(v);
833
+ if (converted !== void 0) out[k] = converted;
834
+ }
835
+ return out;
836
+ }
837
+ function plainToScriptPropertyValue(value) {
838
+ if (value === null) return null;
839
+ const t = typeof value;
840
+ if (t === "string" || t === "boolean") return value;
841
+ if (typeof value === "number" && Number.isFinite(value)) return value;
842
+ if (Array.isArray(value)) {
843
+ const arr = [];
844
+ for (const item of value) {
845
+ const c = plainToScriptPropertyValue(item);
846
+ if (c !== void 0) arr.push(c);
847
+ }
848
+ return arr;
849
+ }
850
+ if (t === "object" && value !== null) {
851
+ const obj = {};
852
+ for (const [k, v] of Object.entries(value)) {
853
+ const c = plainToScriptPropertyValue(v);
854
+ if (c !== void 0) obj[k] = c;
855
+ }
856
+ return obj;
857
+ }
858
+ }
859
+ //#endregion
657
860
  //#region src/infrastructure/adapters/augmentHostWithCapabilityRegistry.ts
658
861
  /**
659
862
  * Returns a shallow copy of `host` with {@link MICROVERSE_CAPABILITY_REGISTRY} set to `registry`.
@@ -666,61 +869,124 @@ function augmentHostWithCapabilityRegistry(host, registry) {
666
869
  return Object.assign(host, { [MICROVERSE_CAPABILITY_REGISTRY]: registry });
667
870
  }
668
871
  //#endregion
872
+ //#region src/infrastructure/adapters/augmentHostWithScriptContext.ts
873
+ function augmentHostWithScriptContext(host, script) {
874
+ return Object.assign(host, { [MICROVERSE_SCRIPT_CONTEXT]: script });
875
+ }
876
+ //#endregion
669
877
  //#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
878
  var HostScriptSession = class {
686
879
  opts;
687
880
  sandbox;
881
+ hostProps = {};
688
882
  registry;
883
+ context;
689
884
  constructor(opts) {
690
885
  this.opts = opts;
691
886
  this.registry = new InMemoryCapabilityRegistry(createAllowlist([...opts.allowedCapabilities]));
887
+ this.context = opts.script;
692
888
  }
693
- /**
694
- * Allocates the underlying {@link MicroverseSlot} for this `slotKey` on the shared runtime.
695
- */
696
889
  openSession = async () => {
697
890
  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),
891
+ const sb = this.requireMicroverseSlot();
892
+ if (this.opts.enableComponentRuntime !== false) {
893
+ await sb.run({
894
+ script: createMicroverseScript(MICROVERSE_LUA_COMPONENT_SLOT_PRELUDE),
895
+ mergeEnv: this.mergeEnv(),
896
+ timeout: this.opts.defaultTimeout
897
+ });
898
+ await sb.run({
899
+ script: createMicroverseScript(buildBridgeNamesPreludeLua(bridgeNamesFromSurface(this.opts.surface))),
703
900
  mergeEnv: this.mergeEnv(),
704
901
  timeout: this.opts.defaultTimeout
705
902
  });
903
+ const hooks = readComponentHooks(this.opts.surface);
904
+ if (hooks !== void 0) {
905
+ const prelude = buildComponentEventStubPreludeLua(hooks);
906
+ await sb.run({
907
+ script: createMicroverseScript(prelude),
908
+ mergeEnv: this.mergeEnv(),
909
+ timeout: this.opts.defaultTimeout
910
+ });
911
+ }
706
912
  }
707
913
  };
708
- /**
709
- * Exposes the in-memory registry (e.g. to mutate allowlists in advanced tests).
710
- */
711
914
  getCapabilityRegistry = () => this.registry;
712
915
  requireMicroverseSlot() {
713
916
  if (this.sandbox === void 0) throw new Error("HostScriptSession: openSession() was not called");
714
917
  return this.sandbox;
715
918
  }
716
919
  mergeEnv() {
717
- return buildBridgeMergeEnvForHost(augmentHostWithCapabilityRegistry(this.opts.host, this.registry), String(this.opts.slotKey), this.opts.surface);
920
+ return buildBridgeMergeEnvForHost(augmentHostWithScriptContext(augmentHostWithCapabilityRegistry(this.opts.host, this.registry), this.opts.script), String(this.opts.slotKey), this.opts.surface);
921
+ }
922
+ emitAudit(event) {
923
+ this.opts.onScriptAudit?.(event);
924
+ }
925
+ validatePropsBag(bag) {
926
+ if (this.opts.propsSchema !== void 0) {
927
+ const parsed = this.opts.propsSchema.parse(bag);
928
+ assertValidScriptPropertyBag(parsed);
929
+ return parsed;
930
+ }
931
+ assertValidScriptPropertyBag(bag);
932
+ return bag;
718
933
  }
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
- */
934
+ getProps = () => ({ ...this.hostProps });
935
+ setProps = async (bag) => {
936
+ const next = this.validatePropsBag(bag);
937
+ const changed = diffScriptProperties(this.hostProps, next);
938
+ applyScriptPropertyChanges(this.hostProps, next, changed);
939
+ await this.requireMicroverseSlot().run({
940
+ script: createMicroverseScript("local f = rawget(_ENV, \"__microverse_lua_component_apply_incoming\")\nif type(f) == \"function\" then f() end"),
941
+ mergeEnv: {
942
+ ...this.mergeEnv(),
943
+ __microverseIncomingProps: scriptPropertyBagToMergeEnv(this.hostProps)
944
+ },
945
+ timeout: this.opts.defaultTimeout
946
+ });
947
+ if (changed.length > 0) {
948
+ this.emitAudit({
949
+ kind: "propsPatched",
950
+ context: this.opts.script,
951
+ changedKeys: changed
952
+ });
953
+ for (const key of changed) {
954
+ const value = next[key];
955
+ if (value !== void 0) await this.invokeComponentHook("onPropsChanged", key, value);
956
+ }
957
+ }
958
+ };
959
+ patchProps = async (partial) => {
960
+ await this.setProps({
961
+ ...this.hostProps,
962
+ ...partial
963
+ });
964
+ };
965
+ flushDirtyProps = async () => {
966
+ if (this.opts.enableComponentRuntime === false) return null;
967
+ const collected = {};
968
+ await this.requireMicroverseSlot().run({
969
+ script: createMicroverseScript("local f = rawget(_ENV, \"__microverse_lua_component_flush_to_sink\")\nif type(f) == \"function\" then f() end"),
970
+ mergeEnv: {
971
+ ...this.mergeEnv(),
972
+ __microverseFlushPush: (key, value) => {
973
+ const converted = plainToScriptPropertyValue(value);
974
+ if (converted !== void 0) collected[key] = converted;
975
+ }
976
+ },
977
+ timeout: this.opts.defaultTimeout
978
+ });
979
+ const keys = Object.keys(collected);
980
+ if (keys.length === 0) return null;
981
+ const dirty = collected;
982
+ for (const key of keys) this.hostProps[key] = dirty[key];
983
+ this.emitAudit({
984
+ kind: "propsFlushed",
985
+ context: this.opts.script,
986
+ dirtyKeys: keys
987
+ });
988
+ return dirty;
989
+ };
724
990
  runChunk = async (source) => {
725
991
  return this.requireMicroverseSlot().run({
726
992
  script: createMicroverseScript(source),
@@ -728,37 +994,46 @@ var HostScriptSession = class {
728
994
  timeout: this.opts.defaultTimeout
729
995
  });
730
996
  };
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);
997
+ invokeComponentHook = async (hookName, ...args) => {
998
+ assertSafeLuaGlobalName(hookName);
999
+ this.emitAudit({
1000
+ kind: "hookInvoked",
1001
+ context: this.opts.script,
1002
+ hookName
1003
+ });
740
1004
  const sb = this.requireMicroverseSlot();
741
- const tbl = luaTableLiteralFromPlainRecord(payload);
742
- const src = readWorkflowHooks(this.opts.surface) !== void 0 ? buildWorkflowHookInvokeLuaSource(globalName, tbl) : buildGlobalHookInvokeLuaSource(globalName, tbl);
1005
+ const argLiterals = args.map((a) => {
1006
+ if (typeof a === "object" && a !== null && !Array.isArray(a)) return luaTableLiteralFromPlainRecord(a);
1007
+ return luaValueLiteral(a);
1008
+ }).join(", ");
1009
+ const src = [
1010
+ `local impl = rawget(_ENV, "__microverse_lua_ComponentImpl")`,
1011
+ `if type(impl) == "table" then`,
1012
+ ` local attach = rawget(_ENV, "__microverse_lua_attach_bridges")`,
1013
+ ` if type(attach) == "function" then attach(impl) end`,
1014
+ ` local m = rawget(impl, ${JSON.stringify(hookName)})`,
1015
+ ` if type(m) == "function" then`,
1016
+ argLiterals.length > 0 ? ` m(impl, ${argLiterals})` : ` m(impl)`,
1017
+ ` end`,
1018
+ `end`
1019
+ ].join("\n");
743
1020
  return sb.run({
744
1021
  script: createMicroverseScript(src),
745
1022
  mergeEnv: this.mergeEnv(),
746
1023
  timeout: this.opts.defaultTimeout
747
1024
  });
1025
+ };
1026
+ invokeComponentEventHook = (async (hookName, payload) => {
1027
+ assertSafeLuaGlobalName(hookName);
1028
+ return this.invokeComponentHook(hookName, payload);
748
1029
  });
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
1030
  call = async (tableName, methodName, payload) => {
758
1031
  const sb = this.requireMicroverseSlot();
759
1032
  const tbl = luaTableLiteralFromUnknownRecord(payload);
760
1033
  const src = [
761
- `local t = _ENV[${JSON.stringify(tableName)}]`,
1034
+ `local impl = rawget(_ENV, "__microverse_lua_ComponentImpl")`,
1035
+ `local bridges = type(impl) == "table" and impl.bridges or nil`,
1036
+ `local t = type(bridges) == "table" and bridges[${JSON.stringify(tableName)}] or nil`,
762
1037
  `local f = type(t) == "table" and t[${JSON.stringify(methodName)}] or nil`,
763
1038
  `if type(f) == "function" then`,
764
1039
  ` f(${tbl})`,
@@ -770,61 +1045,54 @@ var HostScriptSession = class {
770
1045
  timeout: this.opts.defaultTimeout
771
1046
  });
772
1047
  };
773
- /**
774
- * Releases the slot in the runtime adapter and clears the session handle.
775
- */
776
1048
  dispose = async () => {
777
1049
  if (this.sandbox !== void 0) {
1050
+ if (this.opts.enableComponentRuntime !== false) await this.invokeComponentHook("onDestroy");
778
1051
  await this.sandbox.dispose();
779
1052
  this.sandbox = void 0;
780
1053
  }
781
1054
  };
1055
+ /** Seeds host props bag without Lua sync (call before setProps after mount). */
1056
+ seedHostProps = (bag) => {
1057
+ const cloned = cloneScriptPropertyBag(bag);
1058
+ for (const k of Object.keys(this.hostProps)) delete this.hostProps[k];
1059
+ Object.assign(this.hostProps, cloned);
1060
+ };
782
1061
  };
783
1062
  function assertSafeLuaGlobalName(name) {
784
1063
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error(`unsafe Lua global name: ${name}`);
785
1064
  }
786
- function readWorkflowHooks(surface) {
787
- if (!("workflowHooks" in surface)) return;
788
- return surface.workflowHooks;
1065
+ function readComponentHooks(surface) {
1066
+ if (!("componentHooks" in surface)) return;
1067
+ return surface.componentHooks;
789
1068
  }
790
- function buildWorkflowStubPreludeLua(hooks) {
1069
+ function buildBridgeNamesPreludeLua(bridgeNames) {
1070
+ return `rawset(_ENV, "__microverse_bridge_names", { ${bridgeNames.map((n) => JSON.stringify(n)).join(", ")} })`;
1071
+ }
1072
+ function buildComponentEventStubPreludeLua(hooks) {
791
1073
  return [
792
1074
  "local Base = {}",
793
1075
  "for _, name in ipairs({",
794
1076
  ...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}`);
1077
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(kind)) throw new Error(`unsafe component hook kind: ${kind}`);
796
1078
  return JSON.stringify(luaGlobalHookName(kind));
797
1079
  }).map((h) => ` ${h},`),
798
1080
  "}) do",
799
1081
  " rawset(Base, name, function() end)",
800
1082
  "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
- "})"
1083
+ "rawset(_ENV, \"__microverse_component_hook_base\", Base)"
808
1084
  ].join("\n");
809
1085
  }
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");
1086
+ function luaValueLiteral(value) {
1087
+ if (typeof value === "string") return JSON.stringify(value);
1088
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
1089
+ if (typeof value === "boolean") return value ? "true" : "false";
1090
+ if (value === null) return "nil";
1091
+ if (Array.isArray(value)) return `{ ${value.map((v) => luaValueLiteral(v)).join(", ")} }`;
1092
+ const record = value;
1093
+ const parts = [];
1094
+ for (const k of Object.keys(record)) parts.push(`${k} = ${luaValueLiteral(record[k])}`);
1095
+ return `{ ${parts.join(", ")} }`;
828
1096
  }
829
1097
  function luaTableLiteralFromPlainRecord(o) {
830
1098
  const parts = [];
@@ -843,6 +1111,6 @@ function luaTableLiteralFromUnknownRecord(o) {
843
1111
  return `{ ${parts.join(", ")} }`;
844
1112
  }
845
1113
  //#endregion
846
- export { BridgeBuilder, HostScriptSession, MICROVERSE_CAPABILITY_REGISTRY, SurfaceBuilder, augmentHostWithCapabilityRegistry, buildBridgeMergeEnvForHost, collectCapabilitiesFromHostSurfaceSpec, compileHostSurface, compileHostSurfaceFor, createBridgeDeclarationsFromHostSurfaceSpec, defineHostSurface, defineHostSurfaceFor, luaGlobalHookName, luaType, pickSurfaceCapabilities, zodToLuaTypeRef };
1114
+ export { BridgeBuilder, HostScriptSession, MICROVERSE_CAPABILITY_REGISTRY, MICROVERSE_LUA_COMPONENT_SLOT_PRELUDE, MICROVERSE_SCRIPT_CONTEXT, SurfaceBuilder, augmentHostWithCapabilityRegistry, augmentHostWithScriptContext, buildBridgeMergeEnvForHost, collectCapabilitiesFromHostSurfaceSpec, compileHostSurface, compileHostSurfaceFor, createBridgeDeclarationsFromHostSurfaceSpec, defineHostSurface, defineHostSurfaceFor, luaGlobalHookName, luaType, mergeEnvSinkToScriptPropertyBag, pickSurfaceCapabilities, scriptPropertyBagToMergeEnv, scriptPropertyValueToPlain, zodToLuaTypeRef };
847
1115
 
848
1116
  //# sourceMappingURL=index.js.map