@forgeax/engine-host 0.1.27 → 0.1.29

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 CHANGED
@@ -30,3 +30,12 @@ capability: requests fail, `connected` becomes false, and the frontend status
30
30
  becomes `failed` with a transport error. Consumer plugins own their domain
31
31
  resources through Cordis effects; the host owns only its transport and Loader
32
32
  lifetime.
33
+
34
+ Embedding owners can observe accepted frontend reports with
35
+ `backend.subscribeActivationReports(listener)`; the returned disposer removes
36
+ only that listener and preserves `onActivationReport`. A staged consumer may
37
+ call `backend.update(input, { bootstrap: true })` to replace the one bootstrap
38
+ revision admitted alongside the current assembly. Later updates retain that
39
+ bootstrap revision for reconnects; arbitrary stale revisions still fail. The
40
+ owner must serialize assembly updates and keep backend Entries in the input
41
+ when changing the frontend projection.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=startup.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"startup.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/startup.test.ts"],"names":[],"mappings":""}
package/dist/backend.d.ts CHANGED
@@ -31,7 +31,13 @@ export interface BackendHost {
31
31
  readonly transport: HostTransportServer;
32
32
  readonly ownedContext: boolean;
33
33
  /** Reconcile backend Entries and publish the matching frontend assembly atomically. */
34
- update(input: HostAssemblyInput | readonly GamePluginEntry[]): Promise<HostAssembly>;
34
+ update(input: HostAssemblyInput | readonly GamePluginEntry[],
35
+ /** Replace the one admitted bootstrap revision for staged frontend reconnects. */
36
+ options?: {
37
+ readonly bootstrap?: boolean;
38
+ }): Promise<HostAssembly>;
39
+ /** Observe accepted reports without replacing the owning host's callback. */
40
+ subscribeActivationReports(listener: (report: HostActivationReport) => void | Promise<void>): () => void;
35
41
  dispose(): Promise<void>;
36
42
  }
37
43
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAC1E,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,4BAA4B,EACjC,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,WAAW,EAEjB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EAGpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACnE;AAED,MAAM,WAAW,kBAAkB;IACjC,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,gEAAgE;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IAC9C,iFAAiF;IACjF,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IACzC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtF;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAC;IAC5C,QAAQ,CAAC,SAAS,EAAE,mBAAmB,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,uFAAuF;IACvF,MAAM,CAAC,KAAK,EAAE,iBAAiB,GAAG,SAAS,eAAe,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACrF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAwFD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC,CAqL9F;AAED,YAAY,EAAE,4BAA4B,EAAE,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAC1E,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,4BAA4B,EACjC,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,WAAW,EAEjB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EAGpB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,SAAS,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACnE;AAED,MAAM,WAAW,kBAAkB;IACjC,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,gEAAgE;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IAC9C,iFAAiF;IACjF,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IACzC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtF;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAC;IAC5C,QAAQ,CAAC,SAAS,EAAE,mBAAmB,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,uFAAuF;IACvF,MAAM,CACJ,KAAK,EAAE,iBAAiB,GAAG,SAAS,eAAe,EAAE;IACrD,kFAAkF;IAClF,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GACzC,OAAO,CAAC,YAAY,CAAC,CAAC;IACzB,6EAA6E;IAC7E,0BAA0B,CACxB,QAAQ,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAC/D,MAAM,IAAI,CAAC;IACd,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAwFD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC,CAqM9F;AAED,YAAY,EAAE,4BAA4B,EAAE,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
package/dist/backend.mjs CHANGED
@@ -350,6 +350,30 @@ function modulesFromCatalog(catalog, realm, version = "static") {
350
350
  ...record.digest === void 0 ? {} : { digest: record.digest }
351
351
  }));
352
352
  }
353
+ async function createHostStartup(options = {}) {
354
+ const context = options.context ?? new Context();
355
+ const ownedContext = options.context === void 0;
356
+ const fibers = [];
357
+ let disposed = false;
358
+ try {
359
+ for (const plugin of options.startupPlugins ?? []) fibers.push(await context.plugin(plugin));
360
+ } catch (error) {
361
+ for (const fiber of fibers.reverse()) await fiber.dispose();
362
+ if (ownedContext) await context.fiber.dispose();
363
+ throw error;
364
+ }
365
+ return {
366
+ context,
367
+ ownedContext,
368
+ fibers,
369
+ async dispose() {
370
+ if (disposed) return;
371
+ disposed = true;
372
+ for (const fiber of [...fibers].reverse()) await fiber.dispose();
373
+ if (ownedContext) await context.fiber.dispose();
374
+ }
375
+ };
376
+ }
353
377
 
354
378
  // src/transport.ts
355
379
  var HOST_ASSEMBLY_SERVICE = "host/assembly.get";
@@ -650,7 +674,6 @@ function assertBackendCatalogIdentity(catalog, modules) {
650
674
  }
