@zhin.js/plugin-runtime 1.1.13 → 1.1.15

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
@@ -27,8 +27,20 @@ prepare shadow state -> validate -> handoff -> atomic commit
27
27
  局部 HMR 只缩小 prepare/setup/dispose 范围;每次 commit 始终发布完整一致的 snapshot。
28
28
  长期 socket、worker 或 admission loop 必须参加 generation handoff,不能成为模块级副作用。
29
29
 
30
+ `SnapshotReader.acquire()` 返回的 `SnapshotLease.retirementSignal` 在该运行代被新一代替换、
31
+ 或 Root 关闭时同步触发,即使仍有 lease 持有旧资源。它供交互等待等需要随代结束的操作主动取消;
32
+ 不会代替 `lease.release()`、强制中断普通旧操作或提前销毁资源。被拒绝的候选不会触发当前代的信号。
33
+
30
34
  本包没有生产依赖。YAML、AJV、watcher、IM 与 AI 都属于上层 adapter 或 Runtime。
31
35
 
36
+ ## 可选管理员任务端口
37
+
38
+ `PluginTaskProvider` 与 `pluginTaskProviderToken` 定义插件自己的管理员任务服务:插件通过 `provide()` 发布,Host 从当前运行代的所属 Scope 读取,不额外建立调度器。聊天发送者的个人任务不进入这个端口。
39
+
40
+ 任务快照包含真实任务、`revision` 与有界持久 `receipts`。创建或修改提交客户端操作 UUID 和原 `expectedRevision`;同一操作必须按相同内容幂等核对,不同内容复用 ID 或过期版本均应拒绝。回执确认的是持久命令,不是外部消息投递。未拿到响应时先只读核对,不能自动重发。服务必须携带 `generationAdmissionSource` 并参与实例退役。
41
+
42
+ `PluginTaskError.message` 是可以向管理员展示的业务说明,不应含凭据或原始异常。Host 自己的输入、权限与运行代校验使用固定说明,普通异常不直接暴露。任务使用已有 `DeliveryTarget` 保留完整账号、会话、父容器与话题;`actions` 只列实际支持的操作,`unknown` 状态不能被包装成可安全重试。
43
+
32
44
  验证:`pnpm --filter @zhin.js/plugin-runtime test && pnpm --filter @zhin.js/plugin-runtime build`。
33
45
 
34
46
  详细契约见 [Kernel 与 Generation](../../../docs/architecture/target-implementation/kernel-and-generation.md)。
package/lib/index.d.ts CHANGED
@@ -36,3 +36,4 @@ export * from './persistent-state.js';
36
36
  export * from './inbox.js';
37
37
  export * from './system-log.js';
38
38
  export * from './plugin-verification.js';
39
+ export * from './plugin-tasks.js';
package/lib/index.js CHANGED
@@ -36,3 +36,4 @@ export * from './persistent-state.js';
36
36
  export * from './inbox.js';
37
37
  export * from './system-log.js';
38
38
  export * from './plugin-verification.js';
39
+ export * from './plugin-tasks.js';
@@ -1,4 +1,4 @@
1
- /** Canonical `zhin.config.*#plugins` shape keyed by Plugin instanceKey. */
1
+ /** Plugin business configuration keyed by the aliases in a v2 application declaration. */
2
2
  export type PluginConfigurationMap = Readonly<Record<string, unknown>>;
3
3
  /** Raised when a Root config still uses a non-canonical Plugin configuration shape. */
4
4
  export declare class PluginConfigurationShapeError extends TypeError {
@@ -6,10 +6,6 @@ export declare class PluginConfigurationShapeError extends TypeError {
6
6
  constructor(source?: string);
7
7
  }
8
8
  /**
9
- * Reads the canonical Plugin instance configuration map.
10
- *
11
- * An omitted `plugins` key means that the Root has no configured child instances.
12
- * Every present value must be an object map; legacy package-name arrays belong only
13
- * to the explicit migration pipeline.
9
+ * Projects top-level business blocks for the declared application instances.
14
10
  */
15
11
  export declare function readPluginConfigurationMap(document: Readonly<Record<string, unknown>>, source?: string): PluginConfigurationMap;
@@ -9,18 +9,20 @@ export class PluginConfigurationShapeError extends TypeError {
9
9
  }
10
10
  }
11
11
  /**
12
- * Reads the canonical Plugin instance configuration map.
13
- *
14
- * An omitted `plugins` key means that the Root has no configured child instances.
15
- * Every present value must be an object map; legacy package-name arrays belong only
16
- * to the explicit migration pipeline.
12
+ * Projects top-level business blocks for the declared application instances.
17
13
  */
18
14
  export function readPluginConfigurationMap(document, source = 'Root config') {
15
+ if (document.configVersion !== 2) {
16
+ throw new TypeError(`${source}.configVersion must be 2; see docs/migrations/plugin-instance-declarations.md`);
17
+ }
19
18
  const value = document.plugins;
20
19
  if (value === undefined)
21
20
  return EMPTY_PLUGIN_CONFIGURATION;
22
21
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
23
22
  throw new PluginConfigurationShapeError(source);
24
23
  }
