@demlik/tea 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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-CMG2KMWX.js → chunk-2WZHS4P2.js} +220 -177
- package/dist/chunk-2WZHS4P2.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-5VNWZADP.js +49 -0
- package/dist/chunk-5VNWZADP.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-7VEG4KHB.js +202 -0
- package/dist/chunk-7VEG4KHB.js.map +1 -0
- package/dist/{chunk-PYBQREFU.js → chunk-A56GNPI7.js} +20 -6
- package/dist/chunk-A56GNPI7.js.map +1 -0
- package/dist/chunk-BNJYE4AU.js +157 -0
- package/dist/chunk-BNJYE4AU.js.map +1 -0
- package/dist/chunk-BYQUGDD3.js +16 -0
- package/dist/chunk-BYQUGDD3.js.map +1 -0
- package/dist/chunk-D5EQDLDV.js +186 -0
- package/dist/chunk-D5EQDLDV.js.map +1 -0
- package/dist/chunk-EDGDR5XX.js +60 -0
- package/dist/chunk-EDGDR5XX.js.map +1 -0
- package/dist/chunk-EIZJTYKR.js +93 -0
- package/dist/chunk-EIZJTYKR.js.map +1 -0
- package/dist/chunk-EQRUAP2A.js +56 -0
- package/dist/chunk-EQRUAP2A.js.map +1 -0
- package/dist/chunk-FH5RUIIL.js +61 -0
- package/dist/chunk-FH5RUIIL.js.map +1 -0
- package/dist/chunk-HJO6VNQI.js +15 -0
- package/dist/chunk-HJO6VNQI.js.map +1 -0
- package/dist/chunk-IFBQJYGI.js +132 -0
- package/dist/chunk-IFBQJYGI.js.map +1 -0
- package/dist/chunk-IN33Q4VJ.js +52 -0
- package/dist/chunk-IN33Q4VJ.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-LEC4U6CX.js +91 -0
- package/dist/chunk-LEC4U6CX.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-QGZW4W3E.js +20 -0
- package/dist/chunk-QGZW4W3E.js.map +1 -0
- package/dist/{chunk-PYOCUBQJ.js → chunk-RGTPPF4U.js} +22 -15
- package/dist/chunk-RGTPPF4U.js.map +1 -0
- package/dist/chunk-RVJFGRGK.js +222 -0
- package/dist/chunk-RVJFGRGK.js.map +1 -0
- package/dist/chunk-RVU3RBE6.js +31 -0
- package/dist/chunk-RVU3RBE6.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-TAO4YOLO.js +87 -0
- package/dist/chunk-TAO4YOLO.js.map +1 -0
- package/dist/chunk-TC4HWABA.js +506 -0
- package/dist/chunk-TC4HWABA.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-WUV3PSCZ.js +54 -0
- package/dist/chunk-WUV3PSCZ.js.map +1 -0
- package/dist/chunk-XRXKH7KA.js +268 -0
- package/dist/chunk-XRXKH7KA.js.map +1 -0
- package/dist/circuit-breaker/index.js +1 -51
- package/dist/circuit-breaker/index.js.map +1 -1
- package/dist/core-CymXZCeY.d.ts +178 -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.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 +1732 -92
- package/dist/do/index.js +677 -31
- package/dist/do/index.js.map +1 -1
- package/dist/durable-effects-2MDxDs45.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 +1 -1
- package/dist/machine-viz/index.js +1 -1
- 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 +86 -0
- package/dist/parity/index.js +77 -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 +1 -1
- package/dist/pbt/index.js +5 -4
- 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 +109 -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 +39 -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 +251 -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/saga/index.d.ts +225 -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.js +1 -52
- package/dist/throttle/index.js.map +1 -1
- package/dist/throttled-input/index.d.ts +372 -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-N9LZbdnr.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +196 -0
- package/dist/with-deadline/index.js +130 -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 +274 -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 +95 -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 +444 -0
- package/dist/workflow/index.js +225 -0
- package/dist/workflow/index.js.map +1 -0
- package/package.json +162 -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,259 @@
|
|
|
1
|
+
import { C as Cmd } from '../core-CymXZCeY.js';
|
|
2
|
+
export { Debounced, debounce } from '../debounce/index.js';
|
|
3
|
+
import { DeadlineSub, DeadlineExceeded } from '../deadline/index.js';
|
|
4
|
+
import { S as SubscribeHandler } from '../types-N9LZbdnr.js';
|
|
5
|
+
export { EnqueueInput } from '../work-queue/index.js';
|
|
6
|
+
import '../index.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @demlik/tea/batch-window — coalesce a stream of items into size- or
|
|
10
|
+
* time-bounded BATCHES, then flush each batch as a single Cmd.
|
|
11
|
+
*
|
|
12
|
+
* What this module actually DELEGATES to, vs. what it merely RE-EXPORTS:
|
|
13
|
+
*
|
|
14
|
+
* - `../deadline` — REAL delegation (the only one). The window timer IS a
|
|
15
|
+
* `deadline` Sub: `subsFor` returns `deadlineSub` keyed on the window's
|
|
16
|
+
* identity, the subscribe cell is `subscribeDeadline` verbatim, and the
|
|
17
|
+
* expiry Msg is `deadlineExceeded`. Nothing about the timer is redrawn here.
|
|
18
|
+
* "Fire at an absolute instant, even across a rehydrate" is exactly
|
|
19
|
+
* `deadline`'s absolute-target lifecycle: a late subscribe (post-`Store`
|
|
20
|
+
* rehydrate) recomputes the remaining delay from the current clock and still
|
|
21
|
+
* fires at the correct moment.
|
|
22
|
+
* - `../work-queue` — TYPE re-export only, NOT a sink this module calls. A
|
|
23
|
+
* flush emits a Cmd; the CONSUMER's interpret handler is what enqueues onto a
|
|
24
|
+
* `createQueue(store)`. This module never touches queue status — it has no
|
|
25
|
+
* queue state and no status flip to delegate. It re-exports `EnqueueInput<I>`
|
|
26
|
+
* purely so the consumer's flush Cmd and queue agree on the item shape; any
|
|
27
|
+
* actual status flip belongs in the consumer and is delegated to
|
|
28
|
+
* `../work-queue/ops` there, not here.
|
|
29
|
+
* - `../debounce` — VALUE/TYPE re-export only, NOT wired into `update`. A
|
|
30
|
+
* bursty source (keystrokes, scroll, websocket frames) can be `debounce`d at
|
|
31
|
+
* the host BEFORE it ever becomes an `add` Msg, so the batch window only sees
|
|
32
|
+
* settled items. `debounce` is a sink transformer OUTSIDE `update`;
|
|
33
|
+
* batch-window is the durable, replayable half inside it. Re-exported for
|
|
34
|
+
* one-import ergonomics — this module does not call it.
|
|
35
|
+
*
|
|
36
|
+
* Why this is a knob and not hand-wired logic: the managed state (the buffer +
|
|
37
|
+
* the window-open instant) lives in the Model, so it survives DO eviction /
|
|
38
|
+
* reload (durable) and every transition is still a Msg through `update`
|
|
39
|
+
* (replayable). A closure holding a `setTimeout` and a mutable array would break
|
|
40
|
+
* both — banned. `add` / `onWindow` are PURE: time arrives as an `at`
|
|
41
|
+
* parameter, never `Date.now()`; they return NEW slice values, never mutate.
|
|
42
|
+
*
|
|
43
|
+
* Typical wiring (combinator form):
|
|
44
|
+
*
|
|
45
|
+
* const bw = createBatchWindow<LogLine, Msg>({
|
|
46
|
+
* maxItems: 100,
|
|
47
|
+
* maxMs: 5_000,
|
|
48
|
+
* flush: (items) => ({ type: "FlushBatch", items }),
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* // Model: { batch: BatchWindow<LogLine>, ... }
|
|
52
|
+
* // init: (loaded) => [loaded ?? { batch: bw.init(), ... }, []]
|
|
53
|
+
* // update cells:
|
|
54
|
+
* // LogArrived: (s, m) => mapBatch(s, bw.add(s.batch, m.line, m.at))
|
|
55
|
+
* // WindowFired: (s, m) => mapBatch(s, bw.onWindow(s.batch, m.atMs))
|
|
56
|
+
* // subscriptions: (s) => bw.subs(s.batch)
|
|
57
|
+
* // subscribe: { ...bw.handlers() }
|
|
58
|
+
*
|
|
59
|
+
* where `mapBatch(s, [batch, cmds]) => [{ ...s, batch }, cmds]` lifts the slice
|
|
60
|
+
* transition into the consumer's Model. The `FlushBatch` Cmd's interpret handler
|
|
61
|
+
* is the consumer's — it is where the batch meets I/O (an `enqueue`, a network
|
|
62
|
+
* POST, an R2 write).
|
|
63
|
+
*
|
|
64
|
+
* NOT exported from the package root — reached via the `@demlik/tea/batch-window`
|
|
65
|
+
* subpath, same one-shape-per-package rule as `deadline`, `cache`, and
|
|
66
|
+
* `work-queue`.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Configuration for a batch window — the knob. Pure config, no mutable state
|
|
71
|
+
* (the `BatchWindow<I>` slice carries that). One config object is shared across
|
|
72
|
+
* every window of one logical stream (often a module-scope const).
|
|
73
|
+
*
|
|
74
|
+
* `flush` is a pure Cmd CONSTRUCTOR, not a closure-with-state: it maps the
|
|
75
|
+
* buffered items to the single Cmd the runtime performs when a window closes.
|
|
76
|
+
* Keeping it a `(items) => Cmd` (not a `(items) => void`) is what keeps the
|
|
77
|
+
* trigger replayable — the flush is a Cmd in the transition's output, never a
|
|
78
|
+
* side effect inside the verb.
|
|
79
|
+
*/
|
|
80
|
+
interface BatchWindowConfig<I, C extends Cmd> {
|
|
81
|
+
/**
|
|
82
|
+
* Size trigger. A batch flushes the instant its buffer reaches `maxItems`,
|
|
83
|
+
* WITHOUT waiting for the time window — the classic "send as soon as the
|
|
84
|
+
* page is full" cutoff. Must be `>= 1`; a config of `maxItems: 0` would flush
|
|
85
|
+
* an empty batch on every `add`, which is never useful, so callers treat `1`
|
|
86
|
+
* as the floor (the verb still behaves: with `maxItems <= 1` every item
|
|
87
|
+
* flushes on arrival, a degenerate but legal "no batching" window).
|
|
88
|
+
*/
|
|
89
|
+
readonly maxItems: number;
|
|
90
|
+
/**
|
|
91
|
+
* Time trigger, in milliseconds. A batch flushes no later than `openedAt +
|
|
92
|
+
* maxMs` — `openedAt` being the `at` of the item that opened the window. This
|
|
93
|
+
* is the latency ceiling: an item never waits longer than `maxMs` to ship,
|
|
94
|
+
* even if the buffer never reaches `maxItems`.
|
|
95
|
+
*/
|
|
96
|
+
readonly maxMs: number;
|
|
97
|
+
/**
|
|
98
|
+
* Map a flushed batch to the single Cmd the runtime performs. Called with the
|
|
99
|
+
* buffered items in arrival order; the returned Cmd is what `add` / `onWindow`
|
|
100
|
+
* emit when a window closes. PURE constructor — no I/O, no clock; the I/O
|
|
101
|
+
* happens later in the consumer's interpret handler for the returned Cmd.
|
|
102
|
+
*/
|
|
103
|
+
readonly flush: (items: readonly I[]) => C;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The Model slice a batch window owns. `buffer` is the items accumulated for the
|
|
107
|
+
* current open window; `openedAt` is the `at` of the item that OPENED it (the
|
|
108
|
+
* anchor the time trigger measures against), or `null` when the window is
|
|
109
|
+
* closed (buffer empty). The pair is the single source of truth for "is a
|
|
110
|
+
* window open, and when must it flush by?" — both plain data so the whole slice
|
|
111
|
+
* is trivially JSON-serializable into a consumer's `Store<S>`.
|
|
112
|
+
*
|
|
113
|
+
* Invariant the verbs preserve: `buffer.length === 0` IFF `openedAt === null`.
|
|
114
|
+
* A non-empty buffer always has an `openedAt`; an empty buffer never does. The
|
|
115
|
+
* window timer Sub keys its arm/disarm off exactly this invariant.
|
|
116
|
+
*/
|
|
117
|
+
interface BatchWindow<I> {
|
|
118
|
+
readonly buffer: readonly I[];
|
|
119
|
+
readonly openedAt: number | null;
|
|
120
|
+
}
|
|
121
|
+
/** The starting slice: closed window, empty buffer. */
|
|
122
|
+
declare function initBatchWindow<I>(): BatchWindow<I>;
|
|
123
|
+
/**
|
|
124
|
+
* The Sub a batch window's open timer produces: it IS `../deadline`'s
|
|
125
|
+
* `DeadlineSub`, not a re-tagged copy. `atMs` is the absolute flush-by instant
|
|
126
|
+
* (`openedAt + maxMs`). Aliasing the deadline type (rather than redeclaring an
|
|
127
|
+
* identical shape) is what lets `subsFor` return `deadlineSub(...)` with no
|
|
128
|
+
* cast — the value's static type already satisfies this name. Consumers running
|
|
129
|
+
* several windows route each one by the Sub's `id`, not by a distinct type.
|
|
130
|
+
*/
|
|
131
|
+
type BatchWindowSub = DeadlineSub;
|
|
132
|
+
/** The Msg a closed-by-time window dispatches. Alias of `deadline`'s Msg. */
|
|
133
|
+
type BatchWindowExpired = DeadlineExceeded;
|
|
134
|
+
/**
|
|
135
|
+
* Construct the window-expired Msg for the window identified by `id`, flushing
|
|
136
|
+
* by `atMs`. Re-exported alias of `deadlineExceeded` so the subscribe cell and
|
|
137
|
+
* the consumer's reducer share one constructor rather than two literals that
|
|
138
|
+
* can drift. The consumer handles it in a reducer cell by calling `onWindow`.
|
|
139
|
+
*/
|
|
140
|
+
declare function batchWindowExpired(id: string, atMs: number): BatchWindowExpired;
|
|
141
|
+
/**
|
|
142
|
+
* The bound knob returned by `createBatchWindow`. `init` + the two pure verbs +
|
|
143
|
+
* `subs` + `handlers`, all closing over one `BatchWindowConfig`. The verbs
|
|
144
|
+
* return `readonly [BatchWindow<I>, readonly C[]]`: the next slice and the Cmds
|
|
145
|
+
* to emit (one `flush` Cmd when a window closes, none otherwise).
|
|
146
|
+
*/
|
|
147
|
+
interface BatchWindowKnob<I, C extends Cmd> {
|
|
148
|
+
/** The starting slice — `initBatchWindow()`. */
|
|
149
|
+
init(): BatchWindow<I>;
|
|
150
|
+
/**
|
|
151
|
+
* Buffer `item` (arrival-stamped `at`), flushing immediately if that brings
|
|
152
|
+
* the buffer to `maxItems`. See `addItem`.
|
|
153
|
+
*/
|
|
154
|
+
add(state: BatchWindow<I>, item: I, at: number): readonly [BatchWindow<I>, readonly C[]];
|
|
155
|
+
/**
|
|
156
|
+
* Flush whatever is buffered because the time window closed. See `onWindow`.
|
|
157
|
+
*/
|
|
158
|
+
onWindow(state: BatchWindow<I>, at: number): readonly [BatchWindow<I>, readonly C[]];
|
|
159
|
+
/**
|
|
160
|
+
* The window timer Sub, armed only while a window is open. See `subsFor`.
|
|
161
|
+
* `id` keys the deadline so several windows on one machine route distinctly.
|
|
162
|
+
*/
|
|
163
|
+
subs(state: BatchWindow<I>, id?: string): readonly BatchWindowSub[];
|
|
164
|
+
/**
|
|
165
|
+
* The `subscribe` cells this knob splices into `machine.subscribe`. One cell,
|
|
166
|
+
* keyed `"deadline"`, delegating to `../deadline`'s subscribe handler. Spread
|
|
167
|
+
* into the consumer's subscribe map: `subscribe: { ...bw.handlers() }`.
|
|
168
|
+
*/
|
|
169
|
+
handlers(): {
|
|
170
|
+
deadline: SubscribeHandler<BatchWindowSub, BatchWindowExpired, unknown>;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Buffer `item` and decide whether the size trigger fires. PURE.
|
|
175
|
+
*
|
|
176
|
+
* - Empty (closed) window → `item` OPENS it: `openedAt` becomes `at`, buffer
|
|
177
|
+
* becomes `[item]`. This is the anchor the time window measures against.
|
|
178
|
+
* - Open window → `item` appends; `openedAt` is left untouched (the window's
|
|
179
|
+
* latency clock keeps running from the FIRST item, not this one).
|
|
180
|
+
*
|
|
181
|
+
* After buffering, if the buffer has reached `maxItems` the window flushes
|
|
182
|
+
* immediately: emit `config.flush(buffer)` and RESET to the closed slice. The
|
|
183
|
+
* size trigger pre-empts the time trigger — a full page ships now, the window
|
|
184
|
+
* Sub for the (now closed) window disarms on the next reconcile because `subs`
|
|
185
|
+
* returns `[]` for an empty buffer.
|
|
186
|
+
*
|
|
187
|
+
* `maxItems <= 1` is the degenerate "no batching" case: every item flushes on
|
|
188
|
+
* arrival (buffer reaches 1 immediately). Legal and useful as a feature flag to
|
|
189
|
+
* disable batching without restructuring the wiring.
|
|
190
|
+
*
|
|
191
|
+
* @param config the knob (maxItems / flush constructor)
|
|
192
|
+
* @param state the current slice
|
|
193
|
+
* @param item the item to buffer
|
|
194
|
+
* @param at the arrival instant (epoch ms) — the caller stamps it; the verb
|
|
195
|
+
* never reads the clock
|
|
196
|
+
*/
|
|
197
|
+
declare function addItem<I, C extends Cmd>(config: BatchWindowConfig<I, C>, state: BatchWindow<I>, item: I, at: number): readonly [BatchWindow<I>, readonly C[]];
|
|
198
|
+
/**
|
|
199
|
+
* Flush the open window because its time bound was reached. PURE.
|
|
200
|
+
*
|
|
201
|
+
* - Open window (buffer non-empty) → emit `config.flush(buffer)` and RESET to
|
|
202
|
+
* the closed slice.
|
|
203
|
+
* - Closed window (buffer empty) → no-op: return the same slice and no Cmds.
|
|
204
|
+
* This guards the race where the size trigger already flushed and reset the
|
|
205
|
+
* window before the time Sub's Msg lands — a stale `onWindow` after a
|
|
206
|
+
* size-flush must NOT emit an empty batch.
|
|
207
|
+
*
|
|
208
|
+
* `at` is accepted for symmetry with `add` and so the consumer's reducer cell
|
|
209
|
+
* has a uniform `(state, at)` shape; the verb doesn't need it for the flush
|
|
210
|
+
* decision (the buffer's emptiness is the whole signal), but taking it keeps a
|
|
211
|
+
* future "flush only if the window is genuinely past `openedAt + maxMs`"
|
|
212
|
+
* tightening a non-breaking change.
|
|
213
|
+
*
|
|
214
|
+
* @param config the knob (flush constructor)
|
|
215
|
+
* @param state the current slice
|
|
216
|
+
* @param at the firing instant (epoch ms) — the caller stamps it; the verb
|
|
217
|
+
* never reads the clock
|
|
218
|
+
*/
|
|
219
|
+
declare function onWindow<I, C extends Cmd>(config: BatchWindowConfig<I, C>, state: BatchWindow<I>, _at: number): readonly [BatchWindow<I>, readonly C[]];
|
|
220
|
+
/**
|
|
221
|
+
* The window timer Sub, derived from the slice. PURE.
|
|
222
|
+
*
|
|
223
|
+
* Returns the single `deadline` Sub targeting `openedAt + maxMs` while a window
|
|
224
|
+
* is open (buffer non-empty), or `[]` while it is closed. The substrate's
|
|
225
|
+
* reconcile pass arms the timer when the array gains the Sub and disarms it
|
|
226
|
+
* (clearing the pending `setTimeout`) when the array loses it — so a size-flush
|
|
227
|
+
* that empties the buffer auto-cancels the now-irrelevant time trigger on the
|
|
228
|
+
* next transition.
|
|
229
|
+
*
|
|
230
|
+
* `id` defaults to `"batch-window"`; pass a distinct id per window when a
|
|
231
|
+
* machine runs several so the dispatched `BatchWindowExpired` Msg (and its
|
|
232
|
+
* reconcile identity) routes to the right one.
|
|
233
|
+
*
|
|
234
|
+
* @param config the knob (maxMs)
|
|
235
|
+
* @param state the current slice
|
|
236
|
+
* @param id stable identity for this window's timer (default
|
|
237
|
+
* `"batch-window"`)
|
|
238
|
+
*/
|
|
239
|
+
declare function subsFor<I, C extends Cmd>(config: BatchWindowConfig<I, C>, state: BatchWindow<I>, id?: string): readonly BatchWindowSub[];
|
|
240
|
+
/**
|
|
241
|
+
* The `subscribe["deadline"]` cell for a batch window's timer — the exact
|
|
242
|
+
* `../deadline` handler. Arms a one-shot timer for `max(0, atMs - now)` and
|
|
243
|
+
* dispatches `batchWindowExpired(...)`; returns a cleanup that clears it. The
|
|
244
|
+
* consumer assigns it via the knob's `handlers()`.
|
|
245
|
+
*/
|
|
246
|
+
declare const subscribeBatchWindow: SubscribeHandler<BatchWindowSub, BatchWindowExpired, unknown>;
|
|
247
|
+
/**
|
|
248
|
+
* Build a batch window knob from a config. Combinator form (the default L2
|
|
249
|
+
* ergonomic): hand it the config, splice the ~4 returned hooks into your
|
|
250
|
+
* machine. The pure verbs (`add` / `onWindow`) close over the config so the
|
|
251
|
+
* consumer's reducer cells stay a uniform `(state, ...args)` shape; `subs` /
|
|
252
|
+
* `handlers` pre-wire the window timer.
|
|
253
|
+
*
|
|
254
|
+
* @typeParam I the item type buffered into each batch
|
|
255
|
+
* @typeParam C the Cmd type `config.flush` returns
|
|
256
|
+
*/
|
|
257
|
+
declare function createBatchWindow<I, C extends Cmd>(config: BatchWindowConfig<I, C>): BatchWindowKnob<I, C>;
|
|
258
|
+
|
|
259
|
+
export { type BatchWindow, type BatchWindowConfig, type BatchWindowExpired, type BatchWindowKnob, type BatchWindowSub, addItem, batchWindowExpired, createBatchWindow, initBatchWindow, onWindow, subsFor, subscribeBatchWindow };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export { debounce } from '../chunk-EQRUAP2A.js';
|
|
2
|
+
import { subscribeDeadline, deadlineExceeded, deadlineSub } from '../chunk-QGZW4W3E.js';
|
|
3
|
+
import '../chunk-2YFLOCWX.js';
|
|
4
|
+
import '../chunk-2WZHS4P2.js';
|
|
5
|
+
import '../chunk-BNJYE4AU.js';
|
|
6
|
+
|
|
7
|
+
// src/batch-window/index.ts
|
|
8
|
+
function initBatchWindow() {
|
|
9
|
+
return { buffer: [], openedAt: null };
|
|
10
|
+
}
|
|
11
|
+
function batchWindowExpired(id, atMs) {
|
|
12
|
+
return deadlineExceeded(id, atMs);
|
|
13
|
+
}
|
|
14
|
+
function addItem(config, state, item, at) {
|
|
15
|
+
const buffer = [...state.buffer, item];
|
|
16
|
+
const openedAt = state.openedAt ?? at;
|
|
17
|
+
if (buffer.length >= Math.max(1, config.maxItems)) {
|
|
18
|
+
return [initBatchWindow(), [config.flush(buffer)]];
|
|
19
|
+
}
|
|
20
|
+
return [{ buffer, openedAt }, []];
|
|
21
|
+
}
|
|
22
|
+
function onWindow(config, state, _at) {
|
|
23
|
+
if (state.buffer.length === 0) return [state, []];
|
|
24
|
+
return [initBatchWindow(), [config.flush(state.buffer)]];
|
|
25
|
+
}
|
|
26
|
+
function subsFor(config, state, id = "batch-window") {
|
|
27
|
+
if (state.openedAt === null) return [];
|
|
28
|
+
return [deadlineSub(id, state.openedAt + config.maxMs)];
|
|
29
|
+
}
|
|
30
|
+
var subscribeBatchWindow = subscribeDeadline;
|
|
31
|
+
function createBatchWindow(config) {
|
|
32
|
+
return {
|
|
33
|
+
init: () => initBatchWindow(),
|
|
34
|
+
add: (state, item, at) => addItem(config, state, item, at),
|
|
35
|
+
onWindow: (state, at) => onWindow(config, state),
|
|
36
|
+
subs: (state, id) => subsFor(config, state, id),
|
|
37
|
+
handlers: () => ({ deadline: subscribeBatchWindow })
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { addItem, batchWindowExpired, createBatchWindow, initBatchWindow, onWindow, subsFor, subscribeBatchWindow };
|
|
42
|
+
//# sourceMappingURL=index.js.map
|
|
43
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/batch-window/index.ts"],"names":[],"mappings":";;;;;;;AA2IO,SAAS,eAAA,GAAqC;AACnD,EAAA,OAAO,EAAE,MAAA,EAAQ,EAAC,EAAG,UAAU,IAAA,EAAK;AACtC;AAqBO,SAAS,kBAAA,CACd,IACA,IAAA,EACoB;AACpB,EAAA,OAAO,gBAAA,CAAiB,IAAI,IAAI,CAAA;AAClC;AAkEO,SAAS,OAAA,CACd,MAAA,EACA,KAAA,EACA,IAAA,EACA,EAAA,EACyC;AACzC,EAAA,MAAM,MAAA,GAAS,CAAC,GAAG,KAAA,CAAM,QAAQ,IAAI,CAAA;AAGrC,EAAA,MAAM,QAAA,GAAW,MAAM,QAAA,IAAY,EAAA;AAInC,EAAA,IAAI,OAAO,MAAA,IAAU,IAAA,CAAK,IAAI,CAAA,EAAG,MAAA,CAAO,QAAQ,CAAA,EAAG;AACjD,IAAA,OAAO,CAAC,iBAAmB,EAAG,CAAC,OAAO,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA;AAAA,EACtD;AAEA,EAAA,OAAO,CAAC,EAAE,MAAA,EAAQ,QAAA,EAAS,EAAG,EAAE,CAAA;AAClC;AAuBO,SAAS,QAAA,CACd,MAAA,EACA,KAAA,EACA,GAAA,EACyC;AAGzC,EAAA,IAAI,KAAA,CAAM,OAAO,MAAA,KAAW,CAAA,SAAU,CAAC,KAAA,EAAO,EAAE,CAAA;AAChD,EAAA,OAAO,CAAC,iBAAmB,EAAG,CAAC,OAAO,KAAA,CAAM,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA;AAC5D;AAqBO,SAAS,OAAA,CACd,MAAA,EACA,KAAA,EACA,EAAA,GAAK,cAAA,EACsB;AAK3B,EAAA,IAAI,KAAA,CAAM,QAAA,KAAa,IAAA,EAAM,OAAO,EAAC;AACrC,EAAA,OAAO,CAAC,WAAA,CAAY,EAAA,EAAI,MAAM,QAAA,GAAW,MAAA,CAAO,KAAK,CAAC,CAAA;AACxD;AAQO,IAAM,oBAAA,GAIT;AAYG,SAAS,kBACd,MAAA,EACuB;AACvB,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,MAAM,eAAA,EAAmB;AAAA,IAC/B,GAAA,EAAK,CAAC,KAAA,EAAO,IAAA,EAAM,OAAO,OAAA,CAAQ,MAAA,EAAQ,KAAA,EAAO,IAAA,EAAM,EAAE,CAAA;AAAA,IACzD,UAAU,CAAC,KAAA,EAAO,OAAO,QAAA,CAAS,MAAA,EAAQ,KAAS,CAAA;AAAA,IACnD,MAAM,CAAC,KAAA,EAAO,OAAO,OAAA,CAAQ,MAAA,EAAQ,OAAO,EAAE,CAAA;AAAA,IAC9C,QAAA,EAAU,OAAO,EAAE,QAAA,EAAU,oBAAA,EAAqB;AAAA,GACpD;AACF","file":"index.js","sourcesContent":["/**\n * @demlik/tea/batch-window — coalesce a stream of items into size- or\n * time-bounded BATCHES, then flush each batch as a single Cmd.\n *\n * What this module actually DELEGATES to, vs. what it merely RE-EXPORTS:\n *\n * - `../deadline` — REAL delegation (the only one). The window timer IS a\n * `deadline` Sub: `subsFor` returns `deadlineSub` keyed on the window's\n * identity, the subscribe cell is `subscribeDeadline` verbatim, and the\n * expiry Msg is `deadlineExceeded`. Nothing about the timer is redrawn here.\n * \"Fire at an absolute instant, even across a rehydrate\" is exactly\n * `deadline`'s absolute-target lifecycle: a late subscribe (post-`Store`\n * rehydrate) recomputes the remaining delay from the current clock and still\n * fires at the correct moment.\n * - `../work-queue` — TYPE re-export only, NOT a sink this module calls. A\n * flush emits a Cmd; the CONSUMER's interpret handler is what enqueues onto a\n * `createQueue(store)`. This module never touches queue status — it has no\n * queue state and no status flip to delegate. It re-exports `EnqueueInput<I>`\n * purely so the consumer's flush Cmd and queue agree on the item shape; any\n * actual status flip belongs in the consumer and is delegated to\n * `../work-queue/ops` there, not here.\n * - `../debounce` — VALUE/TYPE re-export only, NOT wired into `update`. A\n * bursty source (keystrokes, scroll, websocket frames) can be `debounce`d at\n * the host BEFORE it ever becomes an `add` Msg, so the batch window only sees\n * settled items. `debounce` is a sink transformer OUTSIDE `update`;\n * batch-window is the durable, replayable half inside it. Re-exported for\n * one-import ergonomics — this module does not call it.\n *\n * Why this is a knob and not hand-wired logic: the managed state (the buffer +\n * the window-open instant) lives in the Model, so it survives DO eviction /\n * reload (durable) and every transition is still a Msg through `update`\n * (replayable). A closure holding a `setTimeout` and a mutable array would break\n * both — banned. `add` / `onWindow` are PURE: time arrives as an `at`\n * parameter, never `Date.now()`; they return NEW slice values, never mutate.\n *\n * Typical wiring (combinator form):\n *\n * const bw = createBatchWindow<LogLine, Msg>({\n * maxItems: 100,\n * maxMs: 5_000,\n * flush: (items) => ({ type: \"FlushBatch\", items }),\n * });\n *\n * // Model: { batch: BatchWindow<LogLine>, ... }\n * // init: (loaded) => [loaded ?? { batch: bw.init(), ... }, []]\n * // update cells:\n * // LogArrived: (s, m) => mapBatch(s, bw.add(s.batch, m.line, m.at))\n * // WindowFired: (s, m) => mapBatch(s, bw.onWindow(s.batch, m.atMs))\n * // subscriptions: (s) => bw.subs(s.batch)\n * // subscribe: { ...bw.handlers() }\n *\n * where `mapBatch(s, [batch, cmds]) => [{ ...s, batch }, cmds]` lifts the slice\n * transition into the consumer's Model. The `FlushBatch` Cmd's interpret handler\n * is the consumer's — it is where the batch meets I/O (an `enqueue`, a network\n * POST, an R2 write).\n *\n * NOT exported from the package root — reached via the `@demlik/tea/batch-window`\n * subpath, same one-shape-per-package rule as `deadline`, `cache`, and\n * `work-queue`.\n */\n\nimport type { Cmd } from \"../index\";\n\n// `../debounce` re-export (value + type) for one-import ergonomics — see file\n// header. This module does NOT call debounce; a consumer that wants to debounce\n// a bursty source BEFORE it becomes an `add` Msg reaches it from the same import\n// as the batch window.\nexport { type Debounced, debounce } from \"../debounce\";\n\nimport {\n type DeadlineExceeded,\n type DeadlineSub,\n deadlineExceeded,\n deadlineSub,\n subscribeDeadline,\n} from \"../deadline\";\nimport type { SubscribeHandler } from \"../subs/types\";\n\n// `../work-queue` TYPE re-export only (see file header). This module emits a\n// flush Cmd; the CONSUMER's interpret handler enqueues onto a\n// `createQueue(store)`. Re-export the caller-facing enqueue payload type so the\n// consumer's flush Cmd and queue agree on the item shape.\nexport type { EnqueueInput } from \"../work-queue\";\n\n/**\n * Configuration for a batch window — the knob. Pure config, no mutable state\n * (the `BatchWindow<I>` slice carries that). One config object is shared across\n * every window of one logical stream (often a module-scope const).\n *\n * `flush` is a pure Cmd CONSTRUCTOR, not a closure-with-state: it maps the\n * buffered items to the single Cmd the runtime performs when a window closes.\n * Keeping it a `(items) => Cmd` (not a `(items) => void`) is what keeps the\n * trigger replayable — the flush is a Cmd in the transition's output, never a\n * side effect inside the verb.\n */\nexport interface BatchWindowConfig<I, C extends Cmd> {\n /**\n * Size trigger. A batch flushes the instant its buffer reaches `maxItems`,\n * WITHOUT waiting for the time window — the classic \"send as soon as the\n * page is full\" cutoff. Must be `>= 1`; a config of `maxItems: 0` would flush\n * an empty batch on every `add`, which is never useful, so callers treat `1`\n * as the floor (the verb still behaves: with `maxItems <= 1` every item\n * flushes on arrival, a degenerate but legal \"no batching\" window).\n */\n readonly maxItems: number;\n /**\n * Time trigger, in milliseconds. A batch flushes no later than `openedAt +\n * maxMs` — `openedAt` being the `at` of the item that opened the window. This\n * is the latency ceiling: an item never waits longer than `maxMs` to ship,\n * even if the buffer never reaches `maxItems`.\n */\n readonly maxMs: number;\n /**\n * Map a flushed batch to the single Cmd the runtime performs. Called with the\n * buffered items in arrival order; the returned Cmd is what `add` / `onWindow`\n * emit when a window closes. PURE constructor — no I/O, no clock; the I/O\n * happens later in the consumer's interpret handler for the returned Cmd.\n */\n readonly flush: (items: readonly I[]) => C;\n}\n\n/**\n * The Model slice a batch window owns. `buffer` is the items accumulated for the\n * current open window; `openedAt` is the `at` of the item that OPENED it (the\n * anchor the time trigger measures against), or `null` when the window is\n * closed (buffer empty). The pair is the single source of truth for \"is a\n * window open, and when must it flush by?\" — both plain data so the whole slice\n * is trivially JSON-serializable into a consumer's `Store<S>`.\n *\n * Invariant the verbs preserve: `buffer.length === 0` IFF `openedAt === null`.\n * A non-empty buffer always has an `openedAt`; an empty buffer never does. The\n * window timer Sub keys its arm/disarm off exactly this invariant.\n */\nexport interface BatchWindow<I> {\n readonly buffer: readonly I[];\n readonly openedAt: number | null;\n}\n\n/** The starting slice: closed window, empty buffer. */\nexport function initBatchWindow<I>(): BatchWindow<I> {\n return { buffer: [], openedAt: null };\n}\n\n/**\n * The Sub a batch window's open timer produces: it IS `../deadline`'s\n * `DeadlineSub`, not a re-tagged copy. `atMs` is the absolute flush-by instant\n * (`openedAt + maxMs`). Aliasing the deadline type (rather than redeclaring an\n * identical shape) is what lets `subsFor` return `deadlineSub(...)` with no\n * cast — the value's static type already satisfies this name. Consumers running\n * several windows route each one by the Sub's `id`, not by a distinct type.\n */\nexport type BatchWindowSub = DeadlineSub;\n\n/** The Msg a closed-by-time window dispatches. Alias of `deadline`'s Msg. */\nexport type BatchWindowExpired = DeadlineExceeded;\n\n/**\n * Construct the window-expired Msg for the window identified by `id`, flushing\n * by `atMs`. Re-exported alias of `deadlineExceeded` so the subscribe cell and\n * the consumer's reducer share one constructor rather than two literals that\n * can drift. The consumer handles it in a reducer cell by calling `onWindow`.\n */\nexport function batchWindowExpired(\n id: string,\n atMs: number,\n): BatchWindowExpired {\n return deadlineExceeded(id, atMs);\n}\n\n/**\n * The bound knob returned by `createBatchWindow`. `init` + the two pure verbs +\n * `subs` + `handlers`, all closing over one `BatchWindowConfig`. The verbs\n * return `readonly [BatchWindow<I>, readonly C[]]`: the next slice and the Cmds\n * to emit (one `flush` Cmd when a window closes, none otherwise).\n */\nexport interface BatchWindowKnob<I, C extends Cmd> {\n /** The starting slice — `initBatchWindow()`. */\n init(): BatchWindow<I>;\n /**\n * Buffer `item` (arrival-stamped `at`), flushing immediately if that brings\n * the buffer to `maxItems`. See `addItem`.\n */\n add(\n state: BatchWindow<I>,\n item: I,\n at: number,\n ): readonly [BatchWindow<I>, readonly C[]];\n /**\n * Flush whatever is buffered because the time window closed. See `onWindow`.\n */\n onWindow(\n state: BatchWindow<I>,\n at: number,\n ): readonly [BatchWindow<I>, readonly C[]];\n /**\n * The window timer Sub, armed only while a window is open. See `subsFor`.\n * `id` keys the deadline so several windows on one machine route distinctly.\n */\n subs(state: BatchWindow<I>, id?: string): readonly BatchWindowSub[];\n /**\n * The `subscribe` cells this knob splices into `machine.subscribe`. One cell,\n * keyed `\"deadline\"`, delegating to `../deadline`'s subscribe handler. Spread\n * into the consumer's subscribe map: `subscribe: { ...bw.handlers() }`.\n */\n handlers(): {\n deadline: SubscribeHandler<BatchWindowSub, BatchWindowExpired, unknown>;\n };\n}\n\n/**\n * Buffer `item` and decide whether the size trigger fires. PURE.\n *\n * - Empty (closed) window → `item` OPENS it: `openedAt` becomes `at`, buffer\n * becomes `[item]`. This is the anchor the time window measures against.\n * - Open window → `item` appends; `openedAt` is left untouched (the window's\n * latency clock keeps running from the FIRST item, not this one).\n *\n * After buffering, if the buffer has reached `maxItems` the window flushes\n * immediately: emit `config.flush(buffer)` and RESET to the closed slice. The\n * size trigger pre-empts the time trigger — a full page ships now, the window\n * Sub for the (now closed) window disarms on the next reconcile because `subs`\n * returns `[]` for an empty buffer.\n *\n * `maxItems <= 1` is the degenerate \"no batching\" case: every item flushes on\n * arrival (buffer reaches 1 immediately). Legal and useful as a feature flag to\n * disable batching without restructuring the wiring.\n *\n * @param config the knob (maxItems / flush constructor)\n * @param state the current slice\n * @param item the item to buffer\n * @param at the arrival instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function addItem<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n item: I,\n at: number,\n): readonly [BatchWindow<I>, readonly C[]] {\n const buffer = [...state.buffer, item];\n // A closed window opens at THIS item's `at`; an already-open window keeps its\n // original `openedAt` (the latency clock runs from the first item).\n const openedAt = state.openedAt ?? at;\n\n // Size trigger: a full page ships now, regardless of the time window. The\n // floor of 1 means `maxItems <= 1` flushes every item on arrival.\n if (buffer.length >= Math.max(1, config.maxItems)) {\n return [initBatchWindow<I>(), [config.flush(buffer)]];\n }\n\n return [{ buffer, openedAt }, []];\n}\n\n/**\n * Flush the open window because its time bound was reached. PURE.\n *\n * - Open window (buffer non-empty) → emit `config.flush(buffer)` and RESET to\n * the closed slice.\n * - Closed window (buffer empty) → no-op: return the same slice and no Cmds.\n * This guards the race where the size trigger already flushed and reset the\n * window before the time Sub's Msg lands — a stale `onWindow` after a\n * size-flush must NOT emit an empty batch.\n *\n * `at` is accepted for symmetry with `add` and so the consumer's reducer cell\n * has a uniform `(state, at)` shape; the verb doesn't need it for the flush\n * decision (the buffer's emptiness is the whole signal), but taking it keeps a\n * future \"flush only if the window is genuinely past `openedAt + maxMs`\"\n * tightening a non-breaking change.\n *\n * @param config the knob (flush constructor)\n * @param state the current slice\n * @param at the firing instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function onWindow<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n _at: number,\n): readonly [BatchWindow<I>, readonly C[]] {\n // Empty buffer = the size trigger already flushed (or nothing arrived). A\n // stale timer Msg must not ship an empty batch — return the slice untouched.\n if (state.buffer.length === 0) return [state, []];\n return [initBatchWindow<I>(), [config.flush(state.buffer)]];\n}\n\n/**\n * The window timer Sub, derived from the slice. PURE.\n *\n * Returns the single `deadline` Sub targeting `openedAt + maxMs` while a window\n * is open (buffer non-empty), or `[]` while it is closed. The substrate's\n * reconcile pass arms the timer when the array gains the Sub and disarms it\n * (clearing the pending `setTimeout`) when the array loses it — so a size-flush\n * that empties the buffer auto-cancels the now-irrelevant time trigger on the\n * next transition.\n *\n * `id` defaults to `\"batch-window\"`; pass a distinct id per window when a\n * machine runs several so the dispatched `BatchWindowExpired` Msg (and its\n * reconcile identity) routes to the right one.\n *\n * @param config the knob (maxMs)\n * @param state the current slice\n * @param id stable identity for this window's timer (default\n * `\"batch-window\"`)\n */\nexport function subsFor<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n id = \"batch-window\",\n): readonly BatchWindowSub[] {\n // Closed window → no timer. The invariant (empty buffer IFF openedAt null)\n // means either check suffices; we read `openedAt` so the `atMs` arithmetic\n // below is non-null without a `!`. `deadlineSub` already returns a\n // `DeadlineSub`, which IS `BatchWindowSub` — no cast.\n if (state.openedAt === null) return [];\n return [deadlineSub(id, state.openedAt + config.maxMs)];\n}\n\n/**\n * The `subscribe[\"deadline\"]` cell for a batch window's timer — the exact\n * `../deadline` handler. Arms a one-shot timer for `max(0, atMs - now)` and\n * dispatches `batchWindowExpired(...)`; returns a cleanup that clears it. The\n * consumer assigns it via the knob's `handlers()`.\n */\nexport const subscribeBatchWindow: SubscribeHandler<\n BatchWindowSub,\n BatchWindowExpired,\n unknown\n> = subscribeDeadline;\n\n/**\n * Build a batch window knob from a config. Combinator form (the default L2\n * ergonomic): hand it the config, splice the ~4 returned hooks into your\n * machine. The pure verbs (`add` / `onWindow`) close over the config so the\n * consumer's reducer cells stay a uniform `(state, ...args)` shape; `subs` /\n * `handlers` pre-wire the window timer.\n *\n * @typeParam I the item type buffered into each batch\n * @typeParam C the Cmd type `config.flush` returns\n */\nexport function createBatchWindow<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n): BatchWindowKnob<I, C> {\n return {\n init: () => initBatchWindow<I>(),\n add: (state, item, at) => addItem(config, state, item, at),\n onWindow: (state, at) => onWindow(config, state, at),\n subs: (state, id) => subsFor(config, state, id),\n handlers: () => ({ deadline: subscribeBatchWindow }),\n };\n}\n"]}
|
package/dist/cache/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Sub } from '../
|
|
2
|
-
import { S as SubscribeHandler } from '../types-
|
|
1
|
+
import { S as Sub } from '../core-CymXZCeY.js';
|
|
2
|
+
import { S as SubscribeHandler } from '../types-N9LZbdnr.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @demlik/tea/cache — a TTL cache as pure state, PLUS a periodic eviction Sub.
|
package/dist/cache/index.js
CHANGED
|
@@ -1,53 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
export { cacheEvictMsg, cacheEvictionSub, cacheEvictionSubscribe, evictExpired, get, has, initCache, remove, set } from '../chunk-IN33Q4VJ.js';
|
|
2
|
+
import '../chunk-NSDFA6M6.js';
|
|
2
3
|
import '../chunk-2YFLOCWX.js';
|
|
3
|
-
|
|
4
|
-
// src/cache/index.ts
|
|
5
|
-
function initCache() {
|
|
6
|
-
return { entries: {} };
|
|
7
|
-
}
|
|
8
|
-
function isExpired(entry, nowMs) {
|
|
9
|
-
return nowMs >= entry.expiresAtMs;
|
|
10
|
-
}
|
|
11
|
-
function get(cache, key, nowMs) {
|
|
12
|
-
const entry = cache.entries[key];
|
|
13
|
-
if (entry === void 0 || isExpired(entry, nowMs)) return void 0;
|
|
14
|
-
return entry.value;
|
|
15
|
-
}
|
|
16
|
-
function has(cache, key, nowMs) {
|
|
17
|
-
const entry = cache.entries[key];
|
|
18
|
-
if (entry === void 0) return false;
|
|
19
|
-
return !isExpired(entry, nowMs);
|
|
20
|
-
}
|
|
21
|
-
function set(cache, key, value, nowMs, ttlMs) {
|
|
22
|
-
const nextEntries = { ...cache.entries };
|
|
23
|
-
nextEntries[key] = { value, expiresAtMs: nowMs + ttlMs };
|
|
24
|
-
return { ...cache, entries: nextEntries };
|
|
25
|
-
}
|
|
26
|
-
function remove(cache, key) {
|
|
27
|
-
const nextEntries = { ...cache.entries };
|
|
28
|
-
delete nextEntries[key];
|
|
29
|
-
return { ...cache, entries: nextEntries };
|
|
30
|
-
}
|
|
31
|
-
function evictExpired(cache, nowMs) {
|
|
32
|
-
const nextEntries = {};
|
|
33
|
-
for (const [key, entry] of Object.entries(cache.entries)) {
|
|
34
|
-
if (isExpired(entry, nowMs)) continue;
|
|
35
|
-
nextEntries[key] = entry;
|
|
36
|
-
}
|
|
37
|
-
return { ...cache, entries: nextEntries };
|
|
38
|
-
}
|
|
39
|
-
function cacheEvictMsg(id) {
|
|
40
|
-
return { type: "cache_evict", id };
|
|
41
|
-
}
|
|
42
|
-
function cacheEvictionSub(id, everyMs) {
|
|
43
|
-
return {
|
|
44
|
-
id,
|
|
45
|
-
type: "cache",
|
|
46
|
-
intervalMs: everyMs
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
var cacheEvictionSubscribe = fromInterval((sub) => cacheEvictMsg(sub.id));
|
|
50
|
-
|
|
51
|
-
export { cacheEvictMsg, cacheEvictionSub, cacheEvictionSubscribe, evictExpired, get, has, initCache, remove, set };
|
|
52
4
|
//# sourceMappingURL=index.js.map
|
|
53
5
|
//# sourceMappingURL=index.js.map
|
package/dist/cache/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cache/index.ts"],"names":[],"mappings":";;;;AAuEO,SAAS,SAAA,GAA4B;AAC1C,EAAA,OAAO,EAAE,OAAA,EAAS,EAAC,EAAE;AACvB;AAQA,SAAS,SAAA,CAAa,OAAsB,KAAA,EAAwB;AAClE,EAAA,OAAO,SAAS,KAAA,CAAM,WAAA;AACxB;AAQO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACe;AACf,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,UAAU,MAAA,IAAa,SAAA,CAAU,KAAA,EAAO,KAAK,GAAG,OAAO,MAAA;AAC3D,EAAA,OAAO,KAAA,CAAM,KAAA;AACf;AAMO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACS;AACT,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,KAAA,KAAU,QAAW,OAAO,KAAA;AAChC,EAAA,OAAO,CAAC,SAAA,CAAU,KAAA,EAAO,KAAK,CAAA;AAChC;AAQO,SAAS,GAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACA,OACA,KAAA,EACa;AACb,EAAA,MAAM,WAAA,GAA6C,EAAE,GAAG,KAAA,CAAM,OAAA,EAAQ;AACtE,EAAA,WAAA,CAAY,GAAG,CAAA,GAAI,EAAE,KAAA,EAAO,WAAA,EAAa,QAAQ,KAAA,EAAM;AACvD,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AAOO,SAAS,MAAA,CAAU,OAAoB,GAAA,EAA0B;AACtE,EAAA,MAAM,WAAA,GAA6C,EAAE,GAAG,KAAA,CAAM,OAAA,EAAQ;AACtE,EAAA,OAAO,YAAY,GAAG,CAAA;AACtB,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AASO,SAAS,YAAA,CACd,OACA,KAAA,EACa;AACb,EAAA,MAAM,cAA6C,EAAC;AACpD,EAAA,KAAA,MAAW,CAAC,KAAK,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,KAAA,CAAM,OAAO,CAAA,EAAG;AACxD,IAAA,IAAI,SAAA,CAAU,KAAA,EAAO,KAAK,CAAA,EAAG;AAC7B,IAAA,WAAA,CAAY,GAAG,CAAA,GAAI,KAAA;AAAA,EACrB;AACA,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAY;AAC1C;AAmCO,SAAS,cAAc,EAAA,EAA2B;AACvD,EAAA,OAAO,EAAE,IAAA,EAAM,aAAA,EAAe,EAAA,EAAG;AACnC;AAqBO,SAAS,gBAAA,CACd,IACA,OAAA,EACkB;AAClB,EAAA,OAAO;AAAA,IACL,EAAA;AAAA,IACA,IAAA,EAAM,OAAA;AAAA,IACN,UAAA,EAAY;AAAA,GACd;AACF;AAcO,IAAM,yBAIT,YAAA,CAAa,CAAC,QAAQ,aAAA,CAAc,GAAA,CAAI,EAAE,CAAC","file":"index.js","sourcesContent":["/**\n * @demlik/tea/cache — a TTL cache as pure state, PLUS a periodic eviction Sub.\n *\n * Same pure-state-ops shape as `@demlik/tea/idempotency` and\n * `@demlik/tea/work-queue`: a state type plus pure transition functions. Host-\n * agnostic by construction — no `Date.now()`, no timers, no I/O inside the ops.\n * The caller injects the clock (`nowMs`) and folds the result through its own\n * reducer, keeping the store inside TEA's invariant 2 (transitions are pure:\n * same inputs → same outputs) so a consumer can exercise it with `replay`\n * without a fake clock.\n *\n * Every op returns a NEW cache value and never mutates its input (invariant 1 —\n * state is a value, not a place). `get` / `has` are read-shaped; `set` /\n * `remove` / `evictExpired` return a new cache.\n *\n * **Distinct from `/idempotency`.** Both are TTL-bounded key maps, but they\n * answer different questions and so carry different shapes:\n *\n * - `/idempotency` is DEDUPE-focused — \"have I already processed this key?\"\n * TTL is a single store-level policy (`ttlMs`), each entry stores the\n * `atMs` it was remembered at, and expiry is derived (`now - atMs >=\n * ttlMs`). It carries an `order` ledger + `capacity` because the use case\n * (webhook receivers, retried messages) needs bounded LRU eviction.\n * - `/cache` is RETRIEVAL-focused — \"what is the value for this key right\n * now?\" TTL is set PER ENTRY at write time: `set(key, value, nowMs,\n * ttlMs)` bakes the absolute `expiresAtMs = nowMs + ttlMs` into the entry.\n * Different keys can carry different lifetimes (a hot key cached for 1s, a\n * cold one for 1h) — the dimension idempotency's single store-level\n * `ttlMs` cannot express. No `order`, no `capacity`: a cache bounds itself\n * by TTL + periodic eviction, not by LRU count.\n *\n * The eviction Sub is the \"eviction is a Msg\" half: a `setInterval`-shaped Sub\n * (composed directly on `@demlik/tea/subs`' `fromInterval`) that periodically\n * dispatches a `cache_evict` Msg the consumer's reducer handles by calling\n * `evictExpired`. Time still never enters the reducer — the Sub fires the Msg,\n * the reducer reads `nowMs` off the Msg (the consumer stamps it at the Sub\n * boundary), and `evictExpired` does the pure drop.\n *\n * NOT exported from the package root — reached via the `@demlik/tea/cache`\n * subpath, same one-shape-per-package rule as `work-queue`, `idempotency`, and\n * `rate-limit`.\n */\n\nimport type { Sub } from \"../index\";\nimport { fromInterval } from \"../subs\";\nimport type { SubscribeHandler } from \"../subs/types\";\n\n/**\n * A cached entry: the `value` and the absolute clock reading `expiresAtMs` it\n * expires at. Unlike `/idempotency`'s entry (which stores `atMs` and derives\n * expiry from a store-level `ttlMs`), the expiry instant is baked in at write\n * time — so per-entry TTLs are a first-class capability. Both fields are plain\n * data so the whole cache is trivially JSON-serializable into a consumer's\n * `Store<S>`.\n */\nexport interface CacheEntry<V> {\n readonly value: V;\n readonly expiresAtMs: number;\n}\n\n/**\n * The cache: a map of `key → entry`. No `order` ledger and no `capacity` (cf.\n * `/idempotency`) — a TTL cache bounds itself by expiry + periodic eviction,\n * not by LRU count. Each entry carries its own `expiresAtMs`, so the cache\n * needs no store-level TTL policy field.\n */\nexport interface TtlCache<V> {\n readonly entries: Readonly<Record<string, CacheEntry<V>>>;\n}\n\n/** Create an empty cache. */\nexport function initCache<V>(): TtlCache<V> {\n return { entries: {} };\n}\n\n/**\n * Whether `entry` is expired at `nowMs`. Half-open: an entry whose\n * `expiresAtMs` exactly equals `nowMs` is expired (`nowMs >= expiresAtMs`),\n * matching `/idempotency` and `/rate-limit`'s window-edge cutoff — a value\n * cached at `t` with a `ttlMs` lifetime is gone at exactly `t + ttlMs`.\n */\nfunction isExpired<V>(entry: CacheEntry<V>, nowMs: number): boolean {\n return nowMs >= entry.expiresAtMs;\n}\n\n/**\n * The cached `value` for `key` iff present AND unexpired at `nowMs`, else\n * `undefined`. An expired entry reads as absent even though it is physically\n * still in `entries` until the next `evictExpired` (or an overwriting `set`)\n * drops it. PURE — reads only, never mutates.\n */\nexport function get<V>(\n cache: TtlCache<V>,\n key: string,\n nowMs: number,\n): V | undefined {\n const entry = cache.entries[key];\n if (entry === undefined || isExpired(entry, nowMs)) return undefined;\n return entry.value;\n}\n\n/**\n * True iff `key` is present AND unexpired at `nowMs`. The boolean sibling of\n * `get` for callers that only need presence, not the value. PURE.\n */\nexport function has<V>(\n cache: TtlCache<V>,\n key: string,\n nowMs: number,\n): boolean {\n const entry = cache.entries[key];\n if (entry === undefined) return false;\n return !isExpired(entry, nowMs);\n}\n\n/**\n * Write `key → value` with an absolute expiry of `nowMs + ttlMs`. Re-`set`ting\n * an existing key overwrites both the value and the expiry — the TTL clock\n * restarts from the new `nowMs`. PURE — returns a new cache; the input is\n * untouched (a fresh shallow copy of `entries`).\n */\nexport function set<V>(\n cache: TtlCache<V>,\n key: string,\n value: V,\n nowMs: number,\n ttlMs: number,\n): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = { ...cache.entries };\n nextEntries[key] = { value, expiresAtMs: nowMs + ttlMs };\n return { ...cache, entries: nextEntries };\n}\n\n/**\n * Drop `key` regardless of expiry. PURE — returns a new cache; the input is\n * untouched. Removing an absent key returns an equivalent cache (the shallow\n * copy still happens, but no entry is dropped).\n */\nexport function remove<V>(cache: TtlCache<V>, key: string): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = { ...cache.entries };\n delete nextEntries[key];\n return { ...cache, entries: nextEntries };\n}\n\n/**\n * Physically drop every entry expired at `nowMs` (`nowMs >= expiresAtMs`).\n * Returns a NEW cache with the survivors. This is the pure work the eviction\n * Sub's `cache_evict` Msg triggers — `get` / `has` already treat expired\n * entries as absent, so eviction is purely a memory-reclaim pass, not a\n * correctness one. PURE — the input cache is never mutated.\n */\nexport function evictExpired<V>(\n cache: TtlCache<V>,\n nowMs: number,\n): TtlCache<V> {\n const nextEntries: Record<string, CacheEntry<V>> = {};\n for (const [key, entry] of Object.entries(cache.entries)) {\n if (isExpired(entry, nowMs)) continue;\n nextEntries[key] = entry;\n }\n return { ...cache, entries: nextEntries };\n}\n\n// ---------------------------------------------------------------------------\n// Eviction Sub — \"eviction is a Msg\".\n//\n// A periodic timer Sub that dispatches `cache_evict` so the consumer's reducer\n// can call `evictExpired`. It is `fromInterval` specialized to a fixed Msg\n// shape — the factory carries the interval ON the Sub (per the substrate's\n// reconcile-by-id contract) and dispatches a `cache_evict` Msg tagged with the\n// Sub's own `id`, so a consumer running several caches can route each tick to\n// the right cache by `msg.id`.\n//\n// Mirrors `fromInterval` exactly (factory + SubscribeHandler cell + exported\n// Msg constructor); it does NOT reimplement the timer lifecycle — it composes\n// directly on `fromInterval`, which owns `setInterval` / `clearInterval`. That\n// keeps the timer-handle bookkeeping (and its cleanup) in one place and this\n// module purely declarative.\n//\n// Strengthens invariant 4 (external time is a Sub, reconciled by id) and\n// invariant 9 (named, small Sub surface composed on the shared factory).\n// ---------------------------------------------------------------------------\n\n/**\n * The Msg the eviction Sub dispatches each tick. `id` is the Sub's own id so a\n * consumer with multiple caches can route the tick to the matching cache. The\n * reducer reads `Date.now()`-free time by stamping `nowMs` itself at the Sub\n * boundary if needed; the canonical reducer cell is\n * `cache_evict: (s) => [{ ...s, cache: evictExpired(s.cache, nowMs) }, []]`.\n */\nexport interface CacheEvictMsg {\n readonly type: \"cache_evict\";\n readonly id: string;\n}\n\n/** Construct a `cache_evict` Msg for the cache identified by `id`. */\nexport function cacheEvictMsg(id: string): CacheEvictMsg {\n return { type: \"cache_evict\", id };\n}\n\n/**\n * The Sub shape the eviction factory installs: a `setInterval`-shaped Sub\n * carrying its tick period (`intervalMs`) per the substrate's\n * reconcile-by-id-with-data-on-the-Sub contract. `type: \"cache\"` is the\n * lowercase source-noun Sub convention (canon §2.5 / naming-style).\n */\nexport type CacheEvictionSub = Sub<\"cache\"> & { readonly intervalMs: number };\n\n/**\n * Declare an eviction Sub for the cache identified by `id`, ticking every\n * `everyMs`. Drop the result into `subscriptions(state)`:\n *\n * subscriptions: (s) => [cacheEvictionSub(\"session-cache\", 30_000)]\n *\n * Same id ⇒ same running interval across transitions (no reconcile churn);\n * changing `everyMs` alone will NOT re-arm the timer (the substrate leaves a\n * same-id Sub running) — change the `id` too if the period must change. This\n * mirrors `fromInterval`'s data-on-the-Sub tradeoff exactly.\n */\nexport function cacheEvictionSub(\n id: string,\n everyMs: number,\n): CacheEvictionSub {\n return {\n id: id as CacheEvictionSub[\"id\"],\n type: \"cache\",\n intervalMs: everyMs,\n };\n}\n\n/**\n * The `subscribe` cell for the eviction Sub. Assign it to\n * `machine.subscribe.cache`:\n *\n * subscribe: { cache: cacheEvictionSubscribe }\n *\n * Composed directly on `fromInterval` — it owns the `setInterval` /\n * `clearInterval` lifecycle; this cell only pins the Msg the tick dispatches\n * (`cacheEvictMsg(sub.id)`). The cleanup `fromInterval` returns stops the\n * interval when the Sub is reconciled out (the cache's state exits the phase\n * that declared it).\n */\nexport const cacheEvictionSubscribe: SubscribeHandler<\n CacheEvictionSub,\n CacheEvictMsg,\n unknown\n> = fromInterval((sub) => cacheEvictMsg(sub.id));\n"]}
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|