@demlik/tea 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +49 -0
  2. package/dist/agent/index.d.ts +16 -770
  3. package/dist/agent/index.js +18 -16
  4. package/dist/authed-call/index.d.ts +3 -3
  5. package/dist/authed-call/index.js +22 -36
  6. package/dist/authed-call/index.js.map +1 -1
  7. package/dist/await-terminal/index.d.ts +2 -2
  8. package/dist/await-terminal/index.js +2 -2
  9. package/dist/batch-window/index.d.ts +3 -3
  10. package/dist/batch-window/index.js +3 -3
  11. package/dist/cache/index.d.ts +2 -2
  12. package/dist/cache/index.js +5 -2
  13. package/dist/{chunk-3545BOYL.js → chunk-25LBDCBC.js} +6 -5
  14. package/dist/chunk-25LBDCBC.js.map +1 -0
  15. package/dist/{chunk-IK4S5DJN.js → chunk-2AHAR7CX.js} +7 -14
  16. package/dist/chunk-2AHAR7CX.js.map +1 -0
  17. package/dist/{chunk-IN33Q4VJ.js → chunk-2KNLKQVN.js} +3 -3
  18. package/dist/{chunk-IN33Q4VJ.js.map → chunk-2KNLKQVN.js.map} +1 -1
  19. package/dist/{chunk-VFGKTGGC.js → chunk-5W7CNAHB.js} +191 -178
  20. package/dist/chunk-5W7CNAHB.js.map +1 -0
  21. package/dist/{chunk-6GIGQT6C.js → chunk-B5RAZVXW.js} +12 -11
  22. package/dist/chunk-B5RAZVXW.js.map +1 -0
  23. package/dist/{chunk-6VDCTR7N.js → chunk-D4NJBQAH.js} +258 -151
  24. package/dist/chunk-D4NJBQAH.js.map +1 -0
  25. package/dist/{chunk-UV6UWGEO.js → chunk-D736QUGF.js} +6 -5
  26. package/dist/chunk-D736QUGF.js.map +1 -0
  27. package/dist/{chunk-EGWTT5XJ.js → chunk-ET6QQNSQ.js} +76 -95
  28. package/dist/chunk-ET6QQNSQ.js.map +1 -0
  29. package/dist/chunk-EWJP3YYP.js +21 -0
  30. package/dist/chunk-EWJP3YYP.js.map +1 -0
  31. package/dist/{chunk-M6UZT7HS.js → chunk-FCKP6N7Y.js} +6 -5
  32. package/dist/chunk-FCKP6N7Y.js.map +1 -0
  33. package/dist/{chunk-TGG26XMK.js → chunk-FV3Z5MBE.js} +10 -5
  34. package/dist/chunk-FV3Z5MBE.js.map +1 -0
  35. package/dist/chunk-G5E74IJB.js +8 -0
  36. package/dist/chunk-G5E74IJB.js.map +1 -0
  37. package/dist/{chunk-G2U7DJHL.js → chunk-JFFVBEDI.js} +3 -3
  38. package/dist/{chunk-G2U7DJHL.js.map → chunk-JFFVBEDI.js.map} +1 -1
  39. package/dist/{chunk-FOGL3TC7.js → chunk-JXYYRKD6.js} +8 -10
  40. package/dist/chunk-JXYYRKD6.js.map +1 -0
  41. package/dist/{chunk-ZDRN3YCE.js → chunk-LJJV6ZD5.js} +17 -24
  42. package/dist/chunk-LJJV6ZD5.js.map +1 -0
  43. package/dist/chunk-LYLDCIPH.js +365 -0
  44. package/dist/chunk-LYLDCIPH.js.map +1 -0
  45. package/dist/{chunk-HJO6VNQI.js → chunk-MU6MCIAC.js} +3 -3
  46. package/dist/{chunk-HJO6VNQI.js.map → chunk-MU6MCIAC.js.map} +1 -1
  47. package/dist/{chunk-UYWUVEP6.js → chunk-MUUROGIH.js} +2 -2
  48. package/dist/chunk-MUUROGIH.js.map +1 -0
  49. package/dist/{chunk-DYRKDCJI.js → chunk-PBDYW2TU.js} +41 -7
  50. package/dist/chunk-PBDYW2TU.js.map +1 -0
  51. package/dist/{chunk-5VNWZADP.js → chunk-Q2UM7BXU.js} +18 -7
  52. package/dist/chunk-Q2UM7BXU.js.map +1 -0
  53. package/dist/chunk-RMX4EOQK.js +26 -0
  54. package/dist/chunk-RMX4EOQK.js.map +1 -0
  55. package/dist/{chunk-2YOE5PIS.js → chunk-S2EGRPCM.js} +8 -6
  56. package/dist/chunk-S2EGRPCM.js.map +1 -0
  57. package/dist/{chunk-5KVHID6A.js → chunk-SPWVPIFQ.js} +4 -4
  58. package/dist/{chunk-5KVHID6A.js.map → chunk-SPWVPIFQ.js.map} +1 -1
  59. package/dist/chunk-SSHDBOXF.js +71 -0
  60. package/dist/chunk-SSHDBOXF.js.map +1 -0
  61. package/dist/{chunk-6OUDPSSG.js → chunk-TWDHNIY6.js} +3 -3
  62. package/dist/{chunk-6OUDPSSG.js.map → chunk-TWDHNIY6.js.map} +1 -1
  63. package/dist/{chunk-SC3FAFZN.js → chunk-UF4MZSLT.js} +2 -2
  64. package/dist/chunk-UF4MZSLT.js.map +1 -0
  65. package/dist/chunk-ZJ5AOYBH.js +8 -0
  66. package/dist/chunk-ZJ5AOYBH.js.map +1 -0
  67. package/dist/{chunk-LEC4U6CX.js → chunk-ZVJZ72HY.js} +3 -3
  68. package/dist/{chunk-LEC4U6CX.js.map → chunk-ZVJZ72HY.js.map} +1 -1
  69. package/dist/circuit-breaker/index.d.ts +1 -0
  70. package/dist/circuit-breaker/index.js +1 -1
  71. package/dist/{core-BmlJOvxM.d.ts → core-42kapW0n.d.ts} +96 -6
  72. package/dist/deadline/index.d.ts +77 -11
  73. package/dist/deadline/index.js +3 -3
  74. package/dist/devtools/index.js +8 -28
  75. package/dist/devtools/index.js.map +1 -1
  76. package/dist/do/index.d.ts +52 -24
  77. package/dist/do/index.js +94 -92
  78. package/dist/do/index.js.map +1 -1
  79. package/dist/{durable-effects-9LxFpmh3.d.ts → durable-effects-CFRngTNA.d.ts} +3 -3
  80. package/dist/extension/index.d.ts +25 -7
  81. package/dist/extension/index.js +4 -3
  82. package/dist/extension/index.js.map +1 -1
  83. package/dist/extension/react.js +4 -3
  84. package/dist/extension/react.js.map +1 -1
  85. package/dist/extension/subs/index.d.ts +2 -2
  86. package/dist/extension/subs/index.js +7 -10
  87. package/dist/extension/subs/index.js.map +1 -1
  88. package/dist/fan-out/index.d.ts +10 -10
  89. package/dist/fan-out/index.js +3 -3
  90. package/dist/idempotent-intake/index.d.ts +2 -2
  91. package/dist/idempotent-intake/index.js +3 -3
  92. package/dist/index.d.ts +50 -489
  93. package/dist/index.js +2 -2
  94. package/dist/llm-call/index.d.ts +3 -2
  95. package/dist/llm-call/index.js +12 -10
  96. package/dist/machine-B_BF4jij.d.ts +804 -0
  97. package/dist/machine-viz/index.d.ts +1 -1
  98. package/dist/machine-viz/index.js +2 -2
  99. package/dist/mem/index.d.ts +3 -2
  100. package/dist/mem/index.js.map +1 -1
  101. package/dist/monitored-run/index.d.ts +54 -30
  102. package/dist/monitored-run/index.js +7 -7
  103. package/dist/node/index.d.ts +3 -2
  104. package/dist/node/index.js +2 -3
  105. package/dist/node/index.js.map +1 -1
  106. package/dist/paginated-walk/index.d.ts +2 -2
  107. package/dist/paginated-walk/index.js +11 -10
  108. package/dist/parity/index.d.ts +6 -5
  109. package/dist/parity/index.js +5 -4
  110. package/dist/parity/index.js.map +1 -1
  111. package/dist/pbt/arbitraries/index.d.ts +1 -1
  112. package/dist/pbt/index.d.ts +1 -1
  113. package/dist/pbt/index.js +3 -3
  114. package/dist/pbt/index.js.map +1 -1
  115. package/dist/pbt/runners/index.d.ts +1 -1
  116. package/dist/pbt/runners/index.js +3 -3
  117. package/dist/poller/index.d.ts +59 -17
  118. package/dist/poller/index.js +56 -20
  119. package/dist/poller/index.js.map +1 -1
  120. package/dist/prediction/index.d.ts +1 -1
  121. package/dist/prediction/index.js +2 -2
  122. package/dist/pure/index.d.ts +1 -1
  123. package/dist/pure/index.js +2 -2
  124. package/dist/rate-limit/index.d.ts +2 -2
  125. package/dist/rate-limit/index.js +1 -1
  126. package/dist/react/index.d.ts +10 -3
  127. package/dist/react/index.js +2 -2
  128. package/dist/react/index.js.map +1 -1
  129. package/dist/reconciler/index.d.ts +2 -2
  130. package/dist/reconciler/index.js +13 -15
  131. package/dist/reconciler/index.js.map +1 -1
  132. package/dist/recorder/index.d.ts +20 -20
  133. package/dist/recorder/index.js +1 -1
  134. package/dist/resilient-call/index.d.ts +31 -6
  135. package/dist/resilient-call/index.js +10 -9
  136. package/dist/retry-backoff/index.d.ts +191 -14
  137. package/dist/retry-backoff/index.js +1 -1
  138. package/dist/retry-to-success/index.js +1 -1
  139. package/dist/runtime-types-CukV8kMX.d.ts +406 -0
  140. package/dist/saga/index.d.ts +3 -2
  141. package/dist/saga/index.js +2 -2
  142. package/dist/saga/index.js.map +1 -1
  143. package/dist/snapshot/index.d.ts +1 -1
  144. package/dist/snapshot/index.js +3 -3
  145. package/dist/subs/index.d.ts +372 -4
  146. package/dist/subs/index.js +4 -1
  147. package/dist/testing/index.d.ts +2 -2
  148. package/dist/testing/index.js +2 -2
  149. package/dist/throttled-input/index.d.ts +2 -2
  150. package/dist/throttled-input/index.js +14 -13
  151. package/dist/throttled-input/index.js.map +1 -1
  152. package/dist/token-refresh/index.d.ts +1 -1
  153. package/dist/token-refresh/index.js +3 -3
  154. package/dist/trace-replay/index.d.ts +12 -7
  155. package/dist/trace-replay/index.js +4 -3
  156. package/dist/{types-BoUWgwSm.d.ts → types-KbL3W1mj.d.ts} +1 -1
  157. package/dist/with-deadline/index.d.ts +1 -1
  158. package/dist/with-deadline/index.js +2 -2
  159. package/dist/with-resilience/index.d.ts +21 -2
  160. package/dist/with-resilience/index.js +12 -23
  161. package/dist/with-resilience/index.js.map +1 -1
  162. package/dist/with-telemetry/index.d.ts +1 -1
  163. package/dist/with-telemetry/index.js +2 -2
  164. package/dist/work-queue/adapter.d.ts +2 -2
  165. package/dist/work-queue/adapter.js +2 -2
  166. package/dist/work-queue/index.d.ts +3 -2
  167. package/dist/work-queue/index.js +1 -1
  168. package/dist/work-queue/index.js.map +1 -1
  169. package/dist/work-queue/ops.d.ts +2 -2
  170. package/dist/work-queue/ops.js +1 -1
  171. package/dist/workflow/index.d.ts +51 -19
  172. package/dist/workflow/index.js +71 -52
  173. package/dist/workflow/index.js.map +1 -1
  174. package/package.json +4 -1
  175. package/dist/chunk-2YOE5PIS.js.map +0 -1
  176. package/dist/chunk-3545BOYL.js.map +0 -1
  177. package/dist/chunk-5VNWZADP.js.map +0 -1
  178. package/dist/chunk-6GIGQT6C.js.map +0 -1
  179. package/dist/chunk-6VDCTR7N.js.map +0 -1
  180. package/dist/chunk-DYRKDCJI.js.map +0 -1
  181. package/dist/chunk-EGWTT5XJ.js.map +0 -1
  182. package/dist/chunk-EMJTPHI7.js +0 -91
  183. package/dist/chunk-EMJTPHI7.js.map +0 -1
  184. package/dist/chunk-FOGL3TC7.js.map +0 -1
  185. package/dist/chunk-IK4S5DJN.js.map +0 -1
  186. package/dist/chunk-M6UZT7HS.js.map +0 -1
  187. package/dist/chunk-NSDFA6M6.js +0 -198
  188. package/dist/chunk-NSDFA6M6.js.map +0 -1
  189. package/dist/chunk-SC3FAFZN.js.map +0 -1
  190. package/dist/chunk-SKAMZKTE.js +0 -20
  191. package/dist/chunk-SKAMZKTE.js.map +0 -1
  192. package/dist/chunk-TGG26XMK.js.map +0 -1
  193. package/dist/chunk-UV6UWGEO.js.map +0 -1
  194. package/dist/chunk-UYWUVEP6.js.map +0 -1
  195. package/dist/chunk-VFGKTGGC.js.map +0 -1
  196. package/dist/chunk-ZDRN3YCE.js.map +0 -1
