chain-functions-behavior 1.0.0 → 1.0.1
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/dist/index.cjs +1559 -0
- package/dist/index.d.cts +334 -0
- package/dist/index.d.ts +334 -0
- package/dist/index.js +1524 -0
- package/package.json +2 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1559 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
PubSubBehavior: () => PubSubBehavior,
|
|
24
|
+
catchError: () => catchError,
|
|
25
|
+
createBehaviorRunner: () => createBehaviorRunner,
|
|
26
|
+
createBehaviorWs: () => createBehaviorWs,
|
|
27
|
+
createChainBehavior: () => createChainBehavior,
|
|
28
|
+
createMemoryTraceSink: () => createMemoryTraceSink,
|
|
29
|
+
createPubSubBehavior: () => createPubSubBehavior,
|
|
30
|
+
defineBehaviorConfig: () => defineBehaviorConfig,
|
|
31
|
+
defineErrorReporter: () => defineErrorReporter
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/helpers/config/defineBehaviorConfig.ts
|
|
36
|
+
var defineBehaviorConfig = (config) => config;
|
|
37
|
+
|
|
38
|
+
// src/helpers/errors/behaviorError.ts
|
|
39
|
+
var behaviorError = (code, message, extras = {}) => ({ code, message, ...extras });
|
|
40
|
+
|
|
41
|
+
// src/helpers/errors/defineErrorReporter.ts
|
|
42
|
+
var defineErrorReporter = (handlers) => typeof handlers === "function" ? handlers : handlers.report;
|
|
43
|
+
|
|
44
|
+
// src/errors.ts
|
|
45
|
+
var BehaviorSyncAsyncError = class extends Error {
|
|
46
|
+
behaviorError;
|
|
47
|
+
constructor(error) {
|
|
48
|
+
super(error.message);
|
|
49
|
+
this.name = "BehaviorSyncAsyncError";
|
|
50
|
+
this.behaviorError = error;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/helpers/actions/coreDelay.ts
|
|
55
|
+
var coreDelay = () => async ({ props }) => {
|
|
56
|
+
const ms = Math.max(0, Number(props.ms ?? 0));
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
58
|
+
return void 0;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/helpers/actions/coreEmit.ts
|
|
62
|
+
var coreEmit = () => ({ props, runtime }) => {
|
|
63
|
+
const type = props.type;
|
|
64
|
+
if (typeof type === "string") {
|
|
65
|
+
runtime.emit({ type, payload: props.payload });
|
|
66
|
+
}
|
|
67
|
+
return void 0;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/helpers/actions/coreFail.ts
|
|
71
|
+
var coreFail = () => ({ props, runtime }) => runtime.fail(String(props.reason ?? "failed"), props.data);
|
|
72
|
+
|
|
73
|
+
// src/helpers/actions/coreNoop.ts
|
|
74
|
+
var coreNoop = () => () => void 0;
|
|
75
|
+
|
|
76
|
+
// src/helpers/actions/corePatch.ts
|
|
77
|
+
var corePatch = () => ({ props, runtime }) => {
|
|
78
|
+
if ("patch" in props) {
|
|
79
|
+
runtime.patch(props.patch);
|
|
80
|
+
}
|
|
81
|
+
return void 0;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// src/helpers/actions/coreSetData.ts
|
|
85
|
+
var coreSetData = () => ({ props, runtime }) => {
|
|
86
|
+
const path = props.path;
|
|
87
|
+
if (typeof path === "string") {
|
|
88
|
+
runtime.setData(path, props.value);
|
|
89
|
+
}
|
|
90
|
+
return props.data ? { data: props.data } : void 0;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/helpers/actions/coreStop.ts
|
|
94
|
+
var coreStop = () => ({ props, runtime }) => runtime.stop(String(props.reason ?? "stopped"));
|
|
95
|
+
|
|
96
|
+
// src/registry/actions.ts
|
|
97
|
+
var createActionsRegistry = () => /* @__PURE__ */ new Map([
|
|
98
|
+
["core.noop", coreNoop()],
|
|
99
|
+
["core.stop", coreStop()],
|
|
100
|
+
["core.fail", coreFail()],
|
|
101
|
+
["core.sequence", coreNoop()],
|
|
102
|
+
["core.selector", coreNoop()],
|
|
103
|
+
["core.parallel", coreNoop()],
|
|
104
|
+
["core.set", coreSetData()],
|
|
105
|
+
["core.setData", coreSetData()],
|
|
106
|
+
["core.emit", coreEmit()],
|
|
107
|
+
["core.patch", corePatch()],
|
|
108
|
+
["core.delay", coreDelay()]
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
// src/helpers/conditions/changedCondition.ts
|
|
112
|
+
var changedCondition = (_args, current, previous) => !Object.is(current, previous);
|
|
113
|
+
|
|
114
|
+
// src/helpers/conditions/cooldownReadyCondition.ts
|
|
115
|
+
var cooldownReadyCondition = (_args, now, lastAt, cooldownMs) => {
|
|
116
|
+
if (lastAt == null) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return Number(now) - Number(lastAt) >= Number(cooldownMs ?? 0);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/helpers/conditions/sizeOf.ts
|
|
123
|
+
var sizeOf = (value) => {
|
|
124
|
+
if (value == null) {
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
if (typeof value === "string" || Array.isArray(value)) {
|
|
128
|
+
return value.length;
|
|
129
|
+
}
|
|
130
|
+
if (value instanceof Map || value instanceof Set) {
|
|
131
|
+
return value.size;
|
|
132
|
+
}
|
|
133
|
+
if (typeof value === "object") {
|
|
134
|
+
return Object.keys(value).length;
|
|
135
|
+
}
|
|
136
|
+
return 0;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// src/helpers/conditions/emptyCondition.ts
|
|
140
|
+
var emptyCondition = (_args, value) => sizeOf(value) === 0;
|
|
141
|
+
|
|
142
|
+
// src/helpers/conditions/eqCondition.ts
|
|
143
|
+
var eqCondition = (_args, left, right) => Object.is(left, right);
|
|
144
|
+
|
|
145
|
+
// src/helpers/conditions/existsCondition.ts
|
|
146
|
+
var existsCondition = (_args, value) => value !== void 0 && value !== null;
|
|
147
|
+
|
|
148
|
+
// src/helpers/conditions/falsyCondition.ts
|
|
149
|
+
var falsyCondition = (_args, value) => !value;
|
|
150
|
+
|
|
151
|
+
// src/helpers/conditions/gtCondition.ts
|
|
152
|
+
var gtCondition = (_args, left, right) => Number(left) > Number(right);
|
|
153
|
+
|
|
154
|
+
// src/helpers/conditions/gteCondition.ts
|
|
155
|
+
var gteCondition = (_args, left, right) => Number(left) >= Number(right);
|
|
156
|
+
|
|
157
|
+
// src/helpers/conditions/includesCondition.ts
|
|
158
|
+
var includesCondition = (_args, collection, value) => {
|
|
159
|
+
if (typeof collection === "string") {
|
|
160
|
+
return collection.includes(String(value));
|
|
161
|
+
}
|
|
162
|
+
if (Array.isArray(collection)) {
|
|
163
|
+
return collection.includes(value);
|
|
164
|
+
}
|
|
165
|
+
if (collection instanceof Set) {
|
|
166
|
+
return collection.has(value);
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// src/helpers/conditions/ltCondition.ts
|
|
172
|
+
var ltCondition = (_args, left, right) => Number(left) < Number(right);
|
|
173
|
+
|
|
174
|
+
// src/helpers/conditions/lteCondition.ts
|
|
175
|
+
var lteCondition = (_args, left, right) => Number(left) <= Number(right);
|
|
176
|
+
|
|
177
|
+
// src/helpers/conditions/missingCondition.ts
|
|
178
|
+
var missingCondition = (_args, value) => value === void 0 || value === null;
|
|
179
|
+
|
|
180
|
+
// src/helpers/conditions/neqCondition.ts
|
|
181
|
+
var neqCondition = (_args, left, right) => !Object.is(left, right);
|
|
182
|
+
|
|
183
|
+
// src/helpers/conditions/notEmptyCondition.ts
|
|
184
|
+
var notEmptyCondition = (_args, value) => sizeOf(value) > 0;
|
|
185
|
+
|
|
186
|
+
// src/helpers/conditions/truthyCondition.ts
|
|
187
|
+
var truthyCondition = (_args, value) => Boolean(value);
|
|
188
|
+
|
|
189
|
+
// src/registry/conditions.ts
|
|
190
|
+
var createConditionsRegistry = () => /* @__PURE__ */ new Map([
|
|
191
|
+
["eq", eqCondition],
|
|
192
|
+
["neq", neqCondition],
|
|
193
|
+
["gt", gtCondition],
|
|
194
|
+
["gte", gteCondition],
|
|
195
|
+
["lt", ltCondition],
|
|
196
|
+
["lte", lteCondition],
|
|
197
|
+
["truthy", truthyCondition],
|
|
198
|
+
["falsy", falsyCondition],
|
|
199
|
+
["exists", existsCondition],
|
|
200
|
+
["missing", missingCondition],
|
|
201
|
+
["empty", emptyCondition],
|
|
202
|
+
["notEmpty", notEmptyCondition],
|
|
203
|
+
["includes", includesCondition],
|
|
204
|
+
["changed", changedCondition],
|
|
205
|
+
["cooldownReady", cooldownReadyCondition]
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
// src/helpers/trace/createMemoryTraceSink.ts
|
|
209
|
+
var createMemoryTraceSink = () => {
|
|
210
|
+
const items = [];
|
|
211
|
+
return {
|
|
212
|
+
push: (entry) => {
|
|
213
|
+
items.push(entry);
|
|
214
|
+
},
|
|
215
|
+
entries: () => [...items]
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/helpers/runner/applyResult.ts
|
|
220
|
+
var applyResult = (result, state, mergeData) => {
|
|
221
|
+
if (result.status === "success" && result.context !== void 0) {
|
|
222
|
+
state.context = result.context;
|
|
223
|
+
}
|
|
224
|
+
if ("data" in result && result.data) {
|
|
225
|
+
state.data = mergeData(state.data, result.data);
|
|
226
|
+
}
|
|
227
|
+
state.patches.push(...result.patches);
|
|
228
|
+
state.events.push(...result.events);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/helpers/path/resolveValue.ts
|
|
232
|
+
var import_objwalk = require("objwalk");
|
|
233
|
+
|
|
234
|
+
// src/helpers/path/pathReferenceRegex.ts
|
|
235
|
+
var pathReferenceRegex = /^\$(context|data|input)(?:\.([A-Za-z0-9_$.[\]-]+))?$/;
|
|
236
|
+
|
|
237
|
+
// src/helpers/path/resolveValue.ts
|
|
238
|
+
var resolveValue = (value, scope) => {
|
|
239
|
+
if (typeof value === "string") {
|
|
240
|
+
const match = value.match(pathReferenceRegex);
|
|
241
|
+
if (!match) {
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
const root = match[1];
|
|
245
|
+
const path = match[2] ?? "";
|
|
246
|
+
const source = scope[root];
|
|
247
|
+
if (!path) {
|
|
248
|
+
return source;
|
|
249
|
+
}
|
|
250
|
+
if (!source || typeof source !== "object") {
|
|
251
|
+
return void 0;
|
|
252
|
+
}
|
|
253
|
+
return (0, import_objwalk.pick)(source, path);
|
|
254
|
+
}
|
|
255
|
+
if (Array.isArray(value)) {
|
|
256
|
+
return value.map((item) => resolveValue(item, scope));
|
|
257
|
+
}
|
|
258
|
+
if (value && typeof value === "object") {
|
|
259
|
+
const record = value;
|
|
260
|
+
if (typeof record.$template === "string") {
|
|
261
|
+
return record.$template.replace(/\{\{\s*([A-Za-z0-9_$.[\]-]+)\s*\}\}/g, (_, key) => {
|
|
262
|
+
const resolved = resolveValue(`$data.${key}`, scope);
|
|
263
|
+
return resolved == null ? "" : String(resolved);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return Object.fromEntries(Object.entries(record).map(([key, item]) => [key, resolveValue(item, scope)]));
|
|
267
|
+
}
|
|
268
|
+
return value;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/helpers/runner/evaluateCondition.ts
|
|
272
|
+
var evaluateCondition = (expression, registry, scope) => {
|
|
273
|
+
if (expression === void 0) {
|
|
274
|
+
return { ok: true, matched: true };
|
|
275
|
+
}
|
|
276
|
+
if (typeof expression === "boolean") {
|
|
277
|
+
return { ok: true, matched: expression };
|
|
278
|
+
}
|
|
279
|
+
const [operator, ...rawArgs] = expression;
|
|
280
|
+
if (operator === "and") {
|
|
281
|
+
for (const item of rawArgs) {
|
|
282
|
+
const result = evaluateCondition(item, registry, scope);
|
|
283
|
+
if (!result.ok || !result.matched) {
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { ok: true, matched: true };
|
|
288
|
+
}
|
|
289
|
+
if (operator === "or") {
|
|
290
|
+
for (const item of rawArgs) {
|
|
291
|
+
const result = evaluateCondition(item, registry, scope);
|
|
292
|
+
if (!result.ok) {
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
if (result.matched) {
|
|
296
|
+
return { ok: true, matched: true };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return { ok: true, matched: false };
|
|
300
|
+
}
|
|
301
|
+
if (operator === "not") {
|
|
302
|
+
const result = evaluateCondition(rawArgs[0], registry, scope);
|
|
303
|
+
return result.ok ? { ok: true, matched: !result.matched } : result;
|
|
304
|
+
}
|
|
305
|
+
const condition = registry.get(operator);
|
|
306
|
+
if (!condition) {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
error: behaviorError("CONDITION_NOT_FOUND", `Condition "${operator}" is not registered`, {
|
|
310
|
+
...scope.strategy ? { strategy: scope.strategy } : {}
|
|
311
|
+
})
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const args = rawArgs.map((arg) => resolveValue(arg, scope));
|
|
315
|
+
return { ok: true, matched: Boolean(condition(scope, ...args)) };
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// src/helpers/runner/createRuntime.ts
|
|
319
|
+
var import_objwalk2 = require("objwalk");
|
|
320
|
+
var runtimePick = (source, path) => {
|
|
321
|
+
if (!path) {
|
|
322
|
+
return source;
|
|
323
|
+
}
|
|
324
|
+
if (!source || typeof source !== "object") {
|
|
325
|
+
return void 0;
|
|
326
|
+
}
|
|
327
|
+
return (0, import_objwalk2.pick)(source, path);
|
|
328
|
+
};
|
|
329
|
+
var createRuntime = (state) => ({
|
|
330
|
+
get: (path) => runtimePick(state.context, path),
|
|
331
|
+
getData: (path) => runtimePick(state.data, path),
|
|
332
|
+
setData: (path, value) => {
|
|
333
|
+
(0, import_objwalk2.set)(state.data, path, value);
|
|
334
|
+
},
|
|
335
|
+
resolve: (value) => resolveValue(value, state),
|
|
336
|
+
signal: state.signal,
|
|
337
|
+
emit: (event) => state.events.push(event),
|
|
338
|
+
patch: (patch) => state.patches.push(patch),
|
|
339
|
+
stop: (reason) => reason ? { type: "stop", reason } : { type: "stop" },
|
|
340
|
+
fail: (reason, data) => ({
|
|
341
|
+
type: "fail",
|
|
342
|
+
...reason ? { reason } : {},
|
|
343
|
+
...data ? { data } : {}
|
|
344
|
+
})
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// src/helpers/runner/executeNext.ts
|
|
348
|
+
var executeNext = (item, depth, state, environment) => {
|
|
349
|
+
const id = typeof item === "string" ? item : item.strategy;
|
|
350
|
+
if (typeof item !== "string" && item.when) {
|
|
351
|
+
const runtime = createRuntime(state);
|
|
352
|
+
const condition = evaluateCondition(item.when, environment.conditionsRegistry, { ...state, runtime, strategy: id });
|
|
353
|
+
if (!condition.ok) {
|
|
354
|
+
return { status: "failed", error: condition.error, patches: [], events: [] };
|
|
355
|
+
}
|
|
356
|
+
if (!condition.matched) {
|
|
357
|
+
return {
|
|
358
|
+
status: "skipped",
|
|
359
|
+
reason: "next condition did not match",
|
|
360
|
+
patches: [],
|
|
361
|
+
events: []
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const extraProps = typeof item === "string" ? {} : item.props ?? {};
|
|
366
|
+
return executeStrategy(id, extraProps, depth, state, environment);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// src/helpers/runner/isPromiseLike.ts
|
|
370
|
+
var isPromiseLike = (value) => Boolean(value && typeof value === "object" && "then" in value && typeof value.then === "function");
|
|
371
|
+
|
|
372
|
+
// src/helpers/runner/executeSequence.ts
|
|
373
|
+
var executeSequence = (items, depth, state, environment) => {
|
|
374
|
+
let index = 0;
|
|
375
|
+
const loop = () => {
|
|
376
|
+
if (index >= items.length) {
|
|
377
|
+
return { status: "success", patches: [], events: [] };
|
|
378
|
+
}
|
|
379
|
+
const result = executeNext(items[index++], depth + 1, state, environment);
|
|
380
|
+
const next = (value) => {
|
|
381
|
+
if (value.status !== "success") {
|
|
382
|
+
return value;
|
|
383
|
+
}
|
|
384
|
+
return loop();
|
|
385
|
+
};
|
|
386
|
+
return isPromiseLike(result) ? result.then(next) : next(result);
|
|
387
|
+
};
|
|
388
|
+
return loop();
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// src/helpers/runner/skippedResult.ts
|
|
392
|
+
var skippedResult = (reason, data) => ({
|
|
393
|
+
status: "skipped",
|
|
394
|
+
patches: [],
|
|
395
|
+
events: [],
|
|
396
|
+
...reason ? { reason } : {},
|
|
397
|
+
...data ? { data } : {}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// src/helpers/runner/successResult.ts
|
|
401
|
+
var successResult = () => ({
|
|
402
|
+
status: "success",
|
|
403
|
+
patches: [],
|
|
404
|
+
events: []
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// src/helpers/runner/executeParallel.ts
|
|
408
|
+
var executeParallel = (items, depth, state, environment) => {
|
|
409
|
+
if (state.sync) {
|
|
410
|
+
return executeSequence(items, depth, state, environment);
|
|
411
|
+
}
|
|
412
|
+
const snapshots = items.map((item) => {
|
|
413
|
+
const child = { ...state, patches: [], events: [], data: { ...state.data } };
|
|
414
|
+
return Promise.resolve(executeNext(item, depth + 1, child, environment)).then((result) => ({ result, child }));
|
|
415
|
+
});
|
|
416
|
+
return Promise.all(snapshots).then((results) => {
|
|
417
|
+
for (const { result } of results) {
|
|
418
|
+
if (result.status === "failed" || result.status === "stopped") {
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
for (const { child } of results) {
|
|
423
|
+
state.patches.push(...child.patches);
|
|
424
|
+
state.events.push(...child.events);
|
|
425
|
+
state.data = environment.mergeData(state.data, child.data);
|
|
426
|
+
state.steps = Math.max(state.steps, child.steps);
|
|
427
|
+
}
|
|
428
|
+
return results.some(({ result }) => result.status === "success") ? successResult() : skippedResult("All parallel branches skipped");
|
|
429
|
+
});
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// src/helpers/runner/executeSelector.ts
|
|
433
|
+
var executeSelector = (items, depth, state, environment) => {
|
|
434
|
+
let index = 0;
|
|
435
|
+
const loop = () => {
|
|
436
|
+
if (index >= items.length) {
|
|
437
|
+
return { status: "skipped", reason: "No selector branch matched", patches: [], events: [] };
|
|
438
|
+
}
|
|
439
|
+
const result = executeNext(items[index++], depth + 1, state, environment);
|
|
440
|
+
const next = (value) => {
|
|
441
|
+
if (value.status === "skipped") {
|
|
442
|
+
return loop();
|
|
443
|
+
}
|
|
444
|
+
return value;
|
|
445
|
+
};
|
|
446
|
+
return isPromiseLike(result) ? result.then(next) : next(result);
|
|
447
|
+
};
|
|
448
|
+
return loop();
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// src/helpers/runner/executeThen.ts
|
|
452
|
+
var executeThen = (strategy, depth, state, environment) => {
|
|
453
|
+
const items = strategy.then ?? [];
|
|
454
|
+
if (items.length === 0) {
|
|
455
|
+
return { status: "success", patches: [], events: [] };
|
|
456
|
+
}
|
|
457
|
+
const mode = strategy.mode ?? "sequence";
|
|
458
|
+
if (mode === "selector") {
|
|
459
|
+
return executeSelector(items, depth, state, environment);
|
|
460
|
+
}
|
|
461
|
+
if (mode === "parallel") {
|
|
462
|
+
return executeParallel(items, depth, state, environment);
|
|
463
|
+
}
|
|
464
|
+
return executeSequence(items, depth, state, environment);
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// src/helpers/errors/withErrorStage.ts
|
|
468
|
+
var compactStage = (stage) => Object.fromEntries(Object.entries(stage).filter(([, value]) => value !== void 0));
|
|
469
|
+
var withErrorStage = (error, stage) => {
|
|
470
|
+
const nextError = {
|
|
471
|
+
...error,
|
|
472
|
+
stage: compactStage({
|
|
473
|
+
...stage,
|
|
474
|
+
...error.stage
|
|
475
|
+
})
|
|
476
|
+
};
|
|
477
|
+
const strategy = error.strategy ?? stage.strategy;
|
|
478
|
+
const fn = error.fn ?? stage.fn;
|
|
479
|
+
if (strategy) {
|
|
480
|
+
nextError.strategy = strategy;
|
|
481
|
+
}
|
|
482
|
+
if (fn) {
|
|
483
|
+
nextError.fn = fn;
|
|
484
|
+
}
|
|
485
|
+
return nextError;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// src/helpers/runner/handleFailure.ts
|
|
489
|
+
var handleFailure = (error, strategy, depth, state, environment) => {
|
|
490
|
+
const stagedError = withErrorStage(error, {
|
|
491
|
+
phase: error.stage?.phase ?? "action",
|
|
492
|
+
strategy: error.strategy,
|
|
493
|
+
fn: error.fn ?? strategy.fn,
|
|
494
|
+
mode: strategy.mode,
|
|
495
|
+
depth,
|
|
496
|
+
step: state.steps
|
|
497
|
+
});
|
|
498
|
+
environment.options.onError?.({
|
|
499
|
+
error: stagedError,
|
|
500
|
+
context: state.context,
|
|
501
|
+
input: state.input,
|
|
502
|
+
data: state.data,
|
|
503
|
+
patches: state.patches,
|
|
504
|
+
events: state.events,
|
|
505
|
+
...state.traceSink?.entries ? { trace: state.traceSink.entries() } : {}
|
|
506
|
+
});
|
|
507
|
+
state.reportedErrors.push(stagedError);
|
|
508
|
+
if (strategy.catch?.length) {
|
|
509
|
+
return executeSequence(strategy.catch, depth, state, environment);
|
|
510
|
+
}
|
|
511
|
+
return { status: "failed", error: stagedError, patches: [], events: [] };
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// src/helpers/runner/normalizeActionResult.ts
|
|
515
|
+
var normalizeActionResult = (raw) => {
|
|
516
|
+
if (raw === false) {
|
|
517
|
+
return skippedResult();
|
|
518
|
+
}
|
|
519
|
+
if (raw == null) {
|
|
520
|
+
return successResult();
|
|
521
|
+
}
|
|
522
|
+
const patches = "patch" in raw && raw.patch !== void 0 ? [].concat(raw.patch) : [];
|
|
523
|
+
const events = "events" in raw && raw.events ? raw.events : [];
|
|
524
|
+
if (raw.type === "skip") {
|
|
525
|
+
return skippedResult(raw.reason, raw.data);
|
|
526
|
+
}
|
|
527
|
+
if (raw.type === "stop") {
|
|
528
|
+
return { status: "stopped", patches, events, ...raw.reason ? { reason: raw.reason } : {} };
|
|
529
|
+
}
|
|
530
|
+
if (raw.type === "fail") {
|
|
531
|
+
return {
|
|
532
|
+
status: "failed",
|
|
533
|
+
error: behaviorError("ACTION_THROWN", raw.reason ?? "Action failed", { cause: raw.error }),
|
|
534
|
+
patches: [],
|
|
535
|
+
events: [],
|
|
536
|
+
...raw.data ? { data: raw.data } : {}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
status: "success",
|
|
541
|
+
patches,
|
|
542
|
+
events,
|
|
543
|
+
...raw.context !== void 0 ? { context: raw.context } : {},
|
|
544
|
+
...raw.data ? { data: raw.data } : {}
|
|
545
|
+
};
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// src/helpers/trace/cloneData.ts
|
|
549
|
+
var cloneData = (data) => ({ ...data });
|
|
550
|
+
|
|
551
|
+
// src/helpers/runner/pushTrace.ts
|
|
552
|
+
var pushTrace = (state, step, depth, strategyId, strategy, status, props, dataBefore, startedAt, reason) => {
|
|
553
|
+
state.traceSink?.push({
|
|
554
|
+
step,
|
|
555
|
+
depth,
|
|
556
|
+
strategy: strategyId,
|
|
557
|
+
fn: strategy.fn,
|
|
558
|
+
mode: strategy.mode,
|
|
559
|
+
status,
|
|
560
|
+
input: state.input,
|
|
561
|
+
props,
|
|
562
|
+
dataBefore,
|
|
563
|
+
dataAfter: cloneData(state.data),
|
|
564
|
+
durationMs: Date.now() - startedAt,
|
|
565
|
+
...reason ? { reason } : {}
|
|
566
|
+
});
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// src/helpers/runner/afterAction.ts
|
|
570
|
+
var afterAction = (raw, id, strategy, depth, state, props, dataBefore, traceStep, startedAt, environment) => {
|
|
571
|
+
const result = normalizeActionResult(raw);
|
|
572
|
+
if (result.status === "failed") {
|
|
573
|
+
result.error = withErrorStage(result.error, {
|
|
574
|
+
phase: "action",
|
|
575
|
+
strategy: id,
|
|
576
|
+
fn: strategy.fn,
|
|
577
|
+
mode: strategy.mode,
|
|
578
|
+
depth,
|
|
579
|
+
step: traceStep
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
applyResult(result, state, environment.mergeData);
|
|
583
|
+
const reason = result.status === "failed" ? result.error.message : "reason" in result ? result.reason : void 0;
|
|
584
|
+
pushTrace(
|
|
585
|
+
state,
|
|
586
|
+
traceStep,
|
|
587
|
+
depth,
|
|
588
|
+
id,
|
|
589
|
+
strategy,
|
|
590
|
+
result.status === "success" ? "success" : result.status,
|
|
591
|
+
props,
|
|
592
|
+
dataBefore,
|
|
593
|
+
startedAt,
|
|
594
|
+
reason
|
|
595
|
+
);
|
|
596
|
+
if (result.status === "failed") {
|
|
597
|
+
return handleFailure(result.error, strategy, depth, state, environment);
|
|
598
|
+
}
|
|
599
|
+
if (result.status === "skipped" || result.status === "stopped") {
|
|
600
|
+
return result;
|
|
601
|
+
}
|
|
602
|
+
if (strategy.terminal) {
|
|
603
|
+
return result;
|
|
604
|
+
}
|
|
605
|
+
return executeThen(strategy, depth, state, environment);
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// src/helpers/runner/runnerDefaults.ts
|
|
609
|
+
var defaultMaxSteps = 100;
|
|
610
|
+
var defaultMaxDepth = 32;
|
|
611
|
+
|
|
612
|
+
// src/helpers/runner/failLimit.ts
|
|
613
|
+
var failLimit = (code, message, id) => ({
|
|
614
|
+
status: "failed",
|
|
615
|
+
error: behaviorError(code, message, { strategy: id, stage: { phase: "limit", strategy: id } }),
|
|
616
|
+
patches: [],
|
|
617
|
+
events: []
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// src/helpers/runner/executeStrategy.ts
|
|
621
|
+
var executeStrategy = (id, extraProps, depth, state, environment) => {
|
|
622
|
+
const config = environment.configRef.current;
|
|
623
|
+
if (!config) {
|
|
624
|
+
return {
|
|
625
|
+
status: "failed",
|
|
626
|
+
error: behaviorError("CONFIG_INVALID", "No behavior config loaded", { stage: { phase: "entrypoint" } }),
|
|
627
|
+
patches: [],
|
|
628
|
+
events: []
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
if (depth > (environment.options.maxDepth ?? defaultMaxDepth)) {
|
|
632
|
+
return failLimit("MAX_DEPTH", `Max depth exceeded at strategy "${id}"`, id);
|
|
633
|
+
}
|
|
634
|
+
if (state.steps >= (environment.options.maxSteps ?? defaultMaxSteps)) {
|
|
635
|
+
return failLimit("MAX_STEPS", `Max steps exceeded at strategy "${id}"`, id);
|
|
636
|
+
}
|
|
637
|
+
if (environment.options.timeoutMs && Date.now() - state.startedAt > environment.options.timeoutMs) {
|
|
638
|
+
return failLimit("TIMEOUT", `Behavior run timed out after ${environment.options.timeoutMs}ms`, id);
|
|
639
|
+
}
|
|
640
|
+
const strategy = config.strategies[id];
|
|
641
|
+
if (!strategy) {
|
|
642
|
+
return {
|
|
643
|
+
status: "failed",
|
|
644
|
+
error: behaviorError("STRATEGY_NOT_FOUND", `Strategy "${id}" is not defined`, {
|
|
645
|
+
strategy: id,
|
|
646
|
+
stage: { phase: "entrypoint", strategy: id, depth }
|
|
647
|
+
}),
|
|
648
|
+
patches: [],
|
|
649
|
+
events: []
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
const action = environment.actionsRegistry.get(strategy.fn);
|
|
653
|
+
if (!action) {
|
|
654
|
+
return {
|
|
655
|
+
status: "failed",
|
|
656
|
+
error: behaviorError("ACTION_NOT_FOUND", `Action "${strategy.fn}" is not registered`, {
|
|
657
|
+
strategy: id,
|
|
658
|
+
fn: strategy.fn,
|
|
659
|
+
stage: { phase: "action", strategy: id, fn: strategy.fn, mode: strategy.mode, depth, step: state.steps + 1 }
|
|
660
|
+
}),
|
|
661
|
+
patches: [],
|
|
662
|
+
events: []
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
const props = resolveValue({ ...strategy.props ?? {}, ...extraProps }, state);
|
|
666
|
+
const runtime = createRuntime(state);
|
|
667
|
+
const dataBefore = cloneData(state.data);
|
|
668
|
+
const traceStep = state.steps + 1;
|
|
669
|
+
const startedAt = Date.now();
|
|
670
|
+
const condition = evaluateCondition(strategy.when, environment.conditionsRegistry, {
|
|
671
|
+
...state,
|
|
672
|
+
runtime,
|
|
673
|
+
strategy: id
|
|
674
|
+
});
|
|
675
|
+
if (!condition.ok) {
|
|
676
|
+
return handleFailure(
|
|
677
|
+
withErrorStage(condition.error, {
|
|
678
|
+
phase: "condition",
|
|
679
|
+
strategy: id,
|
|
680
|
+
fn: strategy.fn,
|
|
681
|
+
mode: strategy.mode,
|
|
682
|
+
depth,
|
|
683
|
+
step: traceStep
|
|
684
|
+
}),
|
|
685
|
+
strategy,
|
|
686
|
+
depth,
|
|
687
|
+
state,
|
|
688
|
+
environment
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
if (!condition.matched) {
|
|
692
|
+
pushTrace(state, traceStep, depth, id, strategy, "skipped", props, dataBefore, startedAt);
|
|
693
|
+
return { status: "skipped", reason: "when condition did not match", patches: [], events: [] };
|
|
694
|
+
}
|
|
695
|
+
state.steps += 1;
|
|
696
|
+
const invoke = () => action({ context: state.context, props, input: state.input, signal: state.signal, runtime });
|
|
697
|
+
try {
|
|
698
|
+
const raw = invoke();
|
|
699
|
+
if (isPromiseLike(raw)) {
|
|
700
|
+
if (state.sync) {
|
|
701
|
+
throw new BehaviorSyncAsyncError(
|
|
702
|
+
behaviorError("ASYNC_IN_SYNC_RUN", `Strategy "${id}" returned a Promise`, {
|
|
703
|
+
strategy: id,
|
|
704
|
+
fn: strategy.fn,
|
|
705
|
+
stage: { phase: "action", strategy: id, fn: strategy.fn, mode: strategy.mode, depth, step: traceStep }
|
|
706
|
+
})
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
return raw.then(
|
|
710
|
+
(value) => afterAction(value, id, strategy, depth, state, props, dataBefore, traceStep, startedAt, environment)
|
|
711
|
+
).catch(
|
|
712
|
+
(cause) => handleFailure(
|
|
713
|
+
behaviorError("ACTION_THROWN", `Action "${strategy.fn}" threw`, {
|
|
714
|
+
strategy: id,
|
|
715
|
+
fn: strategy.fn,
|
|
716
|
+
cause,
|
|
717
|
+
stage: { phase: "action", strategy: id, fn: strategy.fn, mode: strategy.mode, depth, step: traceStep }
|
|
718
|
+
}),
|
|
719
|
+
strategy,
|
|
720
|
+
depth,
|
|
721
|
+
state,
|
|
722
|
+
environment
|
|
723
|
+
)
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
return afterAction(raw, id, strategy, depth, state, props, dataBefore, traceStep, startedAt, environment);
|
|
727
|
+
} catch (cause) {
|
|
728
|
+
if (cause instanceof BehaviorSyncAsyncError) {
|
|
729
|
+
throw cause;
|
|
730
|
+
}
|
|
731
|
+
return handleFailure(
|
|
732
|
+
behaviorError("ACTION_THROWN", `Action "${strategy.fn}" threw`, {
|
|
733
|
+
strategy: id,
|
|
734
|
+
fn: strategy.fn,
|
|
735
|
+
cause,
|
|
736
|
+
stage: { phase: "action", strategy: id, fn: strategy.fn, mode: strategy.mode, depth, step: traceStep }
|
|
737
|
+
}),
|
|
738
|
+
strategy,
|
|
739
|
+
depth,
|
|
740
|
+
state,
|
|
741
|
+
environment
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
// src/helpers/runner/finishRunResult.ts
|
|
747
|
+
var finishRunResult = (result, state, traceSink) => {
|
|
748
|
+
const runResult = {
|
|
749
|
+
status: result.status,
|
|
750
|
+
context: state.context,
|
|
751
|
+
data: state.data,
|
|
752
|
+
patches: state.patches,
|
|
753
|
+
events: state.events,
|
|
754
|
+
steps: state.steps
|
|
755
|
+
};
|
|
756
|
+
if (result.status === "failed") {
|
|
757
|
+
runResult.error = result.error;
|
|
758
|
+
}
|
|
759
|
+
const entries = traceSink?.entries?.();
|
|
760
|
+
if (entries) {
|
|
761
|
+
runResult.trace = entries;
|
|
762
|
+
}
|
|
763
|
+
return runResult;
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// src/helpers/runner/resolveEntrypoint.ts
|
|
767
|
+
var resolveEntrypoint = (entrypoint, environment) => {
|
|
768
|
+
const config = environment.configRef.current;
|
|
769
|
+
if (!config) {
|
|
770
|
+
return { error: behaviorError("CONFIG_INVALID", "No behavior config loaded", { stage: { phase: "entrypoint" } }) };
|
|
771
|
+
}
|
|
772
|
+
const id = config.entrypoints?.[entrypoint] ?? entrypoint;
|
|
773
|
+
if (!config.strategies[id]) {
|
|
774
|
+
return {
|
|
775
|
+
error: behaviorError("STRATEGY_NOT_FOUND", `Strategy "${id}" is not defined`, {
|
|
776
|
+
strategy: id,
|
|
777
|
+
stage: { phase: "entrypoint", entrypoint, strategy: id }
|
|
778
|
+
})
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
return { id };
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// src/helpers/validation/detectCycles.ts
|
|
785
|
+
var detectCycles = (config, errors, warnings) => {
|
|
786
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
787
|
+
const visited = /* @__PURE__ */ new Set();
|
|
788
|
+
const visit = (id, path) => {
|
|
789
|
+
if (visiting.has(id)) {
|
|
790
|
+
const cycle = [...path, id];
|
|
791
|
+
const hasTerminal = cycle.some((item) => config.strategies[item]?.terminal);
|
|
792
|
+
const issue = {
|
|
793
|
+
code: "CYCLE_DETECTED",
|
|
794
|
+
message: `Cycle detected: ${cycle.join(" -> ")}`,
|
|
795
|
+
strategy: id
|
|
796
|
+
};
|
|
797
|
+
(hasTerminal ? warnings : errors).push(issue);
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
if (visited.has(id)) {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
visiting.add(id);
|
|
804
|
+
for (const next of config.strategies[id]?.then ?? []) {
|
|
805
|
+
const target = typeof next === "string" ? next : next.strategy;
|
|
806
|
+
if (config.strategies[target]) {
|
|
807
|
+
visit(target, [...path, id]);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
visiting.delete(id);
|
|
811
|
+
visited.add(id);
|
|
812
|
+
return false;
|
|
813
|
+
};
|
|
814
|
+
Object.keys(config.strategies ?? {}).forEach((id) => visit(id, []));
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
// src/helpers/validation/validationConstants.ts
|
|
818
|
+
var validModes = /* @__PURE__ */ new Set(["sequence", "selector", "parallel"]);
|
|
819
|
+
var controlConditions = /* @__PURE__ */ new Set(["and", "or", "not"]);
|
|
820
|
+
|
|
821
|
+
// src/helpers/path/isPathReference.ts
|
|
822
|
+
var isPathReference = (value) => typeof value === "string" && value.startsWith("$");
|
|
823
|
+
|
|
824
|
+
// src/helpers/path/isValidPathReference.ts
|
|
825
|
+
var isValidPathReference = (value) => pathReferenceRegex.test(value);
|
|
826
|
+
|
|
827
|
+
// src/helpers/validation/validateRefs.ts
|
|
828
|
+
var validateRefs = (value, strategy, path, errors) => {
|
|
829
|
+
if (typeof value === "string") {
|
|
830
|
+
if (isPathReference(value) && !isValidPathReference(value)) {
|
|
831
|
+
errors.push({ code: "PATH_INVALID", message: `Invalid path reference "${value}"`, strategy, path });
|
|
832
|
+
}
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (Array.isArray(value)) {
|
|
836
|
+
value.forEach((item, index) => validateRefs(item, strategy, `${path}.${index}`, errors));
|
|
837
|
+
}
|
|
838
|
+
if (value && typeof value === "object") {
|
|
839
|
+
Object.entries(value).forEach(([key, item]) => validateRefs(item, strategy, `${path}.${key}`, errors));
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
// src/helpers/validation/validateCondition.ts
|
|
844
|
+
var validateCondition = (expression, strategy, path, conditionsRegistry, errors) => {
|
|
845
|
+
if (expression === void 0 || typeof expression === "boolean") {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (!Array.isArray(expression) || typeof expression[0] !== "string") {
|
|
849
|
+
errors.push({ code: "CONDITION_INVALID", message: "Condition expression is invalid", strategy, path });
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const [operator, ...args] = expression;
|
|
853
|
+
if (operator === "and" || operator === "or") {
|
|
854
|
+
args.forEach(
|
|
855
|
+
(arg, index) => validateCondition(
|
|
856
|
+
arg,
|
|
857
|
+
strategy,
|
|
858
|
+
`${path}.${index + 1}`,
|
|
859
|
+
conditionsRegistry,
|
|
860
|
+
errors
|
|
861
|
+
)
|
|
862
|
+
);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (operator === "not") {
|
|
866
|
+
validateCondition(args[0], strategy, `${path}.1`, conditionsRegistry, errors);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (!conditionsRegistry.has(operator) && !controlConditions.has(operator)) {
|
|
870
|
+
errors.push({
|
|
871
|
+
code: "CONDITION_NOT_FOUND",
|
|
872
|
+
message: `Condition "${operator}" is not registered`,
|
|
873
|
+
strategy,
|
|
874
|
+
path
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
args.forEach((arg, index) => validateRefs(arg, strategy, `${path}.${index + 1}`, errors));
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
// src/helpers/validation/validateNextList.ts
|
|
881
|
+
var validateNextList = (config, list, path, strategy, errors) => {
|
|
882
|
+
if (list === void 0) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (!Array.isArray(list)) {
|
|
886
|
+
errors.push({ code: "NEXT_INVALID", message: "then/catch must be arrays", strategy, path });
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
list.forEach((item, index) => {
|
|
890
|
+
const target = typeof item === "string" ? item : item?.strategy;
|
|
891
|
+
if (typeof target !== "string" || !config.strategies[target]) {
|
|
892
|
+
errors.push({
|
|
893
|
+
code: "STRATEGY_NOT_FOUND",
|
|
894
|
+
message: `Next strategy "${String(target)}" is not defined`,
|
|
895
|
+
strategy,
|
|
896
|
+
path: `${path}.${index}`
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
// src/helpers/validation/validateBehaviorConfig.ts
|
|
903
|
+
var validateBehaviorConfig = (config, actionsRegistry, conditionsRegistry) => {
|
|
904
|
+
const errors = [];
|
|
905
|
+
const warnings = [];
|
|
906
|
+
if (!config || typeof config !== "object") {
|
|
907
|
+
return {
|
|
908
|
+
ok: false,
|
|
909
|
+
errors: [{ code: "CONFIG_INVALID", message: "Config must be an object" }],
|
|
910
|
+
warnings
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
if (!config.strategies || typeof config.strategies !== "object") {
|
|
914
|
+
errors.push({ code: "CONFIG_INVALID", message: "Config must include strategies", path: "strategies" });
|
|
915
|
+
}
|
|
916
|
+
for (const [id, strategy] of Object.entries(config.strategies ?? {})) {
|
|
917
|
+
if (!strategy || typeof strategy !== "object") {
|
|
918
|
+
errors.push({ code: "STRATEGY_INVALID", message: "Strategy must be an object", strategy: id });
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (!strategy.fn || typeof strategy.fn !== "string") {
|
|
922
|
+
errors.push({ code: "FN_MISSING", message: "Strategy fn is required", strategy: id, path: `${id}.fn` });
|
|
923
|
+
} else if (!actionsRegistry.has(strategy.fn)) {
|
|
924
|
+
errors.push({
|
|
925
|
+
code: "ACTION_NOT_FOUND",
|
|
926
|
+
message: `Action "${strategy.fn}" is not registered`,
|
|
927
|
+
strategy: id,
|
|
928
|
+
path: `${id}.fn`
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
if (strategy.mode && !validModes.has(strategy.mode)) {
|
|
932
|
+
errors.push({ code: "MODE_INVALID", message: `Mode "${strategy.mode}" is invalid`, strategy: id });
|
|
933
|
+
}
|
|
934
|
+
validateNextList(config, strategy.then, `${id}.then`, id, errors);
|
|
935
|
+
validateNextList(config, strategy.catch, `${id}.catch`, id, errors);
|
|
936
|
+
validateCondition(strategy.when, id, `${id}.when`, conditionsRegistry, errors);
|
|
937
|
+
validateRefs(strategy.props, id, `${id}.props`, errors);
|
|
938
|
+
}
|
|
939
|
+
for (const [name, target] of Object.entries(config.entrypoints ?? {})) {
|
|
940
|
+
if (!config.strategies[target]) {
|
|
941
|
+
errors.push({
|
|
942
|
+
code: "STRATEGY_NOT_FOUND",
|
|
943
|
+
message: `Entrypoint "${name}" references missing strategy "${target}"`,
|
|
944
|
+
path: `entrypoints.${name}`
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
detectCycles(config, errors, warnings);
|
|
949
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
// src/helpers/runner/createBehaviorRunner.ts
|
|
953
|
+
var createBehaviorRunner = (options = {}) => {
|
|
954
|
+
const actionsRegistry = createActionsRegistry();
|
|
955
|
+
const conditionsRegistry = createConditionsRegistry();
|
|
956
|
+
const configRef = {};
|
|
957
|
+
const mergeData = options.mergeData ?? ((current, next) => ({ ...current, ...next }));
|
|
958
|
+
const environment = {
|
|
959
|
+
actionsRegistry,
|
|
960
|
+
conditionsRegistry,
|
|
961
|
+
configRef,
|
|
962
|
+
options,
|
|
963
|
+
mergeData
|
|
964
|
+
};
|
|
965
|
+
const registerAction = (name, action) => {
|
|
966
|
+
actionsRegistry.set(name, action);
|
|
967
|
+
};
|
|
968
|
+
const registerActions = (items) => {
|
|
969
|
+
Object.entries(items).forEach(([name, action]) => registerAction(name, action));
|
|
970
|
+
};
|
|
971
|
+
const registerCondition = (name, condition) => {
|
|
972
|
+
conditionsRegistry.set(name, condition);
|
|
973
|
+
};
|
|
974
|
+
const registerConditions = (items) => {
|
|
975
|
+
Object.entries(items).forEach(([name, condition]) => registerCondition(name, condition));
|
|
976
|
+
};
|
|
977
|
+
const validateConfig = (target = configRef.current) => validateBehaviorConfig(target, actionsRegistry, conditionsRegistry);
|
|
978
|
+
const loadConfig = (nextConfig) => {
|
|
979
|
+
configRef.current = nextConfig;
|
|
980
|
+
return validateConfig(nextConfig);
|
|
981
|
+
};
|
|
982
|
+
const runInternal = (entrypoint, context, input, sync, runOptions) => {
|
|
983
|
+
const traceSink = options.trace === true ? createMemoryTraceSink() : options.trace || void 0;
|
|
984
|
+
const state = {
|
|
985
|
+
context,
|
|
986
|
+
input,
|
|
987
|
+
data: {},
|
|
988
|
+
patches: [],
|
|
989
|
+
events: [],
|
|
990
|
+
steps: 0,
|
|
991
|
+
startedAt: Date.now(),
|
|
992
|
+
sync,
|
|
993
|
+
signal: runOptions.signal ?? new AbortController().signal,
|
|
994
|
+
reportedErrors: [],
|
|
995
|
+
...traceSink ? { traceSink } : {}
|
|
996
|
+
};
|
|
997
|
+
const reportError = (result) => {
|
|
998
|
+
if (result.status !== "failed" || state.reportedErrors.includes(result.error)) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
state.reportedErrors.push(result.error);
|
|
1002
|
+
options.onError?.({
|
|
1003
|
+
error: result.error,
|
|
1004
|
+
context: state.context,
|
|
1005
|
+
input: state.input,
|
|
1006
|
+
data: state.data,
|
|
1007
|
+
patches: state.patches,
|
|
1008
|
+
events: state.events,
|
|
1009
|
+
...traceSink?.entries ? { trace: traceSink.entries() } : {}
|
|
1010
|
+
});
|
|
1011
|
+
};
|
|
1012
|
+
const finish = (result) => finishRunResult(result, state, traceSink || void 0);
|
|
1013
|
+
const start = resolveEntrypoint(entrypoint, environment);
|
|
1014
|
+
if ("error" in start) {
|
|
1015
|
+
const result = { status: "failed", error: start.error, patches: [], events: [] };
|
|
1016
|
+
reportError(result);
|
|
1017
|
+
return finish(result);
|
|
1018
|
+
}
|
|
1019
|
+
const executed = executeStrategy(start.id, {}, 0, state, environment);
|
|
1020
|
+
const done = (result) => {
|
|
1021
|
+
reportError(result);
|
|
1022
|
+
return finish(result);
|
|
1023
|
+
};
|
|
1024
|
+
return isPromiseLike(executed) ? executed.then(done) : done(executed);
|
|
1025
|
+
};
|
|
1026
|
+
const run = async (entrypoint, context, input = {}, runOptions = {}) => runInternal(entrypoint, context, input, false, runOptions);
|
|
1027
|
+
const runSync = (entrypoint, context, input = {}, runOptions = {}) => {
|
|
1028
|
+
const result = runInternal(entrypoint, context, input, true, runOptions);
|
|
1029
|
+
if (isPromiseLike(result)) {
|
|
1030
|
+
throw new BehaviorSyncAsyncError(behaviorError("ASYNC_IN_SYNC_RUN", "runSync encountered an async action"));
|
|
1031
|
+
}
|
|
1032
|
+
return result;
|
|
1033
|
+
};
|
|
1034
|
+
return {
|
|
1035
|
+
registerAction,
|
|
1036
|
+
registerActions,
|
|
1037
|
+
registerCondition,
|
|
1038
|
+
registerConditions,
|
|
1039
|
+
loadConfig,
|
|
1040
|
+
validateConfig,
|
|
1041
|
+
run,
|
|
1042
|
+
runSync
|
|
1043
|
+
};
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
// src/pubSub.ts
|
|
1047
|
+
var import_nanoid = require("nanoid");
|
|
1048
|
+
var serializeError = (error) => ({
|
|
1049
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1050
|
+
});
|
|
1051
|
+
var isBusEvent = (event) => {
|
|
1052
|
+
if (!event || typeof event !== "object") {
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
const candidate = event;
|
|
1056
|
+
return typeof candidate.id === "string" && typeof candidate.topic === "string" && typeof candidate.occurredAt === "number" && typeof candidate.serialized === "string" && "parsed" in candidate && (candidate.origin === void 0 || typeof candidate.origin === "string");
|
|
1057
|
+
};
|
|
1058
|
+
var createPubSubBehavior = (options = {}) => {
|
|
1059
|
+
const subscribers = /* @__PURE__ */ new Map();
|
|
1060
|
+
const dispatch = (event) => {
|
|
1061
|
+
let dispatchedEvent;
|
|
1062
|
+
if (!isBusEvent(event)) {
|
|
1063
|
+
options.onError?.({
|
|
1064
|
+
type: "serialization",
|
|
1065
|
+
topic: "unknown",
|
|
1066
|
+
payload: event,
|
|
1067
|
+
error: new Error("Invalid behavior bus event")
|
|
1068
|
+
});
|
|
1069
|
+
} else {
|
|
1070
|
+
const handlers = subscribers.get(event.topic);
|
|
1071
|
+
if (handlers) {
|
|
1072
|
+
for (const handler of [...handlers]) {
|
|
1073
|
+
try {
|
|
1074
|
+
handler(event);
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
options.onError?.({ type: "subscriber", event, error });
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
dispatchedEvent = event;
|
|
1081
|
+
}
|
|
1082
|
+
return dispatchedEvent;
|
|
1083
|
+
};
|
|
1084
|
+
const on = (event, handler) => {
|
|
1085
|
+
const handlers = subscribers.get(event) ?? /* @__PURE__ */ new Set();
|
|
1086
|
+
const listener = handler;
|
|
1087
|
+
handlers.add(listener);
|
|
1088
|
+
subscribers.set(event, handlers);
|
|
1089
|
+
return () => off(event, handler);
|
|
1090
|
+
};
|
|
1091
|
+
const off = (event, handler) => {
|
|
1092
|
+
if (handler) {
|
|
1093
|
+
const handlers = subscribers.get(event);
|
|
1094
|
+
if (handlers) {
|
|
1095
|
+
handlers.delete(handler);
|
|
1096
|
+
if (handlers.size === 0) {
|
|
1097
|
+
subscribers.delete(event);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
} else {
|
|
1101
|
+
subscribers.delete(event);
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
const emit = (topic, payload, emitOptions = {}) => {
|
|
1105
|
+
let event;
|
|
1106
|
+
try {
|
|
1107
|
+
const serialized = JSON.stringify(payload);
|
|
1108
|
+
if (typeof serialized !== "string") {
|
|
1109
|
+
throw new Error("Payload cannot be serialized");
|
|
1110
|
+
}
|
|
1111
|
+
event = {
|
|
1112
|
+
id: (0, import_nanoid.nanoid)(),
|
|
1113
|
+
topic,
|
|
1114
|
+
occurredAt: Date.now(),
|
|
1115
|
+
...emitOptions.origin ? { origin: emitOptions.origin } : {},
|
|
1116
|
+
parsed: payload,
|
|
1117
|
+
serialized
|
|
1118
|
+
};
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
const parsed = serializeError(error);
|
|
1121
|
+
event = {
|
|
1122
|
+
id: (0, import_nanoid.nanoid)(),
|
|
1123
|
+
topic,
|
|
1124
|
+
occurredAt: Date.now(),
|
|
1125
|
+
...emitOptions.origin ? { origin: emitOptions.origin } : {},
|
|
1126
|
+
parsed,
|
|
1127
|
+
serialized: JSON.stringify(parsed)
|
|
1128
|
+
};
|
|
1129
|
+
options.onError?.({
|
|
1130
|
+
type: "serialization",
|
|
1131
|
+
topic,
|
|
1132
|
+
payload,
|
|
1133
|
+
...emitOptions.origin ? { origin: emitOptions.origin } : {},
|
|
1134
|
+
error
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
return dispatch(event);
|
|
1138
|
+
};
|
|
1139
|
+
return { on, off, emit, dispatch };
|
|
1140
|
+
};
|
|
1141
|
+
var PubSubBehavior = createPubSubBehavior();
|
|
1142
|
+
|
|
1143
|
+
// src/chainBehavior.ts
|
|
1144
|
+
var busBindingPrefix = "[bus] ";
|
|
1145
|
+
var domBindingPrefix = "[dom] ";
|
|
1146
|
+
var defaultMaxQueueSize = 50;
|
|
1147
|
+
var createChainBehavior = (definition, options) => {
|
|
1148
|
+
const runner = createBehaviorRunner(options);
|
|
1149
|
+
const bus = options.bus ?? PubSubBehavior;
|
|
1150
|
+
const unsubscribers = /* @__PURE__ */ new Set();
|
|
1151
|
+
const lanes = /* @__PURE__ */ new Map();
|
|
1152
|
+
const activeRuns = /* @__PURE__ */ new Set();
|
|
1153
|
+
let runCount = 0;
|
|
1154
|
+
runner.registerActions(definition.actions ?? {});
|
|
1155
|
+
runner.registerConditions(definition.conditions ?? {});
|
|
1156
|
+
const emitDiagnostic = (event, payload) => {
|
|
1157
|
+
;
|
|
1158
|
+
bus.emit(event, payload);
|
|
1159
|
+
};
|
|
1160
|
+
const clearQueuedRuns = () => {
|
|
1161
|
+
for (const [binding, lane] of lanes) {
|
|
1162
|
+
for (const input of lane.queue) {
|
|
1163
|
+
emitDiagnostic("cfb.run.dropped", { binding, input, reason: "behavior-stopped" });
|
|
1164
|
+
}
|
|
1165
|
+
lane.queue = [];
|
|
1166
|
+
if (!lane.active) {
|
|
1167
|
+
lanes.delete(binding);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
const stop = (stopOptions = {}) => {
|
|
1172
|
+
for (const unsubscribe of unsubscribers) {
|
|
1173
|
+
unsubscribe();
|
|
1174
|
+
}
|
|
1175
|
+
unsubscribers.clear();
|
|
1176
|
+
clearQueuedRuns();
|
|
1177
|
+
if (stopOptions.force) {
|
|
1178
|
+
for (const run of activeRuns) {
|
|
1179
|
+
run.controller.abort();
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
const getContext = () => typeof options.context === "function" ? options.context() : options.context;
|
|
1184
|
+
const getConcurrency = (target) => target.options?.concurrency ?? options.concurrency ?? {};
|
|
1185
|
+
const startRun = (binding, target, input, key, lane) => {
|
|
1186
|
+
const controller = new AbortController();
|
|
1187
|
+
const run = { controller, id: `run-${++runCount}` };
|
|
1188
|
+
activeRuns.add(run);
|
|
1189
|
+
if (lane) {
|
|
1190
|
+
lane.active = run;
|
|
1191
|
+
}
|
|
1192
|
+
emitDiagnostic("cfb.run.started", { binding, entrypoint: target.entrypoint, key, runId: run.id });
|
|
1193
|
+
void runner.run(target.entrypoint, getContext(), input, { signal: controller.signal }).then((result) => {
|
|
1194
|
+
const payload = { binding, entrypoint: target.entrypoint, key, runId: run.id };
|
|
1195
|
+
if (controller.signal.aborted) {
|
|
1196
|
+
emitDiagnostic("cfb.run.cancelled", payload);
|
|
1197
|
+
} else if (result.status === "failed") {
|
|
1198
|
+
emitDiagnostic("cfb.run.failed", { ...payload, error: result.error });
|
|
1199
|
+
options.onRunnerError?.({
|
|
1200
|
+
error: result.error,
|
|
1201
|
+
result,
|
|
1202
|
+
binding,
|
|
1203
|
+
entrypoint: target.entrypoint,
|
|
1204
|
+
runId: run.id,
|
|
1205
|
+
...key === void 0 ? {} : { key }
|
|
1206
|
+
});
|
|
1207
|
+
} else {
|
|
1208
|
+
emitDiagnostic("cfb.run.finished", { ...payload, status: result.status });
|
|
1209
|
+
}
|
|
1210
|
+
}).catch((error) => {
|
|
1211
|
+
const payload = { binding, entrypoint: target.entrypoint, key, runId: run.id };
|
|
1212
|
+
emitDiagnostic(controller.signal.aborted ? "cfb.run.cancelled" : "cfb.run.failed", {
|
|
1213
|
+
...payload,
|
|
1214
|
+
...controller.signal.aborted ? {} : { error }
|
|
1215
|
+
});
|
|
1216
|
+
}).finally(() => {
|
|
1217
|
+
activeRuns.delete(run);
|
|
1218
|
+
if (lane && lane.active === run) {
|
|
1219
|
+
const nextInput = lane.queue.shift();
|
|
1220
|
+
lane.active = void 0;
|
|
1221
|
+
if (nextInput) {
|
|
1222
|
+
startRun(binding, target, nextInput, key, lane);
|
|
1223
|
+
} else {
|
|
1224
|
+
lanes.delete(key);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
};
|
|
1229
|
+
const scheduleRun = (binding, target, input) => {
|
|
1230
|
+
const concurrency = getConcurrency(target);
|
|
1231
|
+
const mode = concurrency.mode ?? "parallel";
|
|
1232
|
+
if (mode === "parallel") {
|
|
1233
|
+
startRun(binding, target, input);
|
|
1234
|
+
} else {
|
|
1235
|
+
const key = concurrency.key?.(input) ?? "";
|
|
1236
|
+
const laneKey = `${binding}:${key}`;
|
|
1237
|
+
const lane = lanes.get(laneKey) ?? { queue: [] };
|
|
1238
|
+
lanes.set(laneKey, lane);
|
|
1239
|
+
if (!lane.active) {
|
|
1240
|
+
startRun(binding, target, input, key, lane);
|
|
1241
|
+
} else if (mode === "latest") {
|
|
1242
|
+
lane.active.controller.abort();
|
|
1243
|
+
startRun(binding, target, input, key, lane);
|
|
1244
|
+
} else if (mode === "drop") {
|
|
1245
|
+
emitDiagnostic("cfb.run.dropped", { binding, entrypoint: target.entrypoint, key, reason: "run-active" });
|
|
1246
|
+
} else {
|
|
1247
|
+
const maxQueueSize = concurrency.maxQueueSize ?? defaultMaxQueueSize;
|
|
1248
|
+
const queueIsFull = lane.queue.length >= maxQueueSize;
|
|
1249
|
+
const dropsOldest = concurrency.overflow === "drop-oldest";
|
|
1250
|
+
if (queueIsFull) {
|
|
1251
|
+
emitDiagnostic("cfb.queue.overflow", { binding, entrypoint: target.entrypoint, key, maxQueueSize });
|
|
1252
|
+
if (dropsOldest) {
|
|
1253
|
+
const dropped = lane.queue.shift();
|
|
1254
|
+
emitDiagnostic("cfb.run.dropped", {
|
|
1255
|
+
binding,
|
|
1256
|
+
entrypoint: target.entrypoint,
|
|
1257
|
+
key,
|
|
1258
|
+
reason: "queue-overflow",
|
|
1259
|
+
...dropped ? { input: dropped } : {}
|
|
1260
|
+
});
|
|
1261
|
+
} else {
|
|
1262
|
+
emitDiagnostic("cfb.run.dropped", { binding, entrypoint: target.entrypoint, key, reason: "queue-overflow" });
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (!queueIsFull || dropsOldest) {
|
|
1266
|
+
lane.queue.push(input);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
const subscribeBusBinding = (binding, target) => {
|
|
1272
|
+
const event = binding.slice(busBindingPrefix.length);
|
|
1273
|
+
const unsubscribe = bus.on(event, (busEvent) => {
|
|
1274
|
+
scheduleRun(binding, target, busEvent.parsed);
|
|
1275
|
+
});
|
|
1276
|
+
unsubscribers.add(unsubscribe);
|
|
1277
|
+
};
|
|
1278
|
+
const parseDomBinding = (binding) => {
|
|
1279
|
+
const source = binding.slice(domBindingPrefix.length);
|
|
1280
|
+
const separator = source.lastIndexOf(":");
|
|
1281
|
+
if (separator <= 0 || separator === source.length - 1) {
|
|
1282
|
+
return void 0;
|
|
1283
|
+
}
|
|
1284
|
+
return { selector: source.slice(0, separator), eventType: source.slice(separator + 1) };
|
|
1285
|
+
};
|
|
1286
|
+
const collectForm = (element) => {
|
|
1287
|
+
const form = typeof HTMLFormElement !== "undefined" && element instanceof HTMLFormElement ? element : element.closest("form");
|
|
1288
|
+
if (!form) {
|
|
1289
|
+
return void 0;
|
|
1290
|
+
}
|
|
1291
|
+
const values = {};
|
|
1292
|
+
for (const [name, value] of new FormData(form)) {
|
|
1293
|
+
const current = values[name];
|
|
1294
|
+
values[name] = current === void 0 ? value : Array.isArray(current) ? [...current, value] : [current, value];
|
|
1295
|
+
}
|
|
1296
|
+
return values;
|
|
1297
|
+
};
|
|
1298
|
+
const createDomInput = (event, element) => {
|
|
1299
|
+
const value = "value" in element && typeof element.value === "string" ? element.value : void 0;
|
|
1300
|
+
const form = collectForm(element);
|
|
1301
|
+
const dataset = {};
|
|
1302
|
+
if (element instanceof HTMLElement) {
|
|
1303
|
+
for (const [key, item] of Object.entries(element.dataset)) {
|
|
1304
|
+
if (item !== void 0) {
|
|
1305
|
+
dataset[key] = item;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return {
|
|
1310
|
+
type: event.type,
|
|
1311
|
+
...value === void 0 ? {} : { value },
|
|
1312
|
+
dataset,
|
|
1313
|
+
...form ? { form } : {}
|
|
1314
|
+
};
|
|
1315
|
+
};
|
|
1316
|
+
const subscribeDomBinding = (binding, target) => {
|
|
1317
|
+
const parsed = parseDomBinding(binding);
|
|
1318
|
+
const root = options.root ?? (typeof document === "undefined" ? void 0 : document);
|
|
1319
|
+
let active = false;
|
|
1320
|
+
if (parsed && root) {
|
|
1321
|
+
let unsubscribe = () => void 0;
|
|
1322
|
+
const listener = (event) => {
|
|
1323
|
+
const eventTarget = event.target;
|
|
1324
|
+
if (typeof Element !== "undefined" && eventTarget instanceof Element) {
|
|
1325
|
+
const element = eventTarget.closest(parsed.selector);
|
|
1326
|
+
const belongsToRoot = !element || typeof Element === "undefined" || !(root instanceof Element) || root.contains(element);
|
|
1327
|
+
if (element && belongsToRoot) {
|
|
1328
|
+
const preventDefault = target.options?.preventDefault ?? event.type === "submit";
|
|
1329
|
+
if (preventDefault) {
|
|
1330
|
+
event.preventDefault();
|
|
1331
|
+
}
|
|
1332
|
+
if (target.options?.stopPropagation) {
|
|
1333
|
+
event.stopPropagation();
|
|
1334
|
+
}
|
|
1335
|
+
const defaultInput = createDomInput(event, element);
|
|
1336
|
+
const input = target.options?.input?.({ event, element, defaultInput }) ?? defaultInput;
|
|
1337
|
+
scheduleRun(binding, target, input);
|
|
1338
|
+
if (target.options?.once) {
|
|
1339
|
+
unsubscribe();
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
const listenerOptions = target.options?.capture === void 0 ? void 0 : { capture: target.options.capture };
|
|
1345
|
+
root.addEventListener(parsed.eventType, listener, listenerOptions);
|
|
1346
|
+
unsubscribe = () => root.removeEventListener(parsed.eventType, listener, listenerOptions);
|
|
1347
|
+
unsubscribers.add(unsubscribe);
|
|
1348
|
+
active = true;
|
|
1349
|
+
}
|
|
1350
|
+
return active;
|
|
1351
|
+
};
|
|
1352
|
+
const start = () => {
|
|
1353
|
+
stop();
|
|
1354
|
+
const validation = runner.loadConfig(definition.config);
|
|
1355
|
+
const active = [];
|
|
1356
|
+
const inactive = [];
|
|
1357
|
+
if (validation.ok) {
|
|
1358
|
+
const bindings = Object.entries(definition.events ?? {});
|
|
1359
|
+
for (const [binding, target] of bindings) {
|
|
1360
|
+
if (binding.startsWith(busBindingPrefix)) {
|
|
1361
|
+
subscribeBusBinding(binding, target);
|
|
1362
|
+
active.push(binding);
|
|
1363
|
+
} else if (binding.startsWith(domBindingPrefix)) {
|
|
1364
|
+
if (subscribeDomBinding(binding, target)) {
|
|
1365
|
+
active.push(binding);
|
|
1366
|
+
} else {
|
|
1367
|
+
inactive.push({ binding, reason: "dom-unavailable" });
|
|
1368
|
+
}
|
|
1369
|
+
} else {
|
|
1370
|
+
inactive.push({ binding, reason: "unsupported-source" });
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return { active, inactive, validation };
|
|
1375
|
+
};
|
|
1376
|
+
return { runner, start, stop };
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
// src/ws.ts
|
|
1380
|
+
var openState = 1;
|
|
1381
|
+
var maxSeenEvents = 1e3;
|
|
1382
|
+
var createBehaviorWs = (options) => {
|
|
1383
|
+
const inboundTopics = new Set(options.inboundTopics ?? []);
|
|
1384
|
+
const outboundTopics = new Set(options.outboundTopics ?? []);
|
|
1385
|
+
const seenEventIds = /* @__PURE__ */ new Set();
|
|
1386
|
+
const outboundUnsubscribers = /* @__PURE__ */ new Set();
|
|
1387
|
+
const socketUnsubscribers = /* @__PURE__ */ new Set();
|
|
1388
|
+
let socket;
|
|
1389
|
+
let retryTimer;
|
|
1390
|
+
let retryAttempt = 0;
|
|
1391
|
+
let started = false;
|
|
1392
|
+
let currentStatus = "idle";
|
|
1393
|
+
const diagnosticsBus = options.bus;
|
|
1394
|
+
const emitDiagnostic = (topic, payload) => {
|
|
1395
|
+
diagnosticsBus.emit(topic, payload, {
|
|
1396
|
+
...options.origin ? { origin: options.origin } : {}
|
|
1397
|
+
});
|
|
1398
|
+
};
|
|
1399
|
+
const rememberEvent = (id) => {
|
|
1400
|
+
seenEventIds.add(id);
|
|
1401
|
+
if (seenEventIds.size > maxSeenEvents) {
|
|
1402
|
+
const oldest = seenEventIds.values().next();
|
|
1403
|
+
if (!oldest.done) {
|
|
1404
|
+
seenEventIds.delete(oldest.value);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
const clearSocket = () => {
|
|
1409
|
+
for (const unsubscribe of socketUnsubscribers) {
|
|
1410
|
+
unsubscribe();
|
|
1411
|
+
}
|
|
1412
|
+
socketUnsubscribers.clear();
|
|
1413
|
+
socket = void 0;
|
|
1414
|
+
};
|
|
1415
|
+
const getRetryDelay = () => {
|
|
1416
|
+
const initialDelay = options.retry?.initialDelay ?? 500;
|
|
1417
|
+
const maxDelay = options.retry?.maxDelay ?? 1e4;
|
|
1418
|
+
const multiplier = options.retry?.multiplier ?? 2;
|
|
1419
|
+
const base = Math.min(initialDelay * multiplier ** retryAttempt, maxDelay);
|
|
1420
|
+
return options.retry?.jitter === false ? base : Math.round(base * (0.5 + Math.random() * 0.5));
|
|
1421
|
+
};
|
|
1422
|
+
const connect = () => {
|
|
1423
|
+
if (started && !socket) {
|
|
1424
|
+
currentStatus = "connecting";
|
|
1425
|
+
emitDiagnostic("cfb.ws.connecting", { attempt: retryAttempt });
|
|
1426
|
+
try {
|
|
1427
|
+
const current = options.createSocket();
|
|
1428
|
+
socket = current;
|
|
1429
|
+
const listen = (type, listener) => {
|
|
1430
|
+
current.addEventListener(type, listener);
|
|
1431
|
+
socketUnsubscribers.add(() => current.removeEventListener(type, listener));
|
|
1432
|
+
};
|
|
1433
|
+
const disconnect = (reason) => {
|
|
1434
|
+
if (socket === current) {
|
|
1435
|
+
clearSocket();
|
|
1436
|
+
if (started) {
|
|
1437
|
+
const delay = getRetryDelay();
|
|
1438
|
+
const attempt = retryAttempt + 1;
|
|
1439
|
+
const retry = () => {
|
|
1440
|
+
retryTimer = void 0;
|
|
1441
|
+
connect();
|
|
1442
|
+
};
|
|
1443
|
+
currentStatus = "retrying";
|
|
1444
|
+
retryAttempt = attempt;
|
|
1445
|
+
retryTimer = setTimeout(retry, delay);
|
|
1446
|
+
emitDiagnostic("cfb.ws.retrying", { reason, delay, attempt });
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
listen("open", () => {
|
|
1451
|
+
if (socket === current) {
|
|
1452
|
+
retryAttempt = 0;
|
|
1453
|
+
currentStatus = "connected";
|
|
1454
|
+
emitDiagnostic("cfb.ws.connected", {});
|
|
1455
|
+
}
|
|
1456
|
+
});
|
|
1457
|
+
listen("close", () => disconnect("close"));
|
|
1458
|
+
listen("error", () => disconnect("error"));
|
|
1459
|
+
listen("message", (event) => {
|
|
1460
|
+
const data = event.data;
|
|
1461
|
+
if (typeof data === "string") {
|
|
1462
|
+
try {
|
|
1463
|
+
const busEvent = JSON.parse(data);
|
|
1464
|
+
if (busEvent.topic && inboundTopics.has(busEvent.topic)) {
|
|
1465
|
+
if (busEvent.id) {
|
|
1466
|
+
rememberEvent(busEvent.id);
|
|
1467
|
+
}
|
|
1468
|
+
if (!options.bus.dispatch(busEvent)) {
|
|
1469
|
+
emitDiagnostic("cfb.ws.message.rejected", { reason: "invalid-envelope", topic: busEvent.topic });
|
|
1470
|
+
}
|
|
1471
|
+
} else {
|
|
1472
|
+
emitDiagnostic("cfb.ws.message.rejected", { reason: "topic-not-allowed", topic: busEvent.topic });
|
|
1473
|
+
}
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
emitDiagnostic("cfb.ws.message.rejected", { reason: "message-parse-failed", error: String(error) });
|
|
1476
|
+
}
|
|
1477
|
+
} else {
|
|
1478
|
+
emitDiagnostic("cfb.ws.message.rejected", { reason: "message-not-string" });
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
const delay = getRetryDelay();
|
|
1483
|
+
const attempt = retryAttempt + 1;
|
|
1484
|
+
const retry = () => {
|
|
1485
|
+
retryTimer = void 0;
|
|
1486
|
+
connect();
|
|
1487
|
+
};
|
|
1488
|
+
currentStatus = "retrying";
|
|
1489
|
+
retryAttempt = attempt;
|
|
1490
|
+
retryTimer = setTimeout(retry, delay);
|
|
1491
|
+
emitDiagnostic("cfb.ws.retrying", { reason: "socket-create-failed", delay, attempt, error: String(error) });
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
const start = () => {
|
|
1496
|
+
if (!started) {
|
|
1497
|
+
started = true;
|
|
1498
|
+
for (const topic of outboundTopics) {
|
|
1499
|
+
const unsubscribe = options.bus.on(topic, (event) => {
|
|
1500
|
+
if (!seenEventIds.delete(event.id) && event.origin !== options.origin && socket?.readyState === openState) {
|
|
1501
|
+
socket.send(event.serialized);
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
outboundUnsubscribers.add(unsubscribe);
|
|
1505
|
+
}
|
|
1506
|
+
connect();
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
const stop = () => {
|
|
1510
|
+
started = false;
|
|
1511
|
+
if (retryTimer) {
|
|
1512
|
+
clearTimeout(retryTimer);
|
|
1513
|
+
}
|
|
1514
|
+
retryTimer = void 0;
|
|
1515
|
+
for (const unsubscribe of outboundUnsubscribers) {
|
|
1516
|
+
unsubscribe();
|
|
1517
|
+
}
|
|
1518
|
+
outboundUnsubscribers.clear();
|
|
1519
|
+
const current = socket;
|
|
1520
|
+
clearSocket();
|
|
1521
|
+
current?.close();
|
|
1522
|
+
currentStatus = "stopped";
|
|
1523
|
+
emitDiagnostic("cfb.ws.disconnected", { reason: "stopped" });
|
|
1524
|
+
};
|
|
1525
|
+
const reconnect = () => {
|
|
1526
|
+
if (started) {
|
|
1527
|
+
if (retryTimer) {
|
|
1528
|
+
clearTimeout(retryTimer);
|
|
1529
|
+
}
|
|
1530
|
+
const current = socket;
|
|
1531
|
+
retryTimer = void 0;
|
|
1532
|
+
clearSocket();
|
|
1533
|
+
current?.close();
|
|
1534
|
+
connect();
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
return { start, stop, reconnect, status: () => currentStatus };
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
// src/catchError.ts
|
|
1541
|
+
var catchError = (callback) => new Promise((resolve, reject) => {
|
|
1542
|
+
try {
|
|
1543
|
+
resolve(callback());
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
reject(error);
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1549
|
+
0 && (module.exports = {
|
|
1550
|
+
PubSubBehavior,
|
|
1551
|
+
catchError,
|
|
1552
|
+
createBehaviorRunner,
|
|
1553
|
+
createBehaviorWs,
|
|
1554
|
+
createChainBehavior,
|
|
1555
|
+
createMemoryTraceSink,
|
|
1556
|
+
createPubSubBehavior,
|
|
1557
|
+
defineBehaviorConfig,
|
|
1558
|
+
defineErrorReporter
|
|
1559
|
+
});
|