@feng0531/dsh-execution-bus 0.5.1 → 0.7.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.
@@ -0,0 +1,57 @@
1
+ /** 集中全部固定错误码。值即协议 v1 的稳定错误词汇,不得重命名或改动。
2
+ * 新增错误码前先更新 docs/protocol.md 的错误词汇表。 */
3
+ export declare const ERRORS: Readonly<{
4
+ INVALID_OBJECT: "INVALID_OBJECT";
5
+ UNKNOWN_FIELD: "UNKNOWN_FIELD";
6
+ UNSUPPORTED_VERSION: "UNSUPPORTED_VERSION";
7
+ INVALID_ID: "INVALID_ID";
8
+ UNSUPPORTED_TYPE: "UNSUPPORTED_TYPE";
9
+ MESSAGE_TOO_LARGE: "MESSAGE_TOO_LARGE";
10
+ JSON_LIMIT: "JSON_LIMIT";
11
+ INVALID_JSON: "INVALID_JSON";
12
+ CYCLIC_JSON: "CYCLIC_JSON";
13
+ INVALID_JSON_ARRAY: "INVALID_JSON_ARRAY";
14
+ INVALID_JSON_OBJECT: "INVALID_JSON_OBJECT";
15
+ INVALID_JSON_KEY: "INVALID_JSON_KEY";
16
+ INVALID_JSON_PROPERTY: "INVALID_JSON_PROPERTY";
17
+ CLOSED: "CLOSED";
18
+ EXPECTED_REQUEST: "EXPECTED_REQUEST";
19
+ INVALID_REQUEST: "INVALID_REQUEST";
20
+ REQUEST_CONFLICT: "REQUEST_CONFLICT";
21
+ MESSAGE_CONFLICT: "MESSAGE_CONFLICT";
22
+ MESSAGE_LIMIT: "MESSAGE_LIMIT";
23
+ UNKNOWN_HANDLER: "UNKNOWN_HANDLER";
24
+ BACKPRESSURE: "BACKPRESSURE";
25
+ SESSION_BACKPRESSURE: "SESSION_BACKPRESSURE";
26
+ WAITER_LIMIT: "WAITER_LIMIT";
27
+ EXPECTED_CONTROL: "EXPECTED_CONTROL";
28
+ INVALID_CONTROL: "INVALID_CONTROL";
29
+ NOT_FOUND: "NOT_FOUND";
30
+ UNTRUSTED_CONTEXT: "UNTRUSTED_CONTEXT";
31
+ OWNING_SESSION_REQUIRED: "OWNING_SESSION_REQUIRED";
32
+ QUEUE_WAIT_TIMEOUT: "QUEUE_WAIT_TIMEOUT";
33
+ RESULT_NOT_RETAINED: "RESULT_NOT_RETAINED";
34
+ CANCELLED: "CANCELLED";
35
+ CANCELLATION_UNCONFIRMED: "CANCELLATION_UNCONFIRMED";
36
+ HANDLER_FAILED: "HANDLER_FAILED";
37
+ INVALID_ADMISSION_POLICY: "INVALID_ADMISSION_POLICY";
38
+ INVALID_OPTIONS: "INVALID_OPTIONS";
39
+ DUPLICATE_HANDLER: "DUPLICATE_HANDLER";
40
+ MUTATING_REUSE_DENIED: "MUTATING_REUSE_DENIED";
41
+ DUPLICATE_EXECUTION_HANDLE: "DUPLICATE_EXECUTION_HANDLE";
42
+ INVALID_STATE_QUERY: "INVALID_STATE_QUERY";
43
+ HOST_READ_FAILED: "HOST_READ_FAILED";
44
+ HOST_TOOL_FAILED: "HOST_TOOL_FAILED";
45
+ HOST_JOB_UNAVAILABLE: "HOST_JOB_UNAVAILABLE";
46
+ PTC_PRESET_TOO_LARGE: "PTC_PRESET_TOO_LARGE";
47
+ PTC_PRESET_ALREADY_CONTAINS_BUS: "PTC_PRESET_ALREADY_CONTAINS_BUS";
48
+ PTC_PRESET_RELATIVE_BASE_UNSUPPORTED: "PTC_PRESET_RELATIVE_BASE_UNSUPPORTED";
49
+ PTC_PRESET_UNAVAILABLE: "PTC_PRESET_UNAVAILABLE";
50
+ INVALID_TEST_INPUT: "INVALID_TEST_INPUT";
51
+ INVALID_READ_INPUT: "INVALID_READ_INPUT";
52
+ INVALID_EDIT_INPUT: "INVALID_EDIT_INPUT";
53
+ INVALID_WRITE_INPUT: "INVALID_WRITE_INPUT";
54
+ READ_LIMIT: "READ_LIMIT";
55
+ CONTROLLED_FAILURE: "CONTROLLED_FAILURE";
56
+ HOST_CAPABILITY_REQUIRED: "HOST_CAPABILITY_REQUIRED";
57
+ }>;
@@ -0,0 +1,63 @@
1
+ /** 集中全部固定错误码。值即协议 v1 的稳定错误词汇,不得重命名或改动。
2
+ * 新增错误码前先更新 docs/protocol.md 的错误词汇表。 */
3
+ export const ERRORS = Object.freeze({
4
+ // —— 协议解析(protocol) ——
5
+ INVALID_OBJECT: "INVALID_OBJECT",
6
+ UNKNOWN_FIELD: "UNKNOWN_FIELD",
7
+ UNSUPPORTED_VERSION: "UNSUPPORTED_VERSION",
8
+ INVALID_ID: "INVALID_ID",
9
+ UNSUPPORTED_TYPE: "UNSUPPORTED_TYPE",
10
+ MESSAGE_TOO_LARGE: "MESSAGE_TOO_LARGE",
11
+ // —— 严格 JSON(json) ——
12
+ JSON_LIMIT: "JSON_LIMIT",
13
+ INVALID_JSON: "INVALID_JSON",
14
+ CYCLIC_JSON: "CYCLIC_JSON",
15
+ INVALID_JSON_ARRAY: "INVALID_JSON_ARRAY",
16
+ INVALID_JSON_OBJECT: "INVALID_JSON_OBJECT",
17
+ INVALID_JSON_KEY: "INVALID_JSON_KEY",
18
+ INVALID_JSON_PROPERTY: "INVALID_JSON_PROPERTY",
19
+ // —— 总线调度与准入(bus) ——
20
+ CLOSED: "CLOSED",
21
+ EXPECTED_REQUEST: "EXPECTED_REQUEST",
22
+ INVALID_REQUEST: "INVALID_REQUEST",
23
+ REQUEST_CONFLICT: "REQUEST_CONFLICT",
24
+ MESSAGE_CONFLICT: "MESSAGE_CONFLICT",
25
+ MESSAGE_LIMIT: "MESSAGE_LIMIT",
26
+ UNKNOWN_HANDLER: "UNKNOWN_HANDLER",
27
+ BACKPRESSURE: "BACKPRESSURE",
28
+ SESSION_BACKPRESSURE: "SESSION_BACKPRESSURE",
29
+ WAITER_LIMIT: "WAITER_LIMIT",
30
+ EXPECTED_CONTROL: "EXPECTED_CONTROL",
31
+ INVALID_CONTROL: "INVALID_CONTROL",
32
+ NOT_FOUND: "NOT_FOUND",
33
+ UNTRUSTED_CONTEXT: "UNTRUSTED_CONTEXT",
34
+ OWNING_SESSION_REQUIRED: "OWNING_SESSION_REQUIRED",
35
+ QUEUE_WAIT_TIMEOUT: "QUEUE_WAIT_TIMEOUT",
36
+ RESULT_NOT_RETAINED: "RESULT_NOT_RETAINED",
37
+ CANCELLED: "CANCELLED",
38
+ CANCELLATION_UNCONFIRMED: "CANCELLATION_UNCONFIRMED",
39
+ HANDLER_FAILED: "HANDLER_FAILED",
40
+ INVALID_ADMISSION_POLICY: "INVALID_ADMISSION_POLICY",
41
+ INVALID_OPTIONS: "INVALID_OPTIONS",
42
+ DUPLICATE_HANDLER: "DUPLICATE_HANDLER",
43
+ MUTATING_REUSE_DENIED: "MUTATING_REUSE_DENIED",
44
+ // —— 宿主投影(execution / plugin) ——
45
+ DUPLICATE_EXECUTION_HANDLE: "DUPLICATE_EXECUTION_HANDLE",
46
+ INVALID_STATE_QUERY: "INVALID_STATE_QUERY",
47
+ HOST_READ_FAILED: "HOST_READ_FAILED",
48
+ HOST_TOOL_FAILED: "HOST_TOOL_FAILED",
49
+ HOST_JOB_UNAVAILABLE: "HOST_JOB_UNAVAILABLE",
50
+ // —— 预设派生(preset) ——
51
+ PTC_PRESET_TOO_LARGE: "PTC_PRESET_TOO_LARGE",
52
+ PTC_PRESET_ALREADY_CONTAINS_BUS: "PTC_PRESET_ALREADY_CONTAINS_BUS",
53
+ PTC_PRESET_RELATIVE_BASE_UNSUPPORTED: "PTC_PRESET_RELATIVE_BASE_UNSUPPORTED",
54
+ PTC_PRESET_UNAVAILABLE: "PTC_PRESET_UNAVAILABLE",
55
+ // —— 内置 Handler(handlers) ——
56
+ INVALID_TEST_INPUT: "INVALID_TEST_INPUT",
57
+ INVALID_READ_INPUT: "INVALID_READ_INPUT",
58
+ INVALID_EDIT_INPUT: "INVALID_EDIT_INPUT",
59
+ INVALID_WRITE_INPUT: "INVALID_WRITE_INPUT",
60
+ READ_LIMIT: "READ_LIMIT",
61
+ CONTROLLED_FAILURE: "CONTROLLED_FAILURE",
62
+ HOST_CAPABILITY_REQUIRED: "HOST_CAPABILITY_REQUIRED",
63
+ });
@@ -22,6 +22,10 @@ export declare class ExecutionHandle {
22
22
  progress(event: Event): void;
23
23
  private absent;
24
24
  private waitAndClean;
25
+ /** 等待宿主 Job 到达终态(离开 running/stopping)。 */
26
+ private awaitTerminal;
27
+ /** wait 失败后的有界恢复:清理已终态的 Job,但仍向上保留原始错误可见性。 */
28
+ private recoverOnWaitError;
25
29
  }
