@demlik/tea 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/agent/index.d.ts +907 -0
- package/dist/agent/index.js +20 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/authed-call/index.d.ts +119 -0
- package/dist/authed-call/index.js +148 -0
- package/dist/authed-call/index.js.map +1 -0
- package/dist/await-terminal/index.d.ts +99 -0
- package/dist/await-terminal/index.js +75 -0
- package/dist/await-terminal/index.js.map +1 -0
- package/dist/batch-window/index.d.ts +259 -0
- package/dist/batch-window/index.js +43 -0
- package/dist/batch-window/index.js.map +1 -0
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +2 -50
- package/dist/cache/index.js.map +1 -1
- package/dist/chunk-27N2SPTN.js +222 -0
- package/dist/chunk-27N2SPTN.js.map +1 -0
- package/dist/chunk-2YOE5PIS.js +45 -0
- package/dist/chunk-2YOE5PIS.js.map +1 -0
- package/dist/chunk-3545BOYL.js +65 -0
- package/dist/chunk-3545BOYL.js.map +1 -0
- package/dist/chunk-3XJWC7VW.js +61 -0
- package/dist/chunk-3XJWC7VW.js.map +1 -0
- package/dist/chunk-5DEQ5I3U.js +132 -0
- package/dist/chunk-5DEQ5I3U.js.map +1 -0
- package/dist/chunk-5VNWZADP.js +49 -0
- package/dist/chunk-5VNWZADP.js.map +1 -0
- package/dist/chunk-6RGBKODV.js +268 -0
- package/dist/chunk-6RGBKODV.js.map +1 -0
- package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
- package/dist/chunk-7FYQGDMF.js.map +1 -0
- package/dist/chunk-7W4LMPUW.js +186 -0
- package/dist/chunk-7W4LMPUW.js.map +1 -0
- package/dist/chunk-BYQUGDD3.js +16 -0
- package/dist/chunk-BYQUGDD3.js.map +1 -0
- package/dist/{chunk-PYBQREFU.js → chunk-DILMKOZ7.js} +22 -26
- package/dist/chunk-DILMKOZ7.js.map +1 -0
- package/dist/chunk-EHIX7NYZ.js +93 -0
- package/dist/chunk-EHIX7NYZ.js.map +1 -0
- package/dist/chunk-HCEVHFJ7.js +54 -0
- package/dist/chunk-HCEVHFJ7.js.map +1 -0
- package/dist/chunk-HJO6VNQI.js +15 -0
- package/dist/chunk-HJO6VNQI.js.map +1 -0
- package/dist/chunk-IN33Q4VJ.js +52 -0
- package/dist/chunk-IN33Q4VJ.js.map +1 -0
- package/dist/chunk-J6527VRQ.js +56 -0
- package/dist/chunk-J6527VRQ.js.map +1 -0
- package/dist/chunk-JELSZN25.js +47 -0
- package/dist/chunk-JELSZN25.js.map +1 -0
- package/dist/chunk-K575PT4S.js +58 -0
- package/dist/chunk-K575PT4S.js.map +1 -0
- package/dist/chunk-K7Q23EQ4.js +20 -0
- package/dist/chunk-K7Q23EQ4.js.map +1 -0
- package/dist/chunk-LEC4U6CX.js +91 -0
- package/dist/chunk-LEC4U6CX.js.map +1 -0
- package/dist/{chunk-PYOCUBQJ.js → chunk-LP7VQBRG.js} +22 -15
- package/dist/chunk-LP7VQBRG.js.map +1 -0
- package/dist/{chunk-CMG2KMWX.js → chunk-MMH7CW5K.js} +220 -189
- package/dist/chunk-MMH7CW5K.js.map +1 -0
- package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
- package/dist/chunk-NSDFA6M6.js.map +1 -0
- package/dist/chunk-ON24RKSE.js +60 -0
- package/dist/chunk-ON24RKSE.js.map +1 -0
- package/dist/chunk-PS23H6KQ.js +87 -0
- package/dist/chunk-PS23H6KQ.js.map +1 -0
- package/dist/chunk-QLA76GIT.js +162 -0
- package/dist/chunk-QLA76GIT.js.map +1 -0
- package/dist/chunk-S3FQN4SL.js +506 -0
- package/dist/chunk-S3FQN4SL.js.map +1 -0
- package/dist/chunk-SC3FAFZN.js +53 -0
- package/dist/chunk-SC3FAFZN.js.map +1 -0
- package/dist/chunk-SINXE47Y.js +55 -0
- package/dist/chunk-SINXE47Y.js.map +1 -0
- package/dist/chunk-SQTCS44W.js +33 -0
- package/dist/chunk-SQTCS44W.js.map +1 -0
- package/dist/chunk-TGG26XMK.js +52 -0
- package/dist/chunk-TGG26XMK.js.map +1 -0
- package/dist/chunk-UV6UWGEO.js +147 -0
- package/dist/chunk-UV6UWGEO.js.map +1 -0
- package/dist/chunk-UYWUVEP6.js +158 -0
- package/dist/chunk-UYWUVEP6.js.map +1 -0
- package/dist/chunk-VLGARN6H.js +202 -0
- package/dist/chunk-VLGARN6H.js.map +1 -0
- package/dist/circuit-breaker/index.js +1 -51
- package/dist/circuit-breaker/index.js.map +1 -1
- package/dist/core-BoB6t7dB.d.ts +188 -0
- package/dist/deadline/index.d.ts +24 -5
- package/dist/deadline/index.js +4 -18
- package/dist/deadline/index.js.map +1 -1
- package/dist/debounce/index.d.ts +6 -0
- package/dist/debounce/index.js +1 -54
- package/dist/debounce/index.js.map +1 -1
- package/dist/devtools/index.d.ts +1 -1
- package/dist/devtools/index.js +18 -6
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +1723 -92
- package/dist/do/index.js +673 -31
- package/dist/do/index.js.map +1 -1
- package/dist/durable-effects-CO9Uh971.d.ts +180 -0
- package/dist/extension/index.d.ts +31 -13
- package/dist/extension/index.js +5 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/react.js +8 -3
- package/dist/extension/react.js.map +1 -1
- package/dist/extension/subs/index.d.ts +2 -2
- package/dist/extension/subs/index.js.map +1 -1
- package/dist/extension/test-utils.js +11 -3
- package/dist/extension/test-utils.js.map +1 -1
- package/dist/fan-out/index.d.ts +239 -0
- package/dist/fan-out/index.js +5 -0
- package/dist/fan-out/index.js.map +1 -0
- package/dist/idempotency/adapter.d.ts +62 -0
- package/dist/idempotency/adapter.js +4 -0
- package/dist/idempotency/adapter.js.map +1 -0
- package/dist/idempotency/index.js +1 -56
- package/dist/idempotency/index.js.map +1 -1
- package/dist/idempotent-intake/index.d.ts +186 -0
- package/dist/idempotent-intake/index.js +7 -0
- package/dist/idempotent-intake/index.js.map +1 -0
- package/dist/index.d.ts +341 -191
- package/dist/index.js +2 -1
- package/dist/llm-call/index.d.ts +309 -0
- package/dist/llm-call/index.js +14 -0
- package/dist/llm-call/index.js.map +1 -0
- package/dist/machine-viz/index.d.ts +4 -4
- package/dist/machine-viz/index.js +5 -9
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +1 -0
- package/dist/monitored-run/index.d.ts +253 -0
- package/dist/monitored-run/index.js +10 -0
- package/dist/monitored-run/index.js.map +1 -0
- package/dist/node/index.d.ts +5 -5
- package/dist/node/index.js.map +1 -1
- package/dist/paginated-walk/index.d.ts +234 -0
- package/dist/paginated-walk/index.js +15 -0
- package/dist/paginated-walk/index.js.map +1 -0
- package/dist/paginator/index.d.ts +271 -0
- package/dist/paginator/index.js +3 -0
- package/dist/paginator/index.js.map +1 -0
- package/dist/parity/index.d.ts +92 -0
- package/dist/parity/index.js +78 -0
- package/dist/parity/index.js.map +1 -0
- package/dist/pbt/arbitraries/index.d.ts +1 -1
- package/dist/pbt/arbitraries/index.js +1 -1
- package/dist/pbt/index.d.ts +15 -8
- package/dist/pbt/index.js +6 -11
- package/dist/pbt/index.js.map +1 -1
- package/dist/pbt/runners/index.d.ts +1 -1
- package/dist/pbt/runners/index.js +3 -2
- package/dist/poller/index.d.ts +288 -0
- package/dist/poller/index.js +79 -0
- package/dist/poller/index.js.map +1 -0
- package/dist/prediction/index.d.ts +130 -0
- package/dist/prediction/index.js +4 -0
- package/dist/prediction/index.js.map +1 -0
- package/dist/protocol-B5YmJr0d.d.ts +45 -0
- package/dist/pure/index.d.ts +2 -0
- package/dist/pure/index.js +4 -0
- package/dist/pure/index.js.map +1 -0
- package/dist/raft/demo.d.ts +105 -0
- package/dist/raft/demo.js +172 -0
- package/dist/raft/demo.js.map +1 -0
- package/dist/raft/index.d.ts +548 -0
- package/dist/raft/index.js +7 -0
- package/dist/raft/index.js.map +1 -0
- package/dist/raft/sim.d.ts +222 -0
- package/dist/raft/sim.js +8 -0
- package/dist/raft/sim.js.map +1 -0
- package/dist/raft/timeline.d.ts +46 -0
- package/dist/raft/timeline.js +62 -0
- package/dist/raft/timeline.js.map +1 -0
- package/dist/rate-limit/index.js +1 -43
- package/dist/rate-limit/index.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +43 -20
- package/dist/react/index.js.map +1 -1
- package/dist/reconciler/index.d.ts +192 -0
- package/dist/reconciler/index.js +166 -0
- package/dist/reconciler/index.js.map +1 -0
- package/dist/recorder/index.d.ts +7 -3
- package/dist/recorder/index.js +1 -155
- package/dist/recorder/index.js.map +1 -1
- package/dist/resilient-call/index.d.ts +294 -0
- package/dist/resilient-call/index.js +13 -0
- package/dist/resilient-call/index.js.map +1 -0
- package/dist/retry-backoff/index.d.ts +42 -5
- package/dist/retry-backoff/index.js +1 -35
- package/dist/retry-backoff/index.js.map +1 -1
- package/dist/retry-to-success/index.d.ts +109 -0
- package/dist/retry-to-success/index.js +35 -0
- package/dist/retry-to-success/index.js.map +1 -0
- package/dist/saga/index.d.ts +238 -0
- package/dist/saga/index.js +150 -0
- package/dist/saga/index.js.map +1 -0
- package/dist/snapshot/index.d.ts +334 -0
- package/dist/snapshot/index.js +5 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/subs/index.d.ts +48 -10
- package/dist/subs/index.js +1 -1
- package/dist/testing/index.d.ts +107 -2
- package/dist/testing/index.js +204 -6
- package/dist/testing/index.js.map +1 -1
- package/dist/throttle/index.d.ts +9 -0
- package/dist/throttle/index.js +1 -52
- package/dist/throttle/index.js.map +1 -1
- package/dist/throttled-input/index.d.ts +380 -0
- package/dist/throttled-input/index.js +88 -0
- package/dist/throttled-input/index.js.map +1 -0
- package/dist/token-refresh/index.d.ts +215 -0
- package/dist/token-refresh/index.js +6 -0
- package/dist/token-refresh/index.js.map +1 -0
- package/dist/trace-replay/index.d.ts +11 -2
- package/dist/trace-replay/index.js +3 -55
- package/dist/trace-replay/index.js.map +1 -1
- package/dist/{types-CwBC5wMn.d.ts → types-B-8bcAp8.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +196 -0
- package/dist/with-deadline/index.js +111 -0
- package/dist/with-deadline/index.js.map +1 -0
- package/dist/with-resilience/index.d.ts +205 -0
- package/dist/with-resilience/index.js +251 -0
- package/dist/with-resilience/index.js.map +1 -0
- package/dist/with-telemetry/index.d.ts +149 -0
- package/dist/with-telemetry/index.js +70 -0
- package/dist/with-telemetry/index.js.map +1 -0
- package/dist/work-queue/adapter.d.ts +82 -0
- package/dist/work-queue/adapter.js +4 -0
- package/dist/work-queue/adapter.js.map +1 -0
- package/dist/work-queue/index.d.ts +25 -11
- package/dist/work-queue/index.js +1 -46
- package/dist/work-queue/index.js.map +1 -1
- package/dist/work-queue/ops.d.ts +69 -0
- package/dist/work-queue/ops.js +3 -0
- package/dist/work-queue/ops.js.map +1 -0
- package/dist/workflow/index.d.ts +450 -0
- package/dist/workflow/index.js +225 -0
- package/dist/workflow/index.js.map +1 -0
- package/package.json +168 -25
- package/dist/chunk-CMG2KMWX.js.map +0 -1
- package/dist/chunk-PYBQREFU.js.map +0 -1
- package/dist/chunk-PYOCUBQJ.js.map +0 -1
- package/dist/chunk-Y5JM3UUH.js.map +0 -1
- package/dist/chunk-ZRUST6BK.js.map +0 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { C as Cmd, S as Sub, M as Machine } from '../core-BoB6t7dB.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @demlik/tea/with-telemetry — the pattern-setting wrapper of the wrapper tier.
|
|
5
|
+
*
|
|
6
|
+
* `withTelemetry(base, config)` is a **pure function over machine data**: it
|
|
7
|
+
* takes any `Machine<S, M, C, U, Ctx>` and returns a NEW `Machine` over the
|
|
8
|
+
* composed Model `{ base, $telemetry }`. You `run()` / `replay()` the result
|
|
9
|
+
* exactly like any other machine — there is no new runtime, no privileged
|
|
10
|
+
* interception path.
|
|
11
|
+
*
|
|
12
|
+
* This is the SAFEST wrapper and the one every future wrapper (`withDeadline`,
|
|
13
|
+
* `withResilience`) copies: it is **OBSERVE-ONLY**. It observes every base Msg
|
|
14
|
+
* + transition, increments a monotonic counter, and emits a fire-and-forget
|
|
15
|
+
* telemetry Cmd whose interpret handler hands the event to a sink Port. It must
|
|
16
|
+
* NOT transform base Cmds or gate base Msgs — base behavior is byte-identical
|
|
17
|
+
* with or without it. The moment a telemetry wrapper alters the Msg/Cmd stream
|
|
18
|
+
* instead of observing it, it has become saga middleware (the betrayal the
|
|
19
|
+
* research warned of).
|
|
20
|
+
*
|
|
21
|
+
* ## The 5 rules it satisfies (from the wrapper-tier contract)
|
|
22
|
+
*
|
|
23
|
+
* 1. **Named, serializable slice.** Wrapper state lives at `model.$telemetry`,
|
|
24
|
+
* a plain `{ seq: number }` — JSON-serializable, never a closure.
|
|
25
|
+
* 2. **Retag-and-re-emit, never swallow.** The merged `update` runs the base
|
|
26
|
+
* reducer, passes its Cmds through UNCHANGED, and APPENDS its own
|
|
27
|
+
* `$telemetry:emit` Cmd. No base Cmd ever vanishes; nothing is gated.
|
|
28
|
+
* 3. **Every decision is a Msg/Cmd.** Each telemetry emission is a
|
|
29
|
+
* `$telemetry:emit` Cmd through the merged `update`, so "why did this fire"
|
|
30
|
+
* is answerable from reducer + log alone, and `runtime.observe` sees it.
|
|
31
|
+
* 4. **Subs merge by id.** Telemetry has no timers — merged subs/subscribe are
|
|
32
|
+
* the base's, verbatim.
|
|
33
|
+
* 5. **Clock out of update.** The default event carries no wall-clock read.
|
|
34
|
+
* The optional `at` is stamped at the interpret boundary (the `clock`
|
|
35
|
+
* port), never inside the merged `update`.
|
|
36
|
+
*
|
|
37
|
+
* ## Where the clock lives
|
|
38
|
+
*
|
|
39
|
+
* Inside the merged `update`: nowhere. The reducer is pure — it reads `seq` from
|
|
40
|
+
* the slice and projects the event from `(msg, prev, next)`. The ONLY clock read
|
|
41
|
+
* is at the interpret boundary: the `$telemetry:emit` handler stamps `event.at`
|
|
42
|
+
* from the injected `clock` port (if supplied and the projector left `at`
|
|
43
|
+
* unset), exactly as `resilient-call` reads `Date.now()` only inside `handlers`.
|
|
44
|
+
*
|
|
45
|
+
* ## Typical wiring
|
|
46
|
+
*
|
|
47
|
+
* const observed = withTelemetry(baseMachine, {
|
|
48
|
+
* event: (msg, _prev, next) => ({ seq: next.$telemetry.seq, msgType: msg.type }),
|
|
49
|
+
* });
|
|
50
|
+
* const runtime = run(observed, {
|
|
51
|
+
* ctx: { ...baseCtx, telemetrySink: (e) => posthog.capture(e), clock: Date.now },
|
|
52
|
+
* });
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The datum handed to the sink. The default projector emits `{ seq, msgType }`;
|
|
57
|
+
* a custom `event` projector may return any extra fields. `at` is OPTIONAL and
|
|
58
|
+
* plain data — it is stamped at the interpret boundary by the `clock` port, not
|
|
59
|
+
* inside the (pure, clock-free) merged `update`. Every field is JSON-round-trip
|
|
60
|
+
* serializable so a recorded telemetry stream stays durable.
|
|
61
|
+
*/
|
|
62
|
+
interface TelemetryEvent {
|
|
63
|
+
/** Monotonic event counter — equals `$telemetry.seq` after the transition. */
|
|
64
|
+
readonly seq: number;
|
|
65
|
+
/** The `Msg.type` discriminant that drove this transition. */
|
|
66
|
+
readonly msgType: string;
|
|
67
|
+
/**
|
|
68
|
+
* Wall-clock instant the event was emitted, ms since epoch. Absent until the
|
|
69
|
+
* interpret boundary stamps it (the `clock` port). Optional because the
|
|
70
|
+
* merged `update` is clock-free — a verb must never read `Date.now()`.
|
|
71
|
+
*/
|
|
72
|
+
readonly at?: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The wrapper's Model slice. A monotonic event counter — minimal but real, so
|
|
76
|
+
* the named-slice mechanism is exercised for the richer-slice wrappers that
|
|
77
|
+
* follow. `seq` equals the number of base Msgs folded so far: after replaying
|
|
78
|
+
* `N` msgs, `$telemetry.seq === N`. Plain data → serializes into a `Store<S>`.
|
|
79
|
+
*/
|
|
80
|
+
interface TelemetrySlice {
|
|
81
|
+
readonly seq: number;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The composed Model. The base machine's state is nested under `base`; the
|
|
85
|
+
* wrapper's state lives in the NAMED `$telemetry` slice — never hidden in a
|
|
86
|
+
* closure. Both fields are plain data, so the whole composed Model round-trips
|
|
87
|
+
* through `JSON.parse(JSON.stringify(...))` iff the base state does.
|
|
88
|
+
*/
|
|
89
|
+
interface TelemetryModel<S> {
|
|
90
|
+
readonly base: S;
|
|
91
|
+
readonly $telemetry: TelemetrySlice;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The fire-and-forget Cmd the merged `update` appends after every base
|
|
95
|
+
* transition. It carries the projected `event` as plain data (invariant 3 —
|
|
96
|
+
* no closure). The interpret handler hands `event` to the sink and dispatches
|
|
97
|
+
* NOTHING back, so there is no feedback loop.
|
|
98
|
+
*/
|
|
99
|
+
type TelemetryEmitCmd = Cmd<"$telemetry:emit"> & {
|
|
100
|
+
readonly event: TelemetryEvent;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* The side-effecting ports the wrapper adds to `Ctx`. The sink (PostHog / Sentry
|
|
104
|
+
* bridge) lives at the interpret boundary, never in a verb. `clock` is the
|
|
105
|
+
* optional wall-clock source used to stamp `event.at` at that same boundary —
|
|
106
|
+
* pass `Date.now` in production, a fixed `() => 1000` in tests; omit it to leave
|
|
107
|
+
* `at` unset.
|
|
108
|
+
*/
|
|
109
|
+
interface TelemetryPorts {
|
|
110
|
+
/** The side-effecting sink. Fire-and-forget — its result is never awaited into a Msg. */
|
|
111
|
+
readonly telemetrySink: (event: TelemetryEvent) => void | Promise<void>;
|
|
112
|
+
/** Optional clock for stamping `event.at` at the interpret boundary. */
|
|
113
|
+
readonly clock?: () => number;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The telemetry knob. `event` is an OPTIONAL projector mapping
|
|
117
|
+
* `(msg, prev, next)` to a `TelemetryEvent`; omit it and the default emits
|
|
118
|
+
* `{ seq, msgType }` where `seq` is the post-transition counter. The projector
|
|
119
|
+
* runs INSIDE the pure merged `update`, so it must not read the clock — stamp
|
|
120
|
+
* `at` at the sink boundary via the `clock` port instead.
|
|
121
|
+
*/
|
|
122
|
+
interface TelemetryConfig<S, M extends {
|
|
123
|
+
type: string;
|
|
124
|
+
}> {
|
|
125
|
+
readonly event?: (msg: M, prev: TelemetryModel<S>, next: TelemetryModel<S>) => TelemetryEvent;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Wrap `base` with observe-only telemetry. Returns a NEW `Machine` over the
|
|
129
|
+
* composed Model `{ base, $telemetry }`, with the base's Msgs/Cmds/Subs/Ctx
|
|
130
|
+
* extended by the wrapper's own.
|
|
131
|
+
*
|
|
132
|
+
* The composed machine:
|
|
133
|
+
* - `init` rehydrates the base (honoring the `[loaded, []]` contract) and
|
|
134
|
+
* seeds `$telemetry: { seq: 0 }`. Base init Cmds pass through unchanged.
|
|
135
|
+
* - `update` (Reducer form, keyed by every base `Msg.type`) runs the base
|
|
136
|
+
* reducer, increments `seq`, and appends a `$telemetry:emit` Cmd carrying
|
|
137
|
+
* the projected event. Base Cmds pass through UNCHANGED — the observe-only
|
|
138
|
+
* property.
|
|
139
|
+
* - `subscriptions` / `subscribe` are the base's, verbatim (no timers).
|
|
140
|
+
* - `interpret` is `{ ...base.interpret, "$telemetry:emit": sink-handler }`.
|
|
141
|
+
*
|
|
142
|
+
* @param base any `Machine<S, M, C, U, Ctx>`.
|
|
143
|
+
* @param config the telemetry knob (optional `event` projector).
|
|
144
|
+
*/
|
|
145
|
+
declare function withTelemetry<S, M extends {
|
|
146
|
+
type: string;
|
|
147
|
+
}, C extends Cmd, U extends Sub, Ctx>(base: Machine<S, M, C, U, Ctx>, config?: TelemetryConfig<S, M>): Machine<TelemetryModel<S>, M, C | TelemetryEmitCmd, U, Ctx & TelemetryPorts>;
|
|
148
|
+
|
|
149
|
+
export { type TelemetryConfig, type TelemetryEmitCmd, type TelemetryEvent, type TelemetryModel, type TelemetryPorts, type TelemetrySlice, withTelemetry };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import '../chunk-MMH7CW5K.js';
|
|
2
|
+
import { msgKeysOf, applyCell } from '../chunk-QLA76GIT.js';
|
|
3
|
+
|
|
4
|
+
// src/with-telemetry/index.ts
|
|
5
|
+
function withTelemetry(base, config = {}) {
|
|
6
|
+
const project = config.event;
|
|
7
|
+
const msgKeys = msgKeysOf(base);
|
|
8
|
+
const update = {};
|
|
9
|
+
for (const key of msgKeys) {
|
|
10
|
+
update[key] = (state, msg) => {
|
|
11
|
+
const [nextBase, baseCmds] = applyCell(base, state.base, msg);
|
|
12
|
+
const $telemetry = { seq: state.$telemetry.seq + 1 };
|
|
13
|
+
const next = { base: nextBase, $telemetry };
|
|
14
|
+
const event = project ? project(msg, state, next) : { seq: $telemetry.seq, msgType: msg.type };
|
|
15
|
+
const emit = { type: "$telemetry:emit", event };
|
|
16
|
+
return [next, [...baseCmds, emit]];
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const baseInterpret = base.interpret ?? {};
|
|
20
|
+
if (Object.hasOwn(baseInterpret, "$telemetry:emit")) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'withTelemetry: the base machine declares a reserved "$telemetry:emit" interpret handler \u2014 the wrapper owns the "$telemetry:" Cmd namespace. Rename the base handler.'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const interpret = {
|
|
26
|
+
...baseInterpret,
|
|
27
|
+
// OBSERVE-ONLY at the boundary: the sink is fire-and-forget. The runtime
|
|
28
|
+
// `await`s every interpret handler in its dispatch loop, so awaiting the
|
|
29
|
+
// sink here would let a SLOW sink backpressure the wrapped machine (its
|
|
30
|
+
// next dispatch waits) and a FAILING sink crash dispatch (the throw would
|
|
31
|
+
// propagate out of the transition). Both violate the contract: telemetry
|
|
32
|
+
// must never affect base behavior OR latency. So we DO NOT await — we hand
|
|
33
|
+
// the event to the sink, normalize its result to a Promise, swallow any
|
|
34
|
+
// rejection/throw, and return immediately. The handler resolves in the same
|
|
35
|
+
// microtask the dispatch loop reaches it; the machine never waits on the
|
|
36
|
+
// sink, and a rejected sink can never become an unhandled rejection.
|
|
37
|
+
"$telemetry:emit": (cmd, ctx) => {
|
|
38
|
+
const event = cmd.event.at === void 0 && ctx.clock !== void 0 ? { ...cmd.event, at: ctx.clock() } : cmd.event;
|
|
39
|
+
try {
|
|
40
|
+
Promise.resolve(ctx.telemetrySink(event)).catch(() => {
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const baseSubscriptions = base.subscriptions;
|
|
47
|
+
const baseSubscribe = base.subscribe;
|
|
48
|
+
return {
|
|
49
|
+
init: (loaded, ctx) => {
|
|
50
|
+
if (loaded !== null) {
|
|
51
|
+
return [loaded, []];
|
|
52
|
+
}
|
|
53
|
+
const [base0, cmds] = base.init(null, ctx);
|
|
54
|
+
const model = { base: base0, $telemetry: { seq: 0 } };
|
|
55
|
+
return [model, cmds];
|
|
56
|
+
},
|
|
57
|
+
update,
|
|
58
|
+
// Subs merge to the base's, verbatim — telemetry owns no timers. The base
|
|
59
|
+
// subscribes against its own nested slice.
|
|
60
|
+
...baseSubscriptions ? {
|
|
61
|
+
subscriptions: (state) => baseSubscriptions(state.base)
|
|
62
|
+
} : {},
|
|
63
|
+
...baseSubscribe ? { subscribe: baseSubscribe } : {},
|
|
64
|
+
interpret
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { withTelemetry };
|
|
69
|
+
//# sourceMappingURL=index.js.map
|
|
70
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/with-telemetry/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/with-telemetry — the pattern-setting wrapper of the wrapper tier.\n *\n * `withTelemetry(base, config)` is a **pure function over machine data**: it\n * takes any `Machine<S, M, C, U, Ctx>` and returns a NEW `Machine` over the\n * composed Model `{ base, $telemetry }`. You `run()` / `replay()` the result\n * exactly like any other machine — there is no new runtime, no privileged\n * interception path.\n *\n * This is the SAFEST wrapper and the one every future wrapper (`withDeadline`,\n * `withResilience`) copies: it is **OBSERVE-ONLY**. It observes every base Msg\n * + transition, increments a monotonic counter, and emits a fire-and-forget\n * telemetry Cmd whose interpret handler hands the event to a sink Port. It must\n * NOT transform base Cmds or gate base Msgs — base behavior is byte-identical\n * with or without it. The moment a telemetry wrapper alters the Msg/Cmd stream\n * instead of observing it, it has become saga middleware (the betrayal the\n * research warned of).\n *\n * ## The 5 rules it satisfies (from the wrapper-tier contract)\n *\n * 1. **Named, serializable slice.** Wrapper state lives at `model.$telemetry`,\n * a plain `{ seq: number }` — JSON-serializable, never a closure.\n * 2. **Retag-and-re-emit, never swallow.** The merged `update` runs the base\n * reducer, passes its Cmds through UNCHANGED, and APPENDS its own\n * `$telemetry:emit` Cmd. No base Cmd ever vanishes; nothing is gated.\n * 3. **Every decision is a Msg/Cmd.** Each telemetry emission is a\n * `$telemetry:emit` Cmd through the merged `update`, so \"why did this fire\"\n * is answerable from reducer + log alone, and `runtime.observe` sees it.\n * 4. **Subs merge by id.** Telemetry has no timers — merged subs/subscribe are\n * the base's, verbatim.\n * 5. **Clock out of update.** The default event carries no wall-clock read.\n * The optional `at` is stamped at the interpret boundary (the `clock`\n * port), never inside the merged `update`.\n *\n * ## Where the clock lives\n *\n * Inside the merged `update`: nowhere. The reducer is pure — it reads `seq` from\n * the slice and projects the event from `(msg, prev, next)`. The ONLY clock read\n * is at the interpret boundary: the `$telemetry:emit` handler stamps `event.at`\n * from the injected `clock` port (if supplied and the projector left `at`\n * unset), exactly as `resilient-call` reads `Date.now()` only inside `handlers`.\n *\n * ## Typical wiring\n *\n * const observed = withTelemetry(baseMachine, {\n * event: (msg, _prev, next) => ({ seq: next.$telemetry.seq, msgType: msg.type }),\n * });\n * const runtime = run(observed, {\n * ctx: { ...baseCtx, telemetrySink: (e) => posthog.capture(e), clock: Date.now },\n * });\n */\n\nimport type { Cmd, Interpret, Machine, Reducer, Sub } from \"../index\";\nimport { applyCell, msgKeysOf } from \"../index\";\n\n// ===========================================================================\n// The event + the slice + the ports — the telemetry vocabulary.\n// ===========================================================================\n\n/**\n * The datum handed to the sink. The default projector emits `{ seq, msgType }`;\n * a custom `event` projector may return any extra fields. `at` is OPTIONAL and\n * plain data — it is stamped at the interpret boundary by the `clock` port, not\n * inside the (pure, clock-free) merged `update`. Every field is JSON-round-trip\n * serializable so a recorded telemetry stream stays durable.\n */\nexport interface TelemetryEvent {\n /** Monotonic event counter — equals `$telemetry.seq` after the transition. */\n readonly seq: number;\n /** The `Msg.type` discriminant that drove this transition. */\n readonly msgType: string;\n /**\n * Wall-clock instant the event was emitted, ms since epoch. Absent until the\n * interpret boundary stamps it (the `clock` port). Optional because the\n * merged `update` is clock-free — a verb must never read `Date.now()`.\n */\n readonly at?: number;\n}\n\n/**\n * The wrapper's Model slice. A monotonic event counter — minimal but real, so\n * the named-slice mechanism is exercised for the richer-slice wrappers that\n * follow. `seq` equals the number of base Msgs folded so far: after replaying\n * `N` msgs, `$telemetry.seq === N`. Plain data → serializes into a `Store<S>`.\n */\nexport interface TelemetrySlice {\n readonly seq: number;\n}\n\n/**\n * The composed Model. The base machine's state is nested under `base`; the\n * wrapper's state lives in the NAMED `$telemetry` slice — never hidden in a\n * closure. Both fields are plain data, so the whole composed Model round-trips\n * through `JSON.parse(JSON.stringify(...))` iff the base state does.\n */\nexport interface TelemetryModel<S> {\n readonly base: S;\n readonly $telemetry: TelemetrySlice;\n}\n\n/**\n * The fire-and-forget Cmd the merged `update` appends after every base\n * transition. It carries the projected `event` as plain data (invariant 3 —\n * no closure). The interpret handler hands `event` to the sink and dispatches\n * NOTHING back, so there is no feedback loop.\n */\nexport type TelemetryEmitCmd = Cmd<\"$telemetry:emit\"> & {\n readonly event: TelemetryEvent;\n};\n\n/**\n * The side-effecting ports the wrapper adds to `Ctx`. The sink (PostHog / Sentry\n * bridge) lives at the interpret boundary, never in a verb. `clock` is the\n * optional wall-clock source used to stamp `event.at` at that same boundary —\n * pass `Date.now` in production, a fixed `() => 1000` in tests; omit it to leave\n * `at` unset.\n */\nexport interface TelemetryPorts {\n /** The side-effecting sink. Fire-and-forget — its result is never awaited into a Msg. */\n readonly telemetrySink: (event: TelemetryEvent) => void | Promise<void>;\n /** Optional clock for stamping `event.at` at the interpret boundary. */\n readonly clock?: () => number;\n}\n\n/**\n * The telemetry knob. `event` is an OPTIONAL projector mapping\n * `(msg, prev, next)` to a `TelemetryEvent`; omit it and the default emits\n * `{ seq, msgType }` where `seq` is the post-transition counter. The projector\n * runs INSIDE the pure merged `update`, so it must not read the clock — stamp\n * `at` at the sink boundary via the `clock` port instead.\n */\nexport interface TelemetryConfig<S, M extends { type: string }> {\n readonly event?: (\n msg: M,\n prev: TelemetryModel<S>,\n next: TelemetryModel<S>,\n ) => TelemetryEvent;\n}\n\n// ===========================================================================\n// withTelemetry — the wrapper.\n// ===========================================================================\n\n/**\n * Wrap `base` with observe-only telemetry. Returns a NEW `Machine` over the\n * composed Model `{ base, $telemetry }`, with the base's Msgs/Cmds/Subs/Ctx\n * extended by the wrapper's own.\n *\n * The composed machine:\n * - `init` rehydrates the base (honoring the `[loaded, []]` contract) and\n * seeds `$telemetry: { seq: 0 }`. Base init Cmds pass through unchanged.\n * - `update` (Reducer form, keyed by every base `Msg.type`) runs the base\n * reducer, increments `seq`, and appends a `$telemetry:emit` Cmd carrying\n * the projected event. Base Cmds pass through UNCHANGED — the observe-only\n * property.\n * - `subscriptions` / `subscribe` are the base's, verbatim (no timers).\n * - `interpret` is `{ ...base.interpret, \"$telemetry:emit\": sink-handler }`.\n *\n * @param base any `Machine<S, M, C, U, Ctx>`.\n * @param config the telemetry knob (optional `event` projector).\n */\nexport function withTelemetry<\n S,\n M extends { type: string },\n C extends Cmd,\n U extends Sub,\n Ctx,\n>(\n base: Machine<S, M, C, U, Ctx>,\n config: TelemetryConfig<S, M> = {},\n): Machine<\n TelemetryModel<S>,\n M,\n C | TelemetryEmitCmd,\n U,\n Ctx & TelemetryPorts\n> {\n const project = config.event;\n\n // The merged update is a flat Reducer over the composed Model. The composed\n // Model is NOT discriminated on `.type`, so the Transitions form is never an\n // option here — every base Msg variant gets exactly one cell, enumerated via\n // `msgKeysOf(base)` (keyed on the authoritative `__form` tag; #275).\n const msgKeys = msgKeysOf(base);\n\n const update = {} as Record<\n string,\n (\n state: TelemetryModel<S>,\n msg: M,\n ) => readonly [TelemetryModel<S>, readonly (C | TelemetryEmitCmd)[]]\n >;\n for (const key of msgKeys) {\n update[key] = (state, msg) => {\n // 1) Run the base reducer — its result passes through UNCHANGED.\n const [nextBase, baseCmds] = applyCell<S, M, C>(base, state.base, msg);\n // 2) Advance the wrapper slice (monotonic counter). New object — no mutation.\n const $telemetry: TelemetrySlice = { seq: state.$telemetry.seq + 1 };\n const next: TelemetryModel<S> = { base: nextBase, $telemetry };\n // 3) Project the event (pure — no clock). Default: { seq, msgType }.\n const event: TelemetryEvent = project\n ? project(msg, state, next)\n : { seq: $telemetry.seq, msgType: msg.type };\n // 4) Append the fire-and-forget emit Cmd. Base Cmds come FIRST, unchanged.\n const emit: TelemetryEmitCmd = { type: \"$telemetry:emit\", event };\n return [next, [...baseCmds, emit]];\n };\n }\n\n // The base's interpret, extended with the one telemetry handler. The sink is\n // the ONLY side effect, and it lives here at the interpret boundary — never in\n // a verb. `clock` (if provided) stamps `event.at` here too, the one permitted\n // clock read. The handler returns void synchronously — it dispatches NOTHING\n // back (no feedback loop) AND it does NOT await the sink (no backpressure /\n // crash from a slow or failing sink). See the fire-and-forget note below.\n const baseInterpret =\n (base as { interpret?: Interpret<M, C, Ctx> }).interpret ??\n ({} as Interpret<M, C, Ctx>);\n // Namespace guard (mirrors the substrate's SubId / definePort collision\n // asserts). The spread below merges `$telemetry:emit` into the base's\n // interpret map; if the base already declares that key, the spread would\n // SILENTLY clobber the base handler. The `$telemetry:` Cmd namespace is the\n // wrapper's — refuse to wrap a base that squats on it instead of stealing it.\n if (Object.hasOwn(baseInterpret as object, \"$telemetry:emit\")) {\n throw new Error(\n 'withTelemetry: the base machine declares a reserved \"$telemetry:emit\" ' +\n 'interpret handler — the wrapper owns the \"$telemetry:\" Cmd namespace. ' +\n \"Rename the base handler.\",\n );\n }\n const interpret = {\n ...(baseInterpret as Record<string, unknown>),\n // OBSERVE-ONLY at the boundary: the sink is fire-and-forget. The runtime\n // `await`s every interpret handler in its dispatch loop, so awaiting the\n // sink here would let a SLOW sink backpressure the wrapped machine (its\n // next dispatch waits) and a FAILING sink crash dispatch (the throw would\n // propagate out of the transition). Both violate the contract: telemetry\n // must never affect base behavior OR latency. So we DO NOT await — we hand\n // the event to the sink, normalize its result to a Promise, swallow any\n // rejection/throw, and return immediately. The handler resolves in the same\n // microtask the dispatch loop reaches it; the machine never waits on the\n // sink, and a rejected sink can never become an unhandled rejection.\n \"$telemetry:emit\": (\n cmd: TelemetryEmitCmd,\n ctx: Ctx & TelemetryPorts,\n ): void => {\n const event =\n cmd.event.at === undefined && ctx.clock !== undefined\n ? { ...cmd.event, at: ctx.clock() }\n : cmd.event;\n // `Promise.resolve(fn())` covers BOTH failure shapes: a sync `throw` is\n // caught by `Promise.resolve` (the call is the argument, so a throw here\n // would escape — guard it), and an async rejection is caught by `.catch`.\n try {\n Promise.resolve(ctx.telemetrySink(event)).catch(() => {});\n } catch {\n // A sink that throws SYNCHRONOUSLY (before returning a Promise) is\n // swallowed here. Either way the slow/failing sink never reaches the\n // dispatch loop's `await`.\n }\n },\n } as Interpret<M, C | TelemetryEmitCmd, Ctx & TelemetryPorts>;\n\n // Capture once so the narrowing survives into the spread (no non-null assert).\n const baseSubscriptions = base.subscriptions;\n const baseSubscribe = (base as { subscribe?: unknown }).subscribe;\n\n return {\n init: (loaded, ctx) => {\n // Rehydrate path: `loaded !== null` MUST return `[loaded, []]` (no Cmds) —\n // the substrate's replay enforces this. We split the composed loaded Model\n // into its base slice and feed the base its own loaded snapshot, so the\n // base's rehydrate contract is honored transitively.\n if (loaded !== null) {\n return [loaded, []];\n }\n const [base0, cmds] = base.init(null, ctx);\n const model: TelemetryModel<S> = { base: base0, $telemetry: { seq: 0 } };\n // Base init Cmds pass through unchanged; no telemetry Cmd on boot (boot is\n // not a base Msg transition, so it does not advance `seq`).\n return [model, cmds];\n },\n update: update as unknown as Reducer<\n TelemetryModel<S>,\n M,\n C | TelemetryEmitCmd\n >,\n // Subs merge to the base's, verbatim — telemetry owns no timers. The base\n // subscribes against its own nested slice.\n ...(baseSubscriptions\n ? {\n subscriptions: (state: TelemetryModel<S>) =>\n baseSubscriptions(state.base),\n }\n : {}),\n ...(baseSubscribe ? { subscribe: baseSubscribe } : {}),\n interpret,\n } as Machine<\n TelemetryModel<S>,\n M,\n C | TelemetryEmitCmd,\n U,\n Ctx & TelemetryPorts\n >;\n}\n"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { QueueItem } from './index.js';
|
|
2
|
+
import '../index.js';
|
|
3
|
+
import '../core-BoB6t7dB.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @demlik/tea/work-queue/adapter — the verb seam over the pure queue ops.
|
|
7
|
+
*
|
|
8
|
+
* The pure ops in `./ops` are the BLESSED transitions, but their names and
|
|
9
|
+
* argument tuples (`enqueueOp(queue, input, now, id)`, `claimNextOp(queue,
|
|
10
|
+
* now)`, …) and their `{ next, item }` / `{ next, changed }` result shapes are
|
|
11
|
+
* the STORE's vocabulary, not the consumer's. An L2 composition that owns a
|
|
12
|
+
* `QueueItem<I>[]` slice (`monitored-run`, `saga`, `idempotent-intake`,
|
|
13
|
+
* `fan-out`) used to import those ops by name and call them directly, so a
|
|
14
|
+
* change to the op surface — or to `QueueItem`'s fields — cascaded to every
|
|
15
|
+
* call site.
|
|
16
|
+
*
|
|
17
|
+
* `QueueAdapter<I>` is the insulating interface: four VERBS (`enqueue` /
|
|
18
|
+
* `claim` / `markDone` / `patch`) that consumers depend on instead of the raw
|
|
19
|
+
* ops. The verbs keep the ops' pure `(queue, …) => { next, … }` contract — they
|
|
20
|
+
* are pure array transforms, no I/O, no `Date.now` (the clock is injected as
|
|
21
|
+
* `now`, exactly as the ops require it) — so a consumer folding the adapter
|
|
22
|
+
* through its reducer stays inside invariant 2 (pure transitions).
|
|
23
|
+
*
|
|
24
|
+
* `queueAdapter<I>()` is the canonical implementation, a thin binding of the
|
|
25
|
+
* blessed ops. Because the verbs are the only surface consumers touch, a
|
|
26
|
+
* `QueueItem` shape change (a new field, a renamed status, a different op
|
|
27
|
+
* signature) is now absorbed HERE — re-bind the verb once — rather than at the
|
|
28
|
+
* 4 consumer call sites. The `patch` verb deliberately takes the same
|
|
29
|
+
* `(item) => item` closure `patchItemOp` does, so the one place a `QueueItem`
|
|
30
|
+
* literal is spread stays the consumer's patch closure, never a re-rolled
|
|
31
|
+
* record.
|
|
32
|
+
*
|
|
33
|
+
* This is an INTERNAL seam (no public-API port): consumers import the adapter
|
|
34
|
+
* from this subpath the same way they imported the ops. The ops remain
|
|
35
|
+
* exported for the in-process `createQueue` adapter (`./index`) and for any
|
|
36
|
+
* call site that genuinely wants the op directly.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The verb interface over a `QueueItem<I>[]` slice. Each verb mirrors a blessed
|
|
41
|
+
* op's pure contract but speaks the consumer's lifecycle vocabulary, so the
|
|
42
|
+
* consumer never names a `*Op` function or reaches into a `QueueItem` field it
|
|
43
|
+
* did not put there.
|
|
44
|
+
*/
|
|
45
|
+
interface QueueAdapter<I> {
|
|
46
|
+
/** Append a new `pending` item stamped `now` under `id`. */
|
|
47
|
+
enqueue(queue: readonly QueueItem<I>[], input: I, now: number, id: string): {
|
|
48
|
+
next: QueueItem<I>[];
|
|
49
|
+
item: QueueItem<I>;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Claim the first `pending` item — flip it `running`, stamp `startedAt` to
|
|
53
|
+
* `now` — or `null` if nothing is pending.
|
|
54
|
+
*/
|
|
55
|
+
claim(queue: readonly QueueItem<I>[], now: number): {
|
|
56
|
+
next: QueueItem<I>[];
|
|
57
|
+
claimed: QueueItem<I>;
|
|
58
|
+
} | null;
|
|
59
|
+
/** Drop the item `id` from the queue (completed runs leave the queue). */
|
|
60
|
+
markDone(queue: readonly QueueItem<I>[], id: string): {
|
|
61
|
+
next: QueueItem<I>[];
|
|
62
|
+
changed: boolean;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Single-item transition: replace the item `id` with `patch(item)`. The
|
|
66
|
+
* closure is the only place a `QueueItem` literal is constructed, so the
|
|
67
|
+
* field shape stays out of the consumer's hands except through this verb.
|
|
68
|
+
*/
|
|
69
|
+
patch(queue: readonly QueueItem<I>[], id: string, patch: (item: QueueItem<I>) => QueueItem<I>): {
|
|
70
|
+
next: QueueItem<I>[];
|
|
71
|
+
changed: boolean;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The canonical `QueueAdapter<I>`, binding each verb to its blessed op. Pure
|
|
76
|
+
* and stateless — call once at module scope in a consumer and fold its verbs
|
|
77
|
+
* through the reducer. A `QueueItem` shape change is absorbed by re-binding a
|
|
78
|
+
* verb here, not at the consumers.
|
|
79
|
+
*/
|
|
80
|
+
declare function queueAdapter<I>(): QueueAdapter<I>;
|
|
81
|
+
|
|
82
|
+
export { type QueueAdapter, queueAdapter };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"adapter.js"}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { Store } from '../index.js';
|
|
2
|
+
import '../core-BoB6t7dB.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* @demlik/tea/work-queue — substrate-agnostic work-queue lifecycle on `Store<S>`.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Two public surfaces, one shape:
|
|
8
|
+
* - `createQueue(store)` (this file) is the in-process adapter — it binds the
|
|
9
|
+
* pure ops to an injected `Store` and runs `load → mutate → save`.
|
|
10
|
+
* - `@demlik/tea/work-queue/ops` (`./ops`) is the BLESSED, importable surface
|
|
11
|
+
* of those same pure ops. L2 compositions that own their own Model slice
|
|
12
|
+
* (`monitored-run`, `idempotent-intake`, …) import the ops directly and
|
|
13
|
+
* delegate status flips to them instead of re-rolling the transitions.
|
|
14
|
+
* Both routes call the identical ops, so the lifecycle rules live in exactly
|
|
15
|
+
* one place. This mirrors `@demlik/tea/do`'s one-shape-per-package rule.
|
|
9
16
|
*
|
|
10
17
|
* The package is generic in the input payload `I` (audit-specific fields go
|
|
11
18
|
* here in apps/extension). `output` is a free-form side-channel correlation
|
|
@@ -15,15 +22,22 @@ import { Store } from '../index.js';
|
|
|
15
22
|
*/
|
|
16
23
|
|
|
17
24
|
type QueueItemStatus = "pending" | "running" | "done" | "failed" | "cancelled";
|
|
25
|
+
/**
|
|
26
|
+
* A single queue entry. Every field is `readonly`: a persisted slice is
|
|
27
|
+
* immutable, so a transition returns a NEW item (the ops always spread —
|
|
28
|
+
* `{ ...item, status: … }`) rather than mutating in place. This type-enforces
|
|
29
|
+
* the durability invariant the consumers already honor by convention, and
|
|
30
|
+
* keeps a slice round-trippable through JSON unchanged across a reload.
|
|
31
|
+
*/
|
|
18
32
|
interface QueueItem<I> {
|
|
19
|
-
id: string;
|
|
20
|
-
status: QueueItemStatus;
|
|
21
|
-
input: I;
|
|
22
|
-
enqueuedAt: number;
|
|
23
|
-
startedAt?: number;
|
|
24
|
-
finishedAt?: number;
|
|
25
|
-
output?: string;
|
|
26
|
-
error?: string;
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly status: QueueItemStatus;
|
|
35
|
+
readonly input: I;
|
|
36
|
+
readonly enqueuedAt: number;
|
|
37
|
+
readonly startedAt?: number;
|
|
38
|
+
readonly finishedAt?: number;
|
|
39
|
+
readonly output?: string;
|
|
40
|
+
readonly error?: string;
|
|
27
41
|
}
|
|
28
42
|
/**
|
|
29
43
|
* Caller-facing enqueue payload. Just `I` today — kept named so future
|
package/dist/work-queue/index.js
CHANGED
|
@@ -1,49 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
function enqueueOp(queue, input, now, id) {
|
|
3
|
-
const item = {
|
|
4
|
-
id,
|
|
5
|
-
status: "pending",
|
|
6
|
-
input,
|
|
7
|
-
enqueuedAt: now
|
|
8
|
-
};
|
|
9
|
-
return { next: [...queue, item], item };
|
|
10
|
-
}
|
|
11
|
-
function claimNextOp(queue, now) {
|
|
12
|
-
const idx = queue.findIndex((i) => i.status === "pending");
|
|
13
|
-
if (idx === -1) return null;
|
|
14
|
-
const claimed = {
|
|
15
|
-
...queue[idx],
|
|
16
|
-
status: "running",
|
|
17
|
-
startedAt: now
|
|
18
|
-
};
|
|
19
|
-
const next = queue.slice();
|
|
20
|
-
next[idx] = claimed;
|
|
21
|
-
return { next, claimed };
|
|
22
|
-
}
|
|
23
|
-
function markDoneOp(queue, id) {
|
|
24
|
-
const next = queue.filter((i) => i.id !== id);
|
|
25
|
-
return { next, changed: next.length !== queue.length };
|
|
26
|
-
}
|
|
27
|
-
function patchItemOp(queue, id, patch) {
|
|
28
|
-
const idx = queue.findIndex((i) => i.id === id);
|
|
29
|
-
if (idx === -1) return { next: queue.slice(), changed: false };
|
|
30
|
-
const next = queue.slice();
|
|
31
|
-
next[idx] = patch(queue[idx]);
|
|
32
|
-
return { next, changed: true };
|
|
33
|
-
}
|
|
34
|
-
function resetRunningOp(queue) {
|
|
35
|
-
let changed = false;
|
|
36
|
-
const next = queue.map((i) => {
|
|
37
|
-
if (i.status !== "running") return i;
|
|
38
|
-
changed = true;
|
|
39
|
-
return { ...i, status: "pending", startedAt: void 0 };
|
|
40
|
-
});
|
|
41
|
-
return { next, changed };
|
|
42
|
-
}
|
|
43
|
-
function removeOp(queue, id) {
|
|
44
|
-
const next = queue.filter((i) => i.id !== id);
|
|
45
|
-
return { next, changed: next.length !== queue.length };
|
|
46
|
-
}
|
|
1
|
+
import { enqueueOp, claimNextOp, markDoneOp, patchItemOp, resetRunningOp, removeOp } from '../chunk-TGG26XMK.js';
|
|
47
2
|
|
|
48
3
|
// src/work-queue/index.ts
|
|
49
4
|
function genId() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/work-queue/ops.ts","../../src/work-queue/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Pure queue-lifecycle ops over `QueueItem<I>[]`.\n *\n * Each op is `(queue, ...args) => { next, result? }`. No I/O, no Date.now\n * indirection beyond a plain call site — the adapter in `./index.ts` injects\n * `Store<QueueItem<I>[]>` and orchestrates `load → mutate → save`.\n *\n * Substrate-agnostic by construction: nothing here references chrome.storage,\n * Cloudflare, or any concrete persistence layer. The audit-specific shape\n * (journeyId, projectId, ...) is lifted into the caller's `I` parameter.\n *\n * NOT exported from package root. Tests reach this file only through the\n * `createQueue(memoryStore(...))` adapter — same one-shape-per-package rule as\n * `@demlik/tea/do`.\n */\n\nimport type { QueueItem, QueueItemStatus } from \"./index\";\n\n/** Append a new pending item. Returns the new queue and the created item. */\nexport function enqueueOp<I>(\n queue: readonly QueueItem<I>[],\n input: I,\n now: number,\n id: string,\n): { next: QueueItem<I>[]; item: QueueItem<I> } {\n const item: QueueItem<I> = {\n id,\n status: \"pending\",\n input,\n enqueuedAt: now,\n };\n return { next: [...queue, item], item };\n}\n\n/**\n * Claim the first pending item by flipping it to `running` and stamping\n * `startedAt`. Returns the new queue and the claimed item, or `null` if no\n * pending item exists.\n */\nexport function claimNextOp<I>(\n queue: readonly QueueItem<I>[],\n now: number,\n): { next: QueueItem<I>[]; claimed: QueueItem<I> } | null {\n const idx = queue.findIndex((i) => i.status === \"pending\");\n if (idx === -1) return null;\n // `idx !== -1` guarantees the slot is present.\n const claimed: QueueItem<I> = {\n ...queue[idx]!,\n status: \"running\",\n startedAt: now,\n };\n const next = queue.slice();\n next[idx] = claimed;\n return { next, claimed };\n}\n\n/**\n * Done items leave the queue — completed runs live wherever the caller\n * persists their output. Returns the new queue (or the original reference if\n * no item matched, so the adapter can skip a save).\n */\nexport function markDoneOp<I>(\n queue: readonly QueueItem<I>[],\n id: string,\n): { next: QueueItem<I>[]; changed: boolean } {\n const next = queue.filter((i) => i.id !== id);\n return { next, changed: next.length !== queue.length };\n}\n\n/**\n * Generic single-item status transition. Used by `markFailed`,\n * `markCancelled`, `retryItem`, `bindOutput`. The patch closure receives the\n * matched item and returns its replacement; returning the same reference is\n * fine (the adapter only saves on `changed`).\n */\nexport function patchItemOp<I>(\n queue: readonly QueueItem<I>[],\n id: string,\n patch: (item: QueueItem<I>) => QueueItem<I>,\n): { next: QueueItem<I>[]; changed: boolean } {\n const idx = queue.findIndex((i) => i.id === id);\n if (idx === -1) return { next: queue.slice(), changed: false };\n const next = queue.slice();\n // `idx !== -1` guarantees the slot is present.\n next[idx] = patch(queue[idx]!);\n return { next, changed: true };\n}\n\n/** Reset every `running` item back to `pending`, clearing `startedAt`. */\nexport function resetRunningOp<I>(queue: readonly QueueItem<I>[]): {\n next: QueueItem<I>[];\n changed: boolean;\n} {\n let changed = false;\n const next = queue.map((i) => {\n if (i.status !== \"running\") return i;\n changed = true;\n return { ...i, status: \"pending\" as QueueItemStatus, startedAt: undefined };\n });\n return { next, changed };\n}\n\n/** Hard-delete an item regardless of status. */\nexport function removeOp<I>(\n queue: readonly QueueItem<I>[],\n id: string,\n): { next: QueueItem<I>[]; changed: boolean } {\n const next = queue.filter((i) => i.id !== id);\n return { next, changed: next.length !== queue.length };\n}\n","/**\n * @demlik/tea/work-queue — substrate-agnostic work-queue lifecycle on `Store<S>`.\n *\n * The pure ops live in `./ops` and are NOT exported. Tests and consumers reach\n * them only through `createQueue(store)`. This mirrors `@demlik/tea/do`'s\n * one-shape-per-package rule: a single adapter is the public surface.\n *\n * The package is generic in the input payload `I` (audit-specific fields go\n * here in apps/extension). `output` is a free-form side-channel correlation\n * token (a backend id, URL, trace id) — typed as `string` because that's its\n * only honest type. Consumers that need structured per-item data persist it in\n * their own store and put the foreign-key string in `output`.\n */\n\nimport type { Store } from \"../index\";\nimport { claimNextOp, enqueueOp, markDoneOp, patchItemOp, removeOp, resetRunningOp } from \"./ops\";\n\n/**\n * Generate a queue-item id at the adapter boundary. Reads `Date.now()` and\n * `Math.random()` — impure helpers belong in the adapter (`index.ts`),\n * never in `ops.ts` (which is purely-functional by contract). Uses native\n * UUID when available, falls back to timestamp + random for environments\n * without `crypto.randomUUID`.\n */\nfunction genId(): string {\n return (\n globalThis.crypto?.randomUUID?.() ?? `q_${Date.now()}_${Math.random().toString(36).slice(2)}`\n );\n}\n\nexport type QueueItemStatus = \"pending\" | \"running\" | \"done\" | \"failed\" | \"cancelled\";\n\nexport interface QueueItem<I> {\n id: string;\n status: QueueItemStatus;\n input: I;\n enqueuedAt: number;\n startedAt?: number;\n finishedAt?: number;\n output?: string;\n error?: string;\n}\n\n/**\n * Caller-facing enqueue payload. Just `I` today — kept named so future\n * variants (priority, deferUntil, …) can grow without breaking callers.\n */\nexport type EnqueueInput<I> = I;\n\n/**\n * Adapter that binds the pure ops to an injected `Store<QueueItem<I>[]>`.\n *\n * `load → mutate → save` is the only durability discipline this package\n * imposes. The store decides what \"save\" means (chrome.storage, KV, DO\n * storage, in-memory) — that decision belongs at the caller.\n *\n * Ops that don't change the queue (e.g. `markDone` on an unknown id) skip the\n * save call so the underlying store doesn't observe a write churn.\n */\nexport function createQueue<I>(store: Store<QueueItem<I>[]>) {\n // `Store<S>.load()` returns `unknown` (invariant 8 boundary). Route\n // through `store.migrate(raw)` to get the typed value before any\n // queue operation reads it — same wiring the substrate's `run()` uses at\n // boot. The work queue is a substrate-agnostic primitive on top of\n // `Store<S>`, so it honors the same contract.\n async function loadParsed(): Promise<QueueItem<I>[] | null> {\n const raw = await store.load();\n return store.migrate(raw);\n }\n\n async function list(): Promise<QueueItem<I>[]> {\n return (await loadParsed()) ?? [];\n }\n\n async function enqueue(input: EnqueueInput<I>): Promise<QueueItem<I>> {\n const queue = await list();\n const { next, item } = enqueueOp<I>(queue, input, Date.now(), genId());\n await store.save(next);\n return item;\n }\n\n async function claimNext(): Promise<QueueItem<I> | null> {\n const queue = await list();\n const result = claimNextOp<I>(queue, Date.now());\n if (!result) return null;\n await store.save(result.next);\n return result.claimed;\n }\n\n // `markDone` ignores the optional second arg by design: completed runs leave\n // the queue, and the consumer is expected to persist any output (auditId,\n // etc.) wherever those completed records belong. Kept in the signature for\n // call-site readability and forward-compat with `bindOutput`.\n async function markDone(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = markDoneOp<I>(queue, id);\n if (changed) await store.save(next);\n }\n\n async function markFailed(id: string, error: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n status: \"failed\",\n finishedAt: Date.now(),\n error,\n }));\n if (changed) await store.save(next);\n }\n\n async function markCancelled(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n status: \"cancelled\",\n finishedAt: Date.now(),\n }));\n if (changed) await store.save(next);\n }\n\n async function resetRunningToPending(): Promise<void> {\n const queue = await list();\n const { next, changed } = resetRunningOp<I>(queue);\n if (changed) await store.save(next);\n }\n\n async function removeItem(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = removeOp<I>(queue, id);\n if (changed) await store.save(next);\n }\n\n async function retryItem(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n status: \"pending\",\n startedAt: undefined,\n finishedAt: undefined,\n error: undefined,\n output: undefined,\n }));\n if (changed) await store.save(next);\n }\n\n // `bindOutput` attaches a side-channel correlation token (string) to a\n // running item without changing its status. The queue can't reach into\n // `I` to mutate it (the substrate is domain-agnostic), so callers that\n // need to thread a backend id / URL / trace id through the lifecycle\n // route it here. Consumers needing structured payloads should persist\n // them in their own store and put the foreign-key string in `output`.\n async function bindOutput(id: string, value: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n output: value,\n }));\n if (changed) await store.save(next);\n }\n\n return {\n enqueue,\n claimNext,\n markDone,\n markFailed,\n markCancelled,\n resetRunningToPending,\n removeItem,\n retryItem,\n bindOutput,\n list,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/work-queue/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/work-queue — substrate-agnostic work-queue lifecycle on `Store<S>`.\n *\n * Two public surfaces, one shape:\n * - `createQueue(store)` (this file) is the in-process adapter — it binds the\n * pure ops to an injected `Store` and runs `load → mutate → save`.\n * - `@demlik/tea/work-queue/ops` (`./ops`) is the BLESSED, importable surface\n * of those same pure ops. L2 compositions that own their own Model slice\n * (`monitored-run`, `idempotent-intake`, …) import the ops directly and\n * delegate status flips to them instead of re-rolling the transitions.\n * Both routes call the identical ops, so the lifecycle rules live in exactly\n * one place. This mirrors `@demlik/tea/do`'s one-shape-per-package rule.\n *\n * The package is generic in the input payload `I` (audit-specific fields go\n * here in apps/extension). `output` is a free-form side-channel correlation\n * token (a backend id, URL, trace id) — typed as `string` because that's its\n * only honest type. Consumers that need structured per-item data persist it in\n * their own store and put the foreign-key string in `output`.\n */\n\nimport type { Store } from \"../index\";\nimport {\n claimNextOp,\n enqueueOp,\n markDoneOp,\n patchItemOp,\n removeOp,\n resetRunningOp,\n} from \"./ops\";\n\n/**\n * Generate a queue-item id at the adapter boundary. Reads `Date.now()` and\n * `Math.random()` — impure helpers belong in the adapter (`index.ts`),\n * never in `ops.ts` (which is purely-functional by contract). Uses native\n * UUID when available, falls back to timestamp + random for environments\n * without `crypto.randomUUID`.\n */\nfunction genId(): string {\n return (\n globalThis.crypto?.randomUUID?.() ??\n `q_${Date.now()}_${Math.random().toString(36).slice(2)}`\n );\n}\n\nexport type QueueItemStatus =\n | \"pending\"\n | \"running\"\n | \"done\"\n | \"failed\"\n | \"cancelled\";\n\n/**\n * A single queue entry. Every field is `readonly`: a persisted slice is\n * immutable, so a transition returns a NEW item (the ops always spread —\n * `{ ...item, status: … }`) rather than mutating in place. This type-enforces\n * the durability invariant the consumers already honor by convention, and\n * keeps a slice round-trippable through JSON unchanged across a reload.\n */\nexport interface QueueItem<I> {\n readonly id: string;\n readonly status: QueueItemStatus;\n readonly input: I;\n readonly enqueuedAt: number;\n readonly startedAt?: number;\n readonly finishedAt?: number;\n readonly output?: string;\n readonly error?: string;\n}\n\n/**\n * Caller-facing enqueue payload. Just `I` today — kept named so future\n * variants (priority, deferUntil, …) can grow without breaking callers.\n */\nexport type EnqueueInput<I> = I;\n\n/**\n * Adapter that binds the pure ops to an injected `Store<QueueItem<I>[]>`.\n *\n * `load → mutate → save` is the only durability discipline this package\n * imposes. The store decides what \"save\" means (chrome.storage, KV, DO\n * storage, in-memory) — that decision belongs at the caller.\n *\n * Ops that don't change the queue (e.g. `markDone` on an unknown id) skip the\n * save call so the underlying store doesn't observe a write churn.\n */\nexport function createQueue<I>(store: Store<QueueItem<I>[]>) {\n // `Store<S>.load()` returns `unknown` (invariant 8 boundary). Route\n // through `store.migrate(raw)` to get the typed value before any\n // queue operation reads it — same wiring the substrate's `run()` uses at\n // boot. The work queue is a substrate-agnostic primitive on top of\n // `Store<S>`, so it honors the same contract.\n async function loadParsed(): Promise<QueueItem<I>[] | null> {\n const raw = await store.load();\n return store.migrate(raw);\n }\n\n async function list(): Promise<QueueItem<I>[]> {\n return (await loadParsed()) ?? [];\n }\n\n async function enqueue(input: EnqueueInput<I>): Promise<QueueItem<I>> {\n const queue = await list();\n const { next, item } = enqueueOp<I>(queue, input, Date.now(), genId());\n await store.save(next);\n return item;\n }\n\n async function claimNext(): Promise<QueueItem<I> | null> {\n const queue = await list();\n const result = claimNextOp<I>(queue, Date.now());\n if (!result) return null;\n await store.save(result.next);\n return result.claimed;\n }\n\n // `markDone` ignores the optional second arg by design: completed runs leave\n // the queue, and the consumer is expected to persist any output (auditId,\n // etc.) wherever those completed records belong. Kept in the signature for\n // call-site readability and forward-compat with `bindOutput`.\n async function markDone(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = markDoneOp<I>(queue, id);\n if (changed) await store.save(next);\n }\n\n async function markFailed(id: string, error: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n status: \"failed\",\n finishedAt: Date.now(),\n error,\n }));\n if (changed) await store.save(next);\n }\n\n async function markCancelled(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n status: \"cancelled\",\n finishedAt: Date.now(),\n }));\n if (changed) await store.save(next);\n }\n\n async function resetRunningToPending(): Promise<void> {\n const queue = await list();\n const { next, changed } = resetRunningOp<I>(queue);\n if (changed) await store.save(next);\n }\n\n async function removeItem(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = removeOp<I>(queue, id);\n if (changed) await store.save(next);\n }\n\n async function retryItem(id: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n status: \"pending\",\n startedAt: undefined,\n finishedAt: undefined,\n error: undefined,\n output: undefined,\n }));\n if (changed) await store.save(next);\n }\n\n // `bindOutput` attaches a side-channel correlation token (string) to a\n // running item without changing its status. The queue can't reach into\n // `I` to mutate it (the substrate is domain-agnostic), so callers that\n // need to thread a backend id / URL / trace id through the lifecycle\n // route it here. Consumers needing structured payloads should persist\n // them in their own store and put the foreign-key string in `output`.\n async function bindOutput(id: string, value: string): Promise<void> {\n const queue = await list();\n const { next, changed } = patchItemOp<I>(queue, id, (item) => ({\n ...item,\n output: value,\n }));\n if (changed) await store.save(next);\n }\n\n return {\n enqueue,\n claimNext,\n markDone,\n markFailed,\n markCancelled,\n resetRunningToPending,\n removeItem,\n retryItem,\n bindOutput,\n list,\n };\n}\n"]}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { QueueItem } from './index.js';
|
|
2
|
+
import '../index.js';
|
|
3
|
+
import '../core-BoB6t7dB.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pure queue-lifecycle ops over `QueueItem<I>[]` — the BLESSED delegation
|
|
7
|
+
* surface for this package.
|
|
8
|
+
*
|
|
9
|
+
* Each op is `(queue, ...args) => { next, result? }`. No I/O, no Date.now
|
|
10
|
+
* indirection beyond a plain call site — the adapter in `./index.ts` injects
|
|
11
|
+
* `Store<QueueItem<I>[]>` and orchestrates `load → mutate → save`.
|
|
12
|
+
*
|
|
13
|
+
* Substrate-agnostic by construction: nothing here references chrome.storage,
|
|
14
|
+
* Cloudflare, or any concrete persistence layer. The audit-specific shape
|
|
15
|
+
* (journeyId, projectId, ...) is lifted into the caller's `I` parameter.
|
|
16
|
+
*
|
|
17
|
+
* Importable two ways. The `createQueue(...)` adapter in `./index.ts` binds
|
|
18
|
+
* these ops to a `Store` for in-process use (same one-shape-per-package rule
|
|
19
|
+
* as `@demlik/tea/do`). L2 compositions that own their own Model slice
|
|
20
|
+
* (`monitored-run`, `idempotent-intake`, …) instead reach these ops directly
|
|
21
|
+
* via the `@demlik/tea/work-queue/ops` subpath, so a status flip is delegated
|
|
22
|
+
* here rather than re-rolled per call site.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Append a new pending item. Returns the new queue and the created item. */
|
|
26
|
+
declare function enqueueOp<I>(queue: readonly QueueItem<I>[], input: I, now: number, id: string): {
|
|
27
|
+
next: QueueItem<I>[];
|
|
28
|
+
item: QueueItem<I>;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Claim the first pending item by flipping it to `running` and stamping
|
|
32
|
+
* `startedAt`. Returns the new queue and the claimed item, or `null` if no
|
|
33
|
+
* pending item exists.
|
|
34
|
+
*/
|
|
35
|
+
declare function claimNextOp<I>(queue: readonly QueueItem<I>[], now: number): {
|
|
36
|
+
next: QueueItem<I>[];
|
|
37
|
+
claimed: QueueItem<I>;
|
|
38
|
+
} | null;
|
|
39
|
+
/**
|
|
40
|
+
* Done items leave the queue — completed runs live wherever the caller
|
|
41
|
+
* persists their output. Returns the new queue (or the original reference if
|
|
42
|
+
* no item matched, so the adapter can skip a save).
|
|
43
|
+
*/
|
|
44
|
+
declare function markDoneOp<I>(queue: readonly QueueItem<I>[], id: string): {
|
|
45
|
+
next: QueueItem<I>[];
|
|
46
|
+
changed: boolean;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Generic single-item status transition. Used by `markFailed`,
|
|
50
|
+
* `markCancelled`, `retryItem`, `bindOutput`. The patch closure receives the
|
|
51
|
+
* matched item and returns its replacement; returning the same reference is
|
|
52
|
+
* fine (the adapter only saves on `changed`).
|
|
53
|
+
*/
|
|
54
|
+
declare function patchItemOp<I>(queue: readonly QueueItem<I>[], id: string, patch: (item: QueueItem<I>) => QueueItem<I>): {
|
|
55
|
+
next: QueueItem<I>[];
|
|
56
|
+
changed: boolean;
|
|
57
|
+
};
|
|
58
|
+
/** Reset every `running` item back to `pending`, clearing `startedAt`. */
|
|
59
|
+
declare function resetRunningOp<I>(queue: readonly QueueItem<I>[]): {
|
|
60
|
+
next: QueueItem<I>[];
|
|
61
|
+
changed: boolean;
|
|
62
|
+
};
|
|
63
|
+
/** Hard-delete an item regardless of status. */
|
|
64
|
+
declare function removeOp<I>(queue: readonly QueueItem<I>[], id: string): {
|
|
65
|
+
next: QueueItem<I>[];
|
|
66
|
+
changed: boolean;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export { claimNextOp, enqueueOp, markDoneOp, patchItemOp, removeOp, resetRunningOp };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"ops.js"}
|