@actiondock/core 2.2.2 → 2.4.0

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.
Files changed (47) hide show
  1. package/README.md +8 -3
  2. package/dist/app/app.d.ts +2 -0
  3. package/dist/app/app.js +6 -0
  4. package/dist/app/types.d.ts +12 -0
  5. package/dist/errors.d.ts +38 -0
  6. package/dist/errors.js +44 -0
  7. package/dist/execution/service.d.ts +2 -0
  8. package/dist/execution/service.js +13 -2
  9. package/dist/execution/types.d.ts +17 -0
  10. package/dist/export/templates.js +4 -12
  11. package/dist/host/host.js +46 -5
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +1 -0
  14. package/dist/ipc/host.d.ts +5 -0
  15. package/dist/ipc/host.js +43 -1
  16. package/dist/ipc/target.d.ts +18 -0
  17. package/dist/ipc/target.js +74 -6
  18. package/dist/ipc/types.d.ts +10 -1
  19. package/dist/platform/default.d.ts +7 -1
  20. package/dist/platform/default.js +40 -2
  21. package/dist/process/context-process.d.ts +83 -0
  22. package/dist/process/context-process.js +168 -0
  23. package/dist/process/cursor.d.ts +52 -0
  24. package/dist/process/cursor.js +144 -0
  25. package/dist/process/driver.d.ts +214 -0
  26. package/dist/process/driver.js +197 -0
  27. package/dist/process/index.d.ts +6 -0
  28. package/dist/process/index.js +6 -0
  29. package/dist/process/metadata-store.d.ts +205 -0
  30. package/dist/process/metadata-store.js +462 -0
  31. package/dist/process/output-log.d.ts +136 -0
  32. package/dist/process/output-log.js +331 -0
  33. package/dist/process/process-manager.d.ts +279 -0
  34. package/dist/process/process-manager.js +1879 -0
  35. package/dist/profile/client.js +63 -21
  36. package/dist/project/init.js +2 -2
  37. package/dist/registry/registry.js +12 -3
  38. package/dist/runtime/context.d.ts +9 -6
  39. package/dist/runtime/context.js +31 -8
  40. package/dist/runtime/process.d.ts +19 -2
  41. package/dist/runtime/process.js +103 -131
  42. package/dist/runtime/runner.d.ts +45 -24
  43. package/dist/runtime/runner.js +106 -25
  44. package/dist/target/remote.js +7 -0
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +2 -2
@@ -2,6 +2,36 @@ import { randomUUID } from "node:crypto";
2
2
  import { fork } from "node:child_process";
3
3
  import { DiagnosticForwarder } from "./diagnostic.js";
4
4
  import { EXECUTION_ABORTED, HOST_PROCESS_EXITED } from "../errors.js";
