@forgeax/engine-host 0.1.27 → 0.1.28

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.
@@ -0,0 +1,18 @@
1
+ import { Context, type Fiber, type Plugin } from '@forgeax/engine-plugin';
2
+ /**
3
+ * The product-neutral host bootstrap. It owns only a Cordis Context and the
4
+ * supplied startup entries; assembly, transport, Project, and App are all
5
+ * ordinary plugins layered above this seam.
6
+ */
7
+ export interface HostStartupOptions {
8
+ readonly context?: Context;
9
+ readonly startupPlugins?: readonly Plugin[];
10
+ }
11
+ export interface HostStartup {
12
+ readonly context: Context;
13
+ readonly ownedContext: boolean;
14
+ readonly fibers: readonly Fiber[];
15
+ dispose(): Promise<void>;
16
+ }
17
+ export declare function createHostStartup(options?: HostStartupOptions): Promise<HostStartup>;
18
+ //# sourceMappingURL=startup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAE1E;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,wBAAsB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC,CAuB9F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-host",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -45,7 +45,7 @@
45
45
  ],
46
46
  "dependencies": {
47
47
  "@deepseek-ai/cordis-plugin-loader": "1.0.2",
48
- "@forgeax/engine-plugin": "0.1.27"
48
+ "@forgeax/engine-plugin": "0.1.28"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^20.14.0",
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createHostStartup } from '../startup.js';
3
+
4
+ describe('generic host startup', () => {
5
+ it('owns only supplied Cordis startup entries and releases them', async () => {
6
+ let disposed = false;
7
+ const startup = await createHostStartup({
8
+ startupPlugins: [
9
+ {
10
+ name: 'fixture-startup',
11
+ apply(ctx) {
12
+ ctx.effect(
13
+ () => () => {
14
+ disposed = true;
15
+ },
16
+ 'fixture-startup',
17
+ );
18
+ },
19
+ },
20
+ ],
21
+ });
22
+
23
+ expect(startup.ownedContext).toBe(true);
24
+ await startup.dispose();
25
+ expect(disposed).toBe(true);
26
+ });
27
+ });
package/src/backend.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  modulesFromCatalog,
21
21
  validateHostAssembly,
22
22
  } from './protocol.js';