26
30
  export type ShutdownPhase = "open" | "stopping" | "draining" | "releasing" | "closed";
27
31
  /** Owns host projections and active deliveries, not a second execution state machine. */
@@ -56,6 +60,10 @@ export declare class ExecutionOwner {
56
60
  get phase(): ShutdownPhase;
57
61
  get(key: string): ExecutionHandle | undefined;
58
62
  add(key: string, handle: ExecutionHandle): void;
63
+ /** 拒绝关闭后或重复 key 的准入;create/add 共用这一道守卫。 */
64
+ private assertAdmittable;
65
+ /** 登记 handle,并在其结算后从表中移除该登记。 */
66
+ private attach;
59
67
  track<T>(delivery: Promise<T>): Promise<T>;
60
68
  close(drainCore: () => Promise<void>): Promise<void>;
61
69
  }
package/dist/execution.js CHANGED
@@ -1,5 +1,17 @@
1
1
  import { setTimeout as pause } from "node:timers/promises";
2
2
  import { Diagnostics } from "./diagnostics.js";
3
+ import { ERRORS } from "./error-codes.js";
4
+ /** 把执行终态映射为宿主 Job 的结果状态。 */
5
+ function jobOutcome(state) {
6
+ return {
7
+ status: state === "succeeded"
8
+ ? "completed"
9
+ : state === "cancelled"
10
+ ? "killed"
11
+ : "failed",
12
+ detail: state,
13
+ };
14
+ }
3
15
  /** One host projection of a logical execution. The core completion remains authoritative. */
