@zhin.js/plugin-runtime 1.1.6 → 1.1.7

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.
@@ -1,29 +1,30 @@
1
1
  import type { PluginId } from './identity.js';
2
- import { type PreparedGeneration, type RuntimeSnapshot, type SnapshotReader, type SnapshotState } from './snapshot.js';
2
+ import { type CommittedGeneration, type PreparedGeneration, type RuntimeSnapshot, type SnapshotReader, type SnapshotState } from './snapshot.js';
3
3
  export type RootState = 'idle' | 'running' | 'stopping' | 'stopped' | 'failed';
4
- export type PrepareGeneration = (current: RuntimeSnapshot, signal: AbortSignal) => PreparedGeneration | Promise<PreparedGeneration>;
5
- export type PrepareTransaction = (current: RuntimeSnapshot, signal: AbortSignal) => PreparedGeneration | undefined | Promise<PreparedGeneration | undefined>;
4
+ export type PrepareGeneration<TState = undefined> = (current: RuntimeSnapshot, signal: AbortSignal) => PreparedGeneration<TState> | Promise<PreparedGeneration<TState>>;
5
+ export type PrepareTransaction<TState = undefined> = (current: RuntimeSnapshot, signal: AbortSignal) => PreparedGeneration<TState> | undefined | Promise<PreparedGeneration<TState> | undefined>;
6
6
  export type ControlErrorHandler = (error: unknown) => void;
