@bpmnkit/engine 0.1.30 → 0.1.31
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 +1 -1
- package/dist/dmn.js +9 -4
- package/dist/instance.d.ts +7 -0
- package/dist/instance.js +48 -21
- package/dist/variables.d.ts +14 -1
- package/dist/variables.js +43 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://github.com/bpmnkit/monorepo)
|
|
10
10
|
[](https://github.com/bpmnkit/monorepo)
|
|
11
11
|
|
|
12
|
-
[Website](https://bpmnkit.com) · [Documentation](https://
|
|
12
|
+
[Website](https://bpmnkit.com) · [Documentation](https://bpmnkit.com/docs) · [GitHub](https://github.com/bpmnkit/monorepo) · [Changelog](https://github.com/bpmnkit/monorepo/blob/main/packages/engine/CHANGELOG.md)
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
15
|
---
|
package/dist/dmn.js
CHANGED
|
@@ -9,8 +9,14 @@ export function evaluateDecision(decision, vars) {
|
|
|
9
9
|
return null;
|
|
10
10
|
const hitPolicy = table.hitPolicy ?? "UNIQUE";
|
|
11
11
|
const matchedOutputs = [];
|
|
12
|
+
// Input expressions do not depend on the rule, so evaluate each column once
|
|
13
|
+
// instead of once per rule.
|
|
14
|
+
const inputValues = table.inputs.map((col) => {
|
|
15
|
+
const inputExpr = col.inputExpression.text ?? "";
|
|
16
|
+
return inputExpr.trim() === "" ? null : evalExpression(inputExpr, vars);
|
|
17
|
+
});
|
|
12
18
|
for (const rule of table.rules) {
|
|
13
|
-
if (ruleMatches(table.inputs, rule.inputEntries, vars)) {
|
|
19
|
+
if (ruleMatches(table.inputs, inputValues, rule.inputEntries, vars)) {
|
|
14
20
|
const output = {};
|
|
15
21
|
for (let i = 0; i < table.outputs.length; i++) {
|
|
16
22
|
const col = table.outputs[i];
|
|
@@ -27,7 +33,7 @@ export function evaluateDecision(decision, vars) {
|
|
|
27
33
|
}
|
|
28
34
|
return buildResult(hitPolicy, table.aggregation, matchedOutputs, table.outputs.length);
|
|
29
35
|
}
|
|
30
|
-
function ruleMatches(inputs, inputEntries, vars) {
|
|
36
|
+
function ruleMatches(inputs, inputValues, inputEntries, vars) {
|
|
31
37
|
const feelVars = vars;
|
|
32
38
|
for (let i = 0; i < inputs.length; i++) {
|
|
33
39
|
const col = inputs[i];
|
|
@@ -36,8 +42,7 @@ function ruleMatches(inputs, inputEntries, vars) {
|
|
|
36
42
|
continue;
|
|
37
43
|
if (entry.text.trim() === "")
|
|
38
44
|
continue; // empty = "any"
|
|
39
|
-
const
|
|
40
|
-
const inputValue = inputExpr.trim() === "" ? null : evalExpression(inputExpr, vars);
|
|
45
|
+
const inputValue = inputValues[i] ?? null;
|
|
41
46
|
const parsed = parseUnaryTests(entry.text);
|
|
42
47
|
if (parsed.ast === null)
|
|
43
48
|
continue;
|
package/dist/instance.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export declare class ProcessInstance {
|
|
|
9
9
|
private _error;
|
|
10
10
|
/** tokenId → Token */
|
|
11
11
|
private readonly allTokens;
|
|
12
|
+
/** elementId → tokens currently sitting on that element. */
|
|
13
|
+
private readonly tokensByElement;
|
|
14
|
+
/** Zeebe extensions parsed once per element for the life of this instance. */
|
|
15
|
+
private readonly zeebeExtCache;
|
|
12
16
|
/** Scope stack: rootScopeId + any active sub-process scopes */
|
|
13
17
|
private readonly scopes;
|
|
14
18
|
/** Message correlation: messageName → resolve callback */
|
|
@@ -46,6 +50,9 @@ export declare class ProcessInstance {
|
|
|
46
50
|
deliverMessage(messageName: string): void;
|
|
47
51
|
private buildScopeCtx;
|
|
48
52
|
private createToken;
|
|
53
|
+
/** The first token currently on `elementId`, if any. */
|
|
54
|
+
private tokenOn;
|
|
55
|
+
private zeebeExt;
|
|
49
56
|
private removeToken;
|
|
50
57
|
/** elementId → set of incomingFlowIds received */
|
|
51
58
|
private readonly joins;
|
package/dist/instance.js
CHANGED
|
@@ -13,6 +13,10 @@ export class ProcessInstance {
|
|
|
13
13
|
_error;
|
|
14
14
|
/** tokenId → Token */
|
|
15
15
|
allTokens = new Map();
|
|
16
|
+
/** elementId → tokens currently sitting on that element. */
|
|
17
|
+
tokensByElement = new Map();
|
|
18
|
+
/** Zeebe extensions parsed once per element for the life of this instance. */
|
|
19
|
+
zeebeExtCache = new WeakMap();
|
|
16
20
|
/** Scope stack: rootScopeId + any active sub-process scopes */
|
|
17
21
|
scopes = new Map();
|
|
18
22
|
/** Message correlation: messageName → resolve callback */
|
|
@@ -78,6 +82,7 @@ export class ProcessInstance {
|
|
|
78
82
|
this._state = "terminated";
|
|
79
83
|
this.cancelAllTimers();
|
|
80
84
|
this.allTokens.clear();
|
|
85
|
+
this.tokensByElement.clear();
|
|
81
86
|
for (const ctx of this.scopes.values())
|
|
82
87
|
ctx.tokens.clear();
|
|
83
88
|
}
|
|
@@ -141,11 +146,39 @@ export class ProcessInstance {
|
|
|
141
146
|
createToken(elementId, scopeId) {
|
|
142
147
|
const token = { id: generateId("tok"), elementId, scopeId };
|
|
143
148
|
this.allTokens.set(token.id, token);
|
|
149
|
+
const onElement = this.tokensByElement.get(elementId);
|
|
150
|
+
if (onElement)
|
|
151
|
+
onElement.add(token);
|
|
152
|
+
else
|
|
153
|
+
this.tokensByElement.set(elementId, new Set([token]));
|
|
144
154
|
this.scopes.get(scopeId)?.tokens.add(token.id);
|
|
145
155
|
return token;
|
|
146
156
|
}
|
|
157
|
+
/** The first token currently on `elementId`, if any. */
|
|
158
|
+
tokenOn(elementId) {
|
|
159
|
+
const onElement = this.tokensByElement.get(elementId);
|
|
160
|
+
if (onElement === undefined)
|
|
161
|
+
return undefined;
|
|
162
|
+
for (const token of onElement)
|
|
163
|
+
return token;
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
zeebeExt(el) {
|
|
167
|
+
let ext = this.zeebeExtCache.get(el);
|
|
168
|
+
if (ext === undefined) {
|
|
169
|
+
ext = parseZeebeExt(el.extensionElements);
|
|
170
|
+
this.zeebeExtCache.set(el, ext);
|
|
171
|
+
}
|
|
172
|
+
return ext;
|
|
173
|
+
}
|
|
147
174
|
removeToken(token) {
|
|
148
175
|
this.allTokens.delete(token.id);
|
|
176
|
+
const onElement = this.tokensByElement.get(token.elementId);
|
|
177
|
+
if (onElement !== undefined) {
|
|
178
|
+
onElement.delete(token);
|
|
179
|
+
if (onElement.size === 0)
|
|
180
|
+
this.tokensByElement.delete(token.elementId);
|
|
181
|
+
}
|
|
149
182
|
this.scopes.get(token.scopeId)?.tokens.delete(token.id);
|
|
150
183
|
this.timerCancels.get(token.id)?.();
|
|
151
184
|
this.timerCancels.delete(token.id);
|
|
@@ -186,7 +219,7 @@ export class ProcessInstance {
|
|
|
186
219
|
}
|
|
187
220
|
this.emit({ type: "element:entering", elementId, elementName: el.name, elementType: el.type });
|
|
188
221
|
// Apply ioMapping inputs
|
|
189
|
-
const ext =
|
|
222
|
+
const ext = this.zeebeExt(el);
|
|
190
223
|
if (ext.ioMapping) {
|
|
191
224
|
for (const inp of ext.ioMapping.inputs) {
|
|
192
225
|
let val;
|
|
@@ -290,12 +323,11 @@ export class ProcessInstance {
|
|
|
290
323
|
const eventDef = el.eventDefinitions[0];
|
|
291
324
|
if (eventDef?.type === "terminate") {
|
|
292
325
|
this.cancelAllTimers();
|
|
293
|
-
for (const
|
|
294
|
-
|
|
295
|
-
if (tok !== undefined)
|
|
296
|
-
this.scopes.get(tok.scopeId)?.tokens.delete(id);
|
|
326
|
+
for (const tok of this.allTokens.values()) {
|
|
327
|
+
this.scopes.get(tok.scopeId)?.tokens.delete(tok.id);
|
|
297
328
|
}
|
|
298
329
|
this.allTokens.clear();
|
|
330
|
+
this.tokensByElement.clear();
|
|
299
331
|
this.emit({
|
|
300
332
|
type: "element:leaving",
|
|
301
333
|
elementId: el.id,
|
|
@@ -431,7 +463,7 @@ export class ProcessInstance {
|
|
|
431
463
|
});
|
|
432
464
|
return;
|
|
433
465
|
}
|
|
434
|
-
const result = evaluateDecision(decision, this.variables.
|
|
466
|
+
const result = evaluateDecision(decision, this.variables.snapshot(scopeId));
|
|
435
467
|
this.variables.set(scopeId, cd.resultVariable, result);
|
|
436
468
|
this.emit({ type: "variable:set", name: cd.resultVariable, value: result, scopeId });
|
|
437
469
|
}
|
|
@@ -553,12 +585,9 @@ export class ProcessInstance {
|
|
|
553
585
|
const cancel = scheduleTimer(timerDef, () => {
|
|
554
586
|
this.boundaryTimerCancels.delete(elementId);
|
|
555
587
|
// Remove the parent token
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
break;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
588
|
+
const parentToken = this.tokenOn(elementId);
|
|
589
|
+
if (parentToken !== undefined)
|
|
590
|
+
this.removeToken(parentToken);
|
|
562
591
|
if (be.cancelActivity !== false) {
|
|
563
592
|
void this.activate(be.id, scopeId, undefined);
|
|
564
593
|
}
|
|
@@ -579,12 +608,9 @@ export class ProcessInstance {
|
|
|
579
608
|
continue;
|
|
580
609
|
if (errDef.errorRef !== undefined && errDef.errorRef !== errorCode)
|
|
581
610
|
continue;
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
break;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
611
|
+
const hostToken = this.tokenOn(attachedTo);
|
|
612
|
+
if (hostToken !== undefined)
|
|
613
|
+
this.removeToken(hostToken);
|
|
588
614
|
void this.activate(be.id, ctx.scopeId, undefined);
|
|
589
615
|
return;
|
|
590
616
|
}
|
|
@@ -608,7 +634,7 @@ export class ProcessInstance {
|
|
|
608
634
|
const el = ctx.elements.get(token.elementId);
|
|
609
635
|
if (el === undefined)
|
|
610
636
|
return;
|
|
611
|
-
const ext =
|
|
637
|
+
const ext = this.zeebeExt(el);
|
|
612
638
|
// Apply ioMapping outputs
|
|
613
639
|
if (ext.ioMapping) {
|
|
614
640
|
for (const out of ext.ioMapping.outputs) {
|
|
@@ -676,14 +702,15 @@ export class ProcessInstance {
|
|
|
676
702
|
}
|
|
677
703
|
// ── FEEL helpers ───────────────────────────────────────────────────────────
|
|
678
704
|
evalFeel(expr, scopeId, emitCtx) {
|
|
679
|
-
const vars = this.variables.
|
|
705
|
+
const vars = this.variables.snapshot(scopeId);
|
|
680
706
|
// Strip Camunda FEEL prefix ("= expr") — the leading "=" is a type indicator, not part of the expression.
|
|
681
707
|
const normalized = expr.trim().replace(/^=\s*/, "");
|
|
682
708
|
const parsed = parseExpression(normalized);
|
|
683
709
|
if (parsed.ast === null)
|
|
684
710
|
return undefined;
|
|
685
711
|
const result = evaluate(parsed.ast, { vars: vars });
|
|
686
|
-
|
|
712
|
+
// The event carries a copy of every variable; skip building it when nobody listens.
|
|
713
|
+
if (emitCtx !== undefined && this.listeners.length > 0) {
|
|
687
714
|
this.emit({
|
|
688
715
|
type: "feel:evaluated",
|
|
689
716
|
elementId: emitCtx.elementId,
|
package/dist/variables.d.ts
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
export declare class VariableStore {
|
|
7
7
|
private readonly scopes;
|
|
8
8
|
private readonly parents;
|
|
9
|
+
/**
|
|
10
|
+
* Merged root → scope views, built lazily and then kept current by every
|
|
11
|
+
* write, so expression evaluation never re-merges the scope chain.
|
|
12
|
+
*/
|
|
13
|
+
private readonly snapshots;
|
|
9
14
|
createScope(id: string, parentId?: string): void;
|
|
10
15
|
removeScope(id: string): void;
|
|
11
16
|
/** Walk up the chain and return the value, or undefined if not found. */
|
|
@@ -17,8 +22,16 @@ export declare class VariableStore {
|
|
|
17
22
|
set(scopeId: string, name: string, value: unknown): void;
|
|
18
23
|
/** Set a variable in this scope only, regardless of parent state. */
|
|
19
24
|
setLocal(scopeId: string, name: string, value: unknown): void;
|
|
20
|
-
/** Return all variables merged from root → this scope (child wins). */
|
|
25
|
+
/** Return a fresh copy of all variables merged from root → this scope (child wins). */
|
|
21
26
|
getAll(scopeId: string): Record<string, unknown>;
|
|
27
|
+
/**
|
|
28
|
+
* All variables merged from root → this scope, as a shared object that
|
|
29
|
+
* later writes update in place. Callers must treat it as read-only and not
|
|
30
|
+
* hold it across a write.
|
|
31
|
+
*/
|
|
32
|
+
snapshot(scopeId: string): Record<string, unknown>;
|
|
33
|
+
/** Store the value and patch every cached view that sees this scope's copy of `name`. */
|
|
34
|
+
private write;
|
|
22
35
|
private hasOwn;
|
|
23
36
|
private ancestorHas;
|
|
24
37
|
}
|
package/dist/variables.js
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
export class VariableStore {
|
|
7
7
|
scopes = new Map();
|
|
8
8
|
parents = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Merged root → scope views, built lazily and then kept current by every
|
|
11
|
+
* write, so expression evaluation never re-merges the scope chain.
|
|
12
|
+
*/
|
|
13
|
+
snapshots = new Map();
|
|
9
14
|
createScope(id, parentId) {
|
|
10
15
|
this.scopes.set(id, new Map());
|
|
11
16
|
if (parentId !== undefined) {
|
|
@@ -15,6 +20,7 @@ export class VariableStore {
|
|
|
15
20
|
removeScope(id) {
|
|
16
21
|
this.scopes.delete(id);
|
|
17
22
|
this.parents.delete(id);
|
|
23
|
+
this.snapshots.delete(id);
|
|
18
24
|
}
|
|
19
25
|
/** Walk up the chain and return the value, or undefined if not found. */
|
|
20
26
|
get(scopeId, name) {
|
|
@@ -34,7 +40,7 @@ export class VariableStore {
|
|
|
34
40
|
*/
|
|
35
41
|
set(scopeId, name, value) {
|
|
36
42
|
if (this.hasOwn(scopeId, name)) {
|
|
37
|
-
this.
|
|
43
|
+
this.write(scopeId, name, value);
|
|
38
44
|
return;
|
|
39
45
|
}
|
|
40
46
|
const parentId = this.parents.get(scopeId);
|
|
@@ -42,25 +48,55 @@ export class VariableStore {
|
|
|
42
48
|
this.set(parentId, name, value);
|
|
43
49
|
return;
|
|
44
50
|
}
|
|
45
|
-
this.
|
|
51
|
+
this.write(scopeId, name, value);
|
|
46
52
|
}
|
|
47
53
|
/** Set a variable in this scope only, regardless of parent state. */
|
|
48
54
|
setLocal(scopeId, name, value) {
|
|
49
|
-
this.
|
|
55
|
+
this.write(scopeId, name, value);
|
|
50
56
|
}
|
|
51
|
-
/** Return all variables merged from root → this scope (child wins). */
|
|
57
|
+
/** Return a fresh copy of all variables merged from root → this scope (child wins). */
|
|
52
58
|
getAll(scopeId) {
|
|
59
|
+
return { ...this.snapshot(scopeId) };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* All variables merged from root → this scope, as a shared object that
|
|
63
|
+
* later writes update in place. Callers must treat it as read-only and not
|
|
64
|
+
* hold it across a write.
|
|
65
|
+
*/
|
|
66
|
+
snapshot(scopeId) {
|
|
67
|
+
const cached = this.snapshots.get(scopeId);
|
|
68
|
+
if (cached !== undefined)
|
|
69
|
+
return cached;
|
|
53
70
|
const parentId = this.parents.get(scopeId);
|
|
54
|
-
const parentVars = parentId !== undefined ? this.
|
|
71
|
+
const parentVars = parentId !== undefined ? this.snapshot(parentId) : {};
|
|
55
72
|
const scope = this.scopes.get(scopeId);
|
|
56
73
|
if (scope === undefined)
|
|
57
74
|
return parentVars;
|
|
58
75
|
const result = { ...parentVars };
|
|
59
|
-
for (const [k, v] of scope)
|
|
76
|
+
for (const [k, v] of scope)
|
|
60
77
|
result[k] = v;
|
|
61
|
-
|
|
78
|
+
this.snapshots.set(scopeId, result);
|
|
62
79
|
return result;
|
|
63
80
|
}
|
|
81
|
+
/** Store the value and patch every cached view that sees this scope's copy of `name`. */
|
|
82
|
+
write(scopeId, name, value) {
|
|
83
|
+
const scope = this.scopes.get(scopeId);
|
|
84
|
+
if (scope === undefined)
|
|
85
|
+
return;
|
|
86
|
+
scope.set(name, value);
|
|
87
|
+
for (const [viewId, view] of this.snapshots) {
|
|
88
|
+
// Walk from the view's scope up to the writer; a scope in between that
|
|
89
|
+
// owns the name shadows the write for this view.
|
|
90
|
+
let current = viewId;
|
|
91
|
+
while (current !== undefined && current !== scopeId) {
|
|
92
|
+
if (this.scopes.get(current)?.has(name))
|
|
93
|
+
break;
|
|
94
|
+
current = this.parents.get(current);
|
|
95
|
+
}
|
|
96
|
+
if (current === scopeId)
|
|
97
|
+
view[name] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
64
100
|
hasOwn(scopeId, name) {
|
|
65
101
|
return this.scopes.get(scopeId)?.has(name) ?? false;
|
|
66
102
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bpmnkit/engine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"dist/**/*.d.ts"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@bpmnkit/core": "0.
|
|
24
|
+
"@bpmnkit/core": "0.2.0",
|
|
25
25
|
"@bpmnkit/feel": "0.0.20"
|
|
26
26
|
},
|
|
27
27
|
"optionalDependencies": {
|