4
16
  export class ExecutionHandle {
5
17
  jobs;
@@ -18,14 +30,7 @@ export class ExecutionHandle {
18
30
  this.telemetry = telemetry;
19
31
  this.trace = telemetry.begin();
20
32
  this.completion = execution.done;
21
- const outcome = this.completion.then((v) => ({
22
- status: v.state === "succeeded"
23
- ? "completed"
24
- : v.state === "cancelled"
25
- ? "killed"
26
- : "failed",
27
- detail: v.state,
28
- }));
33
+ const outcome = this.completion.then((v) => jobOutcome(v.state));
29
34
  this.job = jobs.start({
30
35
  kind: "execution-bus",
31
36
  label: "萧何 / Xiao He",
@@ -58,49 +63,58 @@ export class ExecutionHandle {
58
63
  }
59
64
  async waitAndClean() {
60
65
  try {
61
- for (;;) {
62
- const view = await this.jobs.wait(this.job, 60000, this.owner);
63
- if (view.status !== "running" && view.status !== "stopping")
64
- break;
65
- }
66
+ await this.awaitTerminal();
66
67
  this.jobs.remove(this.job, this.owner);
67
68
  this.telemetry.record(this.trace, "job_released");
68
69
  }
69
70
  catch (error) {
70
71
  this.telemetry.record(this.trace, "job_error");
71
- const removed = this.absent(error);
72
- // Retain the original execution, including while its caller/owner is being disposed.
73
- await this.completion;
74
- if (removed)
75
- this.telemetry.record(this.trace, "job_released");
76
- if (!removed) {
77
- for (let attempt = 0; attempt < 20; attempt++) {
78
- try {
79
- const view = this.jobs
80
- .list(this.owner)
81
- .find((v) => v.id === this.job && v.owner === this.owner);
82
- if (!view) {
83
- this.telemetry.record(this.trace, "job_released");
84
- break;
85
- }
86
- if (view.status !== "running" && view.status !== "stopping") {
87
- this.jobs.remove(this.job, this.owner);
88
- this.telemetry.record(this.trace, "job_released");
89
- break;
90
- }
91
- }
92
- catch {
93
- break;
94
- }
95
- await pause(10);
96
- }
97
- throw error;
98
- }
72
+ await this.recoverOnWaitError(error);
99
73
  }
100
74
  finally {
101
75
  this.progressHandle = undefined;
102
76
  }
103
77
  }
78
+ /** 等待宿主 Job 到达终态(离开 running/stopping)。 */
79
+ async awaitTerminal() {
80
+ for (;;) {
81
+ const view = await this.jobs.wait(this.job, 60000, this.owner);
82
+ if (view.status !== "running" && view.status !== "stopping")
83
+ return;
84
+ }
85
+ }
86
+ /** wait 失败后的有界恢复:清理已终态的 Job,但仍向上保留原始错误可见性。 */
87
+ async recoverOnWaitError(error) {
88
+ const alreadyRemoved = this.absent(error);
89
+ // 保留原执行结果,即便其调用者/owner 正在被释放。
90
+ await this.completion;
91
+ // Job 已不在列表时仅记录释放,不把 wait 错误视为未处理的清理失败。
92
+ if (alreadyRemoved) {
93
+ this.telemetry.record(this.trace, "job_released");
94
+ return;
95
+ }
96
+ for (let attempt = 0; attempt < 20; attempt++) {
97
+ try {
98
+ const view = this.jobs
99
+ .list(this.owner)
100
+ .find((v) => v.id === this.job && v.owner === this.owner);
101
+ if (!view) {
102
+ this.telemetry.record(this.trace, "job_released");
103
+ break;
104
+ }
105
+ if (view.status !== "running" && view.status !== "stopping") {
106
+ this.jobs.remove(this.job, this.owner);
107
+ this.telemetry.record(this.trace, "job_released");
108
+ break;
109
+ }
110
+ }
111
+ catch {
112
+ break;
113
+ }
114
+ await pause(10);
115
+ }
116
+ throw error;
117
+ }
104
118
  }
105
119
  /** Owns host projections and active deliveries, not a second execution state machine. */
106
120
  export class ExecutionOwner {
@@ -109,12 +123,9 @@ export class ExecutionOwner {
109
123
  closing;
110
124
  telemetry = new Diagnostics();
111
125
  create(key, jobs, owner, execution) {
112
- if (this.phase !== "open")
113
- throw Error("CLOSED");
114
- if (this.handles.has(key))
115
- throw Error("DUPLICATE_EXECUTION_HANDLE");
126
+ this.assertAdmittable(key);
116
127
  const handle = new ExecutionHandle(jobs, owner, execution, this.telemetry);
117
- this.add(key, handle);
128
+ this.attach(key, handle);
118
129
  return handle;
119
130
  }
120
131
  diagnostics() {
@@ -133,10 +144,18 @@ export class ExecutionOwner {
133
144
  return this.handles.get(key);
134
145
  }
135
146
  add(key, handle) {
147
+ this.assertAdmittable(key);
148
+ this.attach(key, handle);
149
+ }
150
+ /** 拒绝关闭后或重复 key 的准入;create/add 共用这一道守卫。 */
151
+ assertAdmittable(key) {
136
152
  if (this.phase !== "open")
137
- throw Error("CLOSED");
153
+ throw Error(ERRORS.CLOSED);
138
154
  if (this.handles.has(key))
139
- throw Error("DUPLICATE_EXECUTION_HANDLE");
155
+ throw Error(ERRORS.DUPLICATE_EXECUTION_HANDLE);
156
+ }
157
+ /** 登记 handle,并在其结算后从表中移除该登记。 */
158
+ attach(key, handle) {
140
159
  this.handles.set(key, handle);
141
160
  const release = () => {
142
161
  if (this.handles.get(key) === handle)
@@ -1,3 +1,15 @@
1
1
  import type { Handler } from "./bus.js";
2
+ /** 宿主工具转发 Handler 工厂:校验后把调用交回宿主工具管线执行。
3
+ * idempotent 由调用方按工具副作用显式声明;prepare 可补默认参数。 */
4
+ export declare function hostToolHandler(toolName: string, opts: {
5
+ idempotent: boolean;
6
+ validate(input: Record<string, unknown>): void;
7
+ prepare?(input: Record<string, unknown>): Record<string, unknown>;
8
+ }): Handler;
2
9
  export declare const testHandler: Handler;
10
+ /** 读取:无副作用,幂等,终态结果可复用。 */
3
11
  export declare const readHandler: Handler;
12
+ /** 写入:有副作用,非幂等,终态结果不可复用。 */
13
+ export declare const writeHandler: Handler;
14
+ /** 编辑:有副作用,非幂等,终态结果不可复用。 */
15
+ export declare const editHandler: Handler;
package/dist/handlers.js CHANGED
@@ -1,13 +1,30 @@
1
1
  import { setTimeout } from "node:timers/promises";
2
- import { LIMITS } from "./product.js";
2
+ import { ERRORS } from "./error-codes.js";
3
+ import { HANDLER_LIMITS } from "./product.js";
4
+ /** 宿主工具转发 Handler 工厂:校验后把调用交回宿主工具管线执行。
5
+ * idempotent 由调用方按工具副作用显式声明;prepare 可补默认参数。 */
6
+ export function hostToolHandler(toolName, opts) {
7
+ // toolName 只表达意图;实际宿主工具已由宿主层按 handler 名绑定到 capability。
8
+ void toolName;
9
+ return {
10
+ idempotent: opts.idempotent,
11
+ validate: opts.validate,
12
+ async run(input, ctx) {
13
+ if (!ctx.invoke)
14
+ throw Error(ERRORS.HOST_CAPABILITY_REQUIRED);
15
+ return ctx.invoke(opts.prepare ? opts.prepare(input) : input, ctx.signal);
16
+ },
17
+ };
18
+ }
3
19
  export const testHandler = {
20
+ idempotent: true,
4
21
  validate(input) {
5
22
  if (Object.keys(input).some((k) => !["delayMs", "fail"].includes(k)) ||
6
23
  !Number.isSafeInteger(input.delayMs) ||
7
24
  Number(input.delayMs) < 0 ||
8
- Number(input.delayMs) > LIMITS.diagnosticDelayMs ||
25
+ Number(input.delayMs) > HANDLER_LIMITS.diagnosticDelayMs ||
9
26
  typeof input.fail !== "boolean")
10
- throw Error("INVALID_TEST_INPUT");
27
+ throw Error(ERRORS.INVALID_TEST_INPUT);
11
28
  },
12
29
  async run(input, ctx) {
13
30
  for (let i = 1; i <= 5; i++) {
@@ -17,27 +34,57 @@ export const testHandler = {
17
34
  ctx.progress(i, i * 20);
18
35
  }
19
36
  if (input.fail)
20
- throw Error("CONTROLLED_FAILURE");
37
+ throw Error(ERRORS.CONTROLLED_FAILURE);
21
38
  return { ok: true };
22
39
  },
23
40
  };
24
- export const readHandler = {
25
- validate(input) {
26
- if (Object.keys(input).some((k) => !["file_path", "offset", "limit"].includes(k)) ||
27
- typeof input.file_path !== "string" ||
28
- !input.file_path ||
29
- input.file_path.length > LIMITS.readPathChars)
30
- throw Error("INVALID_READ_INPUT");
31
- for (const k of ["offset", "limit"])
32
- if (input[k] !== undefined &&
33
- (!Number.isSafeInteger(input[k]) || Number(input[k]) < 1))
34
- throw Error("INVALID_READ_INPUT");
35
- if (Number(input.limit ?? LIMITS.readLines) > LIMITS.readLines)
36
- throw Error("READ_LIMIT");
37
- },
38
- async run(input, ctx) {
39
- if (!ctx.invoke)
40
- throw Error("HOST_CAPABILITY_REQUIRED");
41
- return ctx.invoke({ ...input, limit: input.limit ?? LIMITS.readLines }, ctx.signal);
42
- },
43
- };
41
+ function validateRead(input) {
42
+ if (Object.keys(input).some((k) => !["file_path", "offset", "limit"].includes(k)) ||
43
+ typeof input.file_path !== "string" ||
44
+ !input.file_path ||
45
+ input.file_path.length > HANDLER_LIMITS.readPathChars)
46
+ throw Error(ERRORS.INVALID_READ_INPUT);
47
+ for (const k of ["offset", "limit"])
48
+ if (input[k] !== undefined &&
49
+ (!Number.isSafeInteger(input[k]) || Number(input[k]) < 1))
50
+ throw Error(ERRORS.INVALID_READ_INPUT);
51
+ if (Number(input.limit ?? HANDLER_LIMITS.readLines) > HANDLER_LIMITS.readLines)
52
+ throw Error(ERRORS.READ_LIMIT);
53
+ }
54
+ function validateEdit(input) {
55
+ if (Object.keys(input).some((k) => !["file_path", "old_string", "new_string", "replace_all"].includes(k)) ||
56
+ typeof input.file_path !== "string" ||
57
+ !input.file_path ||
58
+ input.file_path.length > HANDLER_LIMITS.readPathChars ||
59
+ typeof input.old_string !== "string" ||
60
+ typeof input.new_string !== "string" ||
61
+ (input.replace_all !== undefined && typeof input.replace_all !== "boolean"))
62
+ throw Error(ERRORS.INVALID_EDIT_INPUT);
63
+ }
64
+ /** 读取:无副作用,幂等,终态结果可复用。 */
65
+ export const readHandler = hostToolHandler("read", {
66
+ idempotent: true,
67
+ validate: validateRead,
68
+ prepare: (input) => ({
69
+ ...input,
70
+ limit: input.limit ?? HANDLER_LIMITS.readLines,
71
+ }),
72
+ });
73
+ function validateWrite(input) {
74
+ if (Object.keys(input).some((k) => !["file_path", "content"].includes(k)) ||
75
+ typeof input.file_path !== "string" ||
76
+ !input.file_path ||
77
+ input.file_path.length > HANDLER_LIMITS.readPathChars ||
78
+ typeof input.content !== "string")
79
+ throw Error(ERRORS.INVALID_WRITE_INPUT);
80
+ }
81
+ /** 写入:有副作用,非幂等,终态结果不可复用。 */
82
+ export const writeHandler = hostToolHandler("write", {
83
+ idempotent: false,
84
+ validate: validateWrite,
85
+ });
86
+ /** 编辑:有副作用,非幂等,终态结果不可复用。 */
87
+ export const editHandler = hostToolHandler("edit", {
88
+ idempotent: false,
89
+ validate: validateEdit,
90
+ });
package/dist/json.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** Strict JSON tree validation before serialization, cloning or recursive freezing. */
2
2
  export declare function assertJson(value: unknown, depth?: number, active?: Set<object>, budget?: {
3
- left: number;
3
+ left: 65536;
4
4
  }): void;
package/dist/json.js CHANGED
@@ -1,7 +1,9 @@
1
+ import { ERRORS } from "./error-codes.js";
2
+ import { LIMITS } from "./limits.js";
1
3
  /** Strict JSON tree validation before serialization, cloning or recursive freezing. */
2
- export function assertJson(value, depth = 0, active = new Set(), budget = { left: 65536 }) {
3
- if (--budget.left < 0 || depth > 64)
4
- throw Error("JSON_LIMIT");
4
+ export function assertJson(value, depth = 0, active = new Set(), budget = { left: LIMITS.jsonNodeBudget }) {
5
+ if (--budget.left < 0 || depth > LIMITS.jsonMaxDepth)
6
+ throw Error(ERRORS.JSON_LIMIT);
5
7
  if (value === null || typeof value === "string" || typeof value === "boolean")
6
8
  return;
7
9
  if (typeof value === "number" &&
@@ -9,32 +11,32 @@ export function assertJson(value, depth = 0, active = new Set(), budget = { left
9
11
  !Object.is(value, -0))
10
12
  return;
11
13
  if (typeof value !== "object" || value === null)
12
- throw Error("INVALID_JSON");
14
+ throw Error(ERRORS.INVALID_JSON);
13
15
  if (active.has(value))
14
- throw Error("CYCLIC_JSON");
16
+ throw Error(ERRORS.CYCLIC_JSON);
15
17
  const array = Array.isArray(value);
16
18
  if (array && Object.getPrototypeOf(value) !== Array.prototype)
17
- throw Error("INVALID_JSON_ARRAY");
19
+ throw Error(ERRORS.INVALID_JSON_ARRAY);
18
20
  if (!array &&
19
21
  Object.getPrototypeOf(value) !== Object.prototype &&
20
22
  Object.getPrototypeOf(value) !== null)
21
- throw Error("INVALID_JSON_OBJECT");
23
+ throw Error(ERRORS.INVALID_JSON_OBJECT);
22
24
  active.add(value);
23
25
  try {
24
26
  const keys = Reflect.ownKeys(value);
25
27
  if (array && keys.length !== value.length + 1)
26
- throw Error("INVALID_JSON_ARRAY");
28
+ throw Error(ERRORS.INVALID_JSON_ARRAY);
27
29
  for (const key of keys) {
28
30
  if (array && key === "length")
29
31
  continue;
30
32
  if (typeof key !== "string")
31
- throw Error("INVALID_JSON_KEY");
33
+ throw Error(ERRORS.INVALID_JSON_KEY);
32
34
  if (array &&
33
35
  (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= value.length))
34
- throw Error("INVALID_JSON_ARRAY");
36
+ throw Error(ERRORS.INVALID_JSON_ARRAY);
35
37
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
36
38
  if (!descriptor.enumerable || !("value" in descriptor))
37
- throw Error("INVALID_JSON_PROPERTY");
39
+ throw Error(ERRORS.INVALID_JSON_PROPERTY);
38
40
  assertJson(descriptor.value, depth + 1, active, budget);
39
41
  }
40
42
  }
@@ -0,0 +1,20 @@
1
+ export declare const LIMITS: Readonly<{
2
+ /** 单条协议消息 JSON 的最大字节数(parse 拒绝更大消息)。 */
3
+ messageMaxBytes: 16384;
4
+ /** messageId / requestId 的最大字符数。 */
5
+ idMaxLength: 128;
6
+ /** messageId / requestId 的合法字符集。 */
7
+ idPattern: RegExp;
8
+ /** 单个逻辑执行所记账的消息条数上限(含首次、重复请求与查询)。 */
9
+ messageAccountLimit: 128;
10
+ /** 单个执行可同时附着的观察者上限。 */
11
+ waiterLimit: 32;
12
+ /** inspect() 单次返回的任务元数据条数上限。 */
13
+ inspectTaskLimit: 50;
14
+ /** 严格 JSON 树的最大嵌套深度。 */
15
+ jsonMaxDepth: 64;
16
+ /** 严格 JSON 树可访问的节点总数预算。 */
17
+ jsonNodeBudget: 65536;
18
+ /** 诊断环形轨迹的槽位数量。 */
19
+ traceRingSize: 128;
20
+ }>;
package/dist/limits.js ADDED
@@ -0,0 +1,23 @@
1
+ /** 集中定义跨模块共享的固定数值上限。内部实现约束,非用户可配置项。
2
+ * 数值相同时语义依然独立,勿为了“省一个常量”而合并。 */
3
+ const ID_MAX_LENGTH = 128;
4
+ export const LIMITS = Object.freeze({
5
+ /** 单条协议消息 JSON 的最大字节数(parse 拒绝更大消息)。 */
6
+ messageMaxBytes: 16384,
7
+ /** messageId / requestId 的最大字符数。 */
8
+ idMaxLength: ID_MAX_LENGTH,
9
+ /** messageId / requestId 的合法字符集。 */
10
+ idPattern: new RegExp(`^[a-zA-Z0-9_.:-]{1,${ID_MAX_LENGTH}}$`),
11
+ /** 单个逻辑执行所记账的消息条数上限(含首次、重复请求与查询)。 */
12
+ messageAccountLimit: 128,
13
+ /** 单个执行可同时附着的观察者上限。 */
14
+ waiterLimit: 32,
15
+ /** inspect() 单次返回的任务元数据条数上限。 */
16
+ inspectTaskLimit: 50,
17
+ /** 严格 JSON 树的最大嵌套深度。 */
18
+ jsonMaxDepth: 64,
19
+ /** 严格 JSON 树可访问的节点总数预算。 */
20
+ jsonNodeBudget: 65536,
21
+ /** 诊断环形轨迹的槽位数量。 */
22
+ traceRingSize: 128,
23
+ });