@you-agent-factory/factory-replay 0.0.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/LICENSE.md +21 -0
- package/README.md +58 -0
- package/package.json +26 -0
- package/src/activity.js +208 -0
- package/src/index.d.ts +324 -0
- package/src/index.js +477 -0
- package/src/progress.js +117 -0
- package/src/topology.js +398 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { projectFactoryTopology } from "./topology.js";
|
|
2
|
+
import { projectFactoryActivity } from "./activity.js";
|
|
3
|
+
import { projectFactoryWorkProgress } from "./progress.js";
|
|
4
|
+
|
|
5
|
+
export { projectFactoryTopology } from "./topology.js";
|
|
6
|
+
export { projectFactoryActivity } from "./activity.js";
|
|
7
|
+
export { projectFactoryWorkProgress } from "./progress.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compare Factory events using the established dashboard replay order.
|
|
11
|
+
*
|
|
12
|
+
* The event timestamp and id remain deterministic tie-breakers for events
|
|
13
|
+
* that share one logical tick and sequence.
|
|
14
|
+
*
|
|
15
|
+
* @param {import("@you-agent-factory/client").FactoryEvent} left
|
|
16
|
+
* @param {import("@you-agent-factory/client").FactoryEvent} right
|
|
17
|
+
* @returns {number}
|
|
18
|
+
*/
|
|
19
|
+
export function compareFactoryEvents(left, right) {
|
|
20
|
+
if (left.context.tick !== right.context.tick) {
|
|
21
|
+
return left.context.tick - right.context.tick;
|
|
22
|
+
}
|
|
23
|
+
if (left.context.sequence !== right.context.sequence) {
|
|
24
|
+
return left.context.sequence - right.context.sequence;
|
|
25
|
+
}
|
|
26
|
+
if (left.context.eventTime !== right.context.eventTime) {
|
|
27
|
+
return left.context.eventTime.localeCompare(right.context.eventTime);
|
|
28
|
+
}
|
|
29
|
+
return left.id.localeCompare(right.id);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Return the canonical accepted Factory event history without mutating input.
|
|
34
|
+
* The first event in canonical order owns a duplicate id.
|
|
35
|
+
*
|
|
36
|
+
* @param {readonly import("@you-agent-factory/client").FactoryEvent[]} events
|
|
37
|
+
* @returns {import("@you-agent-factory/client").FactoryEvent[]}
|
|
38
|
+
*/
|
|
39
|
+
export function canonicalizeFactoryEvents(events) {
|
|
40
|
+
const acceptedIDs = new Set();
|
|
41
|
+
return [...events]
|
|
42
|
+
.sort(compareFactoryEvents)
|
|
43
|
+
.filter((event) => {
|
|
44
|
+
if (acceptedIDs.has(event.id)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
acceptedIDs.add(event.id);
|
|
48
|
+
return true;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build one deterministic Factory-event replay result for current or fixed
|
|
54
|
+
* logical-tick selection. The reducer owns Factory domain interpretation;
|
|
55
|
+
* this kernel owns canonical ordering, id acceptance, and tick selection.
|
|
56
|
+
*
|
|
57
|
+
* @template State, World
|
|
58
|
+
* @param {import("./index.d.ts").FactoryReplayInitialization<State, World>} input
|
|
59
|
+
* @returns {import("./index.d.ts").FactoryReplayResult<State, World>}
|
|
60
|
+
*/
|
|
61
|
+
export function initializeFactoryReplay(input) {
|
|
62
|
+
const events = canonicalizeFactoryEvents(input.events);
|
|
63
|
+
const latestTick = events.reduce(
|
|
64
|
+
(latest, event) => Math.max(latest, event.context.tick),
|
|
65
|
+
0,
|
|
66
|
+
);
|
|
67
|
+
const selectedTick =
|
|
68
|
+
input.selection.mode === "current" ? latestTick : input.selection.tick;
|
|
69
|
+
const appliedEvents = events.filter(
|
|
70
|
+
(event) => event.context.tick <= selectedTick,
|
|
71
|
+
);
|
|
72
|
+
let state = input.reducer.createState(selectedTick);
|
|
73
|
+
for (const event of appliedEvents) {
|
|
74
|
+
state = input.reducer.applyEvent(state, event);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
appliedEvents,
|
|
79
|
+
events,
|
|
80
|
+
latestTick,
|
|
81
|
+
selectedTick,
|
|
82
|
+
selection: input.selection,
|
|
83
|
+
state,
|
|
84
|
+
world: input.reducer.projectWorld(state),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create an independent checkpoint from an already projected replay result.
|
|
90
|
+
* The caller provides cloning because the kernel cannot know the shape of a
|
|
91
|
+
* domain-specific replay state.
|
|
92
|
+
*
|
|
93
|
+
* @template State, World
|
|
94
|
+
* @param {import("./index.d.ts").FactoryReplayResult<State, World>} result
|
|
95
|
+
* @param {import("./index.d.ts").FactoryReplayStateCloner<State>} cloneState
|
|
96
|
+
* @returns {import("./index.d.ts").FactoryReplayCheckpoint<State>}
|
|
97
|
+
*/
|
|
98
|
+
export function createFactoryReplayCheckpoint(result, cloneState) {
|
|
99
|
+
return {
|
|
100
|
+
acceptedEventIDs: result.appliedEvents.map((event) => event.id),
|
|
101
|
+
selectedTick: result.selectedTick,
|
|
102
|
+
state: cloneState(result.state),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Advance an immutable replay checkpoint with an accepted event tail.
|
|
108
|
+
* Only previously unseen events at or before the target tick are applied.
|
|
109
|
+
* Event IDs, rather than the checkpoint tick, define what has already been
|
|
110
|
+
* applied so later canonical events at the checkpoint's tick are retained.
|
|
111
|
+
* The returned checkpoint is independently cloned so callers can retain it
|
|
112
|
+
* for another historical reconstruction.
|
|
113
|
+
*
|
|
114
|
+
* @template State, World
|
|
115
|
+
* @param {import("./index.d.ts").FactoryReplayAdvanceInput<State, World>} input
|
|
116
|
+
* @returns {import("./index.d.ts").FactoryReplayAdvanceResult<State, World>}
|
|
117
|
+
*/
|
|
118
|
+
export function advanceFactoryReplay(input) {
|
|
119
|
+
const acceptedEventIDs = new Set(input.checkpoint.acceptedEventIDs);
|
|
120
|
+
const appliedEvents = canonicalizeFactoryEvents(input.events).filter(
|
|
121
|
+
(event) => {
|
|
122
|
+
if (
|
|
123
|
+
acceptedEventIDs.has(event.id) ||
|
|
124
|
+
event.context.tick > input.tick
|
|
125
|
+
) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
acceptedEventIDs.add(event.id);
|
|
129
|
+
return true;
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
let state = input.setSelectedTick(
|
|
133
|
+
input.cloneState(input.checkpoint.state),
|
|
134
|
+
input.tick,
|
|
135
|
+
);
|
|
136
|
+
for (const event of appliedEvents) {
|
|
137
|
+
state = input.reducer.applyEvent(state, event);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
appliedEvents,
|
|
142
|
+
checkpoint: {
|
|
143
|
+
acceptedEventIDs: [...acceptedEventIDs],
|
|
144
|
+
selectedTick: input.tick,
|
|
145
|
+
state: input.cloneState(state),
|
|
146
|
+
},
|
|
147
|
+
latestTick: appliedEvents.reduce(
|
|
148
|
+
(latest, event) => Math.max(latest, event.context.tick),
|
|
149
|
+
input.checkpoint.selectedTick,
|
|
150
|
+
),
|
|
151
|
+
selectedTick: input.tick,
|
|
152
|
+
state,
|
|
153
|
+
world: input.reducer.projectWorld(state),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Reconstruct the canonical Factory world at one explicit logical tick.
|
|
159
|
+
*
|
|
160
|
+
* @template State, World
|
|
161
|
+
* @param {Omit<import("./index.d.ts").FactoryReplayInitialization<State, World>, "selection"> & { tick: number }} input
|
|
162
|
+
* @returns {import("./index.d.ts").FactoryReplayResult<State, World>}
|
|
163
|
+
*/
|
|
164
|
+
export function projectFactoryWorldAtTick(input) {
|
|
165
|
+
return initializeFactoryReplay({
|
|
166
|
+
...input,
|
|
167
|
+
selection: { mode: "fixed", tick: input.tick },
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Reconstruct and project the public Factory topology at one logical tick.
|
|
173
|
+
* Only canonical topology replacement events participate in this projection.
|
|
174
|
+
*
|
|
175
|
+
* @param {import("./index.d.ts").FactoryTopologyAtTickInput} input
|
|
176
|
+
* @returns {import("./index.d.ts").FactoryTopologyProjection}
|
|
177
|
+
*/
|
|
178
|
+
export function projectFactoryTopologyAtTick(input) {
|
|
179
|
+
const events = canonicalizeFactoryEvents(input.events).filter(
|
|
180
|
+
(event) => event.context.tick <= input.tick,
|
|
181
|
+
);
|
|
182
|
+
/** @type {import("@you-agent-factory/client").FactoryDefinition | undefined} */
|
|
183
|
+
let factory;
|
|
184
|
+
for (const event of events) {
|
|
185
|
+
if (
|
|
186
|
+
event.type !== "INITIAL_STRUCTURE_REQUEST" &&
|
|
187
|
+
event.type !== "FACTORY_CHANGE"
|
|
188
|
+
) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const payload = event.payload;
|
|
192
|
+
if (
|
|
193
|
+
payload &&
|
|
194
|
+
typeof payload === "object" &&
|
|
195
|
+
"factory" in payload &&
|
|
196
|
+
payload.factory &&
|
|
197
|
+
typeof payload.factory === "object"
|
|
198
|
+
) {
|
|
199
|
+
factory = payload.factory;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return projectFactoryTopology({ factory, selectedTick: input.tick });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Reconstruct active customer Dispatches and resource occupancy at one tick.
|
|
207
|
+
* A response removes its matching request in canonical sequence order, even
|
|
208
|
+
* when both events share a logical tick.
|
|
209
|
+
*
|
|
210
|
+
* @param {import("./index.d.ts").FactoryActivityAtTickInput} input
|
|
211
|
+
* @returns {import("./index.d.ts").FactoryActivityProjection}
|
|
212
|
+
*/
|
|
213
|
+
export function projectFactoryActivityAtTick(input) {
|
|
214
|
+
const events = canonicalizeFactoryEvents(input.events).filter(
|
|
215
|
+
(event) => event.context.tick <= input.tick,
|
|
216
|
+
);
|
|
217
|
+
let factory;
|
|
218
|
+
const activeDispatches = new Map();
|
|
219
|
+
for (const event of events) {
|
|
220
|
+
if (
|
|
221
|
+
event.type === "INITIAL_STRUCTURE_REQUEST" ||
|
|
222
|
+
event.type === "FACTORY_CHANGE"
|
|
223
|
+
) {
|
|
224
|
+
const payload = event.payload;
|
|
225
|
+
if (
|
|
226
|
+
payload &&
|
|
227
|
+
typeof payload === "object" &&
|
|
228
|
+
"factory" in payload &&
|
|
229
|
+
payload.factory &&
|
|
230
|
+
typeof payload.factory === "object"
|
|
231
|
+
) {
|
|
232
|
+
factory = payload.factory;
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (event.type === "DISPATCH_RESPONSE") {
|
|
237
|
+
if (event.context.dispatchId) {
|
|
238
|
+
activeDispatches.delete(event.context.dispatchId);
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (event.type !== "DISPATCH_REQUEST" || !event.context.dispatchId) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const payload = event.payload;
|
|
246
|
+
if (
|
|
247
|
+
!payload ||
|
|
248
|
+
typeof payload !== "object" ||
|
|
249
|
+
!("transitionId" in payload) ||
|
|
250
|
+
typeof payload.transitionId !== "string" ||
|
|
251
|
+
payload.transitionId.startsWith("__system_time:")
|
|
252
|
+
) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const resources =
|
|
256
|
+
"resources" in payload && Array.isArray(payload.resources)
|
|
257
|
+
? payload.resources
|
|
258
|
+
: undefined;
|
|
259
|
+
activeDispatches.set(event.context.dispatchId, {
|
|
260
|
+
id: event.context.dispatchId,
|
|
261
|
+
...(resources
|
|
262
|
+
? {
|
|
263
|
+
resourceNames: resources.flatMap((resource) =>
|
|
264
|
+
resource &&
|
|
265
|
+
typeof resource === "object" &&
|
|
266
|
+
"name" in resource &&
|
|
267
|
+
typeof resource.name === "string"
|
|
268
|
+
? [resource.name]
|
|
269
|
+
: [],
|
|
270
|
+
),
|
|
271
|
+
}
|
|
272
|
+
: {}),
|
|
273
|
+
startedTick: event.context.tick,
|
|
274
|
+
transitionId: payload.transitionId,
|
|
275
|
+
workIds: [...(event.context.workIds ?? [])],
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return projectFactoryActivity({
|
|
279
|
+
activeDispatches: [...activeDispatches.values()],
|
|
280
|
+
factory,
|
|
281
|
+
selectedTick: input.tick,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Reconstruct and exclusively classify customer Work at one logical tick.
|
|
287
|
+
* Canonical sequence order owns same-tick lifecycle and Dispatch changes.
|
|
288
|
+
*
|
|
289
|
+
* @param {import("./index.d.ts").FactoryWorkProgressAtTickInput} input
|
|
290
|
+
* @returns {import("./index.d.ts").FactoryWorkProgressProjection}
|
|
291
|
+
*/
|
|
292
|
+
export function projectFactoryWorkProgressAtTick(input) {
|
|
293
|
+
const events = canonicalizeFactoryEvents(input.events).filter(
|
|
294
|
+
(event) => event.context.tick <= input.tick,
|
|
295
|
+
);
|
|
296
|
+
/** @type {import("@you-agent-factory/client").FactoryDefinition | undefined} */
|
|
297
|
+
let factory;
|
|
298
|
+
const works = new Map();
|
|
299
|
+
const initialWorkIds = new Set();
|
|
300
|
+
const activeDispatches = new Map();
|
|
301
|
+
for (const event of events) {
|
|
302
|
+
if (
|
|
303
|
+
event.type === "INITIAL_STRUCTURE_REQUEST" ||
|
|
304
|
+
event.type === "FACTORY_CHANGE"
|
|
305
|
+
) {
|
|
306
|
+
const payload = objectPayload(event.payload);
|
|
307
|
+
if (payload?.factory && typeof payload.factory === "object") {
|
|
308
|
+
factory = /** @type {import("@you-agent-factory/client").FactoryDefinition} */ (
|
|
309
|
+
payload.factory
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (event.type === "WORK_REQUEST") {
|
|
315
|
+
const payload = objectPayload(event.payload);
|
|
316
|
+
for (const work of Array.isArray(payload?.works) ? payload.works : []) {
|
|
317
|
+
mergeEventWork(works, work);
|
|
318
|
+
const record = objectPayload(work);
|
|
319
|
+
if (
|
|
320
|
+
typeof record?.workId === "string" &&
|
|
321
|
+
!objectPayload(record.state)
|
|
322
|
+
) {
|
|
323
|
+
initialWorkIds.add(record.workId);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (event.type === "WORK_STATE_CHANGE") {
|
|
329
|
+
const payload = objectPayload(event.payload);
|
|
330
|
+
if (typeof payload?.workId !== "string" || !payload.workId) continue;
|
|
331
|
+
const previous = works.get(payload.workId) ?? { id: payload.workId };
|
|
332
|
+
initialWorkIds.delete(payload.workId);
|
|
333
|
+
const state =
|
|
334
|
+
typeof payload.toState === "string"
|
|
335
|
+
? { name: payload.toState }
|
|
336
|
+
: undefined;
|
|
337
|
+
works.set(payload.workId, {
|
|
338
|
+
...previous,
|
|
339
|
+
...(typeof payload.workTypeName === "string"
|
|
340
|
+
? { workTypeId: payload.workTypeName }
|
|
341
|
+
: {}),
|
|
342
|
+
...(state ? { state } : {}),
|
|
343
|
+
});
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (event.type === "DISPATCH_REQUEST" && event.context.dispatchId) {
|
|
347
|
+
const payload = objectPayload(event.payload);
|
|
348
|
+
if (
|
|
349
|
+
typeof payload?.transitionId === "string" &&
|
|
350
|
+
!payload.transitionId.startsWith("__system_time:")
|
|
351
|
+
) {
|
|
352
|
+
const inputIds = Array.isArray(payload.inputs)
|
|
353
|
+
? payload.inputs.flatMap((input) => {
|
|
354
|
+
const record = objectPayload(input);
|
|
355
|
+
return typeof record?.workId === "string"
|
|
356
|
+
? [record.workId]
|
|
357
|
+
: [];
|
|
358
|
+
})
|
|
359
|
+
: [];
|
|
360
|
+
activeDispatches.set(event.context.dispatchId, [
|
|
361
|
+
...new Set([...(event.context.workIds ?? []), ...inputIds]),
|
|
362
|
+
]);
|
|
363
|
+
}
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (event.type !== "DISPATCH_RESPONSE" || !event.context.dispatchId) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const consumedIds = activeDispatches.get(event.context.dispatchId) ?? [];
|
|
370
|
+
activeDispatches.delete(event.context.dispatchId);
|
|
371
|
+
const payload = objectPayload(event.payload);
|
|
372
|
+
const outputWorks = Array.isArray(payload?.outputWork)
|
|
373
|
+
? payload.outputWork
|
|
374
|
+
: [];
|
|
375
|
+
const outputIds = new Set(
|
|
376
|
+
outputWorks.flatMap((work) => {
|
|
377
|
+
const record = objectPayload(work);
|
|
378
|
+
return typeof record?.workId === "string" ? [record.workId] : [];
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
for (const workId of consumedIds) {
|
|
382
|
+
const previous = works.get(workId);
|
|
383
|
+
if (!previous || outputIds.has(workId)) continue;
|
|
384
|
+
initialWorkIds.delete(workId);
|
|
385
|
+
const category = previous.state?.category;
|
|
386
|
+
if (category !== "FAILED" && category !== "TERMINAL") {
|
|
387
|
+
works.set(workId, {
|
|
388
|
+
id: previous.id,
|
|
389
|
+
...(previous.workTypeId ? { workTypeId: previous.workTypeId } : {}),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
for (const work of outputWorks) {
|
|
394
|
+
mergeEventWork(works, work);
|
|
395
|
+
const record = objectPayload(work);
|
|
396
|
+
if (typeof record?.workId === "string") {
|
|
397
|
+
initialWorkIds.delete(record.workId);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return projectFactoryWorkProgress({
|
|
403
|
+
activeWorkIds: [...activeDispatches.values()].flat(),
|
|
404
|
+
factory,
|
|
405
|
+
selectedTick: input.tick,
|
|
406
|
+
works: [...works.values()].map((work) => {
|
|
407
|
+
const initialState = initialWorkIds.has(work.id)
|
|
408
|
+
? initialStateForWork(factory, work.workTypeId)
|
|
409
|
+
: undefined;
|
|
410
|
+
return initialState ? { ...work, state: initialState } : work;
|
|
411
|
+
}),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @param {import("@you-agent-factory/client").FactoryDefinition | undefined} factory
|
|
417
|
+
* @param {string | undefined} workTypeId
|
|
418
|
+
* @returns {import("./index.d.ts").FactoryWorkProgressStateEvidence | undefined}
|
|
419
|
+
*/
|
|
420
|
+
function initialStateForWork(factory, workTypeId) {
|
|
421
|
+
if (!workTypeId) return undefined;
|
|
422
|
+
const workType = (factory?.workTypes ?? []).find(
|
|
423
|
+
(candidate) => candidate.name === workTypeId || candidate.id === workTypeId,
|
|
424
|
+
);
|
|
425
|
+
const state = workType?.states.find((candidate) => candidate.type === "INITIAL");
|
|
426
|
+
return state
|
|
427
|
+
? {
|
|
428
|
+
category: state.type,
|
|
429
|
+
...(state.id ? { id: state.id } : {}),
|
|
430
|
+
name: state.name,
|
|
431
|
+
}
|
|
432
|
+
: undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** @param {unknown} payload @returns {Record<string, unknown> | undefined} */
|
|
436
|
+
function objectPayload(payload) {
|
|
437
|
+
return payload && typeof payload === "object"
|
|
438
|
+
? /** @type {Record<string, unknown>} */ (payload)
|
|
439
|
+
: undefined;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** @param {Map<string, import("./index.d.ts").FactoryWorkProgressEvidence>} works @param {unknown} value */
|
|
443
|
+
function mergeEventWork(works, value) {
|
|
444
|
+
const record = objectPayload(value);
|
|
445
|
+
if (typeof record?.workId !== "string" || !record.workId) return;
|
|
446
|
+
const previous = works.get(record.workId) ?? { id: record.workId };
|
|
447
|
+
const stateRecord = objectPayload(record.state);
|
|
448
|
+
const state =
|
|
449
|
+
stateRecord
|
|
450
|
+
? {
|
|
451
|
+
...(typeof stateRecord.id === "string" ? { id: stateRecord.id } : {}),
|
|
452
|
+
...(typeof stateRecord.name === "string"
|
|
453
|
+
? { name: stateRecord.name }
|
|
454
|
+
: {}),
|
|
455
|
+
...(isWorkStateCategory(stateRecord.type)
|
|
456
|
+
? { category: stateRecord.type }
|
|
457
|
+
: {}),
|
|
458
|
+
}
|
|
459
|
+
: previous.state;
|
|
460
|
+
works.set(record.workId, {
|
|
461
|
+
...previous,
|
|
462
|
+
...(typeof record.workTypeName === "string"
|
|
463
|
+
? { workTypeId: record.workTypeName }
|
|
464
|
+
: {}),
|
|
465
|
+
...(state ? { state } : {}),
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** @param {unknown} value @returns {value is import("./index.d.ts").FactoryWorkStateCategory} */
|
|
470
|
+
function isWorkStateCategory(value) {
|
|
471
|
+
return (
|
|
472
|
+
value === "FAILED" ||
|
|
473
|
+
value === "INITIAL" ||
|
|
474
|
+
value === "PROCESSING" ||
|
|
475
|
+
value === "TERMINAL"
|
|
476
|
+
);
|
|
477
|
+
}
|
package/src/progress.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const SYSTEM_WORK_TYPE_ID = "__system_time";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Partition customer Work into one mutually exclusive progress category.
|
|
5
|
+
*
|
|
6
|
+
* @param {import("./index.d.ts").FactoryWorkProgressProjectionInput} input
|
|
7
|
+
* @returns {import("./index.d.ts").FactoryWorkProgressProjection}
|
|
8
|
+
*/
|
|
9
|
+
export function projectFactoryWorkProgress(input) {
|
|
10
|
+
const statesByWorkType = indexWorkStates(input.factory);
|
|
11
|
+
const activeWorkIds = new Set(input.activeWorkIds);
|
|
12
|
+
const worksById = new Map();
|
|
13
|
+
for (const evidence of input.works) {
|
|
14
|
+
if (!evidence.id || evidence.workTypeId === SYSTEM_WORK_TYPE_ID) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
worksById.set(evidence.id, { ...evidence });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @type {import("./index.d.ts").FactoryWorkProgressProjection} */
|
|
21
|
+
const projection = {
|
|
22
|
+
active: [],
|
|
23
|
+
completed: [],
|
|
24
|
+
counts: { active: 0, completed: 0, failed: 0, queued: 0, unclassified: 0 },
|
|
25
|
+
failed: [],
|
|
26
|
+
queued: [],
|
|
27
|
+
selectedTick: input.selectedTick,
|
|
28
|
+
total: worksById.size,
|
|
29
|
+
unclassified: [],
|
|
30
|
+
};
|
|
31
|
+
for (const evidence of [...worksById.values()].sort((left, right) =>
|
|
32
|
+
left.id.localeCompare(right.id),
|
|
33
|
+
)) {
|
|
34
|
+
const state = resolveState(evidence, statesByWorkType);
|
|
35
|
+
const work = {
|
|
36
|
+
id: evidence.id,
|
|
37
|
+
...(evidence.workTypeId ? { workTypeId: evidence.workTypeId } : {}),
|
|
38
|
+
...(state?.id ? { stateId: state.id } : {}),
|
|
39
|
+
...(state?.name ? { stateName: state.name } : {}),
|
|
40
|
+
};
|
|
41
|
+
const category = classifyWork(state?.category, activeWorkIds.has(evidence.id));
|
|
42
|
+
projection[category].push(work);
|
|
43
|
+
projection.counts[category] += 1;
|
|
44
|
+
}
|
|
45
|
+
return projection;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @typedef {{id?: string, name: string, type: import("./index.d.ts").FactoryWorkStateCategory}} IndexedWorkState */
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {import("@you-agent-factory/client").FactoryDefinition | undefined} factory
|
|
52
|
+
* @returns {Map<string, Map<string, IndexedWorkState>>}
|
|
53
|
+
*/
|
|
54
|
+
function indexWorkStates(factory) {
|
|
55
|
+
const statesByWorkType = new Map();
|
|
56
|
+
for (const workType of factory?.workTypes ?? []) {
|
|
57
|
+
const states = new Map();
|
|
58
|
+
for (const state of workType.states) {
|
|
59
|
+
states.set(state.name, state);
|
|
60
|
+
if (state.id?.trim()) {
|
|
61
|
+
states.set(state.id, state);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
statesByWorkType.set(workType.name, states);
|
|
65
|
+
if (workType.id?.trim()) {
|
|
66
|
+
statesByWorkType.set(workType.id, states);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return statesByWorkType;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {import("./index.d.ts").FactoryWorkProgressEvidence} evidence
|
|
74
|
+
* @param {Map<string, Map<string, IndexedWorkState>>} statesByWorkType
|
|
75
|
+
*/
|
|
76
|
+
function resolveState(evidence, statesByWorkType) {
|
|
77
|
+
if (evidence.state?.category) {
|
|
78
|
+
return evidence.state;
|
|
79
|
+
}
|
|
80
|
+
if (!evidence.workTypeId) {
|
|
81
|
+
return evidence.state;
|
|
82
|
+
}
|
|
83
|
+
const states = statesByWorkType.get(evidence.workTypeId);
|
|
84
|
+
if (evidence.state?.name) {
|
|
85
|
+
return progressState(states?.get(evidence.state.name)) ?? evidence.state;
|
|
86
|
+
}
|
|
87
|
+
if (evidence.state?.id) {
|
|
88
|
+
return progressState(states?.get(evidence.state.id)) ?? evidence.state;
|
|
89
|
+
}
|
|
90
|
+
return evidence.state;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @param {IndexedWorkState | undefined} state @returns {import("./index.d.ts").FactoryWorkProgressStateEvidence | undefined} */
|
|
94
|
+
function progressState(state) {
|
|
95
|
+
return state
|
|
96
|
+
? {
|
|
97
|
+
category: state.type,
|
|
98
|
+
...(state.id ? { id: state.id } : {}),
|
|
99
|
+
name: state.name,
|
|
100
|
+
}
|
|
101
|
+
: undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {import("./index.d.ts").FactoryWorkStateCategory | undefined} stateCategory
|
|
106
|
+
* @param {boolean} active
|
|
107
|
+
* @returns {import("./index.d.ts").FactoryWorkProgressCategory}
|
|
108
|
+
*/
|
|
109
|
+
function classifyWork(stateCategory, active) {
|
|
110
|
+
if (stateCategory === "FAILED") return "failed";
|
|
111
|
+
if (stateCategory === "TERMINAL") return "completed";
|
|
112
|
+
if (active) return "active";
|
|
113
|
+
if (stateCategory === "INITIAL" || stateCategory === "PROCESSING") {
|
|
114
|
+
return "queued";
|
|
115
|
+
}
|
|
116
|
+
return "unclassified";
|
|
117
|
+
}
|