7
- export interface GenerationCommitEvent {
8
- readonly previous: RuntimeSnapshot;
9
- readonly current: RuntimeSnapshot;
7
+ export interface GenerationCommitEvent<TState = undefined> {
8
+ readonly previous: CommittedGeneration<TState>;
9
+ readonly current: CommittedGeneration<TState>;
10
10
  }
11
- export type GenerationCommitListener = (event: GenerationCommitEvent) => void;
11
+ export type GenerationCommitListener<TState = undefined> = (event: GenerationCommitEvent<TState>) => void | Promise<void>;
12
12
  export declare class RootIntegrityError extends Error {
13
13
  constructor(message: string, options?: ErrorOptions);
14
14
  }
15
- export declare class RootController {
15
+ export declare class RootController<TState = undefined> {
16
16
  #private;
17
17
  private readonly onControlError;
18
18
  readonly snapshots: SnapshotReader;
19
- constructor(initial: SnapshotState, onControlError?: ControlErrorHandler);
19
+ constructor(initial: SnapshotState, onControlError?: ControlErrorHandler, ...state: [TState] extends [undefined] ? [initialState?: undefined] : [initialState: TState]);
20
20
  get state(): RootState;
21
21
  get generation(): number;
22
22
  get snapshot(): RuntimeSnapshot;
23
+ get committed(): CommittedGeneration<TState>;
23
24
  /** Observe successful generation commits, including the initial start. */
24
- onGenerationCommit(listener: GenerationCommitListener): () => void;
25
- start(prepare: PrepareGeneration): Promise<RuntimeSnapshot>;
26
- transact(prepare: PrepareTransaction): Promise<RuntimeSnapshot>;
27
- reload(_target: PluginId | string, prepare: PrepareTransaction): Promise<RuntimeSnapshot>;
25
+ onGenerationCommit(listener: GenerationCommitListener<TState>): () => void;
26
+ start(prepare: PrepareGeneration<TState>): Promise<RuntimeSnapshot>;
27
+ transact(prepare: PrepareTransaction<TState>): Promise<RuntimeSnapshot>;
28
+ reload(_target: PluginId | string, prepare: PrepareTransaction<TState>): Promise<RuntimeSnapshot>;
28
29
  stop(): Promise<void>;
29
30
  }
@@ -16,9 +16,9 @@ export class RootController {
16
16
  #stopRequested = false;
17
17
  #activeTransaction;
18
18
  #generationCommitListeners = new Set();
19
- constructor(initial, onControlError = () => undefined) {
19
+ constructor(initial, onControlError = () => undefined, ...state) {
20
20
  this.onControlError = onControlError;
21
- this.#snapshotStore = new SnapshotStore(initial, (error) => this.#failIntegrity(error));
21
+ this.#snapshotStore = new SnapshotStore(initial, (error) => this.#failIntegrity(error), ...state);
22
22
  const store = this.#snapshotStore;
23
23
  this.snapshots = Object.freeze({
24
24
  acquire: () => {
@@ -39,6 +39,9 @@ export class RootController {
39
39
  get snapshot() {
40
40
  return this.#snapshotStore.current;
41
41
  }
42
+ get committed() {
43
+ return this.#snapshotStore.committed;
44
+ }
42
45
  /** Observe successful generation commits, including the initial start. */
43
46
  onGenerationCommit(listener) {
44
47
  this.#generationCommitListeners.add(listener);
@@ -141,21 +144,23 @@ export class RootController {
141
144
  return result;
142
145
  }
143
146
  #commitGeneration(expectedGeneration, prepared) {
144
- const previous = this.#snapshotStore.current;
145
- const snapshot = this.#snapshotStore.commit(expectedGeneration, prepared);
146
- const event = Object.freeze({ previous, current: snapshot });
147
+ const previous = this.#snapshotStore.committed;
148
+ const current = this.#snapshotStore.commit(expectedGeneration, prepared);
149
+ const event = Object.freeze({ previous, current });
147
150
  const listeners = [...this.#generationCommitListeners];
148
151
  queueMicrotask(() => {
149
152
  for (const listener of listeners) {
150
153
  try {
151
- listener(event);
154
+ void Promise.resolve(listener(event)).catch((error) => {
155
+ this.#reportControlError(error);
156
+ });
152
157
  }
153
158
  catch (error) {
154
159
  this.#reportControlError(error);
155
160
  }
156
161
  }
157
162
  });
158
- return snapshot;
163
+ return current.snapshot;
159
164
  }
160
165
  #reportControlError(error) {
161
166
  try {
package/lib/snapshot.d.ts CHANGED
@@ -22,11 +22,20 @@ export interface RuntimeSnapshot {
22
22
  readonly projections: ReadonlyMap<FeatureId, unknown>;
23
23
  }
24
24
  export type SnapshotState = Omit<RuntimeSnapshot, 'generation'>;
25
- export interface PreparedGeneration {
25
+ interface PreparedGenerationBase {
26
26
  readonly snapshot: SnapshotState;
27
27
  readonly dispose: Dispose;
28
28
  readonly handoff?: GenerationHandoff;
29
29
  }
30
+ export type PreparedGeneration<TState = undefined> = PreparedGenerationBase & ([TState] extends [undefined] ? {
31
+ readonly state?: undefined;
32
+ } : {
33
+ readonly state: TState;
34
+ });
35
+ export interface CommittedGeneration<TState = undefined> {
36
+ readonly snapshot: RuntimeSnapshot;
37
+ readonly state: TState;
38
+ }
30
39
  export declare class GenerationConflictError extends Error {
31
40
  readonly expected: number;
32
41
  readonly actual: number;
@@ -46,14 +55,16 @@ export interface SnapshotReader {
46
55
  acquire(): SnapshotLease;
47
56
  owns(lease: SnapshotLease): boolean;
48
57
  }
49
- export declare class SnapshotStore implements SnapshotReader {
58
+ export declare class SnapshotStore<TState = undefined> implements SnapshotReader {
50
59
  #private;
51
60
  private readonly onDisposalError;
52
- constructor(initial: SnapshotState, onDisposalError?: (error: unknown) => void);
61
+ constructor(initial: SnapshotState, onDisposalError?: (error: unknown) => void, ...state: [TState] extends [undefined] ? [initialState?: undefined] : [initialState: TState]);
53
62
  get current(): RuntimeSnapshot;
63
+ /** Complete active record; snapshot and state always belong to one generation. */
64
+ get committed(): CommittedGeneration<TState>;
54
65
  acquire(): SnapshotLease;
55
66
  owns(lease: SnapshotLease): boolean;
56
- commit(expectedGeneration: number, prepared: PreparedGeneration): RuntimeSnapshot;
67
+ commit(expectedGeneration: number, prepared: PreparedGeneration<TState>): CommittedGeneration<TState>;
57
68
  close(): Promise<void>;
58
69
  }
59
70
  export declare function createSnapshotView(generation: number, state: SnapshotState): RuntimeSnapshot;
package/lib/snapshot.js CHANGED
@@ -36,12 +36,16 @@ export class SnapshotStore {
36
36
  #retired = new Set();
37
37
  #leases = new WeakSet();
38
38
  #closed = false;
39
- constructor(initial, onDisposalError = () => undefined) {
39
+ constructor(initial, onDisposalError = () => undefined, ...state) {
40
40
  this.onDisposalError = onDisposalError;
41
+ const initialState = state[0];
41
42
  const snapshot = createSnapshotView(0, initial);
43
+ const committed = Object.freeze({ snapshot, state: initialState });
42
44
  const admissions = collectSnapshotAdmissions(snapshot);
43
45
  const record = {
44
46
  snapshot,
47
+ state: initialState,
48
+ committed,
45
49
  dispose: () => undefined,
46
50
  admissions,
47
51
  leases: 0,
@@ -53,6 +57,10 @@ export class SnapshotStore {
53
57
  get current() {
54
58
  return this.#current.snapshot;
55
59
  }
60
+ /** Complete active record; snapshot and state always belong to one generation. */
61
+ get committed() {
62
+ return this.#current.committed;
63
+ }
56
64
  acquire() {
57
65
  if (this.#closed)
58
66
  throw new Error('SnapshotStore is closed');
@@ -75,9 +83,13 @@ export class SnapshotStore {
75
83
  }
76
84
  const previous = this.#current;
77
85
  const snapshot = createSnapshotView(expectedGeneration + 1, prepared.snapshot);
86
+ const state = prepared.state;
87
+ const committed = Object.freeze({ snapshot, state });
78
88
  const admissions = collectSnapshotAdmissions(snapshot);
79
89
  const next = {
80
90
  snapshot,
91
+ state,
92
+ committed,
81
93
  dispose: prepared.dispose,
82
94
  admissions,
83
95
  leases: 0,
@@ -90,7 +102,7 @@ export class SnapshotStore {
90
102
  previous.retired = true;
91
103
  this.#retired.add(previous);
92
104
  this.#disposeIfReady(previous);
93
- return this.#current.snapshot;
105
+ return this.#current.committed;
94
106
  }
95
107
  async close() {
96
108
  if (this.#closed)
package/lib/token.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface Token<T> {
6
6
  readonly _type?: () => T;
7
7
  }
8
8
  export declare function createToken<T>(id: string, description?: string): Token<T>;
9
+ export declare function createRootPrivateToken<T>(id: string, description?: string): Token<T>;
9
10
  export declare class Scope {
10
11
  #private;
11
12
  readonly owner: PluginId;
package/lib/token.js CHANGED
@@ -1,8 +1,14 @@
1
1
  import { DisposeStack } from './dispose.js';
2
2
  import { tokenId } from './identity.js';
3
+ const rootPrivateTokens = new WeakSet();
3
4
  export function createToken(id, description) {
4
5
  return Object.freeze({ id: tokenId(id), description });
5
6
  }
7
+ export function createRootPrivateToken(id, description) {
8
+ const token = { id: tokenId(id), description };
9
+ rootPrivateTokens.add(token);
10
+ return Object.freeze(token);
11
+ }
6
12
  export class Scope {
7
13
  owner;
8
14
  parent;
@@ -20,20 +26,36 @@ export class Scope {
20
26
  if (this.#bindings.has(token.id)) {
21
27
  throw new Error(`Duplicate resource ${token.id} in ${this.owner}`);
22
28
  }
23
- this.#bindings.set(token.id, Object.freeze({ owner: this.owner, value }));
29
+ this.#bindings.set(token.id, Object.freeze({
30
+ owner: this.owner,
31
+ value,
32
+ rootPrivate: rootPrivateTokens.has(token),
33
+ token,
34
+ }));
24
35
  if (dispose)
25
36
  this.disposers.add(dispose);
26
37
  }
27
38
  use(token) {
28
39
  const binding = this.#bindings.get(token.id);
29
- if (binding)
30
- return binding.value;
31
- if (this.parent)
32
- return this.parent.use(token);
40
+ if (binding) {
41
+ if (!binding.rootPrivate || binding.token === token)
42
+ return binding.value;
43
+ throw new Error(`Missing resource ${token.id} for ${this.owner}`);
44
+ }
45
+ const inherited = this.parent
46
+ ? this.parent.#resolveForDescendant(token.id)
47
+ : undefined;
48
+ if (inherited?.status === 'found')
49
+ return inherited.binding.value;
33
50
  throw new Error(`Missing resource ${token.id} for ${this.owner}`);
34
51
  }
35
52
  has(token) {
36
- return this.#bindings.has(token.id) || Boolean(this.parent?.has(token));
53
+ const binding = this.#bindings.get(token.id);
54
+ if (binding)
55
+ return !binding.rootPrivate || binding.token === token;
56
+ return this.parent
57
+ ? this.parent.#resolveForDescendant(token.id)?.status === 'found'
58
+ : false;
37
59
  }
38
60
  seal() {
39
61
  this.#sealed = true;
@@ -45,8 +67,19 @@ export class Scope {
45
67
  const result = this.parent
46
68
  ? new Map(this.parent.snapshot())
47
69
  : new Map();
48
- for (const [id, binding] of this.#bindings)
49
- result.set(id, binding.value);
70
+ for (const [id, binding] of this.#bindings) {
71
+ if (!binding.rootPrivate)
72
+ result.set(id, binding.value);
73
+ }
50
74
  return result;
51
75
  }
76
+ #resolveForDescendant(id) {
77
+ const binding = this.#bindings.get(id);
78
+ if (binding) {
79
+ return binding.rootPrivate
80
+ ? Object.freeze({ status: 'blocked' })
81
+ : Object.freeze({ status: 'found', binding });
82
+ }
83
+ return this.parent ? this.parent.#resolveForDescendant(id) : undefined;
84
+ }
52
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-runtime",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "Zero-dependency Plugin tree, Scope and atomic generation runtime for Zhin",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",