23
+ import { createHostStartup, type HostStartup } from './startup.js';
23
24
  import {
24
25
  createHostTransport,
25
26
  HOST_ACTIVATION_REPORT_SERVICE,
@@ -157,7 +158,6 @@ function assertBackendCatalogIdentity(
157
158
  */
158
159
  export async function createBackendHost(options: BackendHostOptions = {}): Promise<BackendHost> {
159
160
  const context = options.context ?? new Context();
160
- const ownedContext = options.context === undefined;
161
161
  const realm = options.realm ?? 'engine';
162
162
  const catalog = options.catalog;
163
163
  const pairedBackendEntries =
@@ -188,7 +188,7 @@ export async function createBackendHost(options: BackendHostOptions = {}): Promi
188
188
  });
189
189
  const checkedInitial = validateHostAssembly(initialAssembly);
190
190
  if (!checkedInitial.ok) {
191
- if (ownedContext) await context.fiber.dispose();
191
+ if (options.context === undefined) await context.fiber.dispose();
192
192
  throw checkedInitial.error;
193
193
  }
194
194
  const backendModules =
@@ -205,21 +205,24 @@ export async function createBackendHost(options: BackendHostOptions = {}): Promi
205
205
  // still match the current authority exactly.
206
206
  const bootstrapRevision = checkedInitial.value.revision;
207
207
  const transport = options.transport ?? createHostTransport();
208
- let foundationFiber: Fiber | undefined;
208
+ let startup: HostStartup | undefined;
209
209
  let loaderFiber: Fiber | undefined;
210
210
  let loader: CatalogLoader | undefined;
211
211
  let backendEntries =
212
212
  options.pairs === undefined
213
213
  ? (options.entries ?? options.assembly?.entries ?? [])
214
214
  : pairedBackendEntries;
215
- const startupFibers: Fiber[] = [];
216
215
  let unregisterAssemblyService: (() => void) | undefined;
217
216
  let unregisterActivationService: (() => void) | undefined;
218
217
  let unsubscribeAssembly: (() => void) | undefined;
219
218
  try {
220
- foundationFiber = await context.plugin(hostFoundationPlugin(assembly, transport));
221
- for (const plugin of options.startupPlugins ?? [])
222
- startupFibers.push(await context.plugin(plugin));
219
+ startup = await createHostStartup({
220
+ context,
221
+ startupPlugins: [
222
+ hostFoundationPlugin(assembly, transport),
223
+ ...(options.startupPlugins ?? []),
224
+ ],
225
+ });
223
226
  if (catalog !== undefined) {
224
227
  const bootstrapped = await bootstrapCatalogLoader(context, catalog, realm, {
225
228
  catalogDigest: checkedInitial.value.revision,
@@ -261,9 +264,7 @@ export async function createBackendHost(options: BackendHostOptions = {}): Promi
261
264
  unregisterAssemblyService?.();
262
265
  unsubscribeAssembly?.();
263
266
  await loaderFiber?.dispose();
264
- for (const fiber of startupFibers.reverse()) await fiber.dispose();
265
- await foundationFiber?.dispose();
266
- if (ownedContext) await context.fiber.dispose();
267
+ await startup?.dispose();
267
268
  throw error;
268
269
  }
269
270
 
@@ -273,7 +274,7 @@ export async function createBackendHost(options: BackendHostOptions = {}): Promi
273
274
  ...(loader === undefined ? {} : { loader }),
274
275
  assembly,
275
276
  transport,
276
- ownedContext,
277
+ ownedContext: options.context === undefined,
277
278
  async update(nextInput) {
278
279
  if (disposed) {
279
280
  throw new HostAssemblyError(
@@ -330,9 +331,7 @@ export async function createBackendHost(options: BackendHostOptions = {}): Promi
330
331
  unregisterAssemblyService?.();
331
332
  transport.close();
332
333
  await loaderFiber?.dispose();
333
- for (const fiber of startupFibers.reverse()) await fiber.dispose();
334
- await foundationFiber?.dispose();
335
- if (ownedContext) await context.fiber.dispose();
334
+ await startup?.dispose();
336
335
  },
337
336
  };
338
337
  return host;
package/src/frontend.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  modulesFromCatalog,
20
20
  validateHostAssembly,
21
21
  } from './protocol.js';
22
+ import { createHostStartup, type HostStartup } from './startup.js';
22
23
  import {
23
24
  HOST_ACTIVATION_REPORT_SERVICE,
24
25
  HOST_ASSEMBLY_SERVICE,
@@ -306,8 +307,7 @@ export async function createFrontendHost(options: FrontendHostOptions = {}): Pro
306
307
  return status;
307
308
  },
308
309
  };
309
- const startupFibers: Fiber[] = [];
310
- let foundationFiber: Fiber | undefined;
310
+ let startup: HostStartup | undefined;
311
311
  let loaderFiber: Fiber | undefined;
312
312
  let loader: CatalogLoader | undefined;
313
313
  let disposed = false;
@@ -356,9 +356,13 @@ export async function createFrontendHost(options: FrontendHostOptions = {}): Pro
356
356
  }
357
357
 
358
358
  try {
359
- foundationFiber = await context.plugin(hostFoundationPlugin(state, options.transport));
360
- for (const plugin of options.startupPlugins ?? [])
361
- startupFibers.push(await context.plugin(plugin));
359
+ startup = await createHostStartup({
360
+ context,
361
+ startupPlugins: [
362
+ hostFoundationPlugin(state, options.transport),
363
+ ...(options.startupPlugins ?? []),
364
+ ],
365
+ });
362
366
  if (options.autoActivate !== false) {
363
367
  // Activation is performed below through the returned host so its status
364
368
  // callback and failure rollback cover the first assembly as well.
@@ -393,9 +397,7 @@ export async function createFrontendHost(options: FrontendHostOptions = {}): Pro
393
397
  } catch (error) {
394
398
  removeTransportDisconnect?.();
395
399
  await loaderFiber?.dispose();
396
- for (const fiber of startupFibers.reverse()) await fiber.dispose();
397
- await foundationFiber?.dispose();
398
- if (ownedContext) await context.fiber.dispose();
400
+ await startup?.dispose();
399
401
  throw error;
400
402
  }
401
403
 
@@ -434,9 +436,7 @@ export async function createFrontendHost(options: FrontendHostOptions = {}): Pro
434
436
  disposed = true;
435
437
  removeTransportDisconnect?.();
436
438
  await loaderFiber?.dispose();
437
- for (const fiber of startupFibers.reverse()) await fiber.dispose();
438
- await foundationFiber?.dispose();
439
- if (ownedContext) await context.fiber.dispose();
439
+ await startup?.dispose();
440
440
  status = { state: 'disposed', revision: current.revision };
441
441
  },
442
442
  };
package/src/index.ts CHANGED
@@ -36,6 +36,7 @@ export {
36
36
  modulesFromCatalog,
37
37
  validateHostAssembly,
38
38
  } from './protocol.js';
39
+ export { createHostStartup, type HostStartup, type HostStartupOptions } from './startup.js';
39
40
  export {
40
41
  attachHostWebSocketServer,
41
42
  connectHostWebSocket,
package/src/startup.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { Context, type Fiber, type Plugin } from '@forgeax/engine-plugin';
2
+
3
+ /**
4
+ * The product-neutral host bootstrap. It owns only a Cordis Context and the
5
+ * supplied startup entries; assembly, transport, Project, and App are all
6
+ * ordinary plugins layered above this seam.
7
+ */
8
+ export interface HostStartupOptions {
9
+ readonly context?: Context;
10
+ readonly startupPlugins?: readonly Plugin[];
11
+ }
12
+
13
+ export interface HostStartup {
14
+ readonly context: Context;
15
+ readonly ownedContext: boolean;
16
+ readonly fibers: readonly Fiber[];
17
+ dispose(): Promise<void>;
18
+ }
19
+
20
+ export async function createHostStartup(options: HostStartupOptions = {}): Promise<HostStartup> {
21
+ const context = options.context ?? new Context();
22
+ const ownedContext = options.context === undefined;
23
+ const fibers: Fiber[] = [];
24
+ let disposed = false;
25
+ try {
26
+ for (const plugin of options.startupPlugins ?? []) fibers.push(await context.plugin(plugin));
27
+ } catch (error) {
28
+ for (const fiber of fibers.reverse()) await fiber.dispose();
29
+ if (ownedContext) await context.fiber.dispose();
30
+ throw error;
31
+ }
32
+ return {
33
+ context,
34
+ ownedContext,
35
+ fibers,
36
+ async dispose(): Promise<void> {
37
+ if (disposed) return;
38
+ disposed = true;
39
+ for (const fiber of [...fibers].reverse()) await fiber.dispose();
40
+ if (ownedContext) await context.fiber.dispose();
41
+ },
42
+ };
43
+ }