5
+ /**
6
+ * 跨进程取消信号占位标记字段名。
7
+ * AbortSignal 不可序列化,序列化 options 时把 signal 字段替换为该标记,
8
+ * 宿主侧识别后重建 AbortController 并接入 abort 消息通知链路。
9
+ */
10
+ export const IPC_SIGNAL_MARKER = "__ipcSignal";
11
+ /**
12
+ * 判断执行选项中是否携带跨进程取消信号占位标记。
13
+ *
14
+ * @param value 待检查的选项值
15
+ */
16
+ export function hasIpcSignalMarker(value) {
17
+ return (typeof value === "object" &&
18
+ value !== null &&
19
+ value[IPC_SIGNAL_MARKER] === true);
20
+ }
21
+ /**
22
+ * 序列化执行选项:剥离不可序列化的 AbortSignal 并写入占位标记。
23
+ * 未携带 signal 时返回浅拷贝,避免消息通道直接持有调用方原始对象。
24
+ *
25
+ * @param options 原始执行选项
26
+ */
27
+ function markIpcSignal(options) {
28
+ if (!options)
29
+ return undefined;
30
+ const { signal: _signal, ...rest } = options;
31
+ if (!_signal)
32
+ return rest;
33
+ return { ...rest, [IPC_SIGNAL_MARKER]: true };
34
+ }
5
35
  /**
6
36
  * 基于 Node IPC 监督进程通信通道的 ActionDockTarget 实现。
7
37
  *
@@ -126,7 +156,7 @@ export class IpcActionDockTarget {
126
156
  get process() {
127
157
  return this.child;
128
158
  }
129
- async callRemote(method, args) {
159
+ async callRemote(method, args, signal) {
130
160
  if (this.isClosed || this.exitError) {
131
161
  if (method === "runAction") {
132
162
  return {
@@ -147,6 +177,16 @@ export class IpcActionDockTarget {
147
177
  method,
148
178
  args,
149
179
  };
180
+ // 事件驱动跨进程取消:signal 存在且未中止时注册 abort 监听,
181
+ // 触发即向宿主子进程发送 abort 消息,由宿主侧中止同调用控制器;
182
+ // 调用结束(无论成败)后注销监听,避免监听器泄漏
183
+ let onAbort;
184
+ if (signal && !signal.aborted) {
185
+ onAbort = () => {
186
+ this.sendAbort(id);
187
+ };
188
+ signal.addEventListener("abort", onAbort, { once: true });
189
+ }
150
190
  return new Promise((resolve, reject) => {
151
191
  this.pendingCalls.set(id, { resolve, reject, method });
152
192
  try {
@@ -162,8 +202,31 @@ export class IpcActionDockTarget {
162
202
  this.pendingCalls.delete(id);
163
203
  reject(err);
164
204
  }
205
+ }).finally(() => {
206
+ if (onAbort && signal) {
207
+ signal.removeEventListener("abort", onAbort);
208
+ }
165
209
  });
166
210
  }
211
+ /**
212
+ * 向宿主子进程发送跨进程取消通知,发送异常不阻断父侧调用链路。
213
+ *
214
+ * @param id 目标调用的唯一标识
215
+ */
216
+ sendAbort(id) {
217
+ if (this.isClosed)
218
+ return;
219
+ try {
220
+ const abortMsg = { id, type: "abort" };
221
+ if (this.child.send) {
222
+ this.child.send(abortMsg);
223
+ }
224
+ }
225
+ catch (err) {
226
+ // 通道已断开时发送失败属预期场景:宿主退出路径会另行以结构化错误回包
227
+ process.stderr.write(`[IPC Target] Failed to send abort for call '${id}': ${err instanceof Error ? err.message : String(err)}\n`);
228
+ }
229
+ }
167
230
  async info() {
168
231
  return this.callRemote("info", []);
169
232
  }
@@ -183,8 +246,7 @@ export class IpcActionDockTarget {
183
246
  return this.callRemote("describePlaybook", [id]);
184
247
  }
