@demlik/tea 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chart/index.d.ts +72 -789
- package/dist/chart/index.js +4 -232
- package/dist/chart/index.js.map +1 -1
- package/dist/chart/inspect/index.d.ts +85 -0
- package/dist/chart/inspect/index.js +5 -0
- package/dist/chart/inspect/index.js.map +1 -0
- package/dist/chart/inspect/react.d.ts +112 -0
- package/dist/chart/inspect/react.js +11 -0
- package/dist/chart/inspect/react.js.map +1 -0
- package/dist/chart/inspect/styles.css +286 -0
- package/dist/chart/lane/index.d.ts +361 -0
- package/dist/chart/lane/index.js +9 -0
- package/dist/chart/lane/index.js.map +1 -0
- package/dist/chart/lane/react.d.ts +83 -0
- package/dist/chart/lane/react.js +559 -0
- package/dist/chart/lane/react.js.map +1 -0
- package/dist/chart/lane/styles.css +498 -0
- package/dist/chart/report/index.d.ts +116 -0
- package/dist/chart/report/index.js +32 -0
- package/dist/chart/report/index.js.map +1 -0
- package/dist/chunk-5KJKJOFC.js +758 -0
- package/dist/chunk-5KJKJOFC.js.map +1 -0
- package/dist/chunk-E6GEDSF4.js +307 -0
- package/dist/chunk-E6GEDSF4.js.map +1 -0
- package/dist/chunk-JER5O7PQ.js +416 -0
- package/dist/chunk-JER5O7PQ.js.map +1 -0
- package/dist/chunk-UOC2PL46.js +13 -0
- package/dist/chunk-UOC2PL46.js.map +1 -0
- package/dist/chunk-WRAA7GVZ.js +209 -0
- package/dist/chunk-WRAA7GVZ.js.map +1 -0
- package/dist/chunk-ZCL7SCBK.js +685 -0
- package/dist/chunk-ZCL7SCBK.js.map +1 -0
- package/dist/chunk-ZD6PNZBK.js +457 -0
- package/dist/chunk-ZD6PNZBK.js.map +1 -0
- package/dist/devtools/index.js +2 -207
- package/dist/devtools/index.js.map +1 -1
- package/dist/fold-Blbhru48.d.ts +435 -0
- package/dist/graph-CcjWZVrd.d.ts +923 -0
- package/dist/machine-viz/index.d.ts +18 -1
- package/dist/machine-viz/index.js +2 -8
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/reducer-BqyoVyBG.d.ts +447 -0
- package/dist/run-BVeqJDBF.d.ts +713 -0
- package/dist/samples-DCt6nF8H.d.ts +59 -0
- package/package.json +24 -1
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import { inspectState, describeChart } from './chunk-ZD6PNZBK.js';
|
|
2
|
+
import { foldLane, phaseStandings, trippedTasks, statesOf, RETRY_BUDGET, timeline, walkedEdgeKey, walkedEdges, initialOf, waitingOn, endPolarityOf, laneTerminalReached, bareEvent, drawTask, originOf } from './chunk-5KJKJOFC.js';
|
|
3
|
+
import { compileTable } from './chunk-E6GEDSF4.js';
|
|
4
|
+
|
|
5
|
+
// src/chart/lane/inspect.ts
|
|
6
|
+
var describe = (chart) => describeChart(chart);
|
|
7
|
+
function stuckAt(chart, state) {
|
|
8
|
+
const node = statesOf(chart).get(state.type);
|
|
9
|
+
const polarity = endPolarityOf(node);
|
|
10
|
+
if (polarity === "error") return { kind: "tripped", state: state.type };
|
|
11
|
+
if (polarity === true) return null;
|
|
12
|
+
const edges = Object.entries(node?.on ?? {});
|
|
13
|
+
if (edges.length === 0) return { kind: "dead-end", state: state.type };
|
|
14
|
+
if (state.retries >= state.maxRetries) {
|
|
15
|
+
for (const [event, edge] of edges) {
|
|
16
|
+
if ("when" in edge) {
|
|
17
|
+
return {
|
|
18
|
+
kind: "budget-spent",
|
|
19
|
+
state: state.type,
|
|
20
|
+
event,
|
|
21
|
+
landsOn: edge.otherwise
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const origins = edges.map(([event]) => ({
|
|
27
|
+
event,
|
|
28
|
+
origin: originOf(chart, event)
|
|
29
|
+
}));
|
|
30
|
+
const started = state.type !== initialOf(chart);
|
|
31
|
+
if (started && origins.length > 0 && origins.every((o) => typeof o.origin === "object" && o.origin !== null)) {
|
|
32
|
+
const roles = [];
|
|
33
|
+
for (const { origin } of origins) {
|
|
34
|
+
const role = origin.world;
|
|
35
|
+
if (!roles.includes(role)) roles.push(role);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
kind: "awaiting-world",
|
|
39
|
+
state: state.type,
|
|
40
|
+
roles,
|
|
41
|
+
events: origins.map((o) => o.event)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function inspectLane(lane, entries, optsFor) {
|
|
47
|
+
return inspectLaneStates(lane, foldLane(lane, entries), optsFor);
|
|
48
|
+
}
|
|
49
|
+
function inspectLaneStates(lane, states, optsFor) {
|
|
50
|
+
const stands = phaseStandings(lane, states);
|
|
51
|
+
const active = stands.find((stand) => stand.standing === "active");
|
|
52
|
+
const trippedPhase = stands.find((stand) => stand.standing === "tripped");
|
|
53
|
+
const stuck = [];
|
|
54
|
+
const inspectTask = (stand, taskId) => {
|
|
55
|
+
const chart = lane.charts[taskId];
|
|
56
|
+
const state = states[taskId];
|
|
57
|
+
if (chart === void 0 || state === void 0) return null;
|
|
58
|
+
const desc = describe(chart);
|
|
59
|
+
const events = inspectState(desc, { ...state }, optsFor?.(taskId));
|
|
60
|
+
const reason = stuckAt(chart, state);
|
|
61
|
+
if (reason !== null) stuck.push({ task: taskId, reason });
|
|
62
|
+
return {
|
|
63
|
+
task: taskId,
|
|
64
|
+
phase: stand.name,
|
|
65
|
+
state: state.type,
|
|
66
|
+
endPolarity: endPolarityOf(statesOf(chart).get(state.type)),
|
|
67
|
+
...state.was === void 0 ? {} : { was: state.was },
|
|
68
|
+
retries: state.retries,
|
|
69
|
+
maxRetries: state.maxRetries,
|
|
70
|
+
waitingOn: waitingOn(chart, state.type),
|
|
71
|
+
stuck: reason,
|
|
72
|
+
events,
|
|
73
|
+
refusals: events.flatMap(
|
|
74
|
+
(preview) => preview.status === "refused" && preview.reason !== void 0 ? [{ from: state.type, event: preview.event, reason: preview.reason }] : []
|
|
75
|
+
),
|
|
76
|
+
legal: events.filter((preview) => preview.status === "legal").map((preview) => preview.event)
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
const phases = stands.map((stand) => ({
|
|
80
|
+
name: stand.name,
|
|
81
|
+
index: stand.index,
|
|
82
|
+
standing: stand.standing,
|
|
83
|
+
tripped: stand.tripped,
|
|
84
|
+
tasks: stand.tasks.flatMap((taskId) => inspectTask(stand, taskId) ?? [])
|
|
85
|
+
}));
|
|
86
|
+
return {
|
|
87
|
+
...lane.id === void 0 ? {} : { id: lane.id },
|
|
88
|
+
status: active === void 0 ? "done" : "active",
|
|
89
|
+
terminal: active !== void 0 ? null : trippedPhase !== void 0 ? lane.terminals.tripped : lane.terminals.complete,
|
|
90
|
+
activePhase: active?.name ?? null,
|
|
91
|
+
phases,
|
|
92
|
+
tripped: trippedTasks(lane, states),
|
|
93
|
+
stuck
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/chart/lane/structure.ts
|
|
98
|
+
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
99
|
+
var lowerEdge = (edge) => {
|
|
100
|
+
if (typeof edge === "string") return { target: edge };
|
|
101
|
+
if (!isRecord(edge)) return void 0;
|
|
102
|
+
if (isRecord(edge.resume) && typeof edge.resume.fallback === "string") {
|
|
103
|
+
return { resume: { fallback: edge.resume.fallback } };
|
|
104
|
+
}
|
|
105
|
+
if (typeof edge.target !== "string") return void 0;
|
|
106
|
+
const target = edge.target;
|
|
107
|
+
const cmds = {
|
|
108
|
+
...edge.cmd === void 0 ? {} : { cmd: edge.cmd },
|
|
109
|
+
...edge.otherwiseCmd === void 0 ? {} : { otherwiseCmd: edge.otherwiseCmd }
|
|
110
|
+
};
|
|
111
|
+
return typeof edge.when === "string" && typeof edge.otherwise === "string" ? { target, when: edge.when, otherwise: edge.otherwise, ...cmds } : { target, ...cmds };
|
|
112
|
+
};
|
|
113
|
+
var isString = (v) => typeof v === "string";
|
|
114
|
+
var scopeList = (scope) => typeof scope === "string" ? [scope] : Array.isArray(scope) ? scope.filter(isString) : ["edges"];
|
|
115
|
+
var refusalsAt = (region, group, node, routed) => {
|
|
116
|
+
const ignored = new Set(
|
|
117
|
+
Array.isArray(node.ignore) ? node.ignore.filter(isString) : []
|
|
118
|
+
);
|
|
119
|
+
const isFinal = node.end !== void 0;
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const [event, decl] of Object.entries(region.events)) {
|
|
122
|
+
if (routed.has(event)) continue;
|
|
123
|
+
const scope = scopeList(isRecord(decl) ? decl.scope : void 0);
|
|
124
|
+
const broadcast = scope.some((s) => s !== "edges");
|
|
125
|
+
const live = scope.some((s) => s === "all" || s === group);
|
|
126
|
+
if (ignored.has(event) || broadcast && (!live || isFinal)) {
|
|
127
|
+
out.push(event);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
};
|
|
132
|
+
var lowerRegion = (taskId, region, defects) => {
|
|
133
|
+
const states = {};
|
|
134
|
+
const refusals = {};
|
|
135
|
+
for (const [group, nodes] of Object.entries(region.states)) {
|
|
136
|
+
const lowered = {};
|
|
137
|
+
for (const [name, node] of Object.entries(nodes)) {
|
|
138
|
+
if (!isRecord(node)) {
|
|
139
|
+
defects.push(`task "${taskId}": state "${name}" is not an object`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const on = {};
|
|
143
|
+
if (isRecord(node.on)) {
|
|
144
|
+
for (const [event, edge] of Object.entries(node.on)) {
|
|
145
|
+
const low = lowerEdge(edge);
|
|
146
|
+
if (low === void 0) {
|
|
147
|
+
defects.push(
|
|
148
|
+
`task "${taskId}": "${name}.${event}" delegates its target to a cell \u2014 a lane region routes to targets it DECLARES, because nothing here runs a cell to pick one`
|
|
149
|
+
);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
on[event] = low;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const refused = refusalsAt(region, group, node, new Set(Object.keys(on)));
|
|
156
|
+
if (refused.length > 0) refusals[name] = refused;
|
|
157
|
+
lowered[name] = {
|
|
158
|
+
...node.initial === true ? { initial: true } : {},
|
|
159
|
+
...Object.keys(on).length > 0 ? { on } : {},
|
|
160
|
+
...node.end === true ? { end: true } : node.end === "error" ? { end: "error" } : {}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
states[group] = lowered;
|
|
164
|
+
}
|
|
165
|
+
const events = {};
|
|
166
|
+
for (const [name, decl] of Object.entries(region.events)) {
|
|
167
|
+
const from = isRecord(decl) ? decl.from : void 0;
|
|
168
|
+
events[name] = {
|
|
169
|
+
scope: "edges",
|
|
170
|
+
...from === void 0 ? {} : { from }
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
events,
|
|
175
|
+
states,
|
|
176
|
+
...Object.keys(refusals).length > 0 ? { refusals } : {}
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
var LaneShapeError = class extends Error {
|
|
180
|
+
constructor(defects) {
|
|
181
|
+
super(
|
|
182
|
+
`@demlik/tea: this lane does not hold together \u2014
|
|
183
|
+
` + defects.map((d) => ` \u2022 ${d}`).join("\n")
|
|
184
|
+
);
|
|
185
|
+
this.defects = defects;
|
|
186
|
+
}
|
|
187
|
+
defects;
|
|
188
|
+
name = "LaneShapeError";
|
|
189
|
+
_tag = "LaneShapeError";
|
|
190
|
+
};
|
|
191
|
+
function defineLane(spec) {
|
|
192
|
+
const defects = [];
|
|
193
|
+
const phases = [];
|
|
194
|
+
const charts = {};
|
|
195
|
+
const context = {};
|
|
196
|
+
if (spec.terminals.complete === spec.terminals.tripped) {
|
|
197
|
+
defects.push(
|
|
198
|
+
`both terminals are "${spec.terminals.complete}" \u2014 a lane that ended cannot say which ending it reached`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
for (const [phaseName, tasks] of Object.entries(spec.phases)) {
|
|
202
|
+
const ids = [];
|
|
203
|
+
for (const [taskId, region] of Object.entries(tasks)) {
|
|
204
|
+
ids.push(taskId);
|
|
205
|
+
const chart = lowerRegion(taskId, region, defects);
|
|
206
|
+
charts[taskId] = chart;
|
|
207
|
+
const nodes = [...statesOf(chart).values()];
|
|
208
|
+
const initials = nodes.filter((node) => node.initial === true).length;
|
|
209
|
+
if (initials === 0) {
|
|
210
|
+
defects.push(
|
|
211
|
+
`task "${taskId}": its chart marks no state \`initial: true\` \u2014 the fold has no zero to start from`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (initials > 1) {
|
|
215
|
+
defects.push(
|
|
216
|
+
`task "${taskId}": its chart marks ${initials} states \`initial: true\` \u2014 a fold has one zero, and which one it picks would be an accident of key order`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (!nodes.some((node) => node.end !== void 0)) {
|
|
220
|
+
defects.push(
|
|
221
|
+
`task "${taskId}": its chart declares no final \u2014 phase "${phaseName}" could never complete, so the lane could never advance`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
context[taskId] = {
|
|
225
|
+
maxRetries: spec.retries?.[taskId] ?? RETRY_BUDGET,
|
|
226
|
+
extras: spec.extras?.[taskId] ?? {}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
phases.push({ name: phaseName, tasks: ids });
|
|
230
|
+
}
|
|
231
|
+
if (phases.length === 0) defects.push("the lane declares no phase");
|
|
232
|
+
if (defects.length > 0) throw new LaneShapeError(defects);
|
|
233
|
+
return {
|
|
234
|
+
...spec.id === void 0 ? {} : { id: spec.id },
|
|
235
|
+
...spec.trigger === void 0 ? {} : { trigger: spec.trigger },
|
|
236
|
+
phases,
|
|
237
|
+
terminals: spec.terminals,
|
|
238
|
+
charts,
|
|
239
|
+
context,
|
|
240
|
+
spec
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
var finalsOf = (chart, want) => {
|
|
244
|
+
const out = [];
|
|
245
|
+
for (const [name, node] of statesOf(chart)) {
|
|
246
|
+
if (endPolarityOf(node) === want) out.push(name);
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
};
|
|
250
|
+
function laneShape(lane) {
|
|
251
|
+
const phases = lane.phases.map((phase, index) => ({
|
|
252
|
+
name: phase.name,
|
|
253
|
+
index,
|
|
254
|
+
tasks: phase.tasks,
|
|
255
|
+
// The `onDone` chain, as a reader sees it: every phase but the last hands
|
|
256
|
+
// off to the next one, and the last hands off to the lane's own ending.
|
|
257
|
+
next: lane.phases[index + 1]?.name ?? lane.terminals.complete
|
|
258
|
+
}));
|
|
259
|
+
const tasks = [];
|
|
260
|
+
const cannotTrip = [];
|
|
261
|
+
for (const phase of phases) {
|
|
262
|
+
for (const taskId of phase.tasks) {
|
|
263
|
+
const chart = lane.charts[taskId];
|
|
264
|
+
if (chart === void 0) continue;
|
|
265
|
+
const errorFinals = finalsOf(chart, "error");
|
|
266
|
+
if (errorFinals.length === 0) cannotTrip.push(taskId);
|
|
267
|
+
let initial = "";
|
|
268
|
+
for (const [name, node] of statesOf(chart)) {
|
|
269
|
+
if (node.initial === true && initial === "") initial = name;
|
|
270
|
+
}
|
|
271
|
+
tasks.push({
|
|
272
|
+
id: taskId,
|
|
273
|
+
phase: phase.name,
|
|
274
|
+
phaseIndex: phase.index,
|
|
275
|
+
initial,
|
|
276
|
+
maxRetries: lane.context[taskId]?.maxRetries ?? RETRY_BUDGET,
|
|
277
|
+
successFinals: finalsOf(chart, true),
|
|
278
|
+
errorFinals,
|
|
279
|
+
siblings: phase.tasks.filter((other) => other !== taskId)
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
...lane.id === void 0 ? {} : { id: lane.id },
|
|
285
|
+
...lane.trigger === void 0 ? {} : { trigger: lane.trigger },
|
|
286
|
+
phases,
|
|
287
|
+
tasks,
|
|
288
|
+
terminals: lane.terminals,
|
|
289
|
+
cannotTrip
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/chart/lane/run.ts
|
|
294
|
+
var isForeign = (event) => typeof event === "object" && event !== null && "foreign" in event && event.foreign === true;
|
|
295
|
+
var DOT = "a dispatch key is the task id, a dot, then the event name \u2014 and every reader splits it at the FIRST dot, so a dot here makes two different messages one key";
|
|
296
|
+
var bare = (taskId, key) => key.startsWith(`${taskId}.`) ? key.slice(taskId.length + 1) : key;
|
|
297
|
+
var carryWas = (leaf, was) => {
|
|
298
|
+
if (was !== void 0) return { ...leaf, was };
|
|
299
|
+
const { was: _dropped, ...rest } = leaf;
|
|
300
|
+
return rest;
|
|
301
|
+
};
|
|
302
|
+
function runLane(lane, hands) {
|
|
303
|
+
const byTask = hands;
|
|
304
|
+
const defects = [];
|
|
305
|
+
const tables = /* @__PURE__ */ new Map();
|
|
306
|
+
const boots = /* @__PURE__ */ new Map();
|
|
307
|
+
const resumes = /* @__PURE__ */ new Map();
|
|
308
|
+
for (const phase of lane.phases) {
|
|
309
|
+
for (const taskId of phase.tasks) {
|
|
310
|
+
const region = lane.spec.phases[phase.name]?.[taskId];
|
|
311
|
+
const hand = byTask[taskId];
|
|
312
|
+
const chart = lane.charts[taskId];
|
|
313
|
+
if (region === void 0 || hand === void 0 || chart === void 0) {
|
|
314
|
+
defects.push(
|
|
315
|
+
`task "${taskId}": the lane declares it but ${region === void 0 ? "its spec holds no chart" : chart === void 0 ? "the lane's `charts` hold none for it" : "no hand was given for it"}`
|
|
316
|
+
);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (taskId.includes(".")) {
|
|
320
|
+
defects.push(
|
|
321
|
+
`task "${taskId}": a task id may not carry a dot \u2014 ${DOT}`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
for (const [name, event] of Object.entries(region.events)) {
|
|
325
|
+
if (isForeign(event)) {
|
|
326
|
+
defects.push(
|
|
327
|
+
`task "${taskId}": its chart declares "${name}" foreign \u2014 a foreign event keeps its BARE name under a namespace, so a lane message carrying it addresses no region in particular`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
if (name.includes(".")) {
|
|
331
|
+
defects.push(
|
|
332
|
+
`task "${taskId}": its chart declares the event "${name}" \u2014 an event name may not carry a dot, ${DOT}`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
tables.set(taskId, compileTable(region, hand.parts, taskId));
|
|
337
|
+
boots.set(taskId, hand.boot);
|
|
338
|
+
const sites = /* @__PURE__ */ new Set();
|
|
339
|
+
for (const [name, node] of statesOf(chart)) {
|
|
340
|
+
for (const [event, edge] of Object.entries(node.on ?? {})) {
|
|
341
|
+
if ("resume" in edge) sites.add(`${name}.${event}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
resumes.set(taskId, sites);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (defects.length > 0) throw new LaneShapeError(defects);
|
|
348
|
+
const standingOf = (regions) => laneTerminalReached(phaseStandings(lane, regions), lane.terminals) ?? "running";
|
|
349
|
+
const budgetOf = (taskId) => lane.context[taskId]?.maxRetries ?? RETRY_BUDGET;
|
|
350
|
+
const check = (regions, origin) => {
|
|
351
|
+
const bad = [];
|
|
352
|
+
for (const taskId of Object.keys(regions)) {
|
|
353
|
+
if (!boots.has(taskId)) {
|
|
354
|
+
bad.push(
|
|
355
|
+
`${origin}: it names task "${taskId}", which this lane does not run \u2014 a state written by an older build of the lane is not a state this one can load`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const taskId of boots.keys()) {
|
|
360
|
+
const chart = lane.charts[taskId];
|
|
361
|
+
if (chart === void 0) continue;
|
|
362
|
+
const raw = regions[taskId];
|
|
363
|
+
const leaf = typeof raw === "object" && raw !== null ? raw : void 0;
|
|
364
|
+
if (leaf === void 0 || typeof leaf.type !== "string") {
|
|
365
|
+
bad.push(
|
|
366
|
+
`${origin}: it holds no leaf for task "${taskId}" \u2014 every region of the lane has to be standing somewhere`
|
|
367
|
+
);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const states = statesOf(chart);
|
|
371
|
+
if (!states.has(leaf.type)) {
|
|
372
|
+
bad.push(
|
|
373
|
+
`${origin}: task "${taskId}" stands in "${leaf.type}", which its chart does not declare`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (leaf.was !== void 0 && !states.has(leaf.was)) {
|
|
377
|
+
bad.push(
|
|
378
|
+
`${origin}: task "${taskId}" carries \`was: "${leaf.was}"\`, which its chart does not declare \u2014 a resume would walk it to a state that does not exist`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const budget = budgetOf(taskId);
|
|
382
|
+
if (typeof leaf.maxRetries === "number" && leaf.maxRetries !== budget) {
|
|
383
|
+
bad.push(
|
|
384
|
+
`task "${taskId}": its \`maxRetries\` is ${leaf.maxRetries} and the lane's budget for it is ${budget} \u2014 the lane's \`retries\` is the one source of that number (\`lane.context\`, which \`foldLane\` reads), so a run carrying a second one reports a different task than the one it ran`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return bad;
|
|
389
|
+
};
|
|
390
|
+
const init = (loaded) => {
|
|
391
|
+
if (loaded !== null) {
|
|
392
|
+
const bad2 = check(
|
|
393
|
+
loaded.regions ?? {},
|
|
394
|
+
"this lane cannot resume the state it was handed"
|
|
395
|
+
);
|
|
396
|
+
if (bad2.length > 0) throw new LaneShapeError(bad2);
|
|
397
|
+
return [
|
|
398
|
+
{ regions: loaded.regions, lane: standingOf(loaded.regions) },
|
|
399
|
+
[]
|
|
400
|
+
];
|
|
401
|
+
}
|
|
402
|
+
const regions = {};
|
|
403
|
+
for (const [taskId, boot] of boots) regions[taskId] = boot();
|
|
404
|
+
const bad = check(regions, "this lane cannot boot");
|
|
405
|
+
if (bad.length > 0) throw new LaneShapeError(bad);
|
|
406
|
+
return [{ regions, lane: standingOf(regions) }, []];
|
|
407
|
+
};
|
|
408
|
+
const update = {};
|
|
409
|
+
for (const [taskId, table] of tables) {
|
|
410
|
+
const chart = lane.charts[taskId];
|
|
411
|
+
const resumeSites = resumes.get(taskId) ?? /* @__PURE__ */ new Set();
|
|
412
|
+
if (chart === void 0) continue;
|
|
413
|
+
for (const event of Object.keys(chart.events)) {
|
|
414
|
+
update[`${taskId}.${event}`] = (state, msg) => {
|
|
415
|
+
const leaf = state.regions[taskId];
|
|
416
|
+
const cell = leaf === void 0 ? void 0 : table[leaf.type]?.[msg.type];
|
|
417
|
+
if (leaf === void 0 || cell === void 0) {
|
|
418
|
+
throw new LaneShapeError([
|
|
419
|
+
`task "${taskId}": no cell for "${msg.type}" in state "${leaf?.type ?? "<none>"}"`
|
|
420
|
+
]);
|
|
421
|
+
}
|
|
422
|
+
const [moved, cmds] = cell(leaf, msg);
|
|
423
|
+
const next = resumeSites.has(`${leaf.type}.${bare(taskId, msg.type)}`) ? carryWas(moved, leaf.was) : moved;
|
|
424
|
+
const regions = { ...state.regions, [taskId]: next };
|
|
425
|
+
return [
|
|
426
|
+
{ regions, lane: standingOf(regions) },
|
|
427
|
+
// the cmds leave wearing their task, under the namespace the replies
|
|
428
|
+
// will come back in — BESIDE the payload, never over it.
|
|
429
|
+
cmds.map((cmd) => ({
|
|
430
|
+
...cmd,
|
|
431
|
+
lane: { task: taskId },
|
|
432
|
+
type: `${taskId}.${cmd.type}`
|
|
433
|
+
}))
|
|
434
|
+
];
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return { init, update };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/chart/lane/view.ts
|
|
442
|
+
var draw = (chart, current, walked) => drawTask(chart, { current, walked });
|
|
443
|
+
function declaresEdge(chart, from, event) {
|
|
444
|
+
if (chart === void 0) return true;
|
|
445
|
+
return statesOf(chart).get(from)?.on?.[bareEvent(event)] !== void 0;
|
|
446
|
+
}
|
|
447
|
+
function parksHere(chart, state) {
|
|
448
|
+
if (chart === void 0) return false;
|
|
449
|
+
const on = statesOf(chart).get(state)?.on ?? {};
|
|
450
|
+
return Object.values(on).some(
|
|
451
|
+
(edge) => edge !== void 0 && "resume" in edge
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
var NO_DISPATCH = {
|
|
455
|
+
answerable: false,
|
|
456
|
+
question: "dispatch",
|
|
457
|
+
why: "this lane is a workflow document and its event log \u2014 the code bodies that would run an edge do not exist here, so no control can be sent. The log is the only thing that moves it."
|
|
458
|
+
};
|
|
459
|
+
var NO_CLOCK = {
|
|
460
|
+
answerable: false,
|
|
461
|
+
question: "clock",
|
|
462
|
+
why: "a recorded tape carries the ORDER of the msgs and no wall-clock \u2014 an events log stamps every line, a running machine does not"
|
|
463
|
+
};
|
|
464
|
+
var NO_RETRIES = {
|
|
465
|
+
answerable: false,
|
|
466
|
+
question: "retries",
|
|
467
|
+
why: "this region's own state carries no `retries` \u2014 the retry budget is a fact of the state a chart chose to keep, and this one keeps none"
|
|
468
|
+
};
|
|
469
|
+
var noSample = (event) => ({
|
|
470
|
+
answerable: false,
|
|
471
|
+
question: "dispatch",
|
|
472
|
+
why: `no sample for \`${event}\`'s payload \u2014 \`ty<T>()\` is \`{}\` at runtime, so the shape cannot be derived and the msg cannot be built`
|
|
473
|
+
});
|
|
474
|
+
function replayFeed(lane, entries) {
|
|
475
|
+
const steps = timeline(lane, entries);
|
|
476
|
+
return {
|
|
477
|
+
kind: "replay",
|
|
478
|
+
lane,
|
|
479
|
+
total: entries.length,
|
|
480
|
+
leavesAt: (n) => foldLane(lane, entries.slice(0, n)),
|
|
481
|
+
// NEVER REFUSED, and that is a property of this source rather than of this
|
|
482
|
+
// log: `foldLane` throws `UnreplayableLogError` on a line the chart routes
|
|
483
|
+
// nowhere, so a log holding a refused msg does not produce a lane view at
|
|
484
|
+
// all. A recorded TAPE is the opposite — a click that no-ops is a msg like
|
|
485
|
+
// any other — which is where the mark earns its keep.
|
|
486
|
+
steps: steps.map(({ at: _at, ...rest }) => ({ ...rest, refused: false })),
|
|
487
|
+
clock: steps.map((s) => s.at),
|
|
488
|
+
budget: (task, leaves) => {
|
|
489
|
+
const leaf = leaves[task];
|
|
490
|
+
return leaf === void 0 ? NO_RETRIES : { retries: leaf.retries, maxRetries: leaf.maxRetries };
|
|
491
|
+
},
|
|
492
|
+
// No `optsFor`: a document carries no guard bodies and no payload samples,
|
|
493
|
+
// and the previews degrade to exactly that.
|
|
494
|
+
send: () => NO_DISPATCH,
|
|
495
|
+
noDispatch: NO_DISPATCH
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
var numberAt = (leaf, key) => typeof leaf[key] === "number" ? leaf[key] : void 0;
|
|
499
|
+
function liveFeed(lane, input) {
|
|
500
|
+
const prefixes = [];
|
|
501
|
+
for (let n = 0; n <= input.msgs.length; n += 1) {
|
|
502
|
+
prefixes.push(input.regionsAt(n));
|
|
503
|
+
}
|
|
504
|
+
const tasks = Object.keys(lane.charts);
|
|
505
|
+
const address = (type) => {
|
|
506
|
+
for (const task of tasks) {
|
|
507
|
+
if (type.startsWith(`${task}.`)) {
|
|
508
|
+
return { task, event: type.slice(task.length + 1) };
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return void 0;
|
|
512
|
+
};
|
|
513
|
+
const carriesRetries = /* @__PURE__ */ new Set();
|
|
514
|
+
for (const [task, leaf] of Object.entries(prefixes[0] ?? {})) {
|
|
515
|
+
if (numberAt(leaf, "retries") !== void 0) carriesRetries.add(task);
|
|
516
|
+
}
|
|
517
|
+
const steps = [];
|
|
518
|
+
for (const [index, msg] of input.msgs.entries()) {
|
|
519
|
+
const at = address(msg.type);
|
|
520
|
+
const chart = at === void 0 ? void 0 : lane.charts[at.task];
|
|
521
|
+
const before = at === void 0 ? void 0 : prefixes[index]?.[at.task];
|
|
522
|
+
const after = at === void 0 ? void 0 : prefixes[index + 1]?.[at.task];
|
|
523
|
+
if (at === void 0 || chart === void 0) continue;
|
|
524
|
+
if (before === void 0 || after === void 0) continue;
|
|
525
|
+
steps.push({
|
|
526
|
+
index,
|
|
527
|
+
task: at.task,
|
|
528
|
+
event: at.event,
|
|
529
|
+
from: before.type,
|
|
530
|
+
to: after.type,
|
|
531
|
+
edge: walkedEdgeKey(chart, before.type, at.event, after.type),
|
|
532
|
+
refused: !declaresEdge(chart, before.type, at.event)
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
const leavesAt = (n) => {
|
|
536
|
+
const regions = prefixes[Math.max(0, Math.min(n, prefixes.length - 1))] ?? {};
|
|
537
|
+
const out = {};
|
|
538
|
+
for (const [task, leaf] of Object.entries(regions)) {
|
|
539
|
+
out[task] = {
|
|
540
|
+
// THE REGION'S OWN CTX, CARRIED. `TaskState` names the four fields the
|
|
541
|
+
// FOLD needs, and a running region is a whole state: the guards a live
|
|
542
|
+
// preview evaluates are `(state, msg) => …` over that state, so a leaf
|
|
543
|
+
// narrowed to its four bookkeeping fields makes every guard read
|
|
544
|
+
// `undefined` and answer with its `otherwise` arm. The extra keys are
|
|
545
|
+
// invisible to a `TaskState` reader and exactly what a guard came for.
|
|
546
|
+
...leaf,
|
|
547
|
+
type: leaf.type,
|
|
548
|
+
retries: numberAt(leaf, "retries") ?? 0,
|
|
549
|
+
maxRetries: numberAt(leaf, "maxRetries") ?? lane.context[task]?.maxRetries ?? RETRY_BUDGET,
|
|
550
|
+
...typeof leaf.was === "string" ? { was: leaf.was } : {}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
};
|
|
555
|
+
return {
|
|
556
|
+
kind: "live",
|
|
557
|
+
lane,
|
|
558
|
+
total: input.msgs.length,
|
|
559
|
+
leavesAt,
|
|
560
|
+
steps,
|
|
561
|
+
clock: NO_CLOCK,
|
|
562
|
+
budget: (task, leaves) => {
|
|
563
|
+
const leaf = leaves[task];
|
|
564
|
+
if (leaf === void 0 || !carriesRetries.has(task)) return NO_RETRIES;
|
|
565
|
+
return { retries: leaf.retries, maxRetries: leaf.maxRetries };
|
|
566
|
+
},
|
|
567
|
+
...input.optsFor === void 0 ? {} : { optsFor: input.optsFor },
|
|
568
|
+
// The code bodies exist, so the only thing that can stop a click is a
|
|
569
|
+
// payload nobody could derive.
|
|
570
|
+
send: (task, preview) => preview.msg === void 0 ? noSample(preview.event) : { ...preview.msg, type: `${task}.${preview.event}` }
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function laneView(feed, cursor, title) {
|
|
574
|
+
const total = feed.total;
|
|
575
|
+
const at = Math.max(0, Math.min(cursor, total));
|
|
576
|
+
const leaves = feed.leavesAt(at);
|
|
577
|
+
const seen = feed.steps.filter((s) => s.index < at);
|
|
578
|
+
const inspection = inspectLaneStates(feed.lane, leaves, feed.optsFor);
|
|
579
|
+
const lastRan = inspection.phases.reduce(
|
|
580
|
+
(last, p, i) => p.standing === "waiting" ? last : i,
|
|
581
|
+
-1
|
|
582
|
+
);
|
|
583
|
+
const endsAt = inspection.terminal === null ? -1 : lastRan;
|
|
584
|
+
const phases = inspection.phases.map((phase, index) => {
|
|
585
|
+
const tasks = phase.tasks.map((task) => {
|
|
586
|
+
const chart = feed.lane.charts[task.task];
|
|
587
|
+
const walked = walkedEdges(seen.filter((s) => s.task === task.task));
|
|
588
|
+
return {
|
|
589
|
+
task: task.task,
|
|
590
|
+
phase: task.phase,
|
|
591
|
+
state: task.state,
|
|
592
|
+
endPolarity: task.endPolarity,
|
|
593
|
+
...task.was === void 0 || !parksHere(chart, task.state) ? {} : { was: task.was },
|
|
594
|
+
waitingOn: task.waitingOn,
|
|
595
|
+
stuck: task.stuck,
|
|
596
|
+
moved: chart === void 0 ? true : task.state !== initialOf(chart),
|
|
597
|
+
budget: feed.budget(task.task, leaves),
|
|
598
|
+
controls: task.events.map(
|
|
599
|
+
(preview) => control(feed, task.task, preview)
|
|
600
|
+
),
|
|
601
|
+
diagram: chart === void 0 ? "" : draw(chart, task.state, walked)
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
return {
|
|
605
|
+
name: phase.name,
|
|
606
|
+
index: phase.index,
|
|
607
|
+
standing: phase.standing,
|
|
608
|
+
tripped: phase.tripped,
|
|
609
|
+
tasks,
|
|
610
|
+
expanded: expandedIn(phase.standing, tasks, index === endsAt)
|
|
611
|
+
};
|
|
612
|
+
});
|
|
613
|
+
const phaseOf = (task) => phases.find((p) => p.tasks.some((t) => t.task === task))?.name ?? "";
|
|
614
|
+
return {
|
|
615
|
+
source: feed.kind,
|
|
616
|
+
title: title ?? feed.lane.id ?? "lane",
|
|
617
|
+
status: inspection.status,
|
|
618
|
+
terminal: inspection.terminal,
|
|
619
|
+
activePhase: inspection.activePhase,
|
|
620
|
+
tripped: inspection.tripped,
|
|
621
|
+
stuck: inspection.stuck.map((s) => ({
|
|
622
|
+
task: s.task,
|
|
623
|
+
phase: phaseOf(s.task),
|
|
624
|
+
reason: s.reason
|
|
625
|
+
})),
|
|
626
|
+
phases,
|
|
627
|
+
steps: seen,
|
|
628
|
+
clock: feed.clock,
|
|
629
|
+
cursor: at,
|
|
630
|
+
total,
|
|
631
|
+
scrubbed: at < total,
|
|
632
|
+
...feed.noDispatch === void 0 ? {} : { noDispatch: feed.noDispatch }
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function expandedIn(standing, tasks, ending) {
|
|
636
|
+
if (standing === "tripped") {
|
|
637
|
+
return tasks.filter((t) => t.endPolarity === "error").map((t) => t.task);
|
|
638
|
+
}
|
|
639
|
+
if (standing !== "active" && !ending) return [];
|
|
640
|
+
const moved = tasks.filter((t) => t.moved).map((t) => t.task);
|
|
641
|
+
if (moved.length > 0) return moved;
|
|
642
|
+
const first = tasks[0];
|
|
643
|
+
return first === void 0 ? [] : [first.task];
|
|
644
|
+
}
|
|
645
|
+
function control(feed, task, preview) {
|
|
646
|
+
const refused = preview.status === "refused";
|
|
647
|
+
return {
|
|
648
|
+
task,
|
|
649
|
+
event: preview.event,
|
|
650
|
+
kind: refused ? preview.reason?.kind ?? "undeclared" : "legal",
|
|
651
|
+
refused,
|
|
652
|
+
...preview.why === void 0 ? {} : { why: preview.why },
|
|
653
|
+
outcome: outcomeOf(preview),
|
|
654
|
+
send: refused ? NO_DISPATCH_REFUSED : feed.send(task, preview)
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
var NO_DISPATCH_REFUSED = {
|
|
658
|
+
answerable: false,
|
|
659
|
+
question: "dispatch",
|
|
660
|
+
why: "this event is refused at this state \u2014 see the refusal for which mechanism refused it"
|
|
661
|
+
};
|
|
662
|
+
function guardUnknownLine(why) {
|
|
663
|
+
switch (why) {
|
|
664
|
+
case "no-guard-bag":
|
|
665
|
+
return "cannot say which \u2014 this lane came from a workflow document, so the guard bodies that would decide do not exist here";
|
|
666
|
+
case "no-implementation":
|
|
667
|
+
return "cannot say which \u2014 no implementation was supplied for this guard";
|
|
668
|
+
case "no-sample":
|
|
669
|
+
return "cannot say which \u2014 the guard reads the message payload and no sample was given for this event";
|
|
670
|
+
default:
|
|
671
|
+
return `cannot say which \u2014 evaluating the guard threw (${why})`;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function outcomeOf(v) {
|
|
675
|
+
const cmds = v.cmds.length > 0 ? ` / ${v.cmds.join(", ")}` : "";
|
|
676
|
+
if (v.guard !== void 0) {
|
|
677
|
+
return v.guard.branch === "unknown" ? `${v.targets.join(" | ")} \u2014 ${guardUnknownLine(v.guard.why ?? "")}` : `\u2192 ${v.guard.target} [${v.guard.branch === "then" ? "" : "!"}${v.guard.guard}]${cmds}`;
|
|
678
|
+
}
|
|
679
|
+
if (v.resolved !== void 0) return `\u2192 ${v.resolved}${cmds}`;
|
|
680
|
+
return `${v.targets.join(" | ")}${cmds}`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export { LaneShapeError, defineLane, inspectLane, inspectLaneStates, laneShape, laneView, liveFeed, replayFeed, runLane };
|
|
684
|
+
//# sourceMappingURL=chunk-ZCL7SCBK.js.map
|
|
685
|
+
//# sourceMappingURL=chunk-ZCL7SCBK.js.map
|