@@ -1,6 +1,53 @@
1
- import { S as Sub, P as Port } from '../core-BmlJOvxM.js';
2
- import { S as SubscribeHandler } from '../types-BoUWgwSm.js';
3
- import { Runtime } from '../index.js';
1
+ import { S as Sub, P as Port, D as DepKeyedSub, a as SubId } from '../core-42kapW0n.js';
2
+ import { S as SubscribeHandler } from '../types-KbL3W1mj.js';
3
+ import { R as Runtime } from '../runtime-types-CukV8kMX.js';
4
+
5
+ /**
6
+ * The imperative listener target, expressed as the `add`/`remove` pair the
7
+ * substrate pairs into a reconciled resource. Both halves receive the SAME
8
+ * `listener` reference — the substrate builds it once — so `remove` can never
9
+ * be handed a different function than `add` saw.
10
+ *
11
+ * `add`/`remove` also receive `(sub, ctx)` so the target may be resolved from
12
+ * `ctx` (a runtime, a port, a DOM node looked up lazily) or keyed on the Sub
13
+ * (an alarm name, a channel name). Resolving the target inside `add` and
14
+ * closing over nothing but the listener keeps a target swap from leaking the
15
+ * old target's listener — same rationale as `fromEventTarget`'s captured
16
+ * target.
17
+ *
18
+ * `Args` is the tuple the platform invokes the listener with — `[Alarm]` for
19
+ * `chrome.alarms.onAlarm`, `[message, sender, sendResponse]` for
20
+ * `chrome.runtime.onMessage`, `[changes, areaName]` for
21
+ * `chrome.storage.onChanged`. The substrate builds ONE function of this exact
22
+ * shape and threads the IDENTICAL reference to both `add` and `remove` — so a
23
+ * platform that matches `removeListener` by reference (every chrome surface)
24
+ * removes exactly what it added. A consumer that wrapped the listener inside
25
+ * `add` would break reference identity (leak #2 above); carrying the native
26
+ * `Args` on the listener itself removes the need for any wrapper.
27
+ */
28
+ interface ListenerTarget<Args extends readonly unknown[], S extends Sub, Ctx> {
29
+ add: (listener: (...args: Args) => void, sub: S, ctx: Ctx) => void;
30
+ remove: (listener: (...args: Args) => void, sub: S, ctx: Ctx) => void;
31
+ }
32
+ /**
33
+ * Build a listener-backed Sub factory whose cleanup is mandatory and derived.
34
+ *
35
+ * `Args`, `S`, `Ctx` are fixed when the target is defined — they describe the
36
+ * one listener topology this factory owns (the platform arg tuple, which Sub
37
+ * shape it filters on, what context resolves it). `M` is supplied at the
38
+ * subscribe cell because the same listener feeds different Msgs in different
39
+ * machines. Pinning `S` here (rather than at the `msgFn` step) lets
40
+ * `add`/`remove` read the concrete Sub's fields — `sub.alarmName`,
41
+ * `sub.channel` — with NO cast at the substrate edge: the narrowing happens in
42
+ * author-land where the Sub shape is in scope.
43
+ *
44
+ * The returned function is a `SubscribeHandler` factory in the exact shape of
45
+ * every other `@demlik/tea/subs` battery: supply the `msgFn` projection, get a
46
+ * `(sub, ctx, dispatch) => cleanup` handler assignable to `machine.subscribe`.
47
+ * The cleanup the author NEVER writes is `() => remove(listener, sub, ctx)`,
48
+ * with `listener` the identical reference passed to `add`.
49
+ */
50
+ declare function defineListener<Args extends readonly unknown[], S extends Sub = Sub, Ctx = unknown>(target: ListenerTarget<Args, S, Ctx>): <M>(msgFn: (sub: S, ...args: Args) => M | null) => SubscribeHandler<S, M, Ctx>;
4
51
 