651
675
  async function createBackendHost(options = {}) {
652
676
  const context = options.context ?? new Context();
653
- const ownedContext = options.context === void 0;
654
677
  const realm = options.realm ?? "engine";
655
678
  const catalog = options.catalog;
656
679
  const pairedBackendEntries = options.pairs?.flatMap((pair) => pair.backend === void 0 ? [] : [pair.backend.entry]) ?? [];
@@ -666,27 +689,31 @@ async function createBackendHost(options = {}) {
666
689
  });
667
690
  const checkedInitial = validateHostAssembly(initialAssembly);
668
691
  if (!checkedInitial.ok) {
669
- if (ownedContext) await context.fiber.dispose();
692
+ if (options.context === void 0) await context.fiber.dispose();
670
693
  throw checkedInitial.error;
671
694
  }
672
695
  const backendModules = options.pairs === void 0 ? modules : options.pairs.flatMap((pair) => pair.backend === void 0 ? [] : [pair.backend.module]);
673
696
  assertBackendCatalogIdentity(catalog, backendModules);
674
697
  const authority = authorityOf(checkedInitial.value);
675
698
  const assembly = authority.authority;
676
- const bootstrapRevision = checkedInitial.value.revision;
699
+ let bootstrapRevision = checkedInitial.value.revision;
700
+ const activationListeners = /* @__PURE__ */ new Set();
677
701
  const transport = options.transport ?? createHostTransport();
678
- let foundationFiber;
702
+ let startup;
679
703
  let loaderFiber;
680
704
  let loader;
681
705
  let backendEntries = options.pairs === void 0 ? options.entries ?? options.assembly?.entries ?? [] : pairedBackendEntries;
682
- const startupFibers = [];
683
706
  let unregisterAssemblyService;
684
707
  let unregisterActivationService;
685
708
  let unsubscribeAssembly;
686
709
  try {
687
- foundationFiber = await context.plugin(hostFoundationPlugin(assembly, transport));
688
- for (const plugin of options.startupPlugins ?? [])
689
- startupFibers.push(await context.plugin(plugin));
710
+ startup = await createHostStartup({
711
+ context,
712
+ startupPlugins: [
713
+ hostFoundationPlugin(assembly, transport),
714
+ ...options.startupPlugins ?? []
715
+ ]
716
+ });
690
717
  if (catalog !== void 0) {
691
718
  const bootstrapped = await bootstrapCatalogLoader(context, catalog, realm, {
692
719
  catalogDigest: checkedInitial.value.revision,
@@ -714,6 +741,7 @@ async function createBackendHost(options = {}) {
714
741
  );
715
742
  }
716
743
  await options.onActivationReport?.(report);
744
+ for (const listener of activationListeners) await listener(report);
717
745
  return { accepted: true };
718
746
  }
719
747
  );
@@ -725,9 +753,7 @@ async function createBackendHost(options = {}) {
725
753
  unregisterAssemblyService?.();
726
754
  unsubscribeAssembly?.();
727
755
  await loaderFiber?.dispose();
728
- for (const fiber of startupFibers.reverse()) await fiber.dispose();
729
- await foundationFiber?.dispose();
730
- if (ownedContext) await context.fiber.dispose();
756
+ await startup?.dispose();
731
757
  throw error;
732
758
  }
733
759
  let disposed = false;
@@ -736,8 +762,22 @@ async function createBackendHost(options = {}) {
736
762
  ...loader === void 0 ? {} : { loader },
737
763
  assembly,
738
764
  transport,
739
- ownedContext,
740
- async update(nextInput) {
765
+ ownedContext: options.context === void 0,
766
+ subscribeActivationReports(listener) {
767
+ if (disposed) {
768
+ throw new HostAssemblyError(
769
+ "host-assembly-service-unavailable",
770
+ "the backend host to remain active while observing activation reports",
771
+ "Subscribe on an active host instance.",
772
+ { service: HOST_ACTIVATION_REPORT_SERVICE }
773
+ );
774
+ }
775
+ activationListeners.add(listener);
776
+ return () => {
777
+ activationListeners.delete(listener);
778
+ };
779
+ },
780
+ async update(nextInput, updateOptions) {
741
781
  if (disposed) {
742
782
  throw new HostAssemblyError(
743
783
  "host-assembly-service-unavailable",
@@ -774,19 +814,19 @@ async function createBackendHost(options = {}) {
774
814
  await loader.await();
775
815
  }
776
816
  backendEntries = nextBackendEntries;
817
+ if (updateOptions?.bootstrap === true) bootstrapRevision = checkedCandidate.value.revision;
777
818
  return authority.publish(effectiveInput);
778
819
  },
779
820
  async dispose() {
780
821
  if (disposed) return;
781
822
  disposed = true;
823
+ activationListeners.clear();
782
824
  unsubscribeAssembly?.();
783
825
  unregisterActivationService?.();
784
826
  unregisterAssemblyService?.();
785
827
  transport.close();
786
828
  await loaderFiber?.dispose();
787
- for (const fiber of startupFibers.reverse()) await fiber.dispose();
788
- await foundationFiber?.dispose();
789
- if (ownedContext) await context.fiber.dispose();
829
+ await startup?.dispose();
790
830
  }
791
831
  };
792
832
  return host;