185
248
  async runAction(ref, input, options) {
186
- const { signal, ...serializableOptions } = options || {};
187
- if (signal?.aborted) {
249
+ if (options?.signal?.aborted) {
188
250
  return {
189
251
  ok: false,
190
252
  runId: randomUUID(),
@@ -194,11 +256,17 @@ export class IpcActionDockTarget {
194
256
  },
195
257
  };
196
258
  }
197
- return this.callRemote("runAction", [ref, input, serializableOptions]);
259
+ // AbortSignal 不可序列化:以占位标记替换后随消息通道传递,宿主侧重建控制器
260
+ const serializableOptions = markIpcSignal(options);
261
+ return this.callRemote("runAction", [ref, input, serializableOptions], options?.signal);
198
262
  }
199
263
  async startAction(ref, input, options) {
200
- const { signal, ...serializableOptions } = options || {};
201
- return this.callRemote("startAction", [ref, input, serializableOptions]);
264
+ if (options?.signal?.aborted) {
265
+ throw new Error("Execution was aborted before starting");
266
+ }
267
+ // AbortSignal 不可序列化:以占位标记替换后随消息通道传递,宿主侧重建控制器
268
+ const serializableOptions = markIpcSignal(options);
269
+ return this.callRemote("startAction", [ref, input, serializableOptions], options?.signal);
202
270
  }
203
271
  async getRun(runId) {
204
272
  const res = await this.callRemote("getRun", [runId]);
@@ -1,3 +1,12 @@
1
+ /**
2
+ * 监督进程向宿主子进程发送的跨进程取消通知。
3
+ * id 与对应 IpcCallMessage 的调用 id 一致,宿主侧据此中止同调用的 AbortController。
4
+ */
5
+ export interface IpcAbortMessage {
6
+ id: string;
7
+ type: "abort";
8
+ reason?: string;
9
+ }
1
10
  /**
2
11
  * 监督进程与宿主子进程间调用的 IPC 请求消息。
3
12
  */
@@ -39,4 +48,4 @@ export interface IpcReadyMessage {
39
48
  /**
40
49
  * 联合 IPC 消息类型。
41
50
  */
42
- export type IpcMessage = IpcCallMessage | IpcResponseMessage | IpcEventMessage | IpcReadyMessage;
51
+ export type IpcMessage = IpcCallMessage | IpcResponseMessage | IpcAbortMessage | IpcEventMessage | IpcReadyMessage;
@@ -1,6 +1,8 @@
1
1
  import type { ProcessAPI } from "@actiondock/sdk";
2
2
  import { type Clock } from "../runtime/clock.js";
3
3
  import { type ModuleLoader } from "../runtime/module-loader.js";
4
+ import { ProcessManager } from "../process/process-manager.js";
5
+ import type { ProcessDriver } from "../process/driver.js";
4
6
  import type { FileSystem, RuntimePlatform, StorageFactory } from "./types.js";
5
7
  /**
6
8
  * 默认运行时平台构建选项。
@@ -12,8 +14,12 @@ export interface DefaultPlatformOptions {
12
14
  files?: FileSystem;
13
15
  /** 自定义时间与时钟驱动(默认使用 SystemClock) */
14
16
  clock?: Clock;
15
- /** 自定义进程执行驱动(默认使用 DefaultProcessExecutor) */
17
+ /** 自定义进程执行驱动(默认依托 ProcessManager 与对应驱动) */
16
18
  process?: ProcessAPI;
19
+ /** 可选注入的底层进程驱动 */
20
+ processDriver?: ProcessDriver;
21
+ /** 可选注入的受管进程管理器 */
22
+ processManager?: ProcessManager;
17
23
  /** 自定义源码加载驱动(默认使用 DefaultModuleLoader) */
18
24
  modules?: ModuleLoader;
19
25
  /** 自定义存储工厂(默认基于 core/storage 构造) */
@@ -1,8 +1,33 @@
1
1
  import { SystemClock } from "../runtime/clock.js";
2
2
  import { DefaultModuleLoader } from "../runtime/module-loader.js";
3
- import { DefaultProcessExecutor } from "../runtime/process.js";
3
+ import { ProcessError, UNSUPPORTED_CAPABILITY } from "../errors.js";
4
+ import { ProcessManager } from "../process/process-manager.js";
4
5
  import { createGlobalStorage as coreCreateGlobalStorage, createStorage as coreCreateStorage, } from "../storage/index.js";
5
6
  import { NodeFileSystem } from "./node-fs.js";
7
+ /**
8
+ * 构造不支持进程能力的 ProcessAPI:
9
+ * 默认平台不含进程驱动,任何进程操作均抛出明确错误,
10
+ * 指引调用方显式注入 processDriver 或改用 @actiondock/runtime-node 的 createNodePlatform。
11
+ */
12
+ function createUnsupportedProcessApi() {
13
+ const unsupported = (operation) => Promise.reject(new ProcessError(UNSUPPORTED_CAPABILITY, `The default platform does not bundle a process driver, so process.${operation} is unavailable. ` +
14
+ "Explicitly inject a processDriver (for example MemoryProcessDriver from @actiondock/core for testing) " +
15
+ "or use createNodePlatform from @actiondock/runtime-node, which provides NodeProcessDriver."));
16
+ return {
17
+ run: () => unsupported("run"),
18
+ start: () => unsupported("start"),
19
+ inspect: () => unsupported("inspect"),
20
+ list: () => unsupported("list"),
21
+ acquire: () => unsupported("acquire"),
22
+ renew: () => unsupported("renew"),
23
+ release: () => unsupported("release"),
24
+ write: () => unsupported("write"),
25
+ operation: () => unsupported("operation"),
26
+ read: () => unsupported("read"),
27
+ control: () => unsupported("control"),
28
+ stop: () => unsupported("stop"),
29
+ };
30
+ }
6
31
  /**
7
32
  * 创建默认 RuntimePlatform 平台实例。
8
33
  * 直接基于 Node 原生与 Core 内核能力组装平台,禁止任何全局单例状态。
@@ -12,7 +37,20 @@ export function createDefaultPlatform(options = {}) {
12
37
  const clock = options.clock ?? new SystemClock();
13
38
  const files = options.files ?? new NodeFileSystem({ rootDir: options.rootDir });
14
39
  const modules = options.modules ?? new DefaultModuleLoader();
15
- const process = options.process ?? new DefaultProcessExecutor();
40
+ // 未注入底层驱动时不静默回退内存模拟驱动:默认平台不提供可用进程能力,
41
+ // 避免默认路径下的进程调用无声挂死或必然超时
42
+ const processDriver = options.processDriver;
43
+ const processManager = options.processManager ??
44
+ (processDriver ? new ProcessManager({ driver: processDriver }) : undefined);
45
+ const process = options.process ??
46
+ (processManager
47
+ ? processManager.forOwner({
48
+ tenantId: "default",
49
+ principalId: "default",
50
+ packageInstanceId: "default",
51
+ generationId: "default",
52
+ })
53
+ : createUnsupportedProcessApi());
16
54
  const storage = options.storage ?? {
17
55
  createStorage(packageId, opts) {
18
56
  return coreCreateStorage(packageId, {
@@ -0,0 +1,83 @@
1
+ import type { CallOptions, ControlGrant, OperationReceipt, ProcessAcquireInput, ProcessAPI, ProcessControlInput, ProcessInfo, ProcessListInput, ProcessListResult, ProcessReadInput, ProcessRunInput, ProcessRunResult, ProcessStartInput, ProcessStartResult, ProcessStopInput, ProcessWriteInput, ReadResult } from "@actiondock/sdk";
2
+ import type { ProcessManager, ProcessOwner } from "./process-manager.js";
3
+ /**
4
+ * 绑定当前 ActionContext 上下文凭据与运行标识的 ProcessAPI 适配器实现。
5
+ *
6
+ * 核心保证:
7
+ * - 自动注入当前运行上下文的归属所有者与运行标识。
8
+ * - 跟踪由当前 Run 申请成功持有的控制权令牌。
9
+ * - 拦截 Run 退出或异常生命周期:若当前 Run 未显式 release 释放控制权,自动撤销 grant 并将目标进程置入隔离状态 quarantined。
10
+ */
11
+ export declare class ContextProcessAPI implements ProcessAPI {
12
+ private readonly heldProcesses;
13
+ private isDisposed;
14
+ private onAbortHandler?;
15
+ readonly processManager: ProcessManager;
16
+ readonly owner: ProcessOwner;
17
+ readonly runId: string;
18
+ readonly signal?: AbortSignal;
19
+ /** 是否为运行级作用域凭据:缺省时依据构造时是否显式传入 runId 判定 */
20
+ readonly runScoped: boolean;
21
+ constructor(processManager: ProcessManager, owner: ProcessOwner, runId: string, signal?: AbortSignal, runScoped?: boolean);
22
+ /**
23
+ * 底层进程管理器实例(协同入口的公开只读访问)。
24
+ */
25
+ get manager(): ProcessManager;
26
+ /**
27
+ * 一次性运行外部命令。
28
+ */
29
+ run(input: ProcessRunInput, call?: CallOptions): Promise<ProcessRunResult>;
30
+ /**
31
+ * 启动新的受管进程资源。
32
+ */
33
+ start(input: ProcessStartInput, call?: CallOptions): Promise<ProcessStartResult>;
34
+ /**
35
+ * 查看指定受管进程资源的状态快照。
36
+ */
37
+ inspect(id: string, call?: CallOptions): Promise<ProcessInfo>;
38
+ /**
39
+ * 分页列出当前所有者可见的受管进程列表。
40
+ */
41
+ list(input: ProcessListInput, call?: CallOptions): Promise<ProcessListResult>;
42
+ /**
43
+ * 申请指定受管进程的独占控制令牌。
44
+ */
45
+ acquire(id: string, input: ProcessAcquireInput, call?: CallOptions): Promise<ControlGrant>;
46
+ /**
47
+ * 延长当前有效控制令牌的存活时间。
48
+ */
49
+ renew(id: string, token: string, ttlMs: number, call?: CallOptions): Promise<ControlGrant>;
50
+ /**
51
+ * 显式释放控制令牌。
52
+ */
53
+ release(id: string, token: string, call?: CallOptions): Promise<void>;
54
+ /**
55
+ * 向受管进程输入流写入原始字节数据。
56
+ */
57
+ write(id: string, input: ProcessWriteInput, call?: CallOptions): Promise<OperationReceipt>;
58
+ /**
59
+ * 查询指定请求标识的操作执行收据。
60
+ */
61
+ operation(id: string, requestId: string, call?: CallOptions): Promise<OperationReceipt>;
62
+ /**
63
+ * 按游标读取受管进程输出流。
64
+ */
65
+ read(id: string, input: ProcessReadInput, call?: CallOptions): Promise<ReadResult>;
66
+ /**
67
+ * 向受管进程发送结构化控制指令。
68
+ */
69
+ control(id: string, input: ProcessControlInput, call?: CallOptions): Promise<OperationReceipt>;
70
+ /**
71
+ * 终止指定的受管进程资源。
72
+ */
73
+ stop(id: string, input: ProcessStopInput, call?: CallOptions): Promise<ProcessInfo>;
74
+ /**
75
+ * 拦截 Action Run 生命周期终结:
76
+ * 若当前 Run 持有进程控制权且未显式 release,自动撤销 grant 并将目标进程置入隔离状态。
77
+ */
78
+ dispose(): Promise<void>;
79
+ /**
80
+ * 合并上下文取消信号与单次调用选项中的取消信号。
81
+ */
82
+ private mergeCallOptions;
83
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * 绑定当前 ActionContext 上下文凭据与运行标识的 ProcessAPI 适配器实现。
3
+ *
4
+ * 核心保证:
5
+ * - 自动注入当前运行上下文的归属所有者与运行标识。
6
+ * - 跟踪由当前 Run 申请成功持有的控制权令牌。
7
+ * - 拦截 Run 退出或异常生命周期:若当前 Run 未显式 release 释放控制权,自动撤销 grant 并将目标进程置入隔离状态 quarantined。
8
+ */
9
+ export class ContextProcessAPI {
10
+ heldProcesses = new Map();
11
+ isDisposed = false;
12
+ onAbortHandler;
13
+ processManager;
14
+ owner;
15
+ runId;
16
+ signal;
17
+ /** 是否为运行级作用域凭据:缺省时依据构造时是否显式传入 runId 判定 */
18
+ runScoped;
19
+ constructor(processManager, owner, runId, signal, runScoped) {
20
+ this.processManager = processManager;
21
+ this.owner = owner;
22
+ this.runId = runId;
23
+ this.signal = signal;
24
+ this.runScoped = runScoped ?? Boolean(runId);
25
+ if (this.signal) {
26
+ if (this.signal.aborted) {
27
+ void this.dispose();
28
+ }
29
+ else {
30
+ this.onAbortHandler = () => {
31
+ void this.dispose();
32
+ };
33
+ this.signal.addEventListener("abort", this.onAbortHandler, { once: true });
34
+ }
35
+ }
36
+ }
37
+ /**
38
+ * 底层进程管理器实例(协同入口的公开只读访问)。
39
+ */
40
+ get manager() {
41
+ return this.processManager;
42
+ }
43
+ /**
44
+ * 一次性运行外部命令。
45
+ */
46
+ async run(input, call) {
47
+ return this.processManager.run(this.owner, input, this.mergeCallOptions(call));
48
+ }
49
+ /**
50
+ * 启动新的受管进程资源。
51
+ */
52
+ async start(input, call) {
53
+ return this.processManager.start(this.owner, input, this.mergeCallOptions(call));
54
+ }
55
+ /**
56
+ * 查看指定受管进程资源的状态快照。
57
+ */
58
+ async inspect(id, call) {
59
+ return this.processManager.inspect(this.owner, id, this.mergeCallOptions(call));
60
+ }
61
+ /**
62
+ * 分页列出当前所有者可见的受管进程列表。
63
+ */
64
+ async list(input, call) {
65
+ return this.processManager.list(this.owner, input, this.mergeCallOptions(call));
66
+ }
67
+ /**
68
+ * 申请指定受管进程的独占控制令牌。
69
+ */
70
+ async acquire(id, input, call) {
71
+ const grant = await this.processManager.acquire(this.owner, id, input, this.mergeCallOptions(call), this.runId);
72
+ this.heldProcesses.set(id, grant.token);
73
+ return grant;
74
+ }
75
+ /**
76
+ * 延长当前有效控制令牌的存活时间。
77
+ */
78
+ async renew(id, token, ttlMs, call) {
79
+ const renewed = await this.processManager.renew(this.owner, id, token, ttlMs, this.mergeCallOptions(call));
80
+ this.heldProcesses.set(id, renewed.token);
81
+ return renewed;
82
+ }
83
+ /**
84
+ * 显式释放控制令牌。
85
+ */
86
+ async release(id, token, call) {
87
+ await this.processManager.release(this.owner, id, token, this.mergeCallOptions(call));
88
+ this.heldProcesses.delete(id);
89
+ }
90
+ /**
91
+ * 向受管进程输入流写入原始字节数据。
92
+ */
93
+ async write(id, input, call) {
94
+ return this.processManager.write(this.owner, id, input, this.mergeCallOptions(call));
95
+ }
96
+ /**
97
+ * 查询指定请求标识的操作执行收据。
98
+ */
99
+ async operation(id, requestId, call) {
100
+ return this.processManager.operation(this.owner, id, requestId, this.mergeCallOptions(call));
101
+ }
102
+ /**
103
+ * 按游标读取受管进程输出流。
104
+ */
105
+ async read(id, input, call) {
106
+ return this.processManager.read(this.owner, id, input, this.mergeCallOptions(call));
107
+ }
108
+ /**
109
+ * 向受管进程发送结构化控制指令。
110
+ */
111
+ async control(id, input, call) {
112
+ return this.processManager.control(this.owner, id, input, this.mergeCallOptions(call));
113
+ }
114
+ /**
115
+ * 终止指定的受管进程资源。
116
+ */
117
+ async stop(id, input, call) {
118
+ const info = await this.processManager.stop(this.owner, id, input, this.mergeCallOptions(call));
119
+ this.heldProcesses.delete(id);
120
+ return info;
121
+ }
122
+ /**
123
+ * 拦截 Action Run 生命周期终结:
124
+ * 若当前 Run 持有进程控制权且未显式 release,自动撤销 grant 并将目标进程置入隔离状态。
125
+ */
126
+ async dispose() {
127
+ if (this.isDisposed) {
128
+ return;
129
+ }
130
+ this.isDisposed = true;
131
+ if (this.signal && this.onAbortHandler) {
132
+ this.signal.removeEventListener("abort", this.onAbortHandler);
133
+ this.onAbortHandler = undefined;
134
+ }
135
+ if (this.heldProcesses.size === 0) {
136
+ return;
137
+ }
138
+ const pendingEntries = Array.from(this.heldProcesses.entries());
139
+ this.heldProcesses.clear();
140
+ for (const [processId, token] of pendingEntries) {
141
+ try {
142
+ await this.processManager.quarantineProcess(this.owner, processId, token, "Run terminated without explicitly releasing control");
143
+ }
144
+ catch {
145
+ // 忽略终结清理阶段次级异常
146
+ }
147
+ }
148
+ }
149
+ /**
150
+ * 合并上下文取消信号与单次调用选项中的取消信号。
151
+ */
152
+ mergeCallOptions(call) {
153
+ if (!this.signal && !call?.signal) {
154
+ return call;
155
+ }
156
+ if (this.signal && !call?.signal) {
157
+ return { ...call, signal: this.signal };
158
+ }
159
+ if (!this.signal && call?.signal) {
160
+ return call;
161
+ }
162
+ // 两个信号同时存在时合成单一信号
163
+ const combinedSignal = AbortSignal.any
164
+ ? AbortSignal.any([this.signal, call.signal])
165
+ : call.signal;
166
+ return { ...call, signal: combinedSignal };
167
+ }
168
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 已解析的不透明游标完整载荷。
3
+ */
4
+ export interface ParsedCursor {
5
+ /** 宿主纪元标识 */
6
+ hostEpoch: string;
7
+ /** 受管进程标识 */
8
+ processId: string;
9
+ /** 日志记录全局单调自增序号 */
10
+ sequence: number;
11
+ /** 记录内字节偏移量 */
12
+ offset: number;
13
+ }
14
+ /**
15
+ * 游标逻辑位置坐标。
16
+ */
17
+ export interface CursorPosition {
18
+ /** 日志记录全局单调自增序号 */
19
+ sequence: number;
20
+ /** 记录内字节偏移量 */
21
+ offset: number;
22
+ }
23
+ /**
24
+ * 将内部字段编码为外部不透明游标字符串。
25
+ *
26
+ * 规范约束:
27
+ * - 宿主纪元标识与进程标识必须为非空字符串。
28
+ * - 序号与偏移量必须为非负整数。
29
+ * - 游标对外表现为不透明字符串,调用方严禁假定其格式或拆解字段。
30
+ */
31
+ export declare function encodeCursor(hostEpoch: string, processId: string, sequence: number, offset: number): string;
32
+ /**
33
+ * 解析不透明游标原始载荷。
34
+ *
35
+ * 遇到格式损坏、非法字段或无法解析的游标均抛出 INVALID_CURSOR 异常。
36
+ */
37
+ export declare function decodeCursor(cursorStr: string): ParsedCursor;
38
+ /**
39
+ * 校验并解析游标,校验其宿主纪元与受管进程标识与当前目标是否严格匹配。
40
+ *
41
+ * 遇到格式错误、跨宿主纪元或跨受管进程的游标,抛出 INVALID_CURSOR 异常。
42
+ */
43
+ export declare function parseCursor(cursorStr: string, expectedHostEpoch: string, expectedProcessId: string): CursorPosition;
44
+ /**
45
+ * 比较两个游标坐标的先后次序。
46
+ *
47
+ * 返回值:
48
+ * - 负数:a 位于 b 之前。
49
+ * - 0:a 与 b 处于同一位置。
50
+ * - 正数:a 位于 b 之后。
51
+ */
52
+ export declare function compareCursorPos(a: CursorPosition, b: CursorPosition): number;
@@ -0,0 +1,144 @@
1
+ import { INVALID_CURSOR, ProcessError } from "../errors.js";
2
+ function toBase64Url(str) {
3
+ if (typeof Buffer !== "undefined") {
4
+ return Buffer.from(str, "utf8").toString("base64url");
5
+ }
6
+ const bytes = new TextEncoder().encode(str);
7
+ let binary = "";
8
+ for (let i = 0; i < bytes.byteLength; i++) {
9
+ binary += String.fromCharCode(bytes[i]);
10
+ }
11
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
12
+ }
13
+ function fromBase64Url(base64url) {
14
+ if (typeof Buffer !== "undefined") {
15
+ return Buffer.from(base64url, "base64url").toString("utf8");
16
+ }
17
+ let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
18
+ while (base64.length % 4) {
19
+ base64 += "=";
20
+ }
21
+ const binary = atob(base64);
22
+ const bytes = new Uint8Array(binary.length);
23
+ for (let i = 0; i < binary.length; i++) {
24
+ bytes[i] = binary.charCodeAt(i);
25
+ }
26
+ return new TextDecoder("utf-8").decode(bytes);
27
+ }
28
+ /**
29
+ * 将内部字段编码为外部不透明游标字符串。
30
+ *
31
+ * 规范约束:
32
+ * - 宿主纪元标识与进程标识必须为非空字符串。
33
+ * - 序号与偏移量必须为非负整数。
34
+ * - 游标对外表现为不透明字符串,调用方严禁假定其格式或拆解字段。
35
+ */
36
+ export function encodeCursor(hostEpoch, processId, sequence, offset) {
37
+ if (!hostEpoch || typeof hostEpoch !== "string") {
38
+ throw new ProcessError(INVALID_CURSOR, "Host epoch must be a non-empty string", { hostEpoch });
39
+ }
40
+ if (!processId || typeof processId !== "string") {
41
+ throw new ProcessError(INVALID_CURSOR, "Process ID must be a non-empty string", { processId });
42
+ }
43
+ if (!Number.isInteger(sequence) || sequence < 0) {
44
+ throw new ProcessError(INVALID_CURSOR, "Sequence must be a non-negative integer", { sequence });
45
+ }
46
+ if (!Number.isInteger(offset) || offset < 0) {
47
+ throw new ProcessError(INVALID_CURSOR, "Offset must be a non-negative integer", { offset });
48
+ }
49
+ const payload = JSON.stringify({
50
+ v: 1,
51
+ e: hostEpoch,
52
+ p: processId,
53
+ s: sequence,
54
+ o: offset,
55
+ });
56
+ return `cur_${toBase64Url(payload)}`;
57
+ }
58
+ /**
59
+ * 解析不透明游标原始载荷。
60
+ *
61
+ * 遇到格式损坏、非法字段或无法解析的游标均抛出 INVALID_CURSOR 异常。
62
+ */
63
+ export function decodeCursor(cursorStr) {
64
+ if (typeof cursorStr !== "string" || cursorStr.trim() === "") {
65
+ throw new ProcessError(INVALID_CURSOR, "Cursor must be a non-empty string", { cursor: cursorStr });
66
+ }
67
+ const raw = cursorStr.startsWith("cur_") ? cursorStr.slice(4) : cursorStr;
68
+ let decodedStr;
69
+ try {
70
+ decodedStr = fromBase64Url(raw);
71
+ }
72
+ catch (err) {
73
+ throw new ProcessError(INVALID_CURSOR, "Failed to decode cursor base64url payload", {
74
+ cursor: cursorStr,
75
+ cause: err instanceof Error ? err.message : String(err),
76
+ });
77
+ }
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(decodedStr);
81
+ }
82
+ catch (err) {
83
+ throw new ProcessError(INVALID_CURSOR, "Failed to parse cursor JSON payload", {
84
+ cursor: cursorStr,
85
+ cause: err instanceof Error ? err.message : String(err),
86
+ });
87
+ }
88
+ if (!parsed ||
89
+ typeof parsed !== "object" ||
90
+ typeof parsed.e !== "string" ||
91
+ typeof parsed.p !== "string" ||
92
+ !Number.isInteger(parsed.s) ||
93
+ parsed.s < 0 ||
94
+ !Number.isInteger(parsed.o) ||
95
+ parsed.o < 0) {
96
+ throw new ProcessError(INVALID_CURSOR, "Malformed cursor structure", { cursor: cursorStr });
97
+ }
98
+ return {
99
+ hostEpoch: parsed.e,
100
+ processId: parsed.p,
101
+ sequence: parsed.s,
102
+ offset: parsed.o,
103
+ };
104
+ }
105
+ /**
106
+ * 校验并解析游标,校验其宿主纪元与受管进程标识与当前目标是否严格匹配。
107
+ *
108
+ * 遇到格式错误、跨宿主纪元或跨受管进程的游标,抛出 INVALID_CURSOR 异常。
109
+ */
110
+ export function parseCursor(cursorStr, expectedHostEpoch, expectedProcessId) {
111
+ const decoded = decodeCursor(cursorStr);
112
+ if (decoded.hostEpoch !== expectedHostEpoch) {
113
+ throw new ProcessError(INVALID_CURSOR, `Cursor host epoch mismatch: expected '${expectedHostEpoch}', got '${decoded.hostEpoch}'`, {
114
+ cursor: cursorStr,
115
+ expectedHostEpoch,
116
+ actualHostEpoch: decoded.hostEpoch,
117
+ });
118
+ }
119
+ if (decoded.processId !== expectedProcessId) {
120
+ throw new ProcessError(INVALID_CURSOR, `Cursor process ID mismatch: expected '${expectedProcessId}', got '${decoded.processId}'`, {
121
+ cursor: cursorStr,
122
+ expectedProcessId,
123
+ actualProcessId: decoded.processId,
124
+ });
125
+ }
126
+ return {
127
+ sequence: decoded.sequence,
128
+ offset: decoded.offset,
129
+ };
130
+ }
131
+ /**
132
+ * 比较两个游标坐标的先后次序。
133
+ *
134
+ * 返回值:
135
+ * - 负数:a 位于 b 之前。
136
+ * - 0:a 与 b 处于同一位置。
137
+ * - 正数:a 位于 b 之后。
138
+ */
139
+ export function compareCursorPos(a, b) {
140
+ if (a.sequence !== b.sequence) {
141
+ return a.sequence - b.sequence;
142
+ }
143
+ return a.offset - b.offset;
144
+ }