@floken-io/engine 0.0.1 → 0.0.2
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/CHANGELOG.md +31 -0
- package/dist/conformance.d.ts +1 -1
- package/dist/index.d.ts +49 -2
- package/dist/index.js +148 -6
- package/dist/{spi-BABH0Tcs.d.ts → spi-BVNWxdCQ.d.ts} +19 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,37 @@
|
|
|
3
3
|
本包遵循 [Semantic Versioning](https://semver.org/),格式参考 [Keep a Changelog](https://keepachangelog.com/)。
|
|
4
4
|
0.x 阶段跨包依赖写 `>=x.y.z <1.0.0`(不用 `^`)。
|
|
5
5
|
|
|
6
|
+
## 0.0.2 — 2026-10-02
|
|
7
|
+
|
|
8
|
+
### 新增
|
|
9
|
+
|
|
10
|
+
- ★ **宿主自定义扩展属性(ADR-009)**:`@floken-io/designer` 上配的自定义属性(`node.extension['acme:priority']`)
|
|
11
|
+
现在能被条件直接用,不必再注入 `conditionHandler` 手写回查。分两级:
|
|
12
|
+
- **第一级(恒给,无需配置)**:`ConditionCtx` 增 `nodeExtensions`(当前节点)与
|
|
13
|
+
`targetExtensions`(该分支的**目标节点**)两个只读字段。键**带前缀**原样给出,排除 `floken:*`、**只含标量**。
|
|
14
|
+
- **第二级(opt-in)**:配置 `EngineConfig.extensionVars` 后并入求值上下文,
|
|
15
|
+
挂成 `variables.node` / `variables.target`,条件可直接写 `target.priority = "high"`。
|
|
16
|
+
- ★ **类型还原可按声明做**:`extensionVars.casts`(如 `{ 'acme:slaHours': 'number' }`)。
|
|
17
|
+
起因是 XML 往返后扩展属性的值**一律是字符串**(实测 `48 → "48"`),而 `"48" > 24` 会按字符串比较;
|
|
18
|
+
引擎**不猜类型**(猜 = 静默错误),按声明转换,转不动就抛 `ENGINE_OPTION_INVALID`。
|
|
19
|
+
|
|
20
|
+
### 变更
|
|
21
|
+
|
|
22
|
+
- `ConditionUnresolved` 内部哨兵增 `toNodeId`(原先只带源节点,取不到目标节点的扩展属性)。
|
|
23
|
+
该哨兵**不是**公开 API 的一部分(引擎吞掉并重试),宿主不会收到它。
|
|
24
|
+
|
|
25
|
+
### 兼容性
|
|
26
|
+
|
|
27
|
+
- **不声明 `extensionVars` 时行为与 0.0.1 逐字一致**:只读字段是纯增量,`variables` 一个键都不多。
|
|
28
|
+
- ⚠️ 已知代价(行为已钉死、文档如实写明):**不 opt-in 却在表达式里写 `target.*`
|
|
29
|
+
→ 不报错、静默走另一条分支**(`target` 未定义时等值比较求值为 `false`,
|
|
30
|
+
与 `amount > 5000` 缺变量求值为 `null` 而抛错的语义不同)。
|
|
31
|
+
|
|
32
|
+
### 验证
|
|
33
|
+
|
|
34
|
+
- 单测 **701**(+16,新增 `test/extension-vars.test.ts`);冷启动探针 **103**(+5,跑真 `dist/`);
|
|
35
|
+
`verify` 六道门禁全绿。
|
|
36
|
+
|
|
6
37
|
## 0.0.1 — 2026-10-01
|
|
7
38
|
|
|
8
39
|
首发。令牌制流程内核 + 中国式审批动作,覆盖 `03` 的阶段 **E1~E9**(685 单测 / `verify` 六道门禁全绿 /
|
package/dist/conformance.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as StateStore, T as TaskView, a as TaskProjection, D as DefinitionSource } from './spi-
|
|
1
|
+
import { S as StateStore, T as TaskView, a as TaskProjection, D as DefinitionSource } from './spi-BVNWxdCQ.js';
|
|
2
2
|
import { ProcessDefinition } from '@floken-io/moddle';
|
|
3
3
|
|
|
4
4
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as ActionRecord, I as InstanceStateHeader, b as TaskDelta, c as Token, d as InstanceState, T as TaskView, V as VoteOutcome, e as InstanceParent, E as EngineEvent, f as TimeoutSpec, g as TraceEntry, h as InstanceStatus, i as EventSink, j as InstanceEventName, S as StateStore, D as DefinitionSource, a as TaskProjection, k as ApproverSource, l as ServiceHandler, m as AuthResolver, F as FormProvider, C as ConditionHandler, n as DecisionHandler, o as Scheduler, p as ConditionCtx } from './spi-
|
|
2
|
-
export { q as ApproverCtx, r as AuditEntry, s as DecisionCtx, t as ENGINE_EVENT_NAMES, u as EngineEventName, v as FormCtx, w as INSTANCE_EVENT_NAMES, x as INSTANCE_STATUSES, y as InstanceEvent, z as InstanceStateBody, B as SPI_GROUPS, G as SPI_NAMES, H as STATE_SCHEMA_VERSION, J as ScheduleRequest, K as ServiceCtx, L as ServiceHandlerFn, M as SpiInterfaces, N as SpiName, O as TASK_EVENT_NAMES, P as TASK_STATUSES, Q as TERMINAL_STATUSES, R as TOKEN_STATES, U as TaskEvent, W as TaskEventName, X as TaskStatus, Y as TokenAwait, Z as TokenState, _ as isInstanceEvent, $ as isTaskEvent, a0 as isTerminalStatus, a1 as subjectTokenOf } from './spi-
|
|
1
|
+
import { A as ActionRecord, I as InstanceStateHeader, b as TaskDelta, c as Token, d as InstanceState, T as TaskView, V as VoteOutcome, e as InstanceParent, E as EngineEvent, f as TimeoutSpec, g as TraceEntry, h as InstanceStatus, i as EventSink, j as InstanceEventName, S as StateStore, D as DefinitionSource, a as TaskProjection, k as ApproverSource, l as ServiceHandler, m as AuthResolver, F as FormProvider, C as ConditionHandler, n as DecisionHandler, o as Scheduler, p as ConditionCtx } from './spi-BVNWxdCQ.js';
|
|
2
|
+
export { q as ApproverCtx, r as AuditEntry, s as DecisionCtx, t as ENGINE_EVENT_NAMES, u as EngineEventName, v as FormCtx, w as INSTANCE_EVENT_NAMES, x as INSTANCE_STATUSES, y as InstanceEvent, z as InstanceStateBody, B as SPI_GROUPS, G as SPI_NAMES, H as STATE_SCHEMA_VERSION, J as ScheduleRequest, K as ServiceCtx, L as ServiceHandlerFn, M as SpiInterfaces, N as SpiName, O as TASK_EVENT_NAMES, P as TASK_STATUSES, Q as TERMINAL_STATUSES, R as TOKEN_STATES, U as TaskEvent, W as TaskEventName, X as TaskStatus, Y as TokenAwait, Z as TokenState, _ as isInstanceEvent, $ as isTaskEvent, a0 as isTerminalStatus, a1 as subjectTokenOf } from './spi-BVNWxdCQ.js';
|
|
3
3
|
import { NormalizedApproval, ApprovalMode, OnReject, TimeoutAction, NormalizedTimeout } from '@floken-io/moddle';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -1209,6 +1209,17 @@ interface ProcessGraph {
|
|
|
1209
1209
|
* 而不是等触发 —— 那时已经写了一半状态。
|
|
1210
1210
|
*/
|
|
1211
1211
|
boundaryOf(nodeId: string): readonly BoundaryBinding[];
|
|
1212
|
+
/**
|
|
1213
|
+
* ★ 该节点上的**宿主自定义**扩展属性(ADR-009 细则①②③④)。
|
|
1214
|
+
*
|
|
1215
|
+
* 给出去的是**原样键值**,引擎**不解释** `acme:priority` 是什么意思 ——
|
|
1216
|
+
* 解释权 100% 在宿主(源码里不得出现任何具体宿主前缀)。
|
|
1217
|
+
*
|
|
1218
|
+
* - 排除 `floken:*`(引擎自己的键)与 `_extensionElements`(第三方原样快照);
|
|
1219
|
+
* - **只留标量**;
|
|
1220
|
+
* - **一个都没有 → `undefined`**(不填空对象 —— 调用方要能区分「没配」与「配了但被过滤空」)。
|
|
1221
|
+
*/
|
|
1222
|
+
extensionsOf(nodeId: string): Readonly<Record<string, unknown>> | undefined;
|
|
1212
1223
|
}
|
|
1213
1224
|
|
|
1214
1225
|
/**
|
|
@@ -1790,6 +1801,42 @@ interface EngineConfig {
|
|
|
1790
1801
|
clock?: () => string;
|
|
1791
1802
|
/** INV-17 审计上限 */
|
|
1792
1803
|
maxAuditEntries?: number;
|
|
1804
|
+
/**
|
|
1805
|
+
* ★ ADR-009:把宿主自定义扩展属性**并入求值上下文**(opt-in)。
|
|
1806
|
+
*
|
|
1807
|
+
* 不声明 → 只给 `ctx.nodeExtensions` / `ctx.targetExtensions` 两个**只读**字段,
|
|
1808
|
+
* **不并入** `variables`(0.0.1 语义逐字不变)。
|
|
1809
|
+
*/
|
|
1810
|
+
extensionVars?: ExtensionVarsOption;
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* ★ ADR-009 的 opt-in 配置。
|
|
1814
|
+
*
|
|
1815
|
+
* 为什么并入形态是**两个对象**而不是把键平铺进 `variables`:
|
|
1816
|
+
* 平铺的撞车面是「每个键」(表单字段 / 脚本产出的变量都可能同名),
|
|
1817
|
+
* 撞了就要决出胜负 —— 静默覆盖正是 §7.2 要防的头号事故。
|
|
1818
|
+
* 挂成 `variables.node` / `variables.target` 后,撞车面降到「两个名字」,
|
|
1819
|
+
* 且语义天然分层:**设计期配置** vs **运行期数据**。
|
|
1820
|
+
*/
|
|
1821
|
+
interface ExtensionVarsOption {
|
|
1822
|
+
/**
|
|
1823
|
+
* ★ **当前节点**扩展属性并入后的顶层键名(默认 `'node'`)。
|
|
1824
|
+
*
|
|
1825
|
+
* ⚠️ 若流程变量里已有同名键 → 抛 `ENGINE_OPTION_INVALID`(**不静默覆盖**,细则⑦)。
|
|
1826
|
+
* 撞了就改这个名字,不要去改业务变量 —— 那会把业务数据挤掉。
|
|
1827
|
+
*/
|
|
1828
|
+
key?: string;
|
|
1829
|
+
/** ★ **目标节点**扩展属性并入后的顶层键名(默认 `'target'`);冲突处置同 `key` */
|
|
1830
|
+
targetKey?: string;
|
|
1831
|
+
/**
|
|
1832
|
+
* ★ 类型还原表:`'acme:slaHours'` → `'number'`。
|
|
1833
|
+
*
|
|
1834
|
+
* 为什么必须有:`moddle` 从 XML 读回的扩展属性**一律是字符串**(实测 `48 → "48"`),
|
|
1835
|
+
* 而 `slaHours > 24` 遇到 `"48"` 会按字符串比较 → 抛错。
|
|
1836
|
+
* 引擎**不猜类型**(`"48"` 究竟是数字还是编号?猜 = 静默错误),按本表声明转换。
|
|
1837
|
+
* 二期改由模型内 `floken:extensionTypes` 声明块承载(仍是 `floken:` 命名空间)。
|
|
1838
|
+
*/
|
|
1839
|
+
casts?: Readonly<Record<string, 'number' | 'boolean' | 'iso-date'>>;
|
|
1793
1840
|
}
|
|
1794
1841
|
interface StartOptions {
|
|
1795
1842
|
/** ✅ 必填(AC-E10:实例绑定定义版本,改版不影响在途) */
|
package/dist/index.js
CHANGED
|
@@ -1377,6 +1377,11 @@ function boundaryTokenOf(state, fire, host) {
|
|
|
1377
1377
|
// src/nodes/graph.ts
|
|
1378
1378
|
var WAITING_NODE_TYPES = ["userTask"];
|
|
1379
1379
|
var APPROVAL_EXT_KEY = "floken:approval";
|
|
1380
|
+
var FLOKEN_EXT_PREFIX = "floken:";
|
|
1381
|
+
var RAW_SNAPSHOT_KEY = "_extensionElements";
|
|
1382
|
+
function isScalarValue(v) {
|
|
1383
|
+
return v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
1384
|
+
}
|
|
1380
1385
|
function isWaitingNode(type) {
|
|
1381
1386
|
return WAITING_NODE_TYPES.includes(type);
|
|
1382
1387
|
}
|
|
@@ -1431,6 +1436,7 @@ function createProcessGraph(definition, processId, definitionVersion) {
|
|
|
1431
1436
|
throw definitionMissing(processId, definitionVersion);
|
|
1432
1437
|
}
|
|
1433
1438
|
const approvalCache = /* @__PURE__ */ new Map();
|
|
1439
|
+
const extCache = /* @__PURE__ */ new Map();
|
|
1434
1440
|
const boundaries = /* @__PURE__ */ new Map();
|
|
1435
1441
|
for (const n of flatNodes) {
|
|
1436
1442
|
if (n === null || typeof n !== "object" || n.type !== "boundaryEvent") continue;
|
|
@@ -1515,6 +1521,23 @@ function createProcessGraph(definition, processId, definitionVersion) {
|
|
|
1515
1521
|
return value;
|
|
1516
1522
|
},
|
|
1517
1523
|
hasApproval: (nodeId) => nodes.get(nodeId)?.extension?.[APPROVAL_EXT_KEY] !== void 0,
|
|
1524
|
+
extensionsOf(nodeId) {
|
|
1525
|
+
if (extCache.has(nodeId)) return extCache.get(nodeId);
|
|
1526
|
+
const raw = nodes.get(nodeId)?.extension;
|
|
1527
|
+
let value;
|
|
1528
|
+
if (raw !== void 0 && typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
|
|
1529
|
+
const out2 = {};
|
|
1530
|
+
for (const [key, v] of Object.entries(raw)) {
|
|
1531
|
+
if (key.startsWith(FLOKEN_EXT_PREFIX)) continue;
|
|
1532
|
+
if (key === RAW_SNAPSHOT_KEY) continue;
|
|
1533
|
+
if (!isScalarValue(v)) continue;
|
|
1534
|
+
out2[key] = v;
|
|
1535
|
+
}
|
|
1536
|
+
value = Object.keys(out2).length > 0 ? Object.freeze(out2) : void 0;
|
|
1537
|
+
}
|
|
1538
|
+
extCache.set(nodeId, value);
|
|
1539
|
+
return value;
|
|
1540
|
+
},
|
|
1518
1541
|
scriptOf: (nodeId) => {
|
|
1519
1542
|
const s = nodes.get(nodeId)?.script;
|
|
1520
1543
|
if (typeof s !== "string") return void 0;
|
|
@@ -2753,6 +2776,14 @@ var ConditionUnresolved = class extends Error {
|
|
|
2753
2776
|
flowId;
|
|
2754
2777
|
expression;
|
|
2755
2778
|
nodeId;
|
|
2779
|
+
/**
|
|
2780
|
+
* ★ 该条件所在顺序流的**目标节点**(ADR-009 细则②)。
|
|
2781
|
+
*
|
|
2782
|
+
* 只有带上它,重跑时才能取出「这条分支通向的那个节点」的自定义扩展属性。
|
|
2783
|
+
* 拿不到时为 `undefined`(宿主在门 2 自提供 `conditionsOf` 闭包时不会发生本哨兵,
|
|
2784
|
+
* 但引擎自己的调用点**恒有** `OutFlow.to`)。
|
|
2785
|
+
*/
|
|
2786
|
+
toNodeId;
|
|
2756
2787
|
/**
|
|
2757
2788
|
* ★ **到达该网关那一刻**的变量快照(T17)。
|
|
2758
2789
|
*
|
|
@@ -2761,16 +2792,17 @@ var ConditionUnresolved = class extends Error {
|
|
|
2761
2792
|
* 就被忽略了 —— 表现为「脚本把 amount 改成了 9000,网关却按旧值走了分支」。
|
|
2762
2793
|
*/
|
|
2763
2794
|
variables;
|
|
2764
|
-
constructor(flowId, expression, nodeId, variables = {}) {
|
|
2795
|
+
constructor(flowId, expression, nodeId, variables = {}, toNodeId = void 0) {
|
|
2765
2796
|
super(`condition of flow '${flowId}' is not resolved yet`);
|
|
2766
2797
|
this.flowId = flowId;
|
|
2767
2798
|
this.expression = expression;
|
|
2768
2799
|
this.nodeId = nodeId;
|
|
2769
2800
|
this.variables = variables;
|
|
2801
|
+
this.toNodeId = toNodeId;
|
|
2770
2802
|
}
|
|
2771
2803
|
};
|
|
2772
|
-
function unresolvedCondition(flowId, expression, nodeId, variables = {}) {
|
|
2773
|
-
return new ConditionUnresolved(flowId, expression, nodeId, variables);
|
|
2804
|
+
function unresolvedCondition(flowId, expression, nodeId, variables = {}, toNodeId = void 0) {
|
|
2805
|
+
return new ConditionUnresolved(flowId, expression, nodeId, variables, toNodeId);
|
|
2774
2806
|
}
|
|
2775
2807
|
function asUnresolved(e) {
|
|
2776
2808
|
if (e instanceof ConditionUnresolved) return e;
|
|
@@ -2819,7 +2851,8 @@ var ENGINE_CONFIG_KEYS = [
|
|
|
2819
2851
|
"scheduler",
|
|
2820
2852
|
"hooks",
|
|
2821
2853
|
"clock",
|
|
2822
|
-
"maxAuditEntries"
|
|
2854
|
+
"maxAuditEntries",
|
|
2855
|
+
"extensionVars"
|
|
2823
2856
|
];
|
|
2824
2857
|
function assertEngineConfig(config) {
|
|
2825
2858
|
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
@@ -2862,6 +2895,45 @@ function assertEngineConfig(config) {
|
|
|
2862
2895
|
throw optionInvalid("maxAuditEntries", "must be a positive integer", n);
|
|
2863
2896
|
}
|
|
2864
2897
|
}
|
|
2898
|
+
assertExtensionVars(c.extensionVars);
|
|
2899
|
+
}
|
|
2900
|
+
function assertExtensionVars(v) {
|
|
2901
|
+
if (v === void 0) return;
|
|
2902
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
2903
|
+
throw optionInvalid("extensionVars", "must be a plain object", v);
|
|
2904
|
+
}
|
|
2905
|
+
const o = v;
|
|
2906
|
+
const allowed = ["key", "targetKey", "casts"];
|
|
2907
|
+
for (const key of Object.keys(o)) {
|
|
2908
|
+
if (!allowed.includes(key)) {
|
|
2909
|
+
throw optionUnknown(`extensionVars.${key}`, allowed);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
const assertKeyName = (field) => {
|
|
2913
|
+
const k = o[field];
|
|
2914
|
+
if (k === void 0) return;
|
|
2915
|
+
if (typeof k !== "string" || k.length === 0 || k.includes(".")) {
|
|
2916
|
+
throw optionInvalid(`extensionVars.${field}`, 'must be a non-empty string without "."', k);
|
|
2917
|
+
}
|
|
2918
|
+
};
|
|
2919
|
+
assertKeyName("key");
|
|
2920
|
+
assertKeyName("targetKey");
|
|
2921
|
+
if (o.casts !== void 0) {
|
|
2922
|
+
const casts = o.casts;
|
|
2923
|
+
if (typeof casts !== "object" || casts === null || Array.isArray(casts)) {
|
|
2924
|
+
throw optionInvalid("extensionVars.casts", "must be a plain object", casts);
|
|
2925
|
+
}
|
|
2926
|
+
const kinds = ["number", "boolean", "iso-date"];
|
|
2927
|
+
for (const [key, kind] of Object.entries(casts)) {
|
|
2928
|
+
if (!kinds.includes(kind)) {
|
|
2929
|
+
throw optionInvalid(
|
|
2930
|
+
`extensionVars.casts['${key}']`,
|
|
2931
|
+
`must be one of ${kinds.join(" | ")}`,
|
|
2932
|
+
kind
|
|
2933
|
+
);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2865
2937
|
}
|
|
2866
2938
|
function assertStartOptions(opts) {
|
|
2867
2939
|
if (typeof opts !== "object" || opts === null || Array.isArray(opts)) {
|
|
@@ -2907,6 +2979,70 @@ function newInstanceId() {
|
|
|
2907
2979
|
const rand = Math.random().toString(36).slice(2, 10);
|
|
2908
2980
|
return `pi_${Date.now().toString(36)}_${instanceSeq.toString(36)}_${rand}`;
|
|
2909
2981
|
}
|
|
2982
|
+
function applyExtensionCast(key, value, kind) {
|
|
2983
|
+
if (kind === "number") {
|
|
2984
|
+
if (typeof value === "number") return value;
|
|
2985
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
2986
|
+
const n = Number(value);
|
|
2987
|
+
if (!Number.isNaN(n)) return n;
|
|
2988
|
+
}
|
|
2989
|
+
} else if (kind === "boolean") {
|
|
2990
|
+
if (typeof value === "boolean") return value;
|
|
2991
|
+
if (value === "true") return true;
|
|
2992
|
+
if (value === "false") return false;
|
|
2993
|
+
} else {
|
|
2994
|
+
return value;
|
|
2995
|
+
}
|
|
2996
|
+
throw optionInvalid(
|
|
2997
|
+
`extensionVars.casts['${key}']`,
|
|
2998
|
+
`declared as '${kind}' but the value cannot be converted`,
|
|
2999
|
+
value
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
function stripExtPrefix(key) {
|
|
3003
|
+
const i = key.indexOf(":");
|
|
3004
|
+
return i === -1 ? key : key.slice(i + 1);
|
|
3005
|
+
}
|
|
3006
|
+
function castExtensionBag(bag, casts) {
|
|
3007
|
+
if (bag === void 0) return void 0;
|
|
3008
|
+
const out = {};
|
|
3009
|
+
for (const [key, value] of Object.entries(bag)) {
|
|
3010
|
+
const kind = casts?.[key];
|
|
3011
|
+
const v = kind === void 0 ? value : applyExtensionCast(key, value, kind);
|
|
3012
|
+
const name = stripExtPrefix(key);
|
|
3013
|
+
if (Object.prototype.hasOwnProperty.call(out, name)) {
|
|
3014
|
+
throw optionInvalid(
|
|
3015
|
+
"extensionVars",
|
|
3016
|
+
`two extension keys collapse to the same variable name '${name}' after stripping the namespace prefix`,
|
|
3017
|
+
{ name, bag: Object.keys(bag) }
|
|
3018
|
+
);
|
|
3019
|
+
}
|
|
3020
|
+
out[name] = v;
|
|
3021
|
+
}
|
|
3022
|
+
return out;
|
|
3023
|
+
}
|
|
3024
|
+
function mergeExtensionVars(base, nodeExt, targetExt, opt) {
|
|
3025
|
+
if (opt === void 0) return base;
|
|
3026
|
+
const casts = opt.casts;
|
|
3027
|
+
const nodeBag = castExtensionBag(nodeExt, casts);
|
|
3028
|
+
const targetBag = castExtensionBag(targetExt, casts);
|
|
3029
|
+
if (nodeBag === void 0 && targetBag === void 0) return base;
|
|
3030
|
+
const out = { ...base };
|
|
3031
|
+
const put = (name, bag) => {
|
|
3032
|
+
if (bag === void 0) return;
|
|
3033
|
+
if (Object.prototype.hasOwnProperty.call(out, name)) {
|
|
3034
|
+
throw optionInvalid(
|
|
3035
|
+
"extensionVars",
|
|
3036
|
+
`cannot merge extension vars: a variable named '${name}' already exists \u2014 rename via extensionVars.key / targetKey`,
|
|
3037
|
+
{ name, existing: out[name] }
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
3040
|
+
out[name] = bag;
|
|
3041
|
+
};
|
|
3042
|
+
put(opt.key ?? "node", nodeBag);
|
|
3043
|
+
put(opt.targetKey ?? "target", targetBag);
|
|
3044
|
+
return out;
|
|
3045
|
+
}
|
|
2910
3046
|
function createEngine(config) {
|
|
2911
3047
|
assertEngineConfig(config);
|
|
2912
3048
|
const clock = config.clock ?? defaultClock;
|
|
@@ -2916,6 +3052,7 @@ function createEngine(config) {
|
|
|
2916
3052
|
const scheduler = config.scheduler;
|
|
2917
3053
|
const approverSource = config.approverSource ?? DEFAULT_APPROVER_SOURCE;
|
|
2918
3054
|
const condition = config.conditionHandler ?? createFeelConditionHandler();
|
|
3055
|
+
const extensionVars = config.extensionVars;
|
|
2919
3056
|
const handlers = config.handlers;
|
|
2920
3057
|
const decisionHandler = config.decisionHandler;
|
|
2921
3058
|
const maxAuditEntries = config.maxAuditEntries;
|
|
@@ -2943,7 +3080,7 @@ function createEngine(config) {
|
|
|
2943
3080
|
if (flow.expression === void 0) return true;
|
|
2944
3081
|
const value = conditions.get(flow.id);
|
|
2945
3082
|
if (value === void 0) {
|
|
2946
|
-
throw unresolvedCondition(flow.id, flow.expression, nodeId, variables);
|
|
3083
|
+
throw unresolvedCondition(flow.id, flow.expression, nodeId, variables, flow.to);
|
|
2947
3084
|
}
|
|
2948
3085
|
return value;
|
|
2949
3086
|
};
|
|
@@ -3029,13 +3166,18 @@ function createEngine(config) {
|
|
|
3029
3166
|
instanceId: state.instanceId
|
|
3030
3167
|
});
|
|
3031
3168
|
}
|
|
3169
|
+
const nodeExt = graph.extensionsOf(pending.nodeId);
|
|
3170
|
+
const targetExt = pending.toNodeId === void 0 ? void 0 : graph.extensionsOf(pending.toNodeId);
|
|
3032
3171
|
conditions.set(
|
|
3033
3172
|
pending.flowId,
|
|
3034
3173
|
await evaluateCondition(condition, pending.expression, {
|
|
3035
3174
|
instanceId: state.instanceId,
|
|
3036
3175
|
nodeId: pending.nodeId,
|
|
3037
3176
|
// ★ 用**到达该网关那一刻**的变量快照(哨兵带来),不是提交前的旧值
|
|
3038
|
-
|
|
3177
|
+
// ⚠️ 扩展属性只在**求值上下文**里并入,绝不写回 state(ADR-009 细则⑨)
|
|
3178
|
+
variables: mergeExtensionVars(pending.variables, nodeExt, targetExt, extensionVars),
|
|
3179
|
+
nodeExtensions: nodeExt,
|
|
3180
|
+
targetExtensions: targetExt
|
|
3039
3181
|
})
|
|
3040
3182
|
);
|
|
3041
3183
|
}
|
|
@@ -521,6 +521,25 @@ interface ConditionCtx {
|
|
|
521
521
|
instanceId: string;
|
|
522
522
|
nodeId: string;
|
|
523
523
|
variables: Readonly<Record<string, unknown>>;
|
|
524
|
+
/**
|
|
525
|
+
* ★ **当前节点**(`nodeId`,即条件所在的网关 / 活动)上的宿主自定义扩展属性(ADR-009)。
|
|
526
|
+
*
|
|
527
|
+
* - 键**带前缀**原样给出(`acme:priority`),引擎**不解释**其语义;
|
|
528
|
+
* - 排除 `floken:*`(引擎自己的键);**只含标量**;
|
|
529
|
+
* - 没有 → `undefined`(不填空对象)。
|
|
530
|
+
*
|
|
531
|
+
* ⚠️ 它**恒给**(不需任何配置);"自动并入 `variables`"才需要 opt-in(见 `EngineConfig.extensionVars`)。
|
|
532
|
+
*/
|
|
533
|
+
nodeExtensions: Readonly<Record<string, unknown>> | undefined;
|
|
534
|
+
/**
|
|
535
|
+
* ★ 该条件所在**顺序流的目标节点**上的自定义扩展属性(ADR-009 细则②)。
|
|
536
|
+
*
|
|
537
|
+
* 为什么必须两个都给:条件挂在**顺序流**上,而"加急等级"这类业务属性通常挂在
|
|
538
|
+
* **审批节点**(目标)而不是网关(当前节点)上 —— 只给一个,另一个场景就得回退到手写 handler。
|
|
539
|
+
*
|
|
540
|
+
* ⚠️ 求值时**拿不到目标**(如宿主在门 2 自提供 `conditionsOf` 闭包)→ `undefined`。
|
|
541
|
+
*/
|
|
542
|
+
targetExtensions: Readonly<Record<string, unknown>> | undefined;
|
|
524
543
|
}
|
|
525
544
|
/**
|
|
526
545
|
* 网关分支 / 顺序流条件求值。**不注入 = 内置 `@floken-io/feel`**(S-FEEL 子集)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floken-io/engine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Token-based process engine with Chinese-style approval actions for floken",
|
|
5
5
|
"repository": { "type": "git", "url": "git+https://github.com/floken-io/floken-engine.git" },
|
|
6
6
|
"bugs": { "url": "https://github.com/floken-io/floken-engine/issues" },
|