@zhin.js/runtime 1.0.12 → 1.0.13

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.
@@ -20,6 +20,6 @@ export declare class HmrCoordinator {
20
20
  private readonly options;
21
21
  constructor(options: HmrCoordinatorOptions);
22
22
  start(): Dispose;
23
- stop(): void;
23
+ stop(): Promise<void>;
24
24
  enqueue(source: string): Promise<void>;
25
25
  }
@@ -5,12 +5,19 @@ export class HmrCoordinator {
5
5
  #waiters = [];
6
6
  #draining;
7
7
  #unwatch;
8
+ #closing = false;
9
+ #restartRequired = false;
10
+ #stopResult;
8
11
  constructor(options) {
9
12
  this.options = options;
10
13
  }
11
14
  start() {
12
15
  if (this.#unwatch)
13
16
  throw new Error('HmrCoordinator is already started');
17
+ if (this.#closing)
18
+ throw new Error('HmrCoordinator has been stopped');
19
+ if (this.#restartRequired)
20
+ throw new Error('HmrCoordinator requires a process restart');
14
21
  if (!this.options.modules.watch) {
15
22
  throw new Error('ModuleRuntime does not provide a file watcher');
16
23
  }
@@ -21,10 +28,23 @@ export class HmrCoordinator {
21
28
  return () => this.stop();
22
29
  }
23
30
  stop() {
31
+ if (this.#stopResult)
32
+ return this.#stopResult;
33
+ this.#closing = true;
24
34
  this.#unwatch?.();
25
35
  this.#unwatch = undefined;
36
+ this.#stopResult = (async () => {
37
+ await this.#draining;
38
+ })();
39
+ return this.#stopResult;
26
40
  }
27
41
  enqueue(source) {
42
+ if (this.#closing) {
43
+ return Promise.reject(new Error('HMR coordinator is stopping'));
44
+ }
45
+ if (this.#restartRequired) {
46
+ return Promise.reject(new Error('HMR coordinator requires a process restart'));
47
+ }
28
48
  this.#pending.add(source);
29
49
  const completed = new Promise((resolve, reject) => {
30
50
  this.#waiters.push({ resolve, reject });
@@ -50,14 +70,16 @@ export class HmrCoordinator {
50
70
  this.#pending.clear();
51
71
  const forcedRestart = changed.filter((source) => this.options.modules.requiresProcessRestart?.(source));
52
72
  if (forcedRestart.length > 0) {
53
- await this.options.onRestartRequired(Object.freeze({
73
+ this.#restartRequired = true;
74
+ this.#pending.clear();
75
+ this.#notifyRestart(Object.freeze({
54
76
  kind: 'process',
55
77
  changed: Object.freeze(changed),
56
78
  reasons: Object.freeze([
57
79
  `Module loader cannot safely invalidate: ${forcedRestart.join(', ')}`,
58
80
  ]),
59
81
  }));
60
- continue;
82
+ break;
61
83
  }
62
84
  const dependencyPort = this.options.modules.affectedSources
63
85
  ? {
@@ -65,11 +87,14 @@ export class HmrCoordinator {
65
87
  }
66
88
  : undefined;
67
89
  const plan = new InvalidationPlanner(this.options.ownership(), dependencyPort).plan(changed);
68
- await this.options.onPlan?.(plan);
69
90
  if (plan.kind === 'process') {
70
- await this.options.onRestartRequired(plan);
71
- continue;
91
+ this.#restartRequired = true;
92
+ this.#pending.clear();
93
+ this.#notifyPlan(plan);
94
+ this.#notifyRestart(plan);
95
+ break;
72
96
  }
97
+ this.#notifyPlan(plan);
73
98
  if (plan.kind === 'none')
74
99
  continue;
75
100
  const startedAt = performance.now();
@@ -77,15 +102,30 @@ export class HmrCoordinator {
77
102
  await this.options.modules.invalidate?.(source);
78
103
  }
79
104
  const restart = await this.options.runtime.reload(plan);
80
- if (restart)
81
- await this.options.onRestartRequired(restart);
105
+ if (restart) {
106
+ this.#restartRequired = true;
107
+ this.#pending.clear();
108
+ this.#notifyRestart(restart);
109
+ break;
110
+ }
82
111
  else {
83
112
  // reload resolves only after RootController has committed the new
84
113
  // generation. Read ownership now so failed transactions never make
85
114
  // newly discovered workspace packages observable to the watcher.
86
115
  this.#syncWatchRoots();
87
116
  const durationMs = Number((performance.now() - startedAt).toFixed(1));
88
- await this.options.onReload?.(plan, durationMs);
117
+ try {
118
+ await this.options.onReload?.(plan, durationMs);
119
+ }
120
+ catch (error) {
121
+ // A projection/observer cannot change an already committed reload.
122
+ try {
123
+ await this.options.onError(error);
124
+ }
125
+ catch {
126
+ // Diagnostic reporting is deliberately outside the outcome.
127
+ }
128
+ }
89
129
  }
90
130
  }
91
131
  this.#resolveWaiters();
@@ -105,7 +145,7 @@ export class HmrCoordinator {
105
145
  this.#draining = undefined;
106
146
  // A source may arrive after the loop observed an empty queue but before
107
147
  // this promise settled. Keep its waiter attached to a fresh transaction.
108
- if (this.#pending.size > 0)
148
+ if (!this.#closing && !this.#restartRequired && this.#pending.size > 0)
109
149
  this.#ensureDrain();
110
150
  }
111
151
  }
@@ -120,4 +160,37 @@ export class HmrCoordinator {
120
160
  #syncWatchRoots() {
121
161
  this.options.modules.updateWatchRoots?.(this.options.ownership().watchRoots());
122
162
  }
163
+ #notifyRestart(plan) {
164
+ // Restart is a committed control outcome. Invoke the observer without
165
+ // awaiting it so a Process Host may await RootHost.stop() without waiting
166
+ // on the HMR drain that is currently delivering this notification.
167
+ try {
168
+ void Promise.resolve(this.options.onRestartRequired(plan)).catch((error) => {
169
+ this.#reportDiagnostic(error);
170
+ });
171
+ }
172
+ catch (error) {
173
+ this.#reportDiagnostic(error);
174
+ }
175
+ }
176
+ #notifyPlan(plan) {
177
+ if (!this.options.onPlan)
178
+ return;
179
+ try {
180
+ void Promise.resolve(this.options.onPlan(plan)).catch((error) => {
181
+ this.#reportDiagnostic(error);
182
+ });
183
+ }
184
+ catch (error) {
185
+ this.#reportDiagnostic(error);
186
+ }
187
+ }
188
+ #reportDiagnostic(error) {
189
+ try {
190
+ void Promise.resolve(this.options.onError(error)).catch(() => undefined);
191
+ }
192
+ catch {
193
+ // Diagnostic reporting cannot change an invalidation outcome.
194
+ }
195
+ }
123
196
  }
@@ -8,6 +8,8 @@ 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
+ /** Exact shadow generation being assembled; never inferred from a latest snapshot. */
12
+ readonly generation: number;
11
13
  readonly signal: AbortSignal;
12
14
  readonly resources: Scope;
13
15
  readonly lifecycle: DisposeStack;
@@ -36,13 +38,14 @@ export declare class PluginScopeAssembler {
36
38
  private readonly configResolver;
37
39
  private readonly environment;
38
40
  private readonly primaryConfigDocument;
41
+ private readonly generation;
39
42
  private readonly installResources?;
40
43
  private readonly isolation?;
41
44
  readonly scopes: Map<PluginId, Scope>;
42
45
  readonly tree: Map<PluginId, PluginNodeSnapshot>;
43
46
  readonly config: Map<PluginId, unknown>;
44
47
  readonly resources: Map<PluginId, ReadonlyMap<TokenId, unknown>>;
45
- constructor(modules: ModuleRuntime, configResolver: PluginConfigResolver, environment: RuntimeEnvironment, primaryConfigDocument: RuntimeConfigDocument, installResources?: RootResourceInstaller | undefined, environmentLayers?: EnvironmentLayers, seed?: PluginAssemblySeed, isolation?: IsolatedPluginRuntimePort | undefined);
48
+ constructor(modules: ModuleRuntime, configResolver: PluginConfigResolver, environment: RuntimeEnvironment, primaryConfigDocument: RuntimeConfigDocument, generation: number, installResources?: RootResourceInstaller | undefined, environmentLayers?: EnvironmentLayers, seed?: PluginAssemblySeed, isolation?: IsolatedPluginRuntimePort | undefined);
46
49
  removeSubtrees(roots: readonly PluginId[]): void;
47
50
  installSetupFeatureAliases(aliases: ReadonlyMap<string, FeatureId>): void;
48
51
  setupTree(node: PluginGraphNode, signal: AbortSignal): Promise<void>;
@@ -9,6 +9,7 @@ export class PluginScopeAssembler {
9
9
  configResolver;
10
10
  environment;
11
11
  primaryConfigDocument;
12
+ generation;
12
13
  installResources;
13
14
  isolation;
14
15
  scopes;
@@ -21,11 +22,12 @@ export class PluginScopeAssembler {
21
22
  #admission = createGenerationAdmissionGate();
22
23
  #envStores;
23
24
  #setupFeatureAliases = new Map();
24
- constructor(modules, configResolver, environment, primaryConfigDocument, installResources, environmentLayers = {}, seed, isolation) {
25
+ constructor(modules, configResolver, environment, primaryConfigDocument, generation, installResources, environmentLayers = {}, seed, isolation) {
25
26
  this.modules = modules;
26
27
  this.configResolver = configResolver;
27
28
  this.environment = environment;
28
29
  this.primaryConfigDocument = primaryConfigDocument;
30
+ this.generation = generation;
29
31
  this.installResources = installResources;
30
32
  this.isolation = isolation;
31
33
  this.#envStores = new EnvStoreFactory(environment, environmentLayers);
@@ -68,6 +70,7 @@ export class PluginScopeAssembler {
68
70
  const config = createPrimaryConfig(this.primaryConfigDocument, environment);
69
71
  scope.provide(primaryConfigToken, config);
70
72
  await this.installResources?.({
73
+ generation: this.generation,
71
74
  signal,
72
75
  resources: scope,
73
76
  lifecycle: scope.disposers,
@@ -8,6 +8,7 @@ import type { IsolatedPluginRuntimePort } from './isolation.js';
8
8
  import type { ModuleRuntime } from './module-runtime.js';
9
9
  import { type PluginConfigResolver, type RootResourceInstaller } from './plugin-scope-assembler.js';
10
10
  import { HmrCoordinator, type HmrCoordinatorOptions } from './hmr-coordinator.js';
11
+ import { type RuntimeGenerationState } from './runtime-generation.js';
11
12
  import { RootProcessRestartExecutor, type ProcessRestartAdapter } from './process-restart.js';
12
13
  import { SourceOwnershipIndex } from './source-ownership.js';
13
14
  export type { PluginConfigResolver, RootResourceContext, RootResourceInstaller, } from './plugin-scope-assembler.js';
@@ -27,7 +28,7 @@ export declare class RootRuntime {
27
28
  constructor(options: RootRuntimeOptions);
28
29
  get snapshot(): RuntimeSnapshot;
29
30
  get snapshots(): SnapshotReader;
30
- onGenerationCommit(listener: GenerationCommitListener): () => void;
31
+ onGenerationCommit(listener: GenerationCommitListener<RuntimeGenerationState>): () => void;
31
32
  get sourceOwnership(): SourceOwnershipIndex;
32
33
  start(): Promise<RuntimeSnapshot>;
33
34
  reload(target?: PluginId | string): Promise<RuntimeSnapshot>;
@@ -14,6 +14,7 @@ import { NodePackageResolver } from './package-resolver.js';
14
14
  import { PluginScopeAssembler, } from './plugin-scope-assembler.js';
15
15
  import { ProjectGraphService, } from './project-graph.js';
16
16
  import { HmrCoordinator } from './hmr-coordinator.js';
17
+ import { prepareRuntimeGeneration, } from './runtime-generation.js';
17
18
  import { RootProcessRestartExecutor, } from './process-restart.js';
18
19
  import { SlotGenerationPreparer } from './slot-generation-preparer.js';
19
20
  import { capabilityDeltaFromSlots, capabilityDeltaIds, ConventionCapabilityDeltaResolver, filterCapabilityDelta, mergeCapabilityDeltas, } from './convention-capability-delta.js';
@@ -33,8 +34,6 @@ export class RootRuntime {
33
34
  #configDocument;
34
35
  #installResources;
35
36
  #isolation;
36
- #ownership = SourceOwnershipIndex.empty();
37
- #model;
38
37
  #configPatchTail = Promise.resolve();
39
38
  #stopResult;
40
39
  #controller;
@@ -51,7 +50,7 @@ export class RootRuntime {
51
50
  this.#configDocument = structuredClone(options.config ?? {});
52
51
  this.#installResources = options.installResources;
53
52
  this.#isolation = options.isolation;
54
- this.#controller = new RootController(emptyState(), options.onControlError);
53
+ this.#controller = new RootController(emptyState(), options.onControlError, Object.freeze({ ownership: SourceOwnershipIndex.empty() }));
55
54
  }
56
55
  get snapshot() {
57
56
  return this.#controller.snapshot;
@@ -63,7 +62,7 @@ export class RootRuntime {
63
62
  return this.#controller.onGenerationCommit(listener);
64
63
  }
65
64
  get sourceOwnership() {
66
- return this.#ownership;
65
+ return this.#controller.committed.state.ownership;
67
66
  }
68
67
  async start() {
69
68
  if (this.#configPort) {
@@ -71,21 +70,15 @@ export class RootRuntime {
71
70
  this.#configSnapshot = snapshot;
72
71
  this.#configDocument = structuredClone(snapshot.document);
73
72
  }
74
- let prepared;
75
73
  const snapshot = await this.#controller.start(async (current, signal) => {
76
- prepared = await this.#prepare(current, signal);
77
- return prepared.generation;
74
+ return (await this.#prepare(current, signal)).generation;
78
75
  });
79
- this.#accept(requirePrepared(prepared));
80
76
  return snapshot;
81
77
  }
82
78
  async reload(target = rootPluginId()) {
83
- let prepared;
84
79
  const snapshot = await this.#controller.reload(target, async (current, signal) => {
85
- prepared = await this.#prepare(current, signal);
86
- return prepared.generation;
80
+ return (await this.#prepare(current, signal)).generation;
87
81
  });
88
- this.#accept(requirePrepared(prepared));
89
82
  return snapshot;
90
83
  }
91
84
  patchConfig(patches) {
@@ -98,7 +91,7 @@ export class RootRuntime {
98
91
  return new HmrCoordinator({
99
92
  ...options,
100
93
  modules: this.#modules,
101
- ownership: () => this.#ownership,
94
+ ownership: () => this.sourceOwnership,
102
95
  runtime: {
103
96
  reload: async (plan) => {
104
97
  const result = await this.#reloadPlan(plan);
@@ -125,9 +118,9 @@ export class RootRuntime {
125
118
  return result;
126
119
  }
127
120
  async #reloadPlan(plan) {
128
- let prepared;
129
121
  let restart;
130
122
  const snapshot = await this.#controller.reload(plan.subtrees[0] ?? plan.slots[0] ?? rootPluginId(), async (current, signal) => {
123
+ let prepared;
131
124
  const resolved = await this.#resolveCapabilityDelta(current, plan);
132
125
  const effective = resolved.plan;
133
126
  if (this.#model && effective.manifestSources.length > 0) {
@@ -154,13 +147,10 @@ export class RootRuntime {
154
147
  });
155
148
  if (restart)
156
149
  return restart;
157
- if (prepared)
158
- this.#accept(prepared);
159
150
  return snapshot;
160
151
  }
161
- #accept(prepared) {
162
- this.#ownership = prepared.ownership;
163
- this.#model = prepared.model;
152
+ get #model() {
153
+ return this.#controller.committed.state.model;
164
154
  }
165
155
  async #resolveCapabilityDelta(current, plan) {
166
156
  const known = capabilityDeltaFromSlots(current, plan.slots);
@@ -185,7 +175,7 @@ export class RootRuntime {
185
175
  if (plan.subtrees.length === 0 || plan.subtrees.includes(rootPluginId()))
186
176
  return false;
187
177
  return plan.changed.every((source) => {
188
- const records = this.#ownership.recordsFor(source);
178
+ const records = this.sourceOwnership.recordsFor(source);
189
179
  return records.length > 0 && records.every((record) => record.role === 'plugin' || record.role === 'schema');
190
180
  });
191
181
  }
@@ -247,10 +237,10 @@ export class RootRuntime {
247
237
  await this.#refreshConfigDocument();
248
238
  const currentDocument = requireConfigDocument(this.#configDocument);
249
239
  let plan;
250
- let prepared;
251
240
  let documentTransaction;
252
241
  let committedDocument;
253
242
  const snapshot = await this.#controller.reload(rootPluginId(), async (current, signal) => {
243
+ let prepared;
254
244
  signal.throwIfAborted();
255
245
  const resolver = await NodePackageResolver.create(this.#projectRoot);
256
246
  const graph = await new ProjectGraphService(resolver).inspect(this.#projectRoot);
@@ -305,8 +295,6 @@ export class RootRuntime {
305
295
  }
306
296
  });
307
297
  const completed = requireConfigPatchPlan(plan);
308
- if (prepared)
309
- this.#accept(prepared);
310
298
  this.#configDocument = completed.candidate;
311
299
  if (committedDocument)
312
300
  this.#configSnapshot = committedDocument;
@@ -406,7 +394,7 @@ class GenerationAssembler {
406
394
  this.environmentLayers = environmentLayers;
407
395
  this.isolation = isolation;
408
396
  this.#host = new NodeDiscoveryHost(modules);
409
- this.#plugins = new PluginScopeAssembler(modules, configResolver, environment, primaryConfigDocument, installResources, environmentLayers, undefined, isolation);
397
+ this.#plugins = new PluginScopeAssembler(modules, configResolver, environment, primaryConfigDocument, generation, installResources, environmentLayers, undefined, isolation);
410
398
  }
411
399
  async prepare(signal) {
412
400
  signal.throwIfAborted();
@@ -426,25 +414,21 @@ class GenerationAssembler {
426
414
  const snapshot = createSnapshotView(this.generation, state);
427
415
  const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, this.#featureIdsByPackageRoot);
428
416
  const assets = GenerationAssets.create(this.#plugins.createdScopeDisposers(), this.#projectionDisposers);
429
- return {
430
- generation: {
431
- snapshot: state,
432
- dispose: () => assets.dispose(),
433
- handoff: composeGenerationHandoffs(this.#plugins.generationHandoff(), projected.handoff),
434
- },
435
- ownership,
436
- model: {
437
- graph: this.graph,
438
- providers: new Map(this.#catalog.values().map((provider) => [provider.id, provider])),
439
- rootsByFeature: new Map([...this.#rootsByFeature].map(([feature, roots]) => [
440
- feature,
441
- Object.freeze([...roots]),
442
- ])),
443
- featureIdsByPackageRoot: new Map(this.#featureIdsByPackageRoot),
444
- scopes: new Map(this.#plugins.scopes),
445
- assets,
446
- },
447
- };
417
+ return prepareRuntimeGeneration({
418
+ snapshot: state,
419
+ dispose: () => assets.dispose(),
420
+ handoff: composeGenerationHandoffs(this.#plugins.generationHandoff(), projected.handoff),
421
+ }, ownership, {
422
+ graph: this.graph,
423
+ providers: new Map(this.#catalog.values().map((provider) => [provider.id, provider])),
424
+ rootsByFeature: new Map([...this.#rootsByFeature].map(([feature, roots]) => [
425
+ feature,
426
+ Object.freeze([...roots]),
427
+ ])),
428
+ featureIdsByPackageRoot: new Map(this.#featureIdsByPackageRoot),
429
+ scopes: new Map(this.#plugins.scopes),
430
+ assets,
431
+ });
448
432
  }
449
433
  catch (error) {
450
434
  await disposePreparedParts(this.#plugins.createdScopeDisposers().map(([, dispose]) => dispose), [...this.#projectionDisposers.values()], error);
@@ -507,11 +491,6 @@ function emptyState() {
507
491
  projections: new Map(),
508
492
  };
509
493
  }
510
- function requirePrepared(prepared) {
511
- if (!prepared)
512
- throw new Error('RootController committed without a prepared generation');
513
- return prepared;
514
- }
515
494
  function isProcessPlan(value) {
516
495
  return 'kind' in value && value.kind === 'process';
517
496
  }
@@ -11,8 +11,12 @@ export interface RuntimeGenerationModel {
11
11
  readonly scopes: ReadonlyMap<PluginId, Scope>;
12
12
  readonly assets: GenerationAssets;
13
13
  }
14
- export interface PreparedRuntimeGeneration {
15
- readonly generation: PreparedGeneration;
14
+ /** Sidecar state that must never be observed from a different generation. */
15
+ export interface RuntimeGenerationState {
16
16
  readonly ownership: SourceOwnershipIndex;
17
- readonly model: RuntimeGenerationModel;
17
+ readonly model?: RuntimeGenerationModel;
18
+ }
19
+ export interface PreparedRuntimeGeneration {
20
+ readonly generation: PreparedGeneration<RuntimeGenerationState>;
18
21
  }
22
+ export declare function prepareRuntimeGeneration(generation: PreparedGeneration, ownership: SourceOwnershipIndex, model: RuntimeGenerationModel): PreparedRuntimeGeneration;
@@ -1 +1,8 @@
1
- export {};
1
+ export function prepareRuntimeGeneration(generation, ownership, model) {
2
+ return Object.freeze({
3
+ generation: Object.freeze({
4
+ ...generation,
5
+ state: Object.freeze({ ownership, model }),
6
+ }),
7
+ });
8
+ }
@@ -1,6 +1,6 @@
1
1
  import { type CapabilityId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
2
2
  import type { ModuleRuntime } from './module-runtime.js';
3
- import type { PreparedRuntimeGeneration, RuntimeGenerationModel } from './runtime-generation.js';
3
+ import { type PreparedRuntimeGeneration, type RuntimeGenerationModel } from './runtime-generation.js';
4
4
  import { type CapabilityDelta } from './convention-capability-delta.js';
5
5
  export declare class SlotGenerationPreparer {
6
6
  private readonly modules;
@@ -2,6 +2,7 @@ import { DisposeStack, createSnapshotView, } from '@zhin.js/plugin-runtime';
2
2
  import { FeatureDiscovery } from '@zhin.js/feature-kit';
3
3
  import { FeatureProjector, composeGenerationHandoffs } from './feature-projector.js';
4
4
  import { NodeDiscoveryHost } from './node-discovery-host.js';
5
+ import { prepareRuntimeGeneration, } from './runtime-generation.js';
5
6
  import { SourceOwnershipIndex } from './source-ownership.js';
6
7
  import { capabilityDeltaFromSlots, } from './convention-capability-delta.js';
7
8
  export class SlotGenerationPreparer {
@@ -48,15 +49,11 @@ export class SlotGenerationPreparer {
48
49
  const snapshot = createSnapshotView(current.generation + 1, projected.state);
49
50
  const ownership = SourceOwnershipIndex.fromGeneration(this.model.graph, snapshot, this.model.featureIdsByPackageRoot);
50
51
  const assets = this.model.assets.replaceProjections(selectedByFeature.keys(), projected.disposers);
51
- return {
52
- generation: {
53
- snapshot: projected.state,
54
- dispose: () => assets.dispose(),
55
- handoff: composeGenerationHandoffs(projected.handoff),
56
- },
57
- ownership,
58
- model: { ...this.model, assets },
59
- };
52
+ return prepareRuntimeGeneration({
53
+ snapshot: projected.state,
54
+ dispose: () => assets.dispose(),
55
+ handoff: composeGenerationHandoffs(projected.handoff),
56
+ }, ownership, { ...this.model, assets });
60
57
  }
61
58
  catch (error) {
62
59
  await disposeProjections(projected.disposers.values(), error);
@@ -6,7 +6,7 @@ import type { IsolatedPluginRuntimePort } from './isolation.js';
6
6
  import type { ModuleRuntime } from './module-runtime.js';
7
7
  import { type PluginConfigResolver, type RootResourceInstaller } from './plugin-scope-assembler.js';
8
8
  import type { ProjectGraph } from './project-graph.js';
9
- import type { PreparedRuntimeGeneration, RuntimeGenerationModel } from './runtime-generation.js';
9
+ import { type PreparedRuntimeGeneration, type RuntimeGenerationModel } from './runtime-generation.js';
10
10
  export declare class SubtreeTopologyChangedError extends Error {
11
11
  constructor(message: string);
12
12
  }
@@ -3,6 +3,7 @@ import { FeatureDiscovery } from '@zhin.js/feature-kit';
3
3
  import { FeatureProjector, composeGenerationHandoffs } from './feature-projector.js';
4
4
  import { NodeDiscoveryHost } from './node-discovery-host.js';
5
5
  import { PluginScopeAssembler, } from './plugin-scope-assembler.js';
6
+ import { prepareRuntimeGeneration, } from './runtime-generation.js';
6
7
  import { SourceOwnershipIndex } from './source-ownership.js';
7
8
  import { addCapabilitySlot, featureSetupAliases, mergeSetupCapabilities, } from './setup-capabilities.js';
8
9
  export class SubtreeTopologyChangedError extends Error {
@@ -37,7 +38,7 @@ export class SubtreeGenerationPreparer {
37
38
  signal.throwIfAborted();
38
39
  const nodes = indexGraph(this.graph);
39
40
  assertCompatibleTopology(indexGraph(this.model.graph), nodes, roots);
40
- const plugins = new PluginScopeAssembler(this.modules, this.configResolver, this.environment, this.primaryConfigDocument, this.installResources, this.environmentLayers, {
41
+ const plugins = new PluginScopeAssembler(this.modules, this.configResolver, this.environment, this.primaryConfigDocument, current.generation + 1, this.installResources, this.environmentLayers, {
41
42
  scopes: this.model.scopes,
42
43
  tree: current.tree,
43
44
  config: current.config,
@@ -86,20 +87,16 @@ export class SubtreeGenerationPreparer {
86
87
  // Local prepare still commits a complete immutable generation. The
87
88
  // replacement map controls lifetime ownership, not snapshot granularity.
88
89
  const assets = this.model.assets.replaceScopes([...nodes.keys()], replacements, projectionDisposers);
89
- return {
90
- generation: {
91
- snapshot: projected.state,
92
- dispose: () => assets.dispose(),
93
- handoff: composeGenerationHandoffs(plugins.generationHandoff(), projected.handoff),
94
- },
95
- ownership,
96
- model: {
97
- ...this.model,
98
- graph: this.graph,
99
- scopes: new Map(plugins.scopes),
100
- assets,
101
- },
102
- };
90
+ return prepareRuntimeGeneration({
91
+ snapshot: projected.state,
92
+ dispose: () => assets.dispose(),
93
+ handoff: composeGenerationHandoffs(plugins.generationHandoff(), projected.handoff),
94
+ }, ownership, {
95
+ ...this.model,
96
+ graph: this.graph,
97
+ scopes: new Map(plugins.scopes),
98
+ assets,
99
+ });
103
100
  }
104
101
  catch (error) {
105
102
  await rollback(plugins.createdScopeDisposers().map(([, dispose]) => dispose), [...projectionDisposers.values()], error);
@@ -6,7 +6,7 @@ import type { IsolatedPluginRuntimePort } from './isolation.js';
6
6
  import type { ModuleRuntime } from './module-runtime.js';
7
7
  import { type PluginConfigResolver, type RootResourceInstaller } from './plugin-scope-assembler.js';
8
8
  import type { ProjectGraph } from './project-graph.js';
9
- import type { PreparedRuntimeGeneration, RuntimeGenerationModel } from './runtime-generation.js';
9
+ import { type PreparedRuntimeGeneration, type RuntimeGenerationModel } from './runtime-generation.js';
10
10
  import { type CapabilityDelta } from './convention-capability-delta.js';
11
11
  /** Runtime-local invalidation to commit alongside an ABI-safe manifest change. */
12
12
  export interface TopologyRuntimeDelta {
@@ -5,6 +5,7 @@ import { FeatureCatalog, FeatureDiscovery, } from '@zhin.js/feature-kit';
5
5
  import { FeatureProjector, composeGenerationHandoffs } from './feature-projector.js';
6
6
  import { NodeDiscoveryHost } from './node-discovery-host.js';
7
7
  import { PluginScopeAssembler, } from './plugin-scope-assembler.js';
8
+ import { prepareRuntimeGeneration, } from './runtime-generation.js';
8
9
  import { SourceOwnershipIndex } from './source-ownership.js';
9
10
  import { capabilityDeltaFromSlots, capabilityDeltaIds, } from './convention-capability-delta.js';
10
11
  import { addCapabilitySlot, featureSetupAliases, mergeSetupCapabilities, } from './setup-capabilities.js';
@@ -46,7 +47,7 @@ export class TopologyGenerationPreparer {
46
47
  if (!plan.changed && subtreeRoots.length === 0 && selected.size === 0)
47
48
  return undefined;
48
49
  const featureTopology = await this.#loadFeatureTopology(plan);
49
- const plugins = new PluginScopeAssembler(this.modules, this.configResolver, this.environment, this.primaryConfigDocument, this.installResources, this.environmentLayers, {
50
+ const plugins = new PluginScopeAssembler(this.modules, this.configResolver, this.environment, this.primaryConfigDocument, current.generation + 1, this.installResources, this.environmentLayers, {
50
51
  scopes: this.model.scopes,
51
52
  tree: current.tree,
52
53
  config: current.config,
@@ -91,22 +92,18 @@ export class TopologyGenerationPreparer {
91
92
  const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, featureTopology.featureIdsByPackageRoot);
92
93
  const replacements = new Map(plugins.createdScopeDisposers());
93
94
  const assets = this.model.assets.replaceScopes(graphOrder(this.graph), replacements, projectionDisposers);
94
- return {
95
- generation: {
96
- snapshot: projected.state,
97
- dispose: () => assets.dispose(),
98
- handoff: composeGenerationHandoffs(plugins.generationHandoff(), projected.handoff),
99
- },
100
- ownership,
101
- model: {
102
- graph: this.graph,
103
- providers: featureTopology.providers,
104
- rootsByFeature: featureTopology.rootsByFeature,
105
- featureIdsByPackageRoot: featureTopology.featureIdsByPackageRoot,
106
- scopes: new Map(plugins.scopes),
107
- assets,
108
- },
109
- };
95
+ return prepareRuntimeGeneration({
96
+ snapshot: projected.state,
97
+ dispose: () => assets.dispose(),
98
+ handoff: composeGenerationHandoffs(plugins.generationHandoff(), projected.handoff),
99
+ }, ownership, {
100
+ graph: this.graph,
101
+ providers: featureTopology.providers,
102
+ rootsByFeature: featureTopology.rootsByFeature,
103
+ featureIdsByPackageRoot: featureTopology.featureIdsByPackageRoot,
104
+ scopes: new Map(plugins.scopes),
105
+ assets,
106
+ });
110
107
  }
111
108
  catch (error) {
112
109
  await rollback(plugins.createdScopeDisposers().map(([, dispose]) => dispose), [...projectionDisposers.values()], error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/runtime",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
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.11",
22
- "@zhin.js/plugin-runtime": "1.1.6"
21
+ "@zhin.js/feature-kit": "1.0.12",
22
+ "@zhin.js/plugin-runtime": "1.1.7"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^26.1.2",
26
26
  "typescript": "^6.0.3",
27
- "@zhin.js/adapter": "1.1.10",
28
- "@zhin.js/agent-feature": "1.0.11",
29
- "@zhin.js/command": "1.0.14",
30
- "@zhin.js/component": "1.0.11",
31
- "@zhin.js/layout": "1.0.11",
32
- "@zhin.js/mcp-feature": "1.0.11",
33
- "@zhin.js/middleware": "1.0.11",
34
- "@zhin.js/page": "1.0.11",
35
- "@zhin.js/skill": "1.0.11",
36
- "@zhin.js/tool": "1.0.11"
27
+ "@zhin.js/adapter": "1.1.11",
28
+ "@zhin.js/agent-feature": "1.0.12",
29
+ "@zhin.js/command": "1.0.15",
30
+ "@zhin.js/component": "1.0.12",
31
+ "@zhin.js/mcp-feature": "1.0.12",
32
+ "@zhin.js/page": "1.0.12",
33
+ "@zhin.js/skill": "1.0.12",
34
+ "@zhin.js/layout": "1.0.12",
35
+ "@zhin.js/middleware": "1.0.12",
36
+ "@zhin.js/tool": "1.0.12"
37
37
  },
38
38
  "repository": {
39
39
  "type": "git",