@zhin.js/runtime 1.0.9 → 1.0.11

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.
@@ -37,7 +37,7 @@ export class ConfigComposer {
37
37
  await composeNode(child, ownSchemas),
38
38
  ]));
39
39
  // Host-level keys (`http`, `database`, `ai`, `mcp`, `a2a`, `speech`,
40
- // `htmlRenderer`, `assistant`, `collaboration`, `log_level`) are consumed by CLI Root installers /
40
+ // `htmlRenderer`, `assistant`, `log_level`) are consumed by CLI Root installers /
41
41
  // start-command, not Plugin ConfigViews.
42
42
  const effectiveSchema = Object.freeze({
43
43
  type: 'object',
@@ -82,10 +82,6 @@ export class ConfigComposer {
82
82
  type: 'object',
83
83
  additionalProperties: true,
84
84
  }),
85
- collaboration: Object.freeze({
86
- type: 'object',
87
- additionalProperties: true,
88
- }),
89
85
  log_level: Object.freeze({
90
86
  type: ['string', 'number'],
91
87
  }),
@@ -10,6 +10,6 @@ export interface ProjectedFeatures {
10
10
  export declare class FeatureProjector {
11
11
  private readonly providers;
12
12
  constructor(providers: Iterable<FeatureProvider>);
13
- project(generation: number, base: ProjectionState, retained?: ReadonlyMap<FeatureId, unknown>): Promise<ProjectedFeatures>;
13
+ project(generation: number, base: ProjectionState, signal: AbortSignal, retained?: ReadonlyMap<FeatureId, unknown>): Promise<ProjectedFeatures>;
14
14
  }
15
15
  export declare function composeGenerationHandoffs(...handoffs: readonly (GenerationHandoff | undefined)[]): GenerationHandoff | undefined;
@@ -5,7 +5,7 @@ export class FeatureProjector {
5
5
  constructor(providers) {
6
6
  this.providers = providers;
7
7
  }
8
- async project(generation, base, retained = new Map()) {
8
+ async project(generation, base, signal, retained = new Map()) {
9
9
  // Slot HMR seeds this map with the committed projections. Only providers
10
10
  // passed to this projector replace their entry; every other Feature keeps
11
11
  // its live instance and therefore keeps its external resources running.
@@ -15,10 +15,13 @@ export class FeatureProjector {
15
15
  const state = { ...base, projections };
16
16
  try {
17
17
  for (const provider of this.providers) {
18
+ signal.throwIfAborted();
18
19
  const slots = [...base.capabilities.values()].filter((slot) => slot.feature === provider.id);
19
20
  const projection = await provider.runtime.project(slots, {
20
21
  snapshot: createSnapshotView(generation, state),
22
+ signal,
21
23
  });
24
+ signal.throwIfAborted();
22
25
  projections.set(provider.id, projection.value);
23
26
  if (projection.dispose)
24
27
  disposers.set(provider.id, projection.dispose);
@@ -24,5 +24,5 @@ export interface PreparedIsolatedPlugin {
24
24
  }
25
25
  /** Adapter seam for child Plugin lifecycle that must not execute in the Host realm. */
26
26
  export interface IsolatedPluginRuntimePort {
27
- prepare(request: IsolatedPluginPrepareRequest): Promise<PreparedIsolatedPlugin>;
27
+ prepare(request: IsolatedPluginPrepareRequest, signal: AbortSignal): Promise<PreparedIsolatedPlugin>;
28
28
  }
@@ -8,7 +8,7 @@ const watchedExtensions = new Set([
8
8
  '.cjs', '.js', '.json', '.md', '.mjs', '.ts', '.tsx', '.yaml', '.yml',
9
9
  ]);
10
10
  const capabilityRoots = new Set([
11
- 'adapters', 'agents', 'commands', 'components', 'mcp', 'middlewares', 'pages', 'skills', 'tools',
11
+ 'adapters', 'agents', 'commands', 'components', 'handlers', 'mcp', 'middlewares', 'pages', 'skills', 'tools',
12
12
  ]);
13
13
  /**
14
14
  * Uses Node's native ESM/TypeScript loader and adds only cache busting and watch.
@@ -1,7 +1,7 @@
1
1
  import type { ChildPluginReference, PackageReference } from './manifest.js';
2
2
  /**
3
3
  * Package whose `zhin.features` define the Stable Feature composition
4
- * (adapter / command / component / middleware).
4
+ * (adapter / command / component / middleware / handler).
5
5
  *
6
6
  * Root inherits these when it depends on `@zhin.js/core` directly, or on the
7
7
  * `zhin.js` facade (which depends on `@zhin.js/core`).
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Package whose `zhin.features` define the Stable Feature composition
3
- * (adapter / command / component / middleware).
3
+ * (adapter / command / component / middleware / handler).
4
4
  *
5
5
  * Root inherits these when it depends on `@zhin.js/core` directly, or on the
6
6
  * `zhin.js` facade (which depends on `@zhin.js/core`).
@@ -1,4 +1,4 @@
1
- import { DisposeStack, Scope, type Dispose, type FeatureId, type GenerationHandoff, type GenerationHandoffRegistry, type PluginId, type PluginNodeSnapshot, type SetupCapabilityRegistration, type TokenId } from '@zhin.js/plugin-runtime';
1
+ import { DisposeStack, Scope, type Dispose, type FeatureId, type GenerationHandoff, type GenerationAdmissionGate, type GenerationHandoffRegistry, type PluginId, type PluginNodeSnapshot, type SetupCapabilityRegistration, type TokenId } from '@zhin.js/plugin-runtime';
2
2
  import { type RuntimeEnvironment } from './environment.js';
3
3
  import { type EnvironmentLayers } from './environment-store.js';
4
4
  import type { ModuleRuntime } from './module-runtime.js';
@@ -8,9 +8,12 @@ import { type PrimaryConfig } from './primary-config.js';
8
8
  import type { RuntimeConfigDocument } from './config-composer.js';
9
9
  export type PluginConfigResolver = (node: PluginGraphNode) => unknown;
10
10
  export interface RootResourceContext {
11
+ readonly signal: AbortSignal;
11
12
  readonly resources: Scope;
12
13
  readonly lifecycle: DisposeStack;
13
14
  readonly handoff: GenerationHandoffRegistry;
15
+ /** Candidate-wide gate atomically switched by SnapshotStore at commit. */
16
+ readonly admission: GenerationAdmissionGate;
14
17
  readonly config: PrimaryConfig;
15
18
  /**
16
19
  * Adds a root-owned capability to this shadow generation. The definition is
@@ -42,7 +45,7 @@ export declare class PluginScopeAssembler {
42
45
  constructor(modules: ModuleRuntime, configResolver: PluginConfigResolver, environment: RuntimeEnvironment, primaryConfigDocument: RuntimeConfigDocument, installResources?: RootResourceInstaller | undefined, environmentLayers?: EnvironmentLayers, seed?: PluginAssemblySeed, isolation?: IsolatedPluginRuntimePort | undefined);
43
46
  removeSubtrees(roots: readonly PluginId[]): void;
44
47
  installSetupFeatureAliases(aliases: ReadonlyMap<string, FeatureId>): void;
45
- setupTree(node: PluginGraphNode): Promise<void>;
48
+ setupTree(node: PluginGraphNode, signal: AbortSignal): Promise<void>;
46
49
  synchronizeTree(node: PluginGraphNode): void;
47
50
  createdScopeDisposers(): readonly (readonly [PluginId, Dispose])[];
48
51
  generationHandoff(): GenerationHandoff | undefined;
@@ -1,5 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
- import { GenerationHandoffStack, Scope, capabilityId, createPluginDatabaseHost, createPluginScheduleHost, databaseHostToken, databaseRootHostToken, featureId, rootPluginId, scheduleHostToken, scheduleRootHostToken, unwrapPluginDatabaseHost, unwrapPluginScheduleHost, } from '@zhin.js/plugin-runtime';
2
+ import { GenerationHandoffStack, Scope, capabilityId, createGenerationAdmissionGate, createToken, createPluginDatabaseHost, createPluginScheduleHost, databaseHostToken, databaseRootHostToken, featureId, rootPluginId, scheduleHostToken, scheduleRootHostToken, unwrapPluginDatabaseHost, unwrapPluginScheduleHost, } from '@zhin.js/plugin-runtime';
3
3
  import { runtimeEnvironmentToken } from './environment.js';
4
4
  import { EnvStoreFactory, envStoreToken, } from './environment-store.js';
5
5
  import { createPrimaryConfig, primaryConfigToken, } from './primary-config.js';
@@ -18,6 +18,7 @@ export class PluginScopeAssembler {
18
18
  #setupCapabilities = new Map();
19
19
  #created = [];
20
20
  #handoffs = new GenerationHandoffStack();
21
+ #admission = createGenerationAdmissionGate();
21
22
  #envStores;
22
23
  #setupFeatureAliases = new Map();
23
24
  constructor(modules, configResolver, environment, primaryConfigDocument, installResources, environmentLayers = {}, seed, isolation) {
@@ -48,7 +49,8 @@ export class PluginScopeAssembler {
48
49
  installSetupFeatureAliases(aliases) {
49
50
  this.#setupFeatureAliases = new Map(aliases);
50
51
  }
51
- async setupTree(node) {
52
+ async setupTree(node, signal) {
53
+ signal.throwIfAborted();
52
54
  const manifest = node.package.packageJson.zhin;
53
55
  const parentScope = node.parent ? this.scopes.get(node.parent) : undefined;
54
56
  if (node.parent && !parentScope)
@@ -62,12 +64,15 @@ export class PluginScopeAssembler {
62
64
  const register = (feature, localName, capabilityDefinition) => this.#registerSetupCapability(node, manifest, feature, localName, capabilityDefinition);
63
65
  if (!node.parent) {
64
66
  scope.provide(runtimeEnvironmentToken, this.environment);
67
+ scope.provide(rootGenerationAdmissionToken, this.#admission);
65
68
  const config = createPrimaryConfig(this.primaryConfigDocument, environment);
66
69
  scope.provide(primaryConfigToken, config);
67
70
  await this.installResources?.({
71
+ signal,
68
72
  resources: scope,
69
73
  lifecycle: scope.disposers,
70
74
  handoff: this.#handoffs,
75
+ admission: this.#admission,
71
76
  config,
72
77
  addFeature: register,
73
78
  });
@@ -100,7 +105,8 @@ export class PluginScopeAssembler {
100
105
  entry: resolve(node.package.root, manifest.entry),
101
106
  config,
102
107
  environment: this.environment,
103
- });
108
+ }, signal);
109
+ signal.throwIfAborted();
104
110
  // Ownership transfers to the shadow Scope immediately. Every later
105
111
  // validation or binding failure is then covered by normal rollback.
106
112
  scope.disposers.add(prepared.dispose);
@@ -116,6 +122,7 @@ export class PluginScopeAssembler {
116
122
  }
117
123
  else {
118
124
  const module = await this.modules.load(resolve(node.package.root, manifest.entry));
125
+ signal.throwIfAborted();
119
126
  const definition = module.default;
120
127
  if (!definition || typeof definition.name !== 'string') {
121
128
  throw new TypeError(`${node.package.name} does not default-export a Plugin definition`);
@@ -126,6 +133,7 @@ export class PluginScopeAssembler {
126
133
  }
127
134
  }
128
135
  const setupContext = {
136
+ signal,
129
137
  plugin,
130
138
  config: view,
131
139
  resources: scope,
@@ -137,6 +145,7 @@ export class PluginScopeAssembler {
137
145
  setupContext[method] = (name, value) => register(feature, name, value);
138
146
  }
139
147
  const returned = await definition.setup?.(Object.freeze(setupContext));
148
+ signal.throwIfAborted();
140
149
  if (returned)
141
150
  scope.disposers.add(returned);
142
151
  metadata = definition.metadata;
@@ -154,7 +163,7 @@ export class PluginScopeAssembler {
154
163
  this.config.set(node.id, config);
155
164
  this.resources.set(node.id, scope.snapshot());
156
165
  for (const child of node.children)
157
- await this.setupTree(child);
166
+ await this.setupTree(child, signal);
158
167
  }
159
168
  #registerSetupCapability(node, manifest, feature, localName, definition) {
160
169
  const featureName = featureId(String(feature));
@@ -224,6 +233,7 @@ export class PluginScopeAssembler {
224
233
  }
225
234
  }
226
235
  }
236
+ const rootGenerationAdmissionToken = createToken('zhin.runtime.root-generation-admission');
227
237
  function isWithin(plugin, root) {
228
238
  return plugin === root || plugin.startsWith(`${root}/`);
229
239
  }
@@ -1,4 +1,4 @@
1
- import { RootController, type ControlErrorHandler, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
1
+ import { type ControlErrorHandler, type GenerationCommitListener, type PluginId, type RuntimeSnapshot, type SnapshotReader } from '@zhin.js/plugin-runtime';
2
2
  import { type RuntimeConfigDocument } from './config-composer.js';
3
3
  import { type ConfigDocumentPort } from './config-document.js';
4
4
  import { type ConfigPatch } from './config-patch-planner.js';
@@ -24,9 +24,10 @@ export interface RootRuntimeOptions {
24
24
  export type RootHmrOptions = Omit<HmrCoordinatorOptions, 'modules' | 'ownership' | 'runtime'>;
25
25
  export declare class RootRuntime {
26
26
  #private;
27
- readonly controller: RootController;
28
27
  constructor(options: RootRuntimeOptions);
29
28
  get snapshot(): RuntimeSnapshot;
29
+ get snapshots(): SnapshotReader;
30
+ onGenerationCommit(listener: GenerationCommitListener): () => void;
30
31
  get sourceOwnership(): SourceOwnershipIndex;
31
32
  start(): Promise<RuntimeSnapshot>;
32
33
  reload(target?: PluginId | string): Promise<RuntimeSnapshot>;
@@ -23,7 +23,6 @@ import { SubtreeGenerationPreparer, SubtreeTopologyChangedError, } from './subtr
23
23
  import { TopologyGenerationPreparer } from './topology-generation-preparer.js';
24
24
  import { RestartBoundaryPlanner } from './restart-boundary.js';
25
25
  export class RootRuntime {
26
- controller;
27
26
  #projectRoot;
28
27
  #modules;
29
28
  #environment;
@@ -37,6 +36,8 @@ export class RootRuntime {
37
36
  #ownership = SourceOwnershipIndex.empty();
38
37
  #model;
39
38
  #configPatchTail = Promise.resolve();
39
+ #stopResult;
40
+ #controller;
40
41
  constructor(options) {
41
42
  this.#projectRoot = resolve(options.projectRoot);
42
43
  this.#modules = options.modules;
@@ -50,10 +51,16 @@ export class RootRuntime {
50
51
  this.#configDocument = structuredClone(options.config ?? {});
51
52
  this.#installResources = options.installResources;
52
53
  this.#isolation = options.isolation;
53
- this.controller = new RootController(emptyState(), options.onControlError);
54
+ this.#controller = new RootController(emptyState(), options.onControlError);
54
55
  }
55
56
  get snapshot() {
56
- return this.controller.snapshots.current;
57
+ return this.#controller.snapshot;
58
+ }
59
+ get snapshots() {
60
+ return this.#controller.snapshots;
61
+ }
62
+ onGenerationCommit(listener) {
63
+ return this.#controller.onGenerationCommit(listener);
57
64
  }
58
65
  get sourceOwnership() {
59
66
  return this.#ownership;
@@ -65,8 +72,8 @@ export class RootRuntime {
65
72
  this.#configDocument = structuredClone(snapshot.document);
66
73
  }
67
74
  let prepared;
68
- const snapshot = await this.controller.start(async (current) => {
69
- prepared = await this.#prepare(current);
75
+ const snapshot = await this.#controller.start(async (current, signal) => {
76
+ prepared = await this.#prepare(current, signal);
70
77
  return prepared.generation;
71
78
  });
72
79
  this.#accept(requirePrepared(prepared));
@@ -74,8 +81,8 @@ export class RootRuntime {
74
81
  }
75
82
  async reload(target = rootPluginId()) {
76
83
  let prepared;
77
- const snapshot = await this.controller.reload(target, async (current) => {
78
- prepared = await this.#prepare(current);
84
+ const snapshot = await this.#controller.reload(target, async (current, signal) => {
85
+ prepared = await this.#prepare(current, signal);
79
86
  return prepared.generation;
80
87
  });
81
88
  this.#accept(requirePrepared(prepared));
@@ -103,18 +110,24 @@ export class RootRuntime {
103
110
  createProcessRestartExecutor(adapter) {
104
111
  return new RootProcessRestartExecutor(this, adapter);
105
112
  }
106
- async stop() {
107
- try {
108
- await this.controller.stop();
109
- }
110
- finally {
111
- await this.#modules.close();
112
- }
113
+ stop() {
114
+ if (this.#stopResult)
115
+ return this.#stopResult;
116
+ const result = (async () => {
117
+ try {
118
+ await this.#controller.stop();
119
+ }
120
+ finally {
121
+ await this.#modules.close();
122
+ }
123
+ })();
124
+ this.#stopResult = result;
125
+ return result;
113
126
  }
114
127
  async #reloadPlan(plan) {
115
128
  let prepared;
116
129
  let restart;
117
- const snapshot = await this.controller.reload(plan.subtrees[0] ?? plan.slots[0] ?? rootPluginId(), async (current) => {
130
+ const snapshot = await this.#controller.reload(plan.subtrees[0] ?? plan.slots[0] ?? rootPluginId(), async (current, signal) => {
118
131
  const resolved = await this.#resolveCapabilityDelta(current, plan);
119
132
  const effective = resolved.plan;
120
133
  if (this.#model && effective.manifestSources.length > 0) {
@@ -123,19 +136,19 @@ export class RootRuntime {
123
136
  if (restart)
124
137
  return undefined;
125
138
  prepared = effective.subtrees.includes(rootPluginId())
126
- ? await this.#prepareInspected(current, inspected)
127
- : await new TopologyGenerationPreparer(this.#modules, this.#model, inspected.graph, inspected.configResolver, inspected.primaryConfigDocument, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation).prepare(current, { ...effective, capabilities: resolved.capabilities });
139
+ ? await this.#prepareInspected(current, inspected, signal)
140
+ : await new TopologyGenerationPreparer(this.#modules, this.#model, inspected.graph, inspected.configResolver, inspected.primaryConfigDocument, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation).prepare(current, signal, { ...effective, capabilities: resolved.capabilities });
128
141
  }
129
142
  else if (effective.subtrees.length === 0 && resolved.capabilities.size > 0 && this.#model) {
130
143
  prepared = await new SlotGenerationPreparer(this.#modules, this.#model)
131
- .prepare(current, resolved.capabilities);
144
+ .prepare(current, resolved.capabilities, signal);
132
145
  }
133
146
  else if (this.#model && this.#canPrepareSubtrees(effective)) {
134
147
  const inspected = await this.#inspectProject();
135
- prepared = await this.#prepareSubtrees(current, inspected, effective.subtrees);
148
+ prepared = await this.#prepareSubtrees(current, inspected, effective.subtrees, signal);
136
149
  }
137
150
  else {
138
- prepared = await this.#prepare(current);
151
+ prepared = await this.#prepare(current, signal);
139
152
  }
140
153
  return prepared?.generation;
141
154
  });
@@ -176,8 +189,11 @@ export class RootRuntime {
176
189
  return records.length > 0 && records.every((record) => record.role === 'plugin' || record.role === 'schema');
177
190
  });
178
191
  }
179
- async #prepare(current) {
180
- return this.#prepareInspected(current, await this.#inspectProject());
192
+ async #prepare(current, signal) {
193
+ signal.throwIfAborted();
194
+ const inspected = await this.#inspectProject();
195
+ signal.throwIfAborted();
196
+ return this.#prepareInspected(current, inspected, signal);
181
197
  }
182
198
  async #inspectProject() {
183
199
  const resolver = await NodePackageResolver.create(this.#projectRoot);
@@ -234,7 +250,8 @@ export class RootRuntime {
234
250
  let prepared;
235
251
  let documentTransaction;
236
252
  let committedDocument;
237
- const snapshot = await this.controller.reload(rootPluginId(), async (current) => {
253
+ const snapshot = await this.#controller.reload(rootPluginId(), async (current, signal) => {
254
+ signal.throwIfAborted();
238
255
  const resolver = await NodePackageResolver.create(this.#projectRoot);
239
256
  const graph = await new ProjectGraphService(resolver).inspect(this.#projectRoot);
240
257
  const planned = await new ConfigPatchPlanner().plan(graph, currentDocument, patches);
@@ -269,10 +286,10 @@ export class RootRuntime {
269
286
  // when present so a committed patch cannot leave Host services on the
270
287
  // previous generation's document.
271
288
  if (!this.#installResources && this.#model && !planned.roots.includes(rootPluginId())) {
272
- prepared = await this.#prepareSubtrees(current, inspected, planned.roots);
289
+ prepared = await this.#prepareSubtrees(current, inspected, planned.roots, signal);
273
290
  }
274
291
  else {
275
- prepared = await this.#prepareInspected(current, inspected);
292
+ prepared = await this.#prepareInspected(current, inspected, signal);
276
293
  }
277
294
  return documentTransaction
278
295
  ? withConfigDocumentHandoff(prepared.generation, documentTransaction, (committed) => { committedDocument = committed; })
@@ -295,20 +312,20 @@ export class RootRuntime {
295
312
  this.#configSnapshot = committedDocument;
296
313
  return snapshot;
297
314
  }
298
- #prepareInspected(current, inspected) {
315
+ #prepareInspected(current, inspected, signal) {
299
316
  const assembler = new GenerationAssembler(inspected.graph, this.#modules, inspected.configResolver, inspected.primaryConfigDocument, current.generation + 1, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation);
300
- return assembler.prepare();
317
+ return assembler.prepare(signal);
301
318
  }
302
- async #prepareSubtrees(current, inspected, roots) {
319
+ async #prepareSubtrees(current, inspected, roots, signal) {
303
320
  if (!this.#model)
304
- return this.#prepareInspected(current, inspected);
321
+ return this.#prepareInspected(current, inspected, signal);
305
322
  try {
306
- return await new SubtreeGenerationPreparer(this.#modules, this.#model, inspected.graph, inspected.configResolver, inspected.primaryConfigDocument, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation).prepare(current, roots);
323
+ return await new SubtreeGenerationPreparer(this.#modules, this.#model, inspected.graph, inspected.configResolver, inspected.primaryConfigDocument, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation).prepare(current, roots, signal);
307
324
  }
308
325
  catch (error) {
309
326
  if (!(error instanceof SubtreeTopologyChangedError))
310
327
  throw error;
311
- return this.#prepareInspected(current, inspected);
328
+ return this.#prepareInspected(current, inspected, signal);
312
329
  }
313
330
  }
314
331
  }
@@ -326,8 +343,10 @@ function withConfigDocumentHandoff(generation, document, committed) {
326
343
  // File commit follows Resource activation, so reverse compensation restores
327
344
  // the document before it deactivates the shadow generation.
328
345
  handoffs.add({
329
- async activateNext() {
346
+ async activateNext(signal) {
347
+ signal.throwIfAborted();
330
348
  committed(await document.commit());
349
+ signal.throwIfAborted();
331
350
  },
332
351
  deactivateNext: () => document.rollback(),
333
352
  });
@@ -389,16 +408,17 @@ class GenerationAssembler {
389
408
  this.#host = new NodeDiscoveryHost(modules);
390
409
  this.#plugins = new PluginScopeAssembler(modules, configResolver, environment, primaryConfigDocument, installResources, environmentLayers, undefined, isolation);
391
410
  }
392
- async prepare() {
411
+ async prepare(signal) {
412
+ signal.throwIfAborted();
393
413
  try {
394
414
  // Prepare is deliberately ordered: providers define discovery, setup
395
415
  // creates owner scopes, then definitions can be projected against both.
396
- await this.#loadProviders(this.graph.root);
416
+ await this.#loadProviders(this.graph.root, signal);
397
417
  this.#plugins.installSetupFeatureAliases(featureSetupAliases(this.#catalog.values()));
398
- await this.#plugins.setupTree(this.graph.root);
399
- await this.#discover();
418
+ await this.#plugins.setupTree(this.graph.root, signal);
419
+ await this.#discover(signal);
400
420
  const projected = await new FeatureProjector(this.#catalog.values())
401
- .project(this.generation, this.#projectionState());
421
+ .project(this.generation, this.#projectionState(), signal);
402
422
  for (const [feature, dispose] of projected.disposers) {
403
423
  this.#projectionDisposers.set(feature, dispose);
404
424
  }
@@ -431,10 +451,12 @@ class GenerationAssembler {
431
451
  throw error;
432
452
  }
433
453
  }
434
- async #loadProviders(node) {
454
+ async #loadProviders(node, signal) {
455
+ signal.throwIfAborted();
435
456
  for (const requirement of node.features) {
436
457
  const manifest = requirement.package.packageJson.zhin;
437
458
  const module = await this.modules.load(resolve(requirement.package.root, manifest.entry));
459
+ signal.throwIfAborted();
438
460
  const provider = module.default;
439
461
  if (!provider || provider.protocol !== 1) {
440
462
  throw new TypeError(`${requirement.package.name} does not default-export a Feature provider`);
@@ -451,14 +473,16 @@ class GenerationAssembler {
451
473
  this.#rootsByFeature.set(provider.id, roots);
452
474
  }
453
475
  for (const child of node.children)
454
- await this.#loadProviders(child);
476
+ await this.#loadProviders(child, signal);
455
477
  }
456
- async #discover() {
478
+ async #discover(signal) {
457
479
  mergeSetupCapabilities(this.#capabilities, this.#plugins.setupCapabilities(), new Map(this.#catalog.values().map((provider) => [provider.id, provider])), this.#rootsByFeature);
458
480
  const discovery = new FeatureDiscovery(this.#host);
459
481
  for (const provider of this.#catalog.values()) {
482
+ signal.throwIfAborted();
460
483
  const roots = this.#rootsByFeature.get(provider.id) ?? [];
461
484
  const slots = await discovery.discover(provider, roots);
485
+ signal.throwIfAborted();
462
486
  for (const slot of slots)
463
487
  addCapabilitySlot(this.#capabilities, slot);
464
488
  }
@@ -6,5 +6,5 @@ export declare class SlotGenerationPreparer {
6
6
  private readonly modules;
7
7
  private readonly model;
8
8
  constructor(modules: ModuleRuntime, model: RuntimeGenerationModel);
9
- prepare(current: RuntimeSnapshot, selected: readonly CapabilityId[] | CapabilityDelta): Promise<PreparedRuntimeGeneration>;
9
+ prepare(current: RuntimeSnapshot, selected: readonly CapabilityId[] | CapabilityDelta, signal: AbortSignal): Promise<PreparedRuntimeGeneration>;
10
10
  }
@@ -11,7 +11,8 @@ export class SlotGenerationPreparer {
11
11
  this.modules = modules;
12
12
  this.model = model;
13
13
  }
14
- async prepare(current, selected) {
14
+ async prepare(current, selected, signal) {
15
+ signal.throwIfAborted();
15
16
  const selectedByFeature = Array.isArray(selected)
16
17
  ? capabilityDeltaFromSlots(current, selected)
17
18
  : selected;
@@ -24,6 +25,7 @@ export class SlotGenerationPreparer {
24
25
  for (const id of ids)
25
26
  capabilities.delete(id);
26
27
  const replacements = await discovery.discover(provider, this.model.rootsByFeature.get(feature) ?? [], { capabilities: ids });
28
+ signal.throwIfAborted();
27
29
  for (const slot of replacements)
28
30
  capabilities.set(slot.id, slot);
29
31
  }
@@ -41,7 +43,7 @@ export class SlotGenerationPreparer {
41
43
  config: current.config,
42
44
  resources: current.resources,
43
45
  capabilities,
44
- }, current.projections);
46
+ }, signal, current.projections);
45
47
  try {
46
48
  const snapshot = createSnapshotView(current.generation + 1, projected.state);
47
49
  const ownership = SourceOwnershipIndex.fromGeneration(this.model.graph, snapshot, this.model.featureIdsByPackageRoot);
@@ -22,5 +22,5 @@ export declare class SubtreeGenerationPreparer {
22
22
  private readonly environmentLayers;
23
23
  private readonly isolation?;
24
24
  constructor(modules: ModuleRuntime, model: RuntimeGenerationModel, graph: ProjectGraph, configResolver: PluginConfigResolver, primaryConfigDocument: RuntimeConfigDocument, environment: RuntimeEnvironment, installResources?: RootResourceInstaller | undefined, environmentLayers?: EnvironmentLayers, isolation?: IsolatedPluginRuntimePort | undefined);
25
- prepare(current: RuntimeSnapshot, roots: readonly PluginId[]): Promise<PreparedRuntimeGeneration>;
25
+ prepare(current: RuntimeSnapshot, roots: readonly PluginId[], signal: AbortSignal): Promise<PreparedRuntimeGeneration>;
26
26
  }
@@ -33,7 +33,8 @@ export class SubtreeGenerationPreparer {
33
33
  this.environmentLayers = environmentLayers;
34
34
  this.isolation = isolation;
35
35
  }
36
- async prepare(current, roots) {
36
+ async prepare(current, roots, signal) {
37
+ signal.throwIfAborted();
37
38
  const nodes = indexGraph(this.graph);
38
39
  assertCompatibleTopology(indexGraph(this.model.graph), nodes, roots);
39
40
  const plugins = new PluginScopeAssembler(this.modules, this.configResolver, this.environment, this.primaryConfigDocument, this.installResources, this.environmentLayers, {
@@ -50,7 +51,7 @@ export class SubtreeGenerationPreparer {
50
51
  const node = nodes.get(root);
51
52
  if (!node)
52
53
  throw new SubtreeTopologyChangedError(`Missing subtree root: ${root}`);
53
- await plugins.setupTree(node);
54
+ await plugins.setupTree(node, signal);
54
55
  }
55
56
  const capabilities = new Map(current.capabilities);
56
57
  for (const [id, slot] of capabilities) {
@@ -65,6 +66,7 @@ export class SubtreeGenerationPreparer {
65
66
  if (affectedRoots.length === 0)
66
67
  continue;
67
68
  const slots = await discovery.discover(provider, affectedRoots);
69
+ signal.throwIfAborted();
68
70
  for (const slot of slots)
69
71
  addCapabilitySlot(capabilities, slot);
70
72
  }
@@ -74,7 +76,7 @@ export class SubtreeGenerationPreparer {
74
76
  config: plugins.config,
75
77
  resources: plugins.resources,
76
78
  capabilities,
77
- });
79
+ }, signal);
78
80
  for (const [feature, dispose] of projected.disposers) {
79
81
  projectionDisposers.set(feature, dispose);
80
82
  }
@@ -27,5 +27,5 @@ export declare class TopologyGenerationPreparer {
27
27
  private readonly environmentLayers;
28
28
  private readonly isolation?;
29
29
  constructor(modules: ModuleRuntime, model: RuntimeGenerationModel, graph: ProjectGraph, configResolver: PluginConfigResolver, primaryConfigDocument: RuntimeConfigDocument, environment: RuntimeEnvironment, installResources?: RootResourceInstaller | undefined, environmentLayers?: EnvironmentLayers, isolation?: IsolatedPluginRuntimePort | undefined);
30
- prepare(current: RuntimeSnapshot, delta?: TopologyRuntimeDelta): Promise<PreparedRuntimeGeneration | undefined>;
30
+ prepare(current: RuntimeSnapshot, signal: AbortSignal, delta?: TopologyRuntimeDelta): Promise<PreparedRuntimeGeneration | undefined>;
31
31
  }
@@ -31,7 +31,8 @@ export class TopologyGenerationPreparer {
31
31
  this.environmentLayers = environmentLayers;
32
32
  this.isolation = isolation;
33
33
  }
34
- async prepare(current, delta = { slots: [], subtrees: [] }) {
34
+ async prepare(current, signal, delta = { slots: [], subtrees: [] }) {
35
+ signal.throwIfAborted();
35
36
  const planned = new TopologyTransactionPlanner().plan(this.model.graph, this.graph);
36
37
  const nextNodes = graphNodes(this.graph);
37
38
  const configReplacements = changedConfigRoots(current, nextNodes, this.configResolver);
@@ -69,7 +70,7 @@ export class TopologyGenerationPreparer {
69
70
  const node = nextNodes.get(root);
70
71
  if (!node)
71
72
  throw new Error(`Missing topology setup root: ${root}`);
72
- await plugins.setupTree(node);
73
+ await plugins.setupTree(node, signal);
73
74
  }
74
75
  // Retained parents still need a new immutable children view after add,
75
76
  // remove, move, or reorder operations.
@@ -82,7 +83,7 @@ export class TopologyGenerationPreparer {
82
83
  config: plugins.config,
83
84
  resources: plugins.resources,
84
85
  capabilities,
85
- });
86
+ }, signal);
86
87
  for (const [feature, dispose] of projected.disposers) {
87
88
  projectionDisposers.set(feature, dispose);
88
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/runtime",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Static Plugin graph, generation transaction and HMR Root runtime for Zhin.js",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -18,22 +18,22 @@
18
18
  "dependencies": {
19
19
  "ajv": "8.18.0",
20
20
  "semver": "7.8.5",
21
- "@zhin.js/feature-kit": "1.0.8",
22
- "@zhin.js/plugin-runtime": "1.1.5"
21
+ "@zhin.js/feature-kit": "1.0.10",
22
+ "@zhin.js/plugin-runtime": "1.1.6"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^26.1.2",
26
26
  "typescript": "^6.0.3",
27
- "@zhin.js/adapter": "1.1.7",
28
- "@zhin.js/agent-feature": "1.0.8",
29
- "@zhin.js/command": "1.0.9",
30
- "@zhin.js/component": "1.0.8",
31
- "@zhin.js/layout": "1.0.8",
32
- "@zhin.js/mcp-feature": "1.0.8",
33
- "@zhin.js/middleware": "1.0.8",
34
- "@zhin.js/page": "1.0.8",
35
- "@zhin.js/skill": "1.0.8",
36
- "@zhin.js/tool": "1.0.8"
27
+ "@zhin.js/adapter": "1.1.9",
28
+ "@zhin.js/agent-feature": "1.0.10",
29
+ "@zhin.js/command": "1.0.13",
30
+ "@zhin.js/component": "1.0.10",
31
+ "@zhin.js/layout": "1.0.10",
32
+ "@zhin.js/mcp-feature": "1.0.10",
33
+ "@zhin.js/middleware": "1.0.10",
34
+ "@zhin.js/page": "1.0.10",
35
+ "@zhin.js/skill": "1.0.10",
36
+ "@zhin.js/tool": "1.0.10"
37
37
  },
38
38
  "repository": {
39
39
  "type": "git",