5
52
  interface MinimalEvent {
6
53
  readonly type: string;
@@ -92,4 +139,325 @@ type TimeoutSubData = {
92
139
  };
93
140
  declare function fromTimeout<S extends Sub & TimeoutSubData, M>(msgFn: (sub: S) => M): SubscribeHandler<S, M, unknown>;
94
141
 
95
- export { type CancelTimer, type EventSourceFactoryOpts, type ReconnectingWebSocketFactoryOpts, SubscribeHandler, type WebSocketFactoryOpts, type WebSocketSubData, fromBroadcastChannel, fromEventSource, fromEventTarget, fromInterval, fromPort, fromReconnectingWebSocket, fromTimeout, fromWebSocket };
142
+ /**
143
+ * The Sub the battery builds. The consumer's `Sub` union includes
144
+ * `ManagedResourceSub<TKey>` directly — the substrate reconciles by `id` and
145
+ * `type` like any other Sub.
146
+ */
147
+ interface ManagedResourceSub<TKey> extends Sub<"managed_resource"> {
148
+ /** The lifetime key. Stable while the resource should be held; a change
149
+ * retires the old resource (release) and acquires a fresh one. */
150
+ readonly key: TKey;
151
+ }
152
+ /**
153
+ * A battery paired with a state-gate. Self-contained: it knows its `name`
154
+ * (for the unique-name assert), how to derive its live Sub from a state
155
+ * (`subFor`), how to handle its own `subscribe`, and whether it `owns` a given
156
+ * Sub id.
157
+ *
158
+ * Generic in the consumer state `S`, the shared lifetime key `TKey`, and the
159
+ * shared `Ctx`. `Handle` is the ONLY thing erased — each battery's handle type
160
+ * differs (a checkpoint saver vs. a tool-call bridge) but never crosses this
161
+ * interface, so a `combineManagedResources` array holds entries of
162
+ * heterogeneous handle types with no cast. The sub type stays
163
+ * `ManagedResourceSub<TKey>` end to end, so the substrate's narrowed Sub flows
164
+ * straight in.
165
+ */
166
+ interface GatedManagedResource<S, TKey, Ctx> {
167
+ /** The owning battery's `name`. The combinator asserts these are unique. */
168
+ readonly name: string;
169
+ /**
170
+ * Derive this resource's live Sub for a given state: the battery's
171
+ * `.sub(key)` when `when(state)` returns a key, or `null` when it returns
172
+ * `null` (the resource should be torn down in that state).
173
+ */
174
+ readonly subFor: (state: S) => ManagedResourceSub<TKey> | null;
175
+ /**
176
+ * This battery's own `.subscribe`. The combinator routes an incoming
177
+ * `managed_resource` Sub to the entry whose `owns(sub)` is true, then calls
178
+ * this.
179
+ */
180
+ readonly subscribe: (sub: ManagedResourceSub<TKey>, ctx: Ctx, dispatch: (msg: never) => void) => () => void;
181
+ /**
182
+ * Does this entry own the given Sub? `sub.id === subIdFor(sub.key)` encodes
183
+ * the battery `name`, so a match is unambiguous.
184
+ */
185
+ readonly owns: (sub: ManagedResourceSub<TKey>) => boolean;
186
+ }
187
+ /**
188
+ * What `combineManagedResources` returns: the single managed-resource
189
+ * `subscribe` cell for the machine's `subscribe` record, and a `subs(state)`
190
+ * builder for `subscriptions(state)`.
191
+ */
192
+ interface CombinedManagedResources<S, TKey, Ctx> {
193
+ /**
194
+ * Map every entry through its `when(state)`, drop the `null`s, and build the
195
+ * live Subs. Spread into `subscriptions(state)`. The derived list and the
196
+ * `subscribe` routing share one source — the entries — so they never drift.
197
+ */
198
+ readonly subs: (state: S) => ManagedResourceSub<TKey>[];
199
+ /**
200
+ * The SINGLE `subscribe.managed_resource` handler. Routes an incoming Sub to
201
+ * the owning battery by matching `sub.id === battery.subIdFor(sub.key)` (the
202
+ * id encodes the battery `name`). Throws loudly on an unrouted id — a wiring
203
+ * bug, never a silent no-op resource.
204
+ */
205
+ readonly subscribe: (sub: ManagedResourceSub<TKey>, ctx: Ctx, dispatch: (msg: never) => void) => () => void;
206
+ }
207
+ interface DefineManagedResourceOpts<TKey, Handle, Ctx> {
208
+ /**
209
+ * A short, stable name. The Sub id is `managed:${name}:${stringify(key)}`,
210
+ * so two managed resources in one machine never collide and neither
211
+ * collides with a `fromTransport` Sub keyed on the same identity.
212
+ */
213
+ readonly name: string;
214
+ /**
215
+ * Build the resource. Runs when the Sub appears in `subscriptions(state)`.
216
+ * The returned Handle is the ONLY thing `release` receives — capture
217
+ * everything teardown needs into it. If `acquire` throws, the substrate's
218
+ * reconcile does NOT register the Sub and `release` is never called (no
219
+ * dangling half-built resource).
220
+ */
221
+ readonly acquire: (key: TKey, ctx: Ctx) => Handle;
222
+ /**
223
+ * Tear the resource down. Runs when the Sub leaves the desired set (phase
224
+ * exited) or when its key changes. MANDATORY — the guaranteed teardown is
225
+ * the reason this battery exists. May be sync or async; the substrate's
226
+ * cleanup is fire-and-forget (Rule 2), so a rejected teardown is logged at
227
+ * the boundary, not surfaced as a Msg.
228
+ */
229
+ readonly release: (handle: Handle) => void | Promise<void>;
230
+ }
231
+ /**
232
+ * What the battery returns: a `.sub(key)` builder for `subscriptions`, the
233
+ * `.subscribe` handler for the machine's `subscribe` record, a `.get(key)`
234
+ * accessor so Cmd handlers can reach the live Handle while the resource is
235
+ * held, and a `.subIdFor(key)` for tests.
236
+ */
237
+ interface ManagedResourceBattery<TKey, Handle, Ctx> {
238
+ /**
239
+ * Build the Sub for `subscriptions(state)`. The consumer calls this from
240
+ * inside their `subscriptions` cell for the phases where the resource
241
+ * should be live.
242
+ */
243
+ readonly sub: (key: TKey) => ManagedResourceSub<TKey>;
244
+ /**
245
+ * Pair this battery with a state-gate, producing a self-contained entry for
246
+ * `combineManagedResources`. `when(state)` returns the resource's KEY when
247
+ * it should be live in that state, or `null` when it should be torn down.
248
+ *
249
+ * Folding "is it active?" and "what key?" into ONE declaration at the
250
+ * battery's own definition site is the point: a central `subscriptions(state)`
251
+ * list can silently forget to gate a new phase, but a per-resource gate
252
+ * travels with the resource. The combinator derives BOTH the active-sub list
253
+ * and the subscribe routing from these entries, so the two can never disagree.
254
+ *
255
+ * Generic in the consumer's state `S` (each `.gated` call binds its own `S`).
256
+ * The returned entry erases this battery's `Handle` — `combineManagedResources`
257
+ * holds entries of heterogeneous handle types with no cast — while keeping
258
+ * `TKey` and `Ctx` so the substrate's narrowed `ManagedResourceSub<TKey>`
259
+ * flows straight into the entry's `subscribe`.
260
+ */
261
+ readonly gated: <S>(when: (state: S) => TKey | null) => GatedManagedResource<S, TKey, Ctx>;
262
+ /**
263
+ * Dep-keyed entry for the machine's `subs` array. `when(state)` returns the
264
+ * resource's KEY when it should be held, or `null` when it should be torn
265
+ * down — the exact gate `.gated` takes, now producing a `DepKeyedSub` the
266
+ * kernel folds directly instead of an entry the `combineManagedResources`
267
+ * router multiplexes.
268
+ *
269
+ * This is the simplification the dep-keyed kernel unlocks: two managed
270
+ * resources used to COLLIDE on the single `subscribe.managed_resource` key
271
+ * (both subs share `type: "managed_resource"`), forcing
272
+ * `combineManagedResources` to route by SubId. A dep-keyed Sub has its OWN
273
+ * reconcile slot (its array position), so there is no shared `type` to route
274
+ * — each resource is one `subs` entry, no combinator, no router. `acquire` /
275
+ * `release` still ride the kernel's dispose-on-change / dispose-on-null
276
+ * reconcile, now via the dep-keyed source's `Dispose` instead of the
277
+ * `subscribe` cell's cleanup.
278
+ */
279
+ readonly depKeyed: <S>(when: (state: S) => TKey | null) => DepKeyedSub<S, never, Ctx>;
280
+ /**
281
+ * The `subscribe.managed_resource` handler. Drop straight into the
282
+ * machine's `subscribe` record. Runs `acquire` on install, holds the
283
+ * Handle in the battery's handle table, and returns a cleanup that runs
284
+ * `release` (and forgets the Handle) on reconcile-out.
285
+ */
286
+ readonly subscribe: (sub: ManagedResourceSub<TKey>, ctx: Ctx, dispatch: (msg: never) => void) => () => void;
287
+ /**
288
+ * Accessor for the live Handle, keyed on `key`. Returns `undefined` when
289
+ * the resource is not currently held (no active sub for that key) — the
290
+ * caller decides whether that is an error. A Cmd handler wires its
291
+ * `ctx.getX(key)` through this, so the handler reads the SAME owner the
292
+ * reconciler holds, never a hand-built duplicate.
293
+ */
294
+ readonly get: (key: TKey) => Handle | undefined;
295
+ /** Pre-derived subId for `(key)` — handy for tests. */
296
+ readonly subIdFor: (key: TKey) => SubId;
297
+ }
298
+ /**
299
+ * Build the battery. One call per managed resource at the machine's host
300
+ * file.
301
+ *
302
+ * @example
303
+ * const checkpointStore = defineManagedResource<RunId, CheckpointSaver, Ctx>({
304
+ * name: "checkpoint",
305
+ * acquire: (runId, ctx) =>
306
+ * new CheckpointSaver({ storage: ctx.storage, bucket: ctx.bucket, runId }),
307
+ * release: (saver) => saver.clear(),
308
+ * });
309
+ *
310
+ * // In the machine:
311
+ * subscriptions: (state) =>
312
+ * state.type === "running" ? [checkpointStore.sub(state.runId)] : [],
313
+ * subscribe: { managed_resource: checkpointStore.subscribe },
314
+ *
315
+ * // In interpret (read the live handle the reconciler holds):
316
+ * start_graph: async (cmd, ctx) => {
317
+ * const saver = checkpointStore.get(cmd.runId); // same owner, no rebuild
318
+ * ...
319
+ * }
320
+ */
321
+ declare function defineManagedResource<TKey, Handle, Ctx>(opts: DefineManagedResourceOpts<TKey, Handle, Ctx>): ManagedResourceBattery<TKey, Handle, Ctx>;
322
+ declare function combineManagedResources<S, TKey, Ctx>(entries: readonly GatedManagedResource<S, TKey, Ctx>[]): CombinedManagedResources<S, TKey, Ctx>;
323
+
324
+ /**
325
+ * Duplex transport. The host (DO, worker thread, in-process bus) constructs
326
+ * one from whatever the platform offers. `fromTransport` does not know what
327
+ * a WebSocket is — it knows `send`, `onMessage`, `onClose`.
328
+ */
329
+ interface Transport {
330
+ /** Send a string frame. Best-effort; failures are logged at the boundary. */
331
+ send(data: string): void;
332
+ /**
333
+ * Subscribe to inbound frames. Returns a cleanup that removes the listener.
334
+ * The battery wires this in its Sub's subscribe handler; cleanup runs when
335
+ * the phase exits (the substrate's reconcileSubs).
336
+ */
337
+ onMessage(listener: (data: string) => void): () => void;
338
+ /**
339
+ * Subscribe to a one-shot close. Returns a cleanup that removes the
340
+ * listener. The battery dispatches `lostMsg` on close.
341
+ */
342
+ onClose(listener: () => void): () => void;
343
+ /** Close the underlying channel cleanly. Called by the Sub cleanup. */
344
+ close(): void;
345
+ }
346
+ /**
347
+ * Factory the consumer wires to a platform-specific transport. The battery
348
+ * passes the runtime-derived key (`runId`, or whatever identity is on the
349
+ * phase) so per-run transports can be addressed.
350
+ */
351
+ type TransportFactory<K> = (key: K) => Transport;
352
+ /**
353
+ * The Sub the battery builds. The consumer's `Sub` union includes
354
+ * `TransportSub<TKey>` directly — no wrapping needed, the substrate
355
+ * reconciles by `id` and `type` like any other Sub.
356
+ */
357
+ interface TransportSub<TKey> extends Sub<"transport"> {
358
+ /** The phase / run identity. Stable for the lifetime of the seam. */
359
+ readonly key: TKey;
360
+ }
361
+ interface FromTransportOpts<TKey, Inbound, Outbound, M, Ctx> {
362
+ /**
363
+ * A short, stable name for this seam. The Sub id is derived from
364
+ * `${name}:${stringify(key)}` so two seams in the same machine (e.g.
365
+ * `hands` + `worker`) never collide.
366
+ */
367
+ readonly name: string;
368
+ /**
369
+ * Build the transport. The battery owns the lifetime; the factory is the
370
+ * platform-specific constructor (`new WebSocket(...)` adapter on a DO,
371
+ * `MessagePort` adapter in a worker, in-memory stub in tests).
372
+ */
373
+ readonly openTransport: (key: TKey, ctx: Ctx) => Transport;
374
+ /**
375
+ * Parse one inbound frame. The battery hands `unknown` through (boundary
376
+ * parse is the consumer's responsibility per invariant 8). Returning
377
+ * `null` drops the frame silently — useful for frames the seam doesn't
378
+ * recognize at this layer (e.g. transport-level keepalive).
379
+ */
380
+ readonly parseInbound: (raw: string, key: TKey) => Inbound | null;
381
+ /**
382
+ * Map a parsed inbound to a domain Msg. Returning `null` drops the
383
+ * dispatch (e.g. a transport-level pong that doesn't change domain state).
384
+ */
385
+ readonly onInbound: (inbound: Inbound, key: TKey) => M | null;
386
+ /**
387
+ * The Msg dispatched when the transport closes (peer gone, network
388
+ * dropped, normal close). ONE Msg arm, not a Sub, not a heartbeat-derived
389
+ * deadline — the transport's own close signal is the truth.
390
+ */
391
+ readonly lostMsg: (key: TKey) => M;
392
+ /**
393
+ * Serialize an outbound. Called by the Cmd handler the battery wires.
394
+ * Pure data → string at the seam boundary.
395
+ */
396
+ readonly serializeOutbound: (outbound: Outbound) => string;
397
+ }
398
+ /**
399
+ * What the battery returns: a Sub builder (for `subscriptions`), the Sub's
400
+ * `subscribe` handler (for `subscribe.transport`), and a `send(key, outbound)`
401
+ * helper the consumer's Cmd handler calls. Plus a `subIdFor` factory the
402
+ * consumer can use when keying related Subs (e.g. a deadline) to the same seam
403
+ * identity.
404
+ */
405
+ interface TransportBattery<TKey, Outbound, M, Ctx> {
406
+ /**
407
+ * Build the Sub for `subscriptions(state)`. The consumer calls this from
408
+ * inside their `subscriptions` cell when the seam should be open.
409
+ */
410
+ readonly sub: (key: TKey) => TransportSub<TKey>;
411
+ /**
412
+ * The `subscribe.transport` handler. Drop straight into the machine's
413
+ * `subscribe` record — the battery wires inbound + close in one shot.
414
+ */
415
+ readonly subscribe: (sub: TransportSub<TKey>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;
416
+ /**
417
+ * Outbound send. The Cmd handler calls this; the battery's handle table
418
+ * (built when subscribe ran) routes to the live transport. If the seam
419
+ * is closed (no active sub), the send is dropped honestly (logged, not
420
+ * thrown — the reducer already moved past caring, same shape as Rule 2
421
+ * fire-and-forget Cmds).
422
+ */
423
+ readonly send: (key: TKey, outbound: Outbound) => void;
424
+ /** Pre-derived subId for `(key)` — handy for deadline Subs keyed to the same seam. */
425
+ readonly subIdFor: (key: TKey) => SubId;
426
+ /**
427
+ * Pair this seam with a state-gate, producing a dep-keyed Sub for the
428
+ * machine's `subs` array. `when(state)` returns the seam's KEY when it
429
+ * should be open, or `null` when it should be torn down. The kernel derives
430
+ * the id from `structuralHash(key)` and reconciles the seam's lifetime — so
431
+ * the author neither lists this Sub in a central `subscriptions(state)` nor
432
+ * writes its id. Replaces `subscriptions: (s) => active(s) ? [hands.sub(...)] : []`
433
+ * + `subscribe: { transport: hands.subscribe }` with one `subs` entry.
434
+ *
435
+ * The `source` closes over the battery's own `subscribe` (open inbound +
436
+ * close, wire the handle table) using the same `ctx` the kernel passes every
437
+ * Sub source.
438
+ */
439
+ readonly depKeyed: <S>(when: (state: S) => TKey | null) => DepKeyedSub<S, M, Ctx>;
440
+ }
441
+ /**
442
+ * Build the battery. One call per seam at the machine's host file.
443
+ *
444
+ * @example
445
+ * const handsSeam = fromTransport<RunId, HandsInbound, HandsOutbound, Msg, Ctx>({
446
+ * name: "hands",
447
+ * openTransport: (runId, ctx) => ctx.openHandsWs(runId),
448
+ * parseInbound: (raw) => parseHandsInbound(JSON.parse(raw)),
449
+ * onInbound: (inbound, runId) => ({ type: "hands_said", runId, inbound }),
450
+ * lostMsg: (runId) => ({ type: "hands_lost", runId }),
451
+ * serializeOutbound: (out) => JSON.stringify(out),
452
+ * });
453
+ *
454
+ * // In the machine:
455
+ * subscriptions: (state) => state.type === "auditing" ? [handsSeam.sub(state.runId)] : [],
456
+ * subscribe: { transport: handsSeam.subscribe },
457
+ *
458
+ * // In interpret:
459
+ * send_hands: async (cmd) => { handsSeam.send(cmd.runId, cmd.outbound); },
460
+ */
461
+ declare function fromTransport<TKey, Inbound, Outbound, M, Ctx>(opts: FromTransportOpts<TKey, Inbound, Outbound, M, Ctx>): TransportBattery<TKey, Outbound, M, Ctx>;
462
+
463
+ export { type CancelTimer, type CombinedManagedResources, type DefineManagedResourceOpts, type EventSourceFactoryOpts, type FromTransportOpts, type GatedManagedResource, type ListenerTarget, type ManagedResourceBattery, type ManagedResourceSub, type ReconnectingWebSocketFactoryOpts, SubscribeHandler, type Transport, type TransportBattery, type TransportFactory, type TransportSub, type WebSocketFactoryOpts, type WebSocketSubData, combineManagedResources, defineListener, defineManagedResource, fromBroadcastChannel, fromEventSource, fromEventTarget, fromInterval, fromPort, fromReconnectingWebSocket, fromTimeout, fromTransport, fromWebSocket };
@@ -1,4 +1,7 @@
1
- export { fromBroadcastChannel, fromEventSource, fromEventTarget, fromInterval, fromPort, fromReconnectingWebSocket, fromWebSocket } from '../chunk-NSDFA6M6.js';
1
+ export { combineManagedResources, defineListener, defineManagedResource, fromBroadcastChannel, fromEventSource, fromEventTarget, fromInterval, fromPort, fromReconnectingWebSocket, fromTransport, fromWebSocket } from '../chunk-LYLDCIPH.js';
2
2
  export { fromTimeout } from '../chunk-2YFLOCWX.js';
3
+ import '../chunk-ZJ5AOYBH.js';
4
+ import '../chunk-D4NJBQAH.js';
5
+ import '../chunk-PBDYW2TU.js';
3
6
  //# sourceMappingURL=index.js.map
4
7
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,5 @@
1
- import { C as Cmd, S as Sub, M as Machine } from '../core-BmlJOvxM.js';
2
- import { Runtime } from '../index.js';
1
+ import { C as Cmd, S as Sub, M as Machine } from '../core-42kapW0n.js';
2
+ import { R as Runtime } from '../runtime-types-CukV8kMX.js';
3
3
 
4
4
  /**
5
5
  * The composed Model shape every `withX` wrapper produces: the base machine's
@@ -1,5 +1,5 @@
1
- import { replay } from '../chunk-6VDCTR7N.js';
2
- import '../chunk-DYRKDCJI.js';
1
+ import { replay } from '../chunk-D4NJBQAH.js';
2
+ import '../chunk-PBDYW2TU.js';
3
3
  import { expect, vi } from 'vitest';
4
4
 
5
5
  function assertWrapperFaithful(base, makeWrapped, opts) {
@@ -1,10 +1,10 @@
1
1
  import { TtlCache } from '../cache/index.js';
2
2
  import { DeadlineSub } from '../deadline/index.js';
3
3
  export { DeadlineExceeded as ThrottledInputSettled, subscribeDeadline as subscribeThrottledInput, deadlineExceeded as throttledInputSettled } from '../deadline/index.js';
4
- import { C as Cmd, S as Sub } from '../core-BmlJOvxM.js';
4
+ import { C as Cmd, S as Sub } from '../core-42kapW0n.js';
5
5
  export { Debounced, debounce } from '../debounce/index.js';
6
6
  export { Throttled, throttle } from '../throttle/index.js';
7
- import '../types-BoUWgwSm.js';
7
+ import '../types-KbL3W1mj.js';
8
8
 
9
9
  /**
10
10
  * @demlik/tea/throttled-input — gate a high-frequency input stream into a
@@ -1,12 +1,13 @@
1
1
  export { debounce } from '../chunk-J6527VRQ.js';
2
2
  export { throttle } from '../chunk-HCEVHFJ7.js';
3
- import { deadlineSub } from '../chunk-SKAMZKTE.js';
4
- export { subscribeDeadline as subscribeThrottledInput, deadlineExceeded as throttledInputSettled } from '../chunk-SKAMZKTE.js';
5
- import { initCache, get, set } from '../chunk-IN33Q4VJ.js';
6
- import '../chunk-NSDFA6M6.js';
3
+ import { deadlineSub } from '../chunk-RMX4EOQK.js';
4
+ export { subscribeDeadline as subscribeThrottledInput, deadlineExceeded as throttledInputSettled } from '../chunk-RMX4EOQK.js';
5
+ import { initCache, get, set } from '../chunk-2KNLKQVN.js';
6
+ import '../chunk-LYLDCIPH.js';
7
7
  import '../chunk-2YFLOCWX.js';
8
- import '../chunk-6VDCTR7N.js';
9
- import '../chunk-DYRKDCJI.js';
8
+ import '../chunk-ZJ5AOYBH.js';
9
+ import '../chunk-D4NJBQAH.js';
10
+ import '../chunk-PBDYW2TU.js';
10
11
 
11
12
  // src/throttled-input/index.ts
12
13
  var DEFAULT_SUB_ID = "throttled-input:settle";
@@ -17,21 +18,21 @@ function keyOf(config, value) {
17
18
  return config.cacheKey(value);
18
19
  }
19
20
  function cacheConfig(config, state) {
20
- return config.cacheTtlMs !== void 0 && state.cache !== void 0 ? config : null;
21
+ return config.cacheTtlMs !== void 0 && state.cache !== void 0 ? { config, cache: state.cache } : null;
21
22
  }
22
23
  function isCacheHit(config, state, value, at) {
23
24
  const cached = cacheConfig(config, state);
24
- if (cached === null || state.cache === void 0) return false;
25
- return get(state.cache, keyOf(cached, value), at) !== void 0;
25
+ if (cached === null) return false;
26
+ return get(cached.cache, keyOf(cached.config, value), at) !== void 0;
26
27
  }
27
28
  function commitEmit(config, state, value, at) {
28
29
  const cached = cacheConfig(config, state);
29
- const cache = cached !== null && state.cache !== void 0 ? set(
30
- state.cache,
31
- keyOf(cached, value),
30
+ const cache = cached !== null ? set(
31
+ cached.cache,
32
+ keyOf(cached.config, value),
32
33
  value,
33
34
  at,
34
- cached.cacheTtlMs
35
+ cached.config.cacheTtlMs
35
36
  ) : state.cache;
36
37
  const next = {
37
38
  ...state,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/throttled-input/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/throttled-input — gate a high-frequency input stream into a\n * SETTLED, RATE-CAPPED, optionally DEDUPED sequence of emits, as a TEA knob: one\n * config object + a few pre-wired hooks you spread into your machine.\n *\n * Layering (the throttle-family map): `../throttle` and `../debounce` are the\n * two host-boundary PRIMITIVES — two distinct algorithms\n * (at-most-once-per-window vs collapse-a-burst) behind one signature shape.\n * THIS module is the third name but NOT a third primitive: a TEA machine\n * composed OVER them that re-exports both. Three exports, one layering — not\n * duplication. The reuse list below spells out composition vs reimplemented\n * semantic per sibling.\n *\n * What it reuses, and HOW (composition vs. reimplemented semantic — the two are\n * not the same, so this list keeps them apart):\n *\n * - `../cache` — COMPOSED directly. The DEDUPE / memoize layer is `../cache`'s\n * `TtlCache` plus its `get` / `set` / `initCache` ops, called at runtime —\n * not reinvented. While `cacheTtlMs` is set, an input whose `cacheKey` is\n * already cached and unexpired is served from the cache and emits NOTHING\n * (the downstream effect ran once for that key within its TTL); a fresh emit\n * writes the value into the cache so the next identical input within\n * `cacheTtlMs` is suppressed. The optional eviction Sub is the consumer's to\n * wire if they want memory reclaim (this knob's correctness needs only TTL\n * reads).\n * - `../deadline` — COMPOSED directly. The settle timer IS a `../deadline`\n * Sub: `subsFor` builds it via `deadlineSub`, and the subscribe cell +\n * Msg constructor are RE-EXPORTS of `../deadline`'s own (`subscribeDeadline`\n * → `subscribeThrottledInput`, `deadlineExceeded` → `throttledInputSettled`).\n * The timer lifecycle stays owned by `../deadline`; this knob redraws none\n * of it.\n * - `../throttle` / `../debounce` — TYPE + SEMANTIC reuse only, NOT composed.\n * Those siblings are host-boundary SINK transformers that hold a live\n * `setTimeout`; a `setTimeout` in a closure is exactly what a durable,\n * replayable knob must NOT hold. So their CODE is never called here. Instead\n * the RATE-cap semantic (\"at most once per `ms`\", leading-edge) and the\n * SETTLE semantic (\"wait until quiet\", trailing-edge) are REIMPLEMENTED as\n * pure Model state: `lastAt` is the durable rate cursor (half-open cutoff\n * `at - lastAt >= throttleMs`), and `pending` + the `../deadline` Sub realize\n * the debounce window inside `update`. The `throttle` / `debounce`\n * transformers themselves are RE-EXPORTED (the only reuse of their actual\n * code) so a consumer can pre-throttle / pre-coalesce a genuinely bursty\n * HOST event at the boundary BEFORE it ever becomes an `input` Msg — a\n * separate, optional escape hatch, not part of this knob's gates.\n *\n * \"Omit a brick, omit its gate\" (the resilient-call config rule): every gate\n * field is optional. Omit `throttleMs` → no rate cap (every input passes the\n * rate gate). Omit `debounceMs` → no settle wait (an input that clears the rate\n * gate emits IMMEDIATELY rather than being held). Omit `cacheTtlMs` → no cache\n * slice at all (the `cache` field is absent; no dedupe). A config with all three\n * omitted is a pass-through: `input` always emits.\n *\n * Why this is a knob and not hand-wired logic: the managed state (`lastAt`,\n * `pending`, `cache`) lives in the Model, so it survives DO eviction / reload\n * (durable) and every transition is still a Msg through `update` (replayable). A\n * closure holding a `setTimeout` and a mutable \"last value\" would break both —\n * banned. `input` / `onFlush` are PURE: time arrives as an `at` parameter, never\n * `Date.now()`; they return NEW slice values, never mutate.\n *\n * Typical wiring (combinator form):\n *\n * const search = createThrottledInput<string, Msg>({\n * debounceMs: 200, // wait for the user to stop typing\n * throttleMs: 1_000, // but never fire more than 1/s during a long burst\n * cacheTtlMs: 30_000, // skip a query repeated within 30s\n * cacheKey: (q) => q,\n * emit: (q) => ({ type: \"RunSearch\", query: q }),\n * });\n *\n * // Model: { search: ThrottledInput<string>, ... }\n * // init: (loaded) => [loaded ?? { search: search.init(), ... }, []]\n * // update cells:\n * // QueryTyped: (s, m) => mapInput(s, search.input(s.search, m.value, m.at))\n * // SettleFired: (s, m) => mapInput(s, search.onFlush(s.search, m.atMs))\n * // subscriptions: (s) => search.subs(s.search)\n * // subscribe: { ...search.handlers() }\n *\n * where `mapInput(s, [slice, cmds]) => [{ ...s, search: slice }, cmds]` lifts the\n * slice transition into the consumer's Model. The `RunSearch` Cmd's interpret\n * handler is the consumer's — it is where the settled input meets I/O.\n *\n * NOT a substrate primitive: it depends only on sibling subpaths (`../throttle`,\n * `../debounce`, `../cache`, `../deadline`) and the core `Cmd` / `Sub` types.\n * Consumers reach it via the `@demlik/tea/throttled-input` subpath; the L1\n * escape hatch is those sibling subpaths threaded by hand. NOT exported from the\n * package root — same one-shape-per-package rule as `batch-window` and `poller`.\n */\n\nimport {\n get as cacheGet,\n set as cacheSet,\n initCache,\n type TtlCache,\n} from \"../cache\";\nimport { type DeadlineSub, deadlineSub } from \"../deadline\";\nimport type { Cmd, Sub } from \"../index\";\n\n// `../throttle` + `../debounce` — host-boundary pre-transformers (see file\n// header). Re-exported so a consumer that wants to throttle/debounce a bursty\n// HOST source BEFORE it becomes an `input` Msg reaches them from the same\n// import as the durable gate.\nexport { type Debounced, debounce } from \"../debounce\";\nexport { type Throttled, throttle } from \"../throttle\";\n\n/**\n * The gate fields every throttled-input config shares, regardless of whether\n * dedupe is on. The two dedupe variants below extend this with mutually\n * exclusive cache fields.\n *\n * `emit` is a pure Cmd CONSTRUCTOR, not a closure-with-state: it maps a settled\n * value to the single Cmd the runtime performs when the gates let a value\n * through. Keeping it `(value) => Cmd` (not `(value) => void`) is what keeps the\n * trigger replayable — the emit is a Cmd in the transition's output, never a\n * side effect inside the verb (mirrors `batch-window`'s `flush`).\n */\ninterface ThrottledInputGates<V, C extends Cmd> {\n /**\n * Rate cap, in milliseconds. An input that lands within `throttleMs` of the\n * last EMIT (`at - lastAt < throttleMs`) is dropped from immediate firing —\n * the leading-edge \"at most once per `throttleMs`\" rate-cap rule, applied to\n * emits. The half-open cutoff (`>=` passes) matches `../throttle` and\n * `../cache`'s window-edge convention. Omit for no rate cap.\n *\n * Interacts with `debounceMs`: when both are set, a rate-blocked input is\n * still HELD as `pending` (so the settle timer eventually trails it out at a\n * legal moment) rather than silently dropped — the trailing-edge guarantee\n * that the FINAL sample of a window is never lost.\n */\n readonly throttleMs?: number;\n /**\n * Settle wait, in milliseconds. An input that clears the rate gate is not\n * emitted immediately; it is held as `pending` and a debounce deadline is\n * armed at `pendingAt + debounceMs`. A fresh input re-arms (slides) the\n * window. `onFlush` emits the pending value when the window finally expires.\n * Omit for no settle wait — a passing input emits on arrival.\n */\n readonly debounceMs?: number;\n /**\n * Map a settled value to the single Cmd the runtime performs. Called with the\n * value the gates let through; the returned Cmd is what `input` / `onFlush`\n * emit. PURE constructor — no I/O, no clock; the I/O happens later in the\n * consumer's interpret handler for the returned Cmd.\n */\n readonly emit: (value: V) => C;\n}\n\n/**\n * Configuration for a throttled input — the knob. Pure config, no mutable state\n * (the `ThrottledInput<V>` slice carries that). One config object is shared\n * across every input of one logical stream (often a module-scope const).\n *\n * A DISCRIMINATED UNION on the presence of dedupe, so the type system makes a\n * cache-key collision UNREPRESENTABLE (\"omit a brick, omit its gate\"):\n *\n * - **No dedupe** (`ThrottledInputNoCache`): `cacheTtlMs` absent ⇒ no cache\n * slice, no dedupe. `cacheKey` is statically forbidden here (`never`) — a\n * key with no cache to feed is dead config, so the type rejects it rather\n * than silently ignoring it.\n * - **Dedupe** (`ThrottledInputWithCache`): `cacheTtlMs` set ⇒ `cacheKey` is\n * REQUIRED. There is no `String(value)` default to fall back to, so a\n * structured value can NEVER collapse two distinct values to one slot (the\n * `\"[object Object]\"` collision). The consumer must state the identity that\n * keys the cache; a primitive stream still trivially passes `(v) => v` /\n * `String`, but it does so EXPLICITLY.\n */\nexport type ThrottledInputConfig<V, C extends Cmd> =\n | ThrottledInputNoCache<V, C>\n | ThrottledInputWithCache<V, C>;\n\n/**\n * The no-dedupe config variant: gates only, no cache. `cacheTtlMs` is absent\n * and `cacheKey` is statically `never` so a stray key (which would feed no\n * cache) is a type error, not a silently-ignored field.\n */\nexport interface ThrottledInputNoCache<V, C extends Cmd>\n extends ThrottledInputGates<V, C> {\n readonly cacheTtlMs?: undefined;\n readonly cacheKey?: never;\n}\n\n/**\n * The dedupe config variant: a per-entry TTL plus a REQUIRED `cacheKey`. An\n * input whose `cacheKey` is already cached and unexpired emits NOTHING (the\n * downstream effect ran once for that key within its TTL); a fresh emit writes\n * the value into the cache.\n *\n * `cacheKey` is required (no `String(value)` default) precisely so a structured\n * value cannot silently collide — the consumer names the identity, the cache\n * keys on exactly that.\n */\nexport interface ThrottledInputWithCache<V, C extends Cmd>\n extends ThrottledInputGates<V, C> {\n /**\n * Dedupe / memoize TTL, in milliseconds. An input whose `cacheKey` is already\n * cached and unexpired emits NOTHING; a fresh emit writes the value into the\n * cache so the next identical input within `cacheTtlMs` is suppressed.\n */\n readonly cacheTtlMs: number;\n /**\n * Key a value for the dedupe cache. REQUIRED whenever `cacheTtlMs` is set:\n * there is no `String(value)` fallback, so a structured value's identity is\n * always the consumer's explicit choice and two distinct values can never\n * collapse to one slot. A primitive stream passes `(v) => v` / `String`.\n */\n readonly cacheKey: (value: V) => string;\n}\n\n/**\n * The Model slice a throttled input owns — its visible slice (the knob\n * principle: managed state lives in the Model, never a closure, so it is durable\n * and replayable).\n *\n * - `lastAt` — the `at` of the most recent EMIT, or `null` before the first\n * emit. The `../throttle` rate cap measures against it: an input\n * within `throttleMs` of `lastAt` is rate-blocked. Persisted so a\n * resumed gate keeps its rate cursor.\n * - `pending` — the value HELD for the settle (debounce) window, paired with the\n * `at` it was held (the anchor the deadline targets), or `null`\n * when nothing is waiting. The single source of truth for \"is a\n * settle window open?\" — the debounce timer Sub arms iff this is\n * non-null.\n * - `cache` — the `../cache` TTL store backing dedupe, present IFF\n * `cacheTtlMs` is configured. Absent (the field omitted) for a\n * gate with no dedupe — no per-op cost, nothing to serialize.\n */\nexport interface ThrottledInput<V> {\n readonly lastAt: number | null;\n readonly pending: PendingInput<V> | null;\n readonly cache?: TtlCache<V>;\n}\n\n/**\n * A value held for the settle window: the `value` itself and the `at` it was\n * held (the anchor the debounce deadline targets, `at + debounceMs`). A distinct\n * type (not a bare `V`) because `V` may itself be `null` — a present-but-null\n * value is still a real pending input, distinct from \"nothing pending\", exactly\n * the `null`-sentinel discipline `../throttle` / `../debounce` use for their\n * `pendingArgs`.\n */\nexport interface PendingInput<V> {\n readonly value: V;\n readonly at: number;\n}\n\n/**\n * The Sub variant a throttled input's settle timer produces — a `../deadline`\n * Sub (so the window fires at the correct ABSOLUTE moment even after a late\n * subscribe / rehydrate, and so a consumer running several gates routes each by\n * `id`). `atMs` is the absolute settle-by instant (`pendingAt + debounceMs`).\n */\nexport type ThrottledInputSub = DeadlineSub;\n\n/**\n * The stable Sub id the gate arms its settle deadline under. A single gate arms\n * exactly one settle timer at a time, so one id suffices; a consumer running\n * several gates on one machine namespaces them by composing the knob under\n * distinct slices with distinct ids (the `subs(state, id)` parameter).\n */\nconst DEFAULT_SUB_ID = \"throttled-input:settle\";\n\n/**\n * The starting slice: no prior emit, nothing pending. The `cache` field is\n * seeded only when `cacheTtlMs` is configured (see `createThrottledInput`); the\n * free `initThrottledInput` leaves it absent (the no-dedupe shape).\n */\nexport function initThrottledInput<V>(): ThrottledInput<V> {\n return { lastAt: null, pending: null };\n}\n\n/**\n * Resolve a value's cache key under a DEDUPE config. PURE. Takes the narrowed\n * `ThrottledInputWithCache` (not the union) because `cacheKey` is only present —\n * and only meaningful — when dedupe is on; there is no `String(value)` default\n * to fall back to, so a structured value's identity is always the consumer's\n * explicit choice and two distinct values can never collapse to one slot.\n */\nfunction keyOf<V, C extends Cmd>(\n config: ThrottledInputWithCache<V, C>,\n value: V,\n): string {\n return config.cacheKey(value);\n}\n\n/**\n * Narrow `config` to its dedupe variant iff a cache slice is actually wired —\n * `cacheTtlMs` set AND a `cache` store present. Returns the narrowed config\n * (carrying the required `cacheKey`) or `null` when no dedupe is configured.\n * The single place the union is discriminated, so `isCacheHit` / `commitEmit`\n * share one narrowing rather than re-deriving the predicate.\n */\nfunction cacheConfig<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n): ThrottledInputWithCache<V, C> | null {\n return config.cacheTtlMs !== undefined && state.cache !== undefined\n ? config\n : null;\n}\n\n/**\n * Whether `value` is a cache HIT under `config` at `at` — present and unexpired.\n * PURE — reads only. Always `false` when `cacheTtlMs` is unset or no `cache`\n * slice exists (no dedupe configured), so the dedupe gate is a no-op for an\n * unconfigured gate.\n */\nfunction isCacheHit<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n value: V,\n at: number,\n): boolean {\n const cached = cacheConfig(config, state);\n if (cached === null || state.cache === undefined) return false;\n return cacheGet(state.cache, keyOf(cached, value), at) !== undefined;\n}\n\n/**\n * Emit `value` at clock `at`: bump the rate cursor, refresh the dedupe cache,\n * and clear any pending hold. PURE. The single place an emit's bookkeeping\n * lives, shared by `input` (immediate emit) and `onFlush` (trailing emit) so the\n * two paths can never drift on what an emit updates.\n *\n * - `lastAt` advances to `at` — the next input is rate-measured against this\n * emit, the `../throttle` window cursor.\n * - `cache` (when configured) records `value` under its key with `cacheTtlMs`\n * from `at` — so the next identical input within the TTL is suppressed.\n * - `pending` clears — the value just went out; nothing is waiting.\n */\nfunction commitEmit<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n value: V,\n at: number,\n): readonly [ThrottledInput<V>, readonly C[]] {\n const cached = cacheConfig(config, state);\n const cache =\n cached !== null && state.cache !== undefined\n ? cacheSet(\n state.cache,\n keyOf(cached, value),\n value,\n at,\n cached.cacheTtlMs,\n )\n : state.cache;\n const next: ThrottledInput<V> = {\n ...state,\n lastAt: at,\n pending: null,\n ...(cache !== undefined ? { cache } : {}),\n };\n return [next, [config.emit(value)]];\n}\n\n/**\n * Feed a new input `value` arriving at clock `at` through the gates. PURE.\n *\n * The gate order is DEDUPE → RATE → SETTLE (each skipped when its config field\n * is omitted), the same cache-first-then-gate order `resilient-call` uses (a hit\n * short-circuits before the expensive path):\n *\n * 1. **Dedupe** (`cacheTtlMs` set): if `value`'s key is cached and unexpired,\n * suppress — return the slice UNCHANGED (no emit, no pending, no re-arm).\n * The downstream effect already ran for this key within its TTL.\n * 2. **Rate** (`throttleMs` set): if `at` is within `throttleMs` of the last\n * emit (`at - lastAt < throttleMs`), this input is rate-blocked.\n * - With `debounceMs` set → HOLD it as `pending` (the trailing edge: the\n * settle timer will emit it at a legal moment, so the final sample is\n * never lost). The deadline `subs` arms targets `at + debounceMs`.\n * - Without `debounceMs` → DROP it (pure rate cap, no catch-up).\n * 3. **Settle** (`debounceMs` set): the input cleared the rate gate. HOLD it\n * as `pending` and let the debounce window slide; `onFlush` emits it once\n * the window expires. A fresh input before expiry overwrites `pending` (the\n * trailing edge fires with the LAST value, the `../debounce` default).\n * 4. **Pass-through**: no `debounceMs`, cleared the rate gate → EMIT now.\n *\n * @param config the knob (the three optional gates + `emit`)\n * @param state the current slice\n * @param value the input value\n * @param at the arrival instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function input<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n value: V,\n at: number,\n): readonly [ThrottledInput<V>, readonly C[]] {\n // 1. Dedupe gate — a cached, unexpired key short-circuits everything.\n if (isCacheHit(config, state, value, at)) {\n return [state, []];\n }\n\n // 2. Rate gate — half-open cutoff (`>=` passes). `lastAt === null` (no prior\n // emit) always passes: the first input is never rate-blocked. Shared with\n // `onFlush` via `rateBlockedAt` so the arrival and trailing-edge paths agree\n // on the window.\n if (rateBlockedAt(config, state, at)) {\n // With a settle window, a rate-blocked input is HELD (trailing edge) so it\n // emits at the next legal moment; without one, it is a pure drop.\n if (config.debounceMs !== undefined) {\n return [{ ...state, pending: { value, at } }, []];\n }\n return [state, []];\n }\n\n // 3. Settle gate — passed the rate gate; if a debounce window is configured,\n // hold the value and (re)arm the settle deadline rather than emit now.\n if (config.debounceMs !== undefined) {\n return [{ ...state, pending: { value, at } }, []];\n }\n\n // 4. Pass-through — no settle wait, cleared the rate gate: emit immediately.\n return commitEmit(config, state, value, at);\n}\n\n/**\n * Whether emitting at clock `at` would breach the rate cap — `at` lands within\n * `throttleMs` of the last emit (`at - lastAt < throttleMs`). PURE — reads only.\n * Always `false` when `throttleMs` is unset (no rate cap) or before the first\n * emit (`lastAt === null`), so an unconfigured / never-fired gate never holds on\n * the rate gate. The single rate-gate predicate, shared by `input` (drop / hold\n * on arrival) and `onFlush` (re-hold a still-early trailing edge) so the two\n * paths can never drift on what \"within the rate window\" means.\n */\nfunction rateBlockedAt<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n at: number,\n): boolean {\n return (\n config.throttleMs !== undefined &&\n state.lastAt !== null &&\n at - state.lastAt < config.throttleMs\n );\n}\n\n/**\n * Emit the pending value because the settle (debounce) window closed. PURE.\n *\n * - Pending present, rate gate CLEAR → emit `config.emit(pending.value)`,\n * advance the rate cursor (`lastAt = at`), refresh the dedupe cache, and clear\n * `pending`. The trailing edge of `../debounce` realized as a Model transition.\n * - Pending present, rate gate STILL BLOCKED → re-HOLD: return the slice with\n * `pending` intact and NO Cmds. A settle Msg can fire at `pending.at +\n * debounceMs`, which may still be inside the rate window (`lastAt +\n * throttleMs`) — emitting then would breach the headline \"at most once per\n * `throttleMs`\" cap. Re-holding keeps the value waiting; `subs` re-arms the\n * deadline at `max(pending.at + debounceMs, lastAt + throttleMs)` so the next\n * timer fires at a LEGAL moment and the trailing edge is deferred, never lost.\n * - Pending absent → no-op: return the same slice and no Cmds. Guards the race\n * where an `input` already emitted (or a dedupe hit cleared interest) before\n * the settle timer's Msg lands — a stale `onFlush` must NOT emit. Symmetric\n * with `batch-window`'s closed-window guard.\n *\n * `at` is the firing instant the settle Msg carried (the caller stamps it; the\n * verb never reads the clock). It becomes the new rate cursor and the cache\n * write's `nowMs`, so a settled emit rate-blocks subsequent inputs from its\n * actual fire moment, not the moment the value was first held.\n *\n * @param config the knob (`throttleMs`, `emit`, cache config)\n * @param state the current slice\n * @param at the settle-timer firing instant (epoch ms)\n */\nexport function onFlush<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n at: number,\n): readonly [ThrottledInput<V>, readonly C[]] {\n // No pending value = an immediate emit already fired, or a dedupe hit cleared\n // interest. A stale timer Msg must not emit — return the slice untouched.\n if (state.pending === null) return [state, []];\n // Rate gate re-check: a settle Msg can land before the rate window opens (its\n // naive deadline is pending.at + debounceMs, which `subs` lifts to a legal\n // target — but a stale / early timer can still arrive). Emitting now would\n // breach \"at most once per throttleMs\", so re-hold and let `subs` re-arm at\n // the legal moment. The value stays pending; nothing is dropped.\n if (rateBlockedAt(config, state, at)) return [state, []];\n return commitEmit(config, state, state.pending.value, at);\n}\n\n/**\n * The settle timer Sub, derived from the slice. PURE.\n *\n * Returns the single `../deadline` Sub while a value is held AND a settle window\n * is configured, or `[]` otherwise. The deadline target is the LATER of two\n * constraints (so the timer never fires before BOTH are satisfied):\n *\n * - `pending.at + debounceMs` — the settle (debounce) deadline: the value has\n * stopped sliding, the burst has gone quiet.\n * - `lastAt + throttleMs` — the rate cap: emitting earlier would breach \"at\n * most once per `throttleMs`\". Only applies once a prior emit exists\n * (`lastAt !== null`) and a rate cap is configured (`throttleMs` set).\n *\n * `max(settleAt, rateAt)` is the earliest LEGAL trailing-edge moment. Anchoring\n * the timer here — rather than the naive `pending.at + debounceMs` — is what\n * keeps the rate cap intact across the trailing edge: a value held while still\n * inside the rate window arms its timer at `lastAt + throttleMs`, so `onFlush`\n * fires at a moment the rate gate already permits. (`onFlush` re-checks the gate\n * anyway and re-holds if an early/stale timer slips through, so the cap holds\n * even if a timer fires ahead of this target.)\n *\n * The substrate's reconcile pass arms the timer when the array gains the Sub and\n * disarms it (clearing the pending `setTimeout`) when the array loses it — so an\n * `onFlush` (or a dedupe hit) that clears `pending` auto-cancels the now-stale\n * settle timer on the next transition, and a sliding input that re-stamps\n * `pending.at` re-arms a fresh absolute target.\n *\n * `debounceMs` undefined ⇒ no settle window ⇒ `[]` even with a `pending` value\n * (an unconfigured settle never holds, but the guard keeps `subs` total).\n *\n * @param config the knob (`debounceMs`, `throttleMs`)\n * @param state the current slice\n * @param id stable identity for this gate's settle timer (default\n * `\"throttled-input:settle\"`); pass a distinct id per gate when a\n * machine runs several so the dispatched Msg routes distinctly.\n */\nexport function subsFor<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n id = DEFAULT_SUB_ID,\n): readonly ThrottledInputSub[] {\n if (config.debounceMs === undefined || state.pending === null) return [];\n const settleAt = state.pending.at + config.debounceMs;\n // The rate floor: the earliest moment the rate cap permits an emit. Absent\n // when no rate cap is configured or no prior emit set a cursor — then the\n // settle deadline alone governs.\n const rateAt =\n config.throttleMs !== undefined && state.lastAt !== null\n ? state.lastAt + config.throttleMs\n : settleAt;\n return [deadlineSub(id, Math.max(settleAt, rateAt))];\n}\n\n/**\n * The bound knob returned by `createThrottledInput`. `init` + the two pure verbs\n * + `subs`, all closing over one `ThrottledInputConfig`. The verbs return\n * `readonly [ThrottledInput<V>, readonly C[]]`: the next slice and the Cmds to\n * emit (one `emit` Cmd when a value passes, none while held / dropped / deduped).\n *\n * Pure-ops + subs module: there is no `handlers(ports)` cell. The settle timer's\n * subscribe cell IS `../deadline`'s `subscribeDeadline` (re-exported below) —\n * the consumer wires it into `subscribe` directly; the knob redraws no timer\n * lifecycle of its own. (Compare `poller`, identical: it too reuses\n * `../deadline`'s cell and ships no `handlers`.)\n */\nexport interface ThrottledInputKnob<V, C extends Cmd> {\n /** Seed the Model slice (with a cache iff `cacheTtlMs` is configured). */\n init(): ThrottledInput<V>;\n /**\n * Feed a new input through the dedupe → rate → settle gates. See `input`.\n */\n input(\n state: ThrottledInput<V>,\n value: V,\n at: number,\n ): readonly [ThrottledInput<V>, readonly C[]];\n /**\n * Emit the held value because the settle window closed. See `onFlush`.\n */\n onFlush(\n state: ThrottledInput<V>,\n at: number,\n ): readonly [ThrottledInput<V>, readonly C[]];\n /**\n * The settle timer Sub, armed only while a value is held. See `subsFor`.\n * `id` keys the deadline so several gates on one machine route distinctly.\n */\n subs(state: ThrottledInput<V>, id?: string): readonly Sub[];\n}\n\n/**\n * Build a throttled-input knob from `config`. Combinator form (the default L2\n * ergonomic): hand it the config, splice the returned hooks into your machine.\n * The pure verbs (`input` / `onFlush`) close over the config so the consumer's\n * reducer cells stay a uniform `(state, ...args)` shape; `subs` pre-wires the\n * settle timer.\n *\n * `init()` seeds a `cache` slice IFF `cacheTtlMs` is configured — an unconfigured\n * gate carries no cache field (nothing to serialize, no dedupe). Pure factory —\n * no clock read, no timer; everything happens when the consumer calls the\n * returned verbs / `subs` with an injected `at`.\n *\n * @typeParam V the input value type\n * @typeParam C the Cmd type `config.emit` returns\n */\nexport function createThrottledInput<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n): ThrottledInputKnob<V, C> {\n function init(): ThrottledInput<V> {\n const base = initThrottledInput<V>();\n // Seed the cache only when dedupe is configured — an unconfigured gate\n // carries no cache field at all (the no-dedupe shape).\n return config.cacheTtlMs !== undefined\n ? { ...base, cache: initCache<V>() }\n : base;\n }\n\n return {\n init,\n input: (state, value, at) => input(config, state, value, at),\n onFlush: (state, at) => onFlush(config, state, at),\n subs: (state, id) => subsFor(config, state, id),\n };\n}\n\n/**\n * The settle-timer wiring, RE-EXPORTED from `../deadline` (not redrawn) so the\n * timer lifecycle stays owned there. Consolidated into one `export ... from`:\n *\n * - `subscribeThrottledInput` (= `../deadline`'s `subscribeDeadline`) — the\n * `subscribe[\"deadline\"]` cell. Arms a one-shot timer for `max(0, atMs -\n * now)`, dispatches the deadline Msg, returns a cleanup that clears it. The\n * consumer assigns it: `subscribe: { deadline: subscribeThrottledInput }`.\n * - `throttledInputSettled` (= `../deadline`'s `deadlineExceeded`) +\n * `ThrottledInputSettled` (= `DeadlineExceeded`) — the settle-timer Msg\n * constructor and type the consumer handles via `onFlush`. Sharing one\n * constructor with the subscribe cell keeps the two from drifting; the\n * reducer reads `msg.atMs` as the `at` it threads into `onFlush`.\n */\nexport {\n type DeadlineExceeded as ThrottledInputSettled,\n deadlineExceeded as throttledInputSettled,\n subscribeDeadline as subscribeThrottledInput,\n} from \"../deadline\";\n"]}