25
- return value;
24
+ if (Object.values(value).some(reference => typeof reference !== 'string' || reference.length === 0)) {
25
+ throw new TypeError(`${source}.plugins must map instance aliases to package strings`);
26
+ }
27
+ return Object.freeze(Object.fromEntries(Object.keys(value).map(key => [key, document[key] === undefined ? {} : document[key]])));
26
28
  }
@@ -0,0 +1,62 @@
1
+ import type { DeliveryTarget } from './delivery-target.js';
2
+ import type { JsonValue } from './persistent-state.js';
3
+ import type { GenerationAdmissionSource } from './admission.js';
4
+ export type PluginTaskState = 'scheduled' | 'paused' | 'running' | 'unknown' | 'succeeded' | 'failed' | 'cancelled';
5
+ export type PluginTaskAction = 'pause' | 'resume' | 'cancel' | 'update';
6
+ /** Business task facts. Chat-user tasks are deliberately outside this administrator port. */
7
+ export interface PluginTask {
8
+ readonly id: string;
9
+ readonly ownership: 'administrator';
10
+ readonly title: string;
11
+ readonly target: DeliveryTarget;
12
+ readonly state: PluginTaskState;
13
+ readonly createdAt: number;
14
+ readonly nextRunAt?: number;
15
+ readonly lastRunAt?: number;
16
+ readonly timezone?: string;
17
+ readonly detail?: string;
18
+ readonly parameters: Readonly<Record<string, JsonValue>>;
19
+ readonly actions: readonly PluginTaskAction[];
20
+ }
21
+ export interface PluginTaskSnapshot {
22
+ /** Changes whenever facts change. A stale mutation must fail instead of overwriting. */
23
+ readonly revision: string;
24
+ readonly tasks: readonly PluginTask[];
25
+ /** Bounded durable mutation acknowledgements, distinct from external delivery receipts. */
26
+ readonly receipts: readonly PluginTaskMutationReceipt[];
27
+ }
28
+ export interface PluginTaskMutationReceipt {
29
+ readonly requestId: string;
30
+ readonly taskId: string;
31
+ readonly operation: 'create' | PluginTaskAction;
32
+ }
33
+ export interface PluginTaskMutation {
34
+ /** Client-created operation UUID, retained for reconciliation after a lost response. */
35
+ readonly requestId: string;
36
+ readonly expectedRevision: string;
37
+ }
38
+ /** Optional plugin-owned task service, resolved from the current generation's owner scope. */
39
+ export interface PluginTaskProvider extends GenerationAdmissionSource {
40
+ readonly title: string;
41
+ readonly description: string;
42
+ /** JSON Schema for this provider's creation/update parameters, never a Console-specific form. */
43
+ readonly schema: Readonly<Record<string, JsonValue>>;
44
+ /** Discloses future external effects before the operator creates a task. */
45
+ readonly effects: readonly string[];
46
+ read(): Promise<PluginTaskSnapshot>;
47
+ create(input: PluginTaskMutation & Readonly<{
48
+ target: DeliveryTarget;
49
+ parameters: Readonly<Record<string, JsonValue>>;
50
+ }>): Promise<PluginTaskSnapshot>;
51
+ change(input: PluginTaskMutation & Readonly<{
52
+ id: string;
53
+ action: PluginTaskAction;
54
+ parameters?: Readonly<Record<string, JsonValue>>;
55
+ }>): Promise<PluginTaskSnapshot>;
56
+ }
57
+ /** message is safe business guidance for administrators; never include secrets or raw exceptions. */
58
+ export declare class PluginTaskError extends Error {
59
+ readonly code: 'invalid_input' | 'conflict' | 'not_found' | 'unsupported' | 'unavailable';
60
+ constructor(code: PluginTaskError['code'], message: string);
61
+ }
62
+ export declare const pluginTaskProviderToken: import("./token.js").Token<PluginTaskProvider>;
@@ -0,0 +1,11 @@
1
+ import { createToken } from './token.js';
2
+ /** message is safe business guidance for administrators; never include secrets or raw exceptions. */
3
+ export class PluginTaskError extends Error {
4
+ code;
5
+ constructor(code, message) {
6
+ super(message);
7
+ this.name = 'PluginTaskError';
8
+ this.code = code;
9
+ }
10
+ }
11
+ export const pluginTaskProviderToken = createToken('zhin.plugin-task-provider', 'Plugin-owned persistent administrator tasks, controlled only through authenticated Host operations');
@@ -10,10 +10,13 @@ export type ConfigPatch = {
10
10
  export interface ConfigDocumentSnapshot {
11
11
  readonly document: RuntimeConfigDocument;
12
12
  readonly revision: string;
13
+ /** v2 declaration aliases in source order, including integer-like keys. */
14
+ readonly pluginOrder?: readonly string[];
13
15
  }
14
16
  /** A prepared write is inert until the generation handoff activates it. */
15
17
  export interface PreparedConfigDocument {
16
18
  readonly document: RuntimeConfigDocument;
19
+ readonly pluginOrder?: readonly string[];
17
20
  commit(): Promise<ConfigDocumentSnapshot>;
18
21
  rollback(): Promise<void>;
19
22
  }
package/lib/snapshot.d.ts CHANGED
@@ -50,7 +50,11 @@ export declare class SnapshotLease {
50
50
  #private;
51
51
  readonly value: RuntimeSnapshot;
52
52
  private readonly releaseRecord;
53
- constructor(value: RuntimeSnapshot, releaseRecord: () => void, authority: typeof snapshotLeaseAuthority);
53
+ /** Aborted on publication of a replacement or Root close, before lease disposal. */
54
+ readonly retirementSignal: AbortSignal;
55
+ constructor(value: RuntimeSnapshot, releaseRecord: () => void, authority: typeof snapshotLeaseAuthority,
56
+ /** Aborted on publication of a replacement or Root close, before lease disposal. */
57
+ retirementSignal: AbortSignal);
54
58
  get active(): boolean;
55
59
  release(): void;
56
60
  }
package/lib/snapshot.js CHANGED
@@ -13,10 +13,14 @@ const snapshotLeaseAuthority = Symbol('SnapshotLeaseAuthority');
13
13
  export class SnapshotLease {
14
14
  value;
15
15
  releaseRecord;
16
+ retirementSignal;
16
17
  #released = false;
17
- constructor(value, releaseRecord, authority) {
18
+ constructor(value, releaseRecord, authority,
19
+ /** Aborted on publication of a replacement or Root close, before lease disposal. */
20
+ retirementSignal) {
18
21
  this.value = value;
19
22
  this.releaseRecord = releaseRecord;
23
+ this.retirementSignal = retirementSignal;
20
24
  if (authority !== snapshotLeaseAuthority)
21
25
  throw new TypeError('SnapshotLease is owned by SnapshotStore');
22
26
  }
@@ -43,6 +47,7 @@ export class SnapshotStore {
43
47
  const committed = Object.freeze({ snapshot, state: initialState });
44
48
  const admissions = collectSnapshotAdmissions(snapshot);
45
49
  const record = {
50
+ retirement: new AbortController(),
46
51
  snapshot,
47
52
  state: initialState,
48
53
  committed,
@@ -65,7 +70,7 @@ export class SnapshotStore {
65
70
  if (this.#closed)
66
71
  throw new Error('SnapshotStore is closed');
67
72
  const record = this.#current;
68
- const lease = new SnapshotLease(record.snapshot, this.#retain(record), snapshotLeaseAuthority);
73
+ const lease = new SnapshotLease(record.snapshot, this.#retain(record), snapshotLeaseAuthority, record.retirement.signal);
69
74
  this.#leases.add(lease);
70
75
  return lease;
71
76
  }
@@ -87,6 +92,7 @@ export class SnapshotStore {
87
92
  const committed = Object.freeze({ snapshot, state });
88
93
  const admissions = collectSnapshotAdmissions(snapshot);
89
94
  const next = {
95
+ retirement: new AbortController(),
90
96
  snapshot,
91
97
  state,
92
98
  committed,
@@ -99,6 +105,7 @@ export class SnapshotStore {
99
105
  // Commit only swaps the active pointer. The previous record may still be
100
106
  // serving in-flight work, so disposal is deferred to its final lease.
101
107
  previous.retired = true;
108
+ previous.retirement.abort();
102
109
  this.#retired.add(previous);
103
110
  this.#disposeIfReady(previous);
104
111
  return this.#current.committed;
@@ -109,6 +116,7 @@ export class SnapshotStore {
109
116
  this.#closed = true;
110
117
  replaceGenerationAdmissions(this.#current.admissions, new Set(), this, () => undefined);
111
118
  this.#current.retired = true;
119
+ this.#current.retirement.abort();
112
120
  this.#retired.add(this.#current);
113
121
  this.#disposeIfReady(this.#current);
114
122
  // A Root is not stopped while any historical generation still owns live
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-runtime",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "description": "Zero-dependency Plugin tree, Scope and atomic generation runtime for Zhin",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -8,7 +8,6 @@
8
8
  "exports": {
9
9
  ".": {
10
10
  "types": "./lib/index.d.ts",
11
- "development": "./src/index.ts",
12
11
  "import": "./lib/index.js"
13
12
  }
14
13
  },
@@ -20,7 +19,7 @@
20
19
  "typescript": "^6.0.3"
21
20
  },
22
21
  "engines": {
23
- "node": "^20.19.0 || >=22.12.0"
22
+ "node": ">=22.19.0"
24
23
  },
25
24
  "repository": {
26
25
  "type": "git",