1
+ {"version":3,"sources":["../../src/throttled-input/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @demlik/tea/throttled-input — gate a high-frequency input stream into a\n * SETTLED, RATE-CAPPED, optionally DEDUPED sequence of emits, as a TEA knob: one\n * config object + a few pre-wired hooks you spread into your machine.\n *\n * Layering (the throttle-family map): `../throttle` and `../debounce` are the\n * two host-boundary PRIMITIVES — two distinct algorithms\n * (at-most-once-per-window vs collapse-a-burst) behind one signature shape.\n * THIS module is the third name but NOT a third primitive: a TEA machine\n * composed OVER them that re-exports both. Three exports, one layering — not\n * duplication. The reuse list below spells out composition vs reimplemented\n * semantic per sibling.\n *\n * What it reuses, and HOW (composition vs. reimplemented semantic — the two are\n * not the same, so this list keeps them apart):\n *\n * - `../cache` — COMPOSED directly. The DEDUPE / memoize layer is `../cache`'s\n * `TtlCache` plus its `get` / `set` / `initCache` ops, called at runtime —\n * not reinvented. While `cacheTtlMs` is set, an input whose `cacheKey` is\n * already cached and unexpired is served from the cache and emits NOTHING\n * (the downstream effect ran once for that key within its TTL); a fresh emit\n * writes the value into the cache so the next identical input within\n * `cacheTtlMs` is suppressed. The optional eviction Sub is the consumer's to\n * wire if they want memory reclaim (this knob's correctness needs only TTL\n * reads).\n * - `../deadline` — COMPOSED directly. The settle timer IS a `../deadline`\n * Sub: `subsFor` builds it via `deadlineSub`, and the subscribe cell +\n * Msg constructor are RE-EXPORTS of `../deadline`'s own (`subscribeDeadline`\n * → `subscribeThrottledInput`, `deadlineExceeded` → `throttledInputSettled`).\n * The timer lifecycle stays owned by `../deadline`; this knob redraws none\n * of it.\n * - `../throttle` / `../debounce` — TYPE + SEMANTIC reuse only, NOT composed.\n * Those siblings are host-boundary SINK transformers that hold a live\n * `setTimeout`; a `setTimeout` in a closure is exactly what a durable,\n * replayable knob must NOT hold. So their CODE is never called here. Instead\n * the RATE-cap semantic (\"at most once per `ms`\", leading-edge) and the\n * SETTLE semantic (\"wait until quiet\", trailing-edge) are REIMPLEMENTED as\n * pure Model state: `lastAt` is the durable rate cursor (half-open cutoff\n * `at - lastAt >= throttleMs`), and `pending` + the `../deadline` Sub realize\n * the debounce window inside `update`. The `throttle` / `debounce`\n * transformers themselves are RE-EXPORTED (the only reuse of their actual\n * code) so a consumer can pre-throttle / pre-coalesce a genuinely bursty\n * HOST event at the boundary BEFORE it ever becomes an `input` Msg — a\n * separate, optional escape hatch, not part of this knob's gates.\n *\n * \"Omit a brick, omit its gate\" (the resilient-call config rule): every gate\n * field is optional. Omit `throttleMs` → no rate cap (every input passes the\n * rate gate). Omit `debounceMs` → no settle wait (an input that clears the rate\n * gate emits IMMEDIATELY rather than being held). Omit `cacheTtlMs` → no cache\n * slice at all (the `cache` field is absent; no dedupe). A config with all three\n * omitted is a pass-through: `input` always emits.\n *\n * Why this is a knob and not hand-wired logic: the managed state (`lastAt`,\n * `pending`, `cache`) lives in the Model, so it survives DO eviction / reload\n * (durable) and every transition is still a Msg through `update` (replayable). A\n * closure holding a `setTimeout` and a mutable \"last value\" would break both —\n * banned. `input` / `onFlush` are PURE: time arrives as an `at` parameter, never\n * `Date.now()`; they return NEW slice values, never mutate.\n *\n * Typical wiring (combinator form):\n *\n * const search = createThrottledInput<string, Msg>({\n * debounceMs: 200, // wait for the user to stop typing\n * throttleMs: 1_000, // but never fire more than 1/s during a long burst\n * cacheTtlMs: 30_000, // skip a query repeated within 30s\n * cacheKey: (q) => q,\n * emit: (q) => ({ type: \"RunSearch\", query: q }),\n * });\n *\n * // Model: { search: ThrottledInput<string>, ... }\n * // init: (loaded) => [loaded ?? { search: search.init(), ... }, []]\n * // update cells:\n * // QueryTyped: (s, m) => mapInput(s, search.input(s.search, m.value, m.at))\n * // SettleFired: (s, m) => mapInput(s, search.onFlush(s.search, m.atMs))\n * // subscriptions: (s) => search.subs(s.search)\n * // subscribe: { ...search.handlers() }\n *\n * where `mapInput(s, [slice, cmds]) => [{ ...s, search: slice }, cmds]` lifts the\n * slice transition into the consumer's Model. The `RunSearch` Cmd's interpret\n * handler is the consumer's — it is where the settled input meets I/O.\n *\n * NOT a substrate primitive: it depends only on sibling subpaths (`../throttle`,\n * `../debounce`, `../cache`, `../deadline`) and the core `Cmd` / `Sub` types.\n * Consumers reach it via the `@demlik/tea/throttled-input` subpath; the L1\n * escape hatch is those sibling subpaths threaded by hand. NOT exported from the\n * package root — same one-shape-per-package rule as `batch-window` and `poller`.\n */\n\nimport {\n get as cacheGet,\n set as cacheSet,\n initCache,\n type TtlCache,\n} from \"../cache\";\nimport { type DeadlineSub, deadlineSub } from \"../deadline\";\nimport type { Cmd, Sub } from \"../index\";\n\n// `../throttle` + `../debounce` — host-boundary pre-transformers (see file\n// header). Re-exported so a consumer that wants to throttle/debounce a bursty\n// HOST source BEFORE it becomes an `input` Msg reaches them from the same\n// import as the durable gate.\nexport { type Debounced, debounce } from \"../debounce\";\nexport { type Throttled, throttle } from \"../throttle\";\n\n/**\n * The gate fields every throttled-input config shares, regardless of whether\n * dedupe is on. The two dedupe variants below extend this with mutually\n * exclusive cache fields.\n *\n * `emit` is a pure Cmd CONSTRUCTOR, not a closure-with-state: it maps a settled\n * value to the single Cmd the runtime performs when the gates let a value\n * through. Keeping it `(value) => Cmd` (not `(value) => void`) is what keeps the\n * trigger replayable — the emit is a Cmd in the transition's output, never a\n * side effect inside the verb (mirrors `batch-window`'s `flush`).\n */\ninterface ThrottledInputGates<V, C extends Cmd> {\n /**\n * Rate cap, in milliseconds. An input that lands within `throttleMs` of the\n * last EMIT (`at - lastAt < throttleMs`) is dropped from immediate firing —\n * the leading-edge \"at most once per `throttleMs`\" rate-cap rule, applied to\n * emits. The half-open cutoff (`>=` passes) matches `../throttle` and\n * `../cache`'s window-edge convention. Omit for no rate cap.\n *\n * Interacts with `debounceMs`: when both are set, a rate-blocked input is\n * still HELD as `pending` (so the settle timer eventually trails it out at a\n * legal moment) rather than silently dropped — the trailing-edge guarantee\n * that the FINAL sample of a window is never lost.\n */\n readonly throttleMs?: number;\n /**\n * Settle wait, in milliseconds. An input that clears the rate gate is not\n * emitted immediately; it is held as `pending` and a debounce deadline is\n * armed at `pendingAt + debounceMs`. A fresh input re-arms (slides) the\n * window. `onFlush` emits the pending value when the window finally expires.\n * Omit for no settle wait — a passing input emits on arrival.\n */\n readonly debounceMs?: number;\n /**\n * Map a settled value to the single Cmd the runtime performs. Called with the\n * value the gates let through; the returned Cmd is what `input` / `onFlush`\n * emit. PURE constructor — no I/O, no clock; the I/O happens later in the\n * consumer's interpret handler for the returned Cmd.\n */\n readonly emit: (value: V) => C;\n}\n\n/**\n * Configuration for a throttled input — the knob. Pure config, no mutable state\n * (the `ThrottledInput<V>` slice carries that). One config object is shared\n * across every input of one logical stream (often a module-scope const).\n *\n * A DISCRIMINATED UNION on the presence of dedupe, so the type system makes a\n * cache-key collision UNREPRESENTABLE (\"omit a brick, omit its gate\"):\n *\n * - **No dedupe** (`ThrottledInputNoCache`): `cacheTtlMs` absent ⇒ no cache\n * slice, no dedupe. `cacheKey` is statically forbidden here (`never`) — a\n * key with no cache to feed is dead config, so the type rejects it rather\n * than silently ignoring it.\n * - **Dedupe** (`ThrottledInputWithCache`): `cacheTtlMs` set ⇒ `cacheKey` is\n * REQUIRED. There is no `String(value)` default to fall back to, so a\n * structured value can NEVER collapse two distinct values to one slot (the\n * `\"[object Object]\"` collision). The consumer must state the identity that\n * keys the cache; a primitive stream still trivially passes `(v) => v` /\n * `String`, but it does so EXPLICITLY.\n */\nexport type ThrottledInputConfig<V, C extends Cmd> =\n | ThrottledInputNoCache<V, C>\n | ThrottledInputWithCache<V, C>;\n\n/**\n * The no-dedupe config variant: gates only, no cache. `cacheTtlMs` is absent\n * and `cacheKey` is statically `never` so a stray key (which would feed no\n * cache) is a type error, not a silently-ignored field.\n */\nexport interface ThrottledInputNoCache<V, C extends Cmd>\n extends ThrottledInputGates<V, C> {\n readonly cacheTtlMs?: undefined;\n readonly cacheKey?: never;\n}\n\n/**\n * The dedupe config variant: a per-entry TTL plus a REQUIRED `cacheKey`. An\n * input whose `cacheKey` is already cached and unexpired emits NOTHING (the\n * downstream effect ran once for that key within its TTL); a fresh emit writes\n * the value into the cache.\n *\n * `cacheKey` is required (no `String(value)` default) precisely so a structured\n * value cannot silently collide — the consumer names the identity, the cache\n * keys on exactly that.\n */\nexport interface ThrottledInputWithCache<V, C extends Cmd>\n extends ThrottledInputGates<V, C> {\n /**\n * Dedupe / memoize TTL, in milliseconds. An input whose `cacheKey` is already\n * cached and unexpired emits NOTHING; a fresh emit writes the value into the\n * cache so the next identical input within `cacheTtlMs` is suppressed.\n */\n readonly cacheTtlMs: number;\n /**\n * Key a value for the dedupe cache. REQUIRED whenever `cacheTtlMs` is set:\n * there is no `String(value)` fallback, so a structured value's identity is\n * always the consumer's explicit choice and two distinct values can never\n * collapse to one slot. A primitive stream passes `(v) => v` / `String`.\n */\n readonly cacheKey: (value: V) => string;\n}\n\n/**\n * The Model slice a throttled input owns — its visible slice (the knob\n * principle: managed state lives in the Model, never a closure, so it is durable\n * and replayable).\n *\n * - `lastAt` — the `at` of the most recent EMIT, or `null` before the first\n * emit. The `../throttle` rate cap measures against it: an input\n * within `throttleMs` of `lastAt` is rate-blocked. Persisted so a\n * resumed gate keeps its rate cursor.\n * - `pending` — the value HELD for the settle (debounce) window, paired with the\n * `at` it was held (the anchor the deadline targets), or `null`\n * when nothing is waiting. The single source of truth for \"is a\n * settle window open?\" — the debounce timer Sub arms iff this is\n * non-null.\n * - `cache` — the `../cache` TTL store backing dedupe, present IFF\n * `cacheTtlMs` is configured. Absent (the field omitted) for a\n * gate with no dedupe — no per-op cost, nothing to serialize.\n */\nexport interface ThrottledInput<V> {\n readonly lastAt: number | null;\n readonly pending: PendingInput<V> | null;\n readonly cache?: TtlCache<V>;\n}\n\n/**\n * A value held for the settle window: the `value` itself and the `at` it was\n * held (the anchor the debounce deadline targets, `at + debounceMs`). A distinct\n * type (not a bare `V`) because `V` may itself be `null` — a present-but-null\n * value is still a real pending input, distinct from \"nothing pending\", exactly\n * the `null`-sentinel discipline `../throttle` / `../debounce` use for their\n * `pendingArgs`.\n */\nexport interface PendingInput<V> {\n readonly value: V;\n readonly at: number;\n}\n\n/**\n * The Sub variant a throttled input's settle timer produces — a `../deadline`\n * Sub (so the window fires at the correct ABSOLUTE moment even after a late\n * subscribe / rehydrate, and so a consumer running several gates routes each by\n * `id`). `atMs` is the absolute settle-by instant (`pendingAt + debounceMs`).\n */\nexport type ThrottledInputSub = DeadlineSub;\n\n/**\n * The stable Sub id the gate arms its settle deadline under. A single gate arms\n * exactly one settle timer at a time, so one id suffices; a consumer running\n * several gates on one machine namespaces them by composing the knob under\n * distinct slices with distinct ids (the `subs(state, id)` parameter).\n */\nconst DEFAULT_SUB_ID = \"throttled-input:settle\";\n\n/**\n * The starting slice: no prior emit, nothing pending. The `cache` field is\n * seeded only when `cacheTtlMs` is configured (see `createThrottledInput`); the\n * free `initThrottledInput` leaves it absent (the no-dedupe shape).\n */\nexport function initThrottledInput<V>(): ThrottledInput<V> {\n return { lastAt: null, pending: null };\n}\n\n/**\n * Resolve a value's cache key under a DEDUPE config. PURE. Takes the narrowed\n * `ThrottledInputWithCache` (not the union) because `cacheKey` is only present —\n * and only meaningful — when dedupe is on; there is no `String(value)` default\n * to fall back to, so a structured value's identity is always the consumer's\n * explicit choice and two distinct values can never collapse to one slot.\n */\nfunction keyOf<V, C extends Cmd>(\n config: ThrottledInputWithCache<V, C>,\n value: V,\n): string {\n return config.cacheKey(value);\n}\n\n/**\n * Narrow `config` to its dedupe variant iff a cache slice is actually wired —\n * `cacheTtlMs` set AND a `cache` store present. Returns the narrowed config\n * (carrying the required `cacheKey`) paired with the resolved `cache` store, or\n * `null` when no dedupe is configured. The single place the union is\n * discriminated, so `isCacheHit` / `commitEmit` share one narrowing — and reuse\n * the resolved `cache` — rather than re-deriving the predicate and re-narrowing\n * `state.cache`.\n */\nfunction cacheConfig<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n): {\n readonly config: ThrottledInputWithCache<V, C>;\n readonly cache: TtlCache<V>;\n} | null {\n return config.cacheTtlMs !== undefined && state.cache !== undefined\n ? { config, cache: state.cache }\n : null;\n}\n\n/**\n * Whether `value` is a cache HIT under `config` at `at` — present and unexpired.\n * PURE — reads only. Always `false` when `cacheTtlMs` is unset or no `cache`\n * slice exists (no dedupe configured), so the dedupe gate is a no-op for an\n * unconfigured gate.\n */\nfunction isCacheHit<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n value: V,\n at: number,\n): boolean {\n const cached = cacheConfig(config, state);\n if (cached === null) return false;\n return cacheGet(cached.cache, keyOf(cached.config, value), at) !== undefined;\n}\n\n/**\n * Emit `value` at clock `at`: bump the rate cursor, refresh the dedupe cache,\n * and clear any pending hold. PURE. The single place an emit's bookkeeping\n * lives, shared by `input` (immediate emit) and `onFlush` (trailing emit) so the\n * two paths can never drift on what an emit updates.\n *\n * - `lastAt` advances to `at` — the next input is rate-measured against this\n * emit, the `../throttle` window cursor.\n * - `cache` (when configured) records `value` under its key with `cacheTtlMs`\n * from `at` — so the next identical input within the TTL is suppressed.\n * - `pending` clears — the value just went out; nothing is waiting.\n */\nfunction commitEmit<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n value: V,\n at: number,\n): readonly [ThrottledInput<V>, readonly C[]] {\n const cached = cacheConfig(config, state);\n const cache =\n cached !== null\n ? cacheSet(\n cached.cache,\n keyOf(cached.config, value),\n value,\n at,\n cached.config.cacheTtlMs,\n )\n : state.cache;\n const next: ThrottledInput<V> = {\n ...state,\n lastAt: at,\n pending: null,\n ...(cache !== undefined ? { cache } : {}),\n };\n return [next, [config.emit(value)]];\n}\n\n/**\n * Feed a new input `value` arriving at clock `at` through the gates. PURE.\n *\n * The gate order is DEDUPE → RATE → SETTLE (each skipped when its config field\n * is omitted), the same cache-first-then-gate order `resilient-call` uses (a hit\n * short-circuits before the expensive path):\n *\n * 1. **Dedupe** (`cacheTtlMs` set): if `value`'s key is cached and unexpired,\n * suppress — return the slice UNCHANGED (no emit, no pending, no re-arm).\n * The downstream effect already ran for this key within its TTL.\n * 2. **Rate** (`throttleMs` set): if `at` is within `throttleMs` of the last\n * emit (`at - lastAt < throttleMs`), this input is rate-blocked.\n * - With `debounceMs` set → HOLD it as `pending` (the trailing edge: the\n * settle timer will emit it at a legal moment, so the final sample is\n * never lost). The deadline `subs` arms targets `at + debounceMs`.\n * - Without `debounceMs` → DROP it (pure rate cap, no catch-up).\n * 3. **Settle** (`debounceMs` set): the input cleared the rate gate. HOLD it\n * as `pending` and let the debounce window slide; `onFlush` emits it once\n * the window expires. A fresh input before expiry overwrites `pending` (the\n * trailing edge fires with the LAST value, the `../debounce` default).\n * 4. **Pass-through**: no `debounceMs`, cleared the rate gate → EMIT now.\n *\n * @param config the knob (the three optional gates + `emit`)\n * @param state the current slice\n * @param value the input value\n * @param at the arrival instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function input<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n value: V,\n at: number,\n): readonly [ThrottledInput<V>, readonly C[]] {\n // 1. Dedupe gate — a cached, unexpired key short-circuits everything.\n if (isCacheHit(config, state, value, at)) {\n return [state, []];\n }\n\n // 2. Rate gate — half-open cutoff (`>=` passes). `lastAt === null` (no prior\n // emit) always passes: the first input is never rate-blocked. Shared with\n // `onFlush` via `rateBlockedAt` so the arrival and trailing-edge paths agree\n // on the window.\n if (rateBlockedAt(config, state, at)) {\n // With a settle window, a rate-blocked input is HELD (trailing edge) so it\n // emits at the next legal moment; without one, it is a pure drop.\n if (config.debounceMs !== undefined) {\n return [{ ...state, pending: { value, at } }, []];\n }\n return [state, []];\n }\n\n // 3. Settle gate — passed the rate gate; if a debounce window is configured,\n // hold the value and (re)arm the settle deadline rather than emit now.\n if (config.debounceMs !== undefined) {\n return [{ ...state, pending: { value, at } }, []];\n }\n\n // 4. Pass-through — no settle wait, cleared the rate gate: emit immediately.\n return commitEmit(config, state, value, at);\n}\n\n/**\n * Whether emitting at clock `at` would breach the rate cap — `at` lands within\n * `throttleMs` of the last emit (`at - lastAt < throttleMs`). PURE — reads only.\n * Always `false` when `throttleMs` is unset (no rate cap) or before the first\n * emit (`lastAt === null`), so an unconfigured / never-fired gate never holds on\n * the rate gate. The single rate-gate predicate, shared by `input` (drop / hold\n * on arrival) and `onFlush` (re-hold a still-early trailing edge) so the two\n * paths can never drift on what \"within the rate window\" means.\n */\nfunction rateBlockedAt<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n at: number,\n): boolean {\n return (\n config.throttleMs !== undefined &&\n state.lastAt !== null &&\n at - state.lastAt < config.throttleMs\n );\n}\n\n/**\n * Emit the pending value because the settle (debounce) window closed. PURE.\n *\n * - Pending present, rate gate CLEAR → emit `config.emit(pending.value)`,\n * advance the rate cursor (`lastAt = at`), refresh the dedupe cache, and clear\n * `pending`. The trailing edge of `../debounce` realized as a Model transition.\n * - Pending present, rate gate STILL BLOCKED → re-HOLD: return the slice with\n * `pending` intact and NO Cmds. A settle Msg can fire at `pending.at +\n * debounceMs`, which may still be inside the rate window (`lastAt +\n * throttleMs`) — emitting then would breach the headline \"at most once per\n * `throttleMs`\" cap. Re-holding keeps the value waiting; `subs` re-arms the\n * deadline at `max(pending.at + debounceMs, lastAt + throttleMs)` so the next\n * timer fires at a LEGAL moment and the trailing edge is deferred, never lost.\n * - Pending absent → no-op: return the same slice and no Cmds. Guards the race\n * where an `input` already emitted (or a dedupe hit cleared interest) before\n * the settle timer's Msg lands — a stale `onFlush` must NOT emit. Symmetric\n * with `batch-window`'s closed-window guard.\n *\n * `at` is the firing instant the settle Msg carried (the caller stamps it; the\n * verb never reads the clock). It becomes the new rate cursor and the cache\n * write's `nowMs`, so a settled emit rate-blocks subsequent inputs from its\n * actual fire moment, not the moment the value was first held.\n *\n * @param config the knob (`throttleMs`, `emit`, cache config)\n * @param state the current slice\n * @param at the settle-timer firing instant (epoch ms)\n */\nexport function onFlush<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n at: number,\n): readonly [ThrottledInput<V>, readonly C[]] {\n // No pending value = an immediate emit already fired, or a dedupe hit cleared\n // interest. A stale timer Msg must not emit — return the slice untouched.\n if (state.pending === null) return [state, []];\n // Rate gate re-check: a settle Msg can land before the rate window opens (its\n // naive deadline is pending.at + debounceMs, which `subs` lifts to a legal\n // target — but a stale / early timer can still arrive). Emitting now would\n // breach \"at most once per throttleMs\", so re-hold and let `subs` re-arm at\n // the legal moment. The value stays pending; nothing is dropped.\n if (rateBlockedAt(config, state, at)) return [state, []];\n return commitEmit(config, state, state.pending.value, at);\n}\n\n/**\n * The settle timer Sub, derived from the slice. PURE.\n *\n * Returns the single `../deadline` Sub while a value is held AND a settle window\n * is configured, or `[]` otherwise. The deadline target is the LATER of two\n * constraints (so the timer never fires before BOTH are satisfied):\n *\n * - `pending.at + debounceMs` — the settle (debounce) deadline: the value has\n * stopped sliding, the burst has gone quiet.\n * - `lastAt + throttleMs` — the rate cap: emitting earlier would breach \"at\n * most once per `throttleMs`\". Only applies once a prior emit exists\n * (`lastAt !== null`) and a rate cap is configured (`throttleMs` set).\n *\n * `max(settleAt, rateAt)` is the earliest LEGAL trailing-edge moment. Anchoring\n * the timer here — rather than the naive `pending.at + debounceMs` — is what\n * keeps the rate cap intact across the trailing edge: a value held while still\n * inside the rate window arms its timer at `lastAt + throttleMs`, so `onFlush`\n * fires at a moment the rate gate already permits. (`onFlush` re-checks the gate\n * anyway and re-holds if an early/stale timer slips through, so the cap holds\n * even if a timer fires ahead of this target.)\n *\n * The substrate's reconcile pass arms the timer when the array gains the Sub and\n * disarms it (clearing the pending `setTimeout`) when the array loses it — so an\n * `onFlush` (or a dedupe hit) that clears `pending` auto-cancels the now-stale\n * settle timer on the next transition, and a sliding input that re-stamps\n * `pending.at` re-arms a fresh absolute target.\n *\n * `debounceMs` undefined ⇒ no settle window ⇒ `[]` even with a `pending` value\n * (an unconfigured settle never holds, but the guard keeps `subs` total).\n *\n * @param config the knob (`debounceMs`, `throttleMs`)\n * @param state the current slice\n * @param id stable identity for this gate's settle timer (default\n * `\"throttled-input:settle\"`); pass a distinct id per gate when a\n * machine runs several so the dispatched Msg routes distinctly.\n */\nexport function subsFor<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n state: ThrottledInput<V>,\n id = DEFAULT_SUB_ID,\n): readonly ThrottledInputSub[] {\n if (config.debounceMs === undefined || state.pending === null) return [];\n const settleAt = state.pending.at + config.debounceMs;\n // The rate floor: the earliest moment the rate cap permits an emit. Absent\n // when no rate cap is configured or no prior emit set a cursor — then the\n // settle deadline alone governs.\n const rateAt =\n config.throttleMs !== undefined && state.lastAt !== null\n ? state.lastAt + config.throttleMs\n : settleAt;\n return [deadlineSub(id, Math.max(settleAt, rateAt))];\n}\n\n/**\n * The bound knob returned by `createThrottledInput`. `init` + the two pure verbs\n * + `subs`, all closing over one `ThrottledInputConfig`. The verbs return\n * `readonly [ThrottledInput<V>, readonly C[]]`: the next slice and the Cmds to\n * emit (one `emit` Cmd when a value passes, none while held / dropped / deduped).\n *\n * Pure-ops + subs module: there is no `handlers(ports)` cell. The settle timer's\n * subscribe cell IS `../deadline`'s `subscribeDeadline` (re-exported below) —\n * the consumer wires it into `subscribe` directly; the knob redraws no timer\n * lifecycle of its own. (Compare `poller`, identical: it too reuses\n * `../deadline`'s cell and ships no `handlers`.)\n */\nexport interface ThrottledInputKnob<V, C extends Cmd> {\n /** Seed the Model slice (with a cache iff `cacheTtlMs` is configured). */\n init(): ThrottledInput<V>;\n /**\n * Feed a new input through the dedupe → rate → settle gates. See `input`.\n */\n input(\n state: ThrottledInput<V>,\n value: V,\n at: number,\n ): readonly [ThrottledInput<V>, readonly C[]];\n /**\n * Emit the held value because the settle window closed. See `onFlush`.\n */\n onFlush(\n state: ThrottledInput<V>,\n at: number,\n ): readonly [ThrottledInput<V>, readonly C[]];\n /**\n * The settle timer Sub, armed only while a value is held. See `subsFor`.\n * `id` keys the deadline so several gates on one machine route distinctly.\n */\n subs(state: ThrottledInput<V>, id?: string): readonly Sub[];\n}\n\n/**\n * Build a throttled-input knob from `config`. Combinator form (the default L2\n * ergonomic): hand it the config, splice the returned hooks into your machine.\n * The pure verbs (`input` / `onFlush`) close over the config so the consumer's\n * reducer cells stay a uniform `(state, ...args)` shape; `subs` pre-wires the\n * settle timer.\n *\n * `init()` seeds a `cache` slice IFF `cacheTtlMs` is configured — an unconfigured\n * gate carries no cache field (nothing to serialize, no dedupe). Pure factory —\n * no clock read, no timer; everything happens when the consumer calls the\n * returned verbs / `subs` with an injected `at`.\n *\n * @typeParam V the input value type\n * @typeParam C the Cmd type `config.emit` returns\n */\nexport function createThrottledInput<V, C extends Cmd>(\n config: ThrottledInputConfig<V, C>,\n): ThrottledInputKnob<V, C> {\n function init(): ThrottledInput<V> {\n const base = initThrottledInput<V>();\n // Seed the cache only when dedupe is configured — an unconfigured gate\n // carries no cache field at all (the no-dedupe shape).\n return config.cacheTtlMs !== undefined\n ? { ...base, cache: initCache<V>() }\n : base;\n }\n\n return {\n init,\n input: (state, value, at) => input(config, state, value, at),\n onFlush: (state, at) => onFlush(config, state, at),\n subs: (state, id) => subsFor(config, state, id),\n };\n}\n\n/**\n * The settle-timer wiring, RE-EXPORTED from `../deadline` (not redrawn) so the\n * timer lifecycle stays owned there. Consolidated into one `export ... from`:\n *\n * - `subscribeThrottledInput` (= `../deadline`'s `subscribeDeadline`) — the\n * `subscribe[\"deadline\"]` cell. Arms a one-shot timer for `max(0, atMs -\n * now)`, dispatches the deadline Msg, returns a cleanup that clears it. The\n * consumer assigns it: `subscribe: { deadline: subscribeThrottledInput }`.\n * - `throttledInputSettled` (= `../deadline`'s `deadlineExceeded`) +\n * `ThrottledInputSettled` (= `DeadlineExceeded`) — the settle-timer Msg\n * constructor and type the consumer handles via `onFlush`. Sharing one\n * constructor with the subscribe cell keeps the two from drifting; the\n * reducer reads `msg.atMs` as the `at` it threads into `onFlush`.\n */\nexport {\n type DeadlineExceeded as ThrottledInputSettled,\n deadlineExceeded as throttledInputSettled,\n subscribeDeadline as subscribeThrottledInput,\n} from \"../deadline\";\n"]}