@demlik/tea 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -18
- package/dist/agent/index.d.ts +44 -13
- package/dist/agent/index.js +10 -10
- package/dist/{chunk-PCJA2HTZ.js → chunk-2774I242.js} +7 -7
- package/dist/chunk-2774I242.js.map +1 -0
- package/dist/{chunk-YIMOFXST.js → chunk-2R62Q5UK.js} +3 -3
- package/dist/chunk-2R62Q5UK.js.map +1 -0
- package/dist/{chunk-PQSDUX6B.js → chunk-5HVKDHY6.js} +3 -3
- package/dist/{chunk-PQSDUX6B.js.map → chunk-5HVKDHY6.js.map} +1 -1
- package/dist/{chunk-WTSOW2MD.js → chunk-72CN3VPY.js} +5 -5
- package/dist/{chunk-WTSOW2MD.js.map → chunk-72CN3VPY.js.map} +1 -1
- package/dist/{chunk-JVE2LBFJ.js → chunk-A73IJ27X.js} +3 -3
- package/dist/{chunk-JVE2LBFJ.js.map → chunk-A73IJ27X.js.map} +1 -1
- package/dist/{chunk-I7E7AD2S.js → chunk-AIXC33SE.js} +4 -4
- package/dist/chunk-AIXC33SE.js.map +1 -0
- package/dist/{chunk-FNGZMVIT.js → chunk-DVNXEB7V.js} +3 -3
- package/dist/chunk-DVNXEB7V.js.map +1 -0
- package/dist/{chunk-XVIKSRCK.js → chunk-EA47MI5Q.js} +4 -4
- package/dist/{chunk-XVIKSRCK.js.map → chunk-EA47MI5Q.js.map} +1 -1
- package/dist/{chunk-IHUNKHDX.js → chunk-FTCYY2DO.js} +3 -3
- package/dist/{chunk-IHUNKHDX.js.map → chunk-FTCYY2DO.js.map} +1 -1
- package/dist/{chunk-JVKU2UQC.js → chunk-HX3C6LDK.js} +3 -3
- package/dist/{chunk-JVKU2UQC.js.map → chunk-HX3C6LDK.js.map} +1 -1
- package/dist/{chunk-H2ELKHI6.js → chunk-JAKM53JF.js} +4 -4
- package/dist/chunk-JAKM53JF.js.map +1 -0
- package/dist/{chunk-JYNYG5SF.js → chunk-NGYFBD5G.js} +3 -3
- package/dist/chunk-NGYFBD5G.js.map +1 -0
- package/dist/{chunk-AIIXV4XB.js → chunk-RKVUVALV.js} +2 -2
- package/dist/chunk-RKVUVALV.js.map +1 -0
- package/dist/{chunk-C3G2QFEZ.js → chunk-TAKPC6SD.js} +4 -4
- package/dist/{chunk-C3G2QFEZ.js.map → chunk-TAKPC6SD.js.map} +1 -1
- package/dist/{chunk-BRX3243I.js → chunk-TGJLXZAU.js} +3 -3
- package/dist/{chunk-BRX3243I.js.map → chunk-TGJLXZAU.js.map} +1 -1
- package/dist/{chunk-AHW2CLPT.js → chunk-UHAQ4PI6.js} +4 -4
- package/dist/{chunk-AHW2CLPT.js.map → chunk-UHAQ4PI6.js.map} +1 -1
- package/dist/{chunk-H3MWNH7A.js → chunk-XWIE5V7R.js} +4 -4
- package/dist/{chunk-H3MWNH7A.js.map → chunk-XWIE5V7R.js.map} +1 -1
- package/dist/{core-Ce9ncpuv.d.ts → core-CtvAyPpH.d.ts} +153 -1
- package/dist/devtools/index.d.ts +17 -0
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +12 -12
- package/dist/do/index.js +11 -11
- package/dist/{drive-loop-CsUsW3Wo.d.ts → drive-loop-Dw1sGSsW.d.ts} +1 -1
- package/dist/{durable-effects-BmUrRA-h.d.ts → durable-effects-Ump5lSdO.d.ts} +1 -1
- package/dist/effect/index.d.ts +124 -5
- package/dist/effect/index.js +33 -4
- package/dist/effect/index.js.map +1 -1
- package/dist/extension/index.d.ts +84 -3
- package/dist/extension/index.js +2 -2
- package/dist/extension/index.js.map +1 -1
- package/dist/flow/index.d.ts +29 -13
- package/dist/flow/index.js +11 -11
- package/dist/flow/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +11 -3
- package/dist/idempotency/index.js +3 -3
- package/dist/{index-D8sYVzCI.d.ts → index-Bk2hBUdK.d.ts} +6 -1
- package/dist/{index-2kNxy6az.d.ts → index-CKtEKGyp.d.ts} +15 -3
- package/dist/{index-e36uEyl4.d.ts → index-CVU7SK3a.d.ts} +2 -2
- package/dist/{index-Dy3379ve.d.ts → index-CZZ3HI-Z.d.ts} +4 -4
- package/dist/{index-Ce5GYAuI.d.ts → index-CrcoVfd_.d.ts} +2 -2
- package/dist/{index-DWdg7R1l.d.ts → index-D3NY3VCN.d.ts} +5 -1
- package/dist/{index-B1Wzarkl.d.ts → index-Dl71V7kt.d.ts} +1 -1
- package/dist/{index-Bf9_bNql.d.ts → index-EIgfl83J.d.ts} +2 -2
- package/dist/index.d.ts +66 -5
- package/dist/index.js +2 -2
- package/dist/jev/index.d.ts +39 -27
- package/dist/jev/index.js +12 -8
- package/dist/jev/index.js.map +1 -1
- package/dist/{machine-CuojfZup.d.ts → machine-Dtre2TmN.d.ts} +9 -7
- package/dist/machine-viz/index.d.ts +2 -1
- package/dist/machine-viz/index.js +2 -2
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +2 -2
- package/dist/mem/index.js +2 -2
- package/dist/node/index.d.ts +11 -2
- package/dist/node/index.js +2 -2
- package/dist/node/index.js.map +1 -1
- package/dist/otel/index.d.ts +10 -10
- package/dist/otel/index.js +10 -10
- package/dist/paginate/index.d.ts +5 -4
- package/dist/paginate/index.js +5 -5
- package/dist/parity/index.d.ts +3 -3
- package/dist/parity/index.js +3 -3
- package/dist/pbt/index.d.ts +1 -1
- package/dist/pbt/index.js +2 -2
- package/dist/persistence/index.d.ts +5 -5
- package/dist/persistence/index.js +4 -4
- package/dist/promise/index.d.ts +7 -2
- package/dist/promise/index.js +4 -4
- package/dist/react/index.d.ts +2 -2
- package/dist/resilience/index.d.ts +7 -6
- package/dist/resilience/index.js +5 -5
- package/dist/resilience/index.js.map +1 -1
- package/dist/{index-BnsAqn0-.d.ts → run-fiBe7pOG.d.ts} +8 -5
- package/dist/{runtime-types-BKg2CGMx.d.ts → runtime-types-DEluQgGO.d.ts} +74 -2
- package/dist/testing/effect/index.d.ts +5 -5
- package/dist/testing/effect/index.js +8 -8
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +3 -3
- package/dist/testing/promise/index.d.ts +4 -4
- package/dist/testing/promise/index.js +5 -5
- package/dist/timing/index.d.ts +4 -4
- package/dist/timing/index.js +4 -4
- package/dist/types-CXNzlbAg.d.ts +9 -0
- package/dist/work-queue/index.d.ts +4 -4
- package/dist/work-queue/index.js +2 -2
- package/dist/work-queue/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-AIIXV4XB.js.map +0 -1
- package/dist/chunk-FNGZMVIT.js.map +0 -1
- package/dist/chunk-H2ELKHI6.js.map +0 -1
- package/dist/chunk-I7E7AD2S.js.map +0 -1
- package/dist/chunk-JYNYG5SF.js.map +0 -1
- package/dist/chunk-PCJA2HTZ.js.map +0 -1
- package/dist/chunk-YIMOFXST.js.map +0 -1
- package/dist/types-SzNSHKlV.d.ts +0 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { undefinedOnly, unchecked } from './chunk-4LPY67E4.js';
|
|
2
|
-
import { Cmd } from './chunk-
|
|
2
|
+
import { Cmd } from './chunk-RKVUVALV.js';
|
|
3
3
|
|
|
4
4
|
// src/internal/persistence/snapshot/index.ts
|
|
5
5
|
var DEFAULT_SNAPSHOT_KEY = "@@snapshot";
|
|
@@ -60,5 +60,5 @@ function createSnapshot(config) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export { createSnapshot, snapshotLoadDef, snapshotWriteDef };
|
|
63
|
-
//# sourceMappingURL=chunk-
|
|
64
|
-
//# sourceMappingURL=chunk-
|
|
63
|
+
//# sourceMappingURL=chunk-2R62Q5UK.js.map
|
|
64
|
+
//# sourceMappingURL=chunk-2R62Q5UK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/persistence/snapshot/index.ts"],"names":[],"mappings":";;;;AA0HA,IAAM,oBAAA,GAAuB,YAAA;AAwDtB,SAAS,gBAAA,GAAsB;AACpC,EAAA,OAAO,GAAA,CAAI,OAAO,gBAAA,EAAkB;AAAA,IAClC,OAAO,SAAA,EAKJ;AAAA,IACH,EAAA,EAAI,aAAA;AAAA,IACJ,GAAA,EAAK,CAAC,uBAAuB;AAAA,GAC9B,CAAA;AACH;AAmBO,SAAS,eAAA,GAAqB;AACnC,EAAA,OAAO,GAAA,CAAI,OAAO,eAAA,EAAiB;AAAA,IACjC,OAAO,SAAA,EAAoC;AAAA,IAC3C,IAAI,SAAA,EAAoB;AAAA,IACxB,GAAA,EAAK,CAAC,sBAAsB;AAAA,GAC7B,CAAA;AACH;AAmJO,SAAS,eAAkB,MAAA,EAAyC;AAIzE,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,IAAS,CAAA,GAAI,OAAO,KAAA,GAAQ,CAAA;AACjD,EAAA,MAAM,GAAA,GAAM,OAAO,GAAA,IAAO,oBAAA;AAC1B,EAAA,MAAM,gBAAgB,gBAAA,EAAoB;AAC1C,EAAA,MAAM,eAAe,eAAA,EAAmB;AAQxC,EAAA,SAAS,SAAA,CACP,KAAA,EACA,OAAA,EACA,EAAA,EAC0D;AAC1D,IAAA,MAAM,GAAA,GAAM,MAAM,GAAA,GAAM,CAAA;AACxB,IAAA,MAAM,OAAsB,EAAE,GAAG,KAAA,EAAO,SAAA,EAAW,GAAG,GAAA,EAAI;AAC1D,IAAA,OAAO,CAAC,IAAA,EAAM,CAAC,aAAA,CAAc,EAAE,GAAA,EAAK,GAAA,EAAK,EAAA,EAAI,OAAA,EAAS,CAAC,CAAC,CAAA;AAAA,EAC1D;AAEA,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,aAAA;AAAA,IACP,IAAA,EAAM,YAAA;AAAA,IAEN,IAAA,GAAsB;AACpB,MAAA,OAAO,EAAE,WAAW,CAAA,EAAG,GAAA,EAAK,GAAG,YAAA,EAAc,IAAA,EAAM,aAAa,IAAA,EAAK;AAAA,IACvE,CAAA;AAAA,IAEA,MAAA,CAAO,KAAA,EAAO,OAAA,EAAS,EAAA,EAAI;AACzB,MAAA,MAAM,SAAA,GAAY,MAAM,SAAA,GAAY,CAAA;AAEpC,MAAA,IAAI,YAAY,KAAA,EAAO;AACrB,QAAA,OAAO,CAAC,EAAE,GAAG,OAAO,SAAA,EAAU,EAAG,EAAE,CAAA;AAAA,MACrC;AAEA,MAAA,OAAO,SAAA,CAAU,KAAA,EAAO,OAAA,EAAS,EAAE,CAAA;AAAA,IACrC,CAAA;AAAA,IAEA,KAAA,CAAM,KAAA,EAAO,OAAA,EAAS,EAAA,EAAI;AACxB,MAAA,OAAO,SAAA,CAAU,KAAA,EAAO,OAAA,EAAS,EAAE,CAAA;AAAA,IACrC,CAAA;AAAA,IAEA,OAAA,CAAQ,OAAO,GAAA,EAAK;AAClB,MAAA,MAAM,EAAE,GAAA,EAAK,EAAA,EAAG,GAAI,GAAA,CAAI,GAAA;AAGxB,MAAA,IAAI,KAAA,CAAM,YAAA,KAAiB,IAAA,IAAQ,GAAA,IAAO,MAAM,YAAA,EAAc;AAC5D,QAAA,OAAO,CAAC,KAAA,EAAO,GAAA,CAAI,IAAI,CAAA;AAAA,MACzB;AACA,MAAA,OAAO,CAAC,EAAE,GAAG,KAAA,EAAO,YAAA,EAAc,KAAK,WAAA,EAAa,EAAA,EAAG,EAAG,GAAA,CAAI,IAAI,CAAA;AAAA,IACpE,CAAA;AAAA,IAEA,KAAK,KAAA,EAAO;AACV,MAAA,OAAO,CAAC,EAAE,GAAG,KAAA,EAAO,WAAW,CAAA,EAAE,EAAG,IAAI,IAAI,CAAA;AAAA,IAC9C,CAAA;AAAA,IAEA,YAAY,KAAA,EAAO;AAIjB,MAAA,OAAO,CAAC,OAAO,CAAC,YAAA,CAAa,EAAE,GAAA,EAAK,CAAC,CAAC,CAAA;AAAA,IACxC;AAAA,GACF;AACF","file":"chunk-2R62Q5UK.js","sourcesContent":["/**\n * internal/persistence/snapshot — periodic state checkpoint to a host store\n * (R2/KV-shaped). Internal since #49 — not published on any subpath.\n *\n * The problem it solves: a long-running machine (a multi-stage run, a paginated\n * crawl, a saga) lives in TEA's in-memory `State` and is persisted as ONE blob\n * by the substrate's `Store<S>` on every transition. That blob is fine for\n * crash recovery of the *runtime* — but it is NOT a coarse, named checkpoint a\n * human (or a separate recovery job) can point a restart at. `snapshot` adds\n * the \"every N units of progress, write a durable checkpoint to R2/KV\"\n * discipline on top, so a crashed long run restarts from the last checkpoint\n * instead of from zero (the field-guide pain row).\n *\n * Shape: `createSnapshot(config)` hands back plain functions and two\n * `Cmd.define`d Cmds (`write`, `load`) you call from your own `update` (ADR\n * 0022). The state it owns is a Model slice (durable + replayable, the two\n * non-negotiables); the verbs are PURE decisions; the I/O (the actual `put` /\n * `get` against your store) lives only in the two handlers you write (ADR 0021).\n *\n * It is a LEAF module: it imports only the core substrate (`../index`, for\n * `Cmd`) and no sibling subpath. (It does NOT reuse\n * `@demlik/tea/retry-backoff` — that is an even-leafier utility that depends on\n * nothing at all, not even the core; the kinship here is \"leaf with injected\n * time\", not a shared dependency.)\n * Nothing here reads the clock or the RNG on its own behalf — time enters as an\n * `at`/`now` parameter at the verb boundary and the store I/O is realized only\n * when the runtime performs the emitted `Cmd`. That keeps every verb\n * deterministic (invariant 2) and the checkpoint write a `Cmd` the runtime\n * interprets (invariant 3).\n *\n * The store is the handler's business, never this module's — so it never\n * imports `@cloudflare/workers-types` and can be checkpointed to anything (R2,\n * KV, a DO sub-key, an in-memory map in tests).\n *\n * Typical wiring:\n *\n * const snap = createSnapshot<RunState>({ every: 25, key: \"run/checkpoint\" });\n *\n * // machine state:\n * type State = { run: RunState; snapshot: SnapshotState };\n * cmds: [snap.write, snap.load],\n * init: (loaded) => loaded ? [loaded, []] : [{ run: initRun(), snapshot: snap.init() }, []],\n *\n * // in `update`, on each progress Msg, fold the snapshot slice:\n * progress: (s, msg) => {\n * const run = advance(s.run, msg);\n * const [snapshot, cmds] = snap.record(s.snapshot, run, msg.at);\n * return [{ ...s, run, snapshot }, cmds];\n * },\n * // the write completed → durable ack folds back through update:\n * snapshot_write_ok: (s, msg) => {\n * const [snapshot, cmds] = snap.confirm(s.snapshot, msg);\n * return [{ ...s, snapshot }, cmds];\n * },\n * snapshot_write_err: (s) => [s, []], // errors are data — decide here\n *\n * // RECOVERY — the half that makes a checkpoint a restart point, not just a\n * // backup. A `boot` Msg the host dispatches once after `run(...)` asks the\n * // store for the durable checkpoint; the answer folds the recovered payload\n * // back into the run slice:\n * boot: (s) => snap.requestLoad(s), // emits snapshot_load Cmd\n * snapshot_load_ok: (s, msg) => {\n * if (msg.value === null) return [s, []]; // no checkpoint → keep fresh init\n * const [snapshot, cmds] = snap.boot(s.snapshot);\n * return [{ ...s, run: msg.value, snapshot }, cmds];\n * },\n * snapshot_load_err: (s) => [s, []], // errors are data — decide here\n *\n * // and where the machine runs, the two handlers you write:\n * run(machine, {\n * interpret: {\n * snapshot_write: async (cmd, { ok, err }) => {\n * try { await r2.put(cmd.key, cmd.payload); return ok(undefined); }\n * catch (cause) { return err({ _tag: \"snapshot_write_failed\", cause }); }\n * },\n * snapshot_load: async (cmd, { ok, err }) => {\n * try { return ok(await r2.get(cmd.key)); }\n * catch (cause) { return err({ _tag: \"snapshot_load_failed\", cause }); }\n * },\n * },\n * });\n */\n\nimport {\n Cmd,\n type CmdOf,\n type SettledErr,\n type SettledOk,\n type TaggedError,\n} from \"../../../index\";\nimport { unchecked, undefinedOnly } from \"../../schema\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Config — the knob.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot knob.\n *\n * `every` is the cadence: a checkpoint is written once `every` units of\n * progress have accumulated since the last one (see `record`). It is the\n * `snapshotEvery` field `monitored-run` threads through to this module.\n *\n * `key` is the store key the checkpoint is written under. A single machine\n * keeps a single rolling checkpoint, so the key is fixed at config time rather\n * than passed per write — the latest checkpoint overwrites the previous one\n * (R2/KV are last-write-wins on a key). Defaults to `\"@@snapshot\"`, mirroring\n * `doStore`'s `@@state` default-key convention.\n */\nexport interface SnapshotConfig {\n /**\n * Write a checkpoint once this many progress units have accumulated since the\n * last write. `every: 1` checkpoints on every `record`; a value `<= 0` is\n * treated as `1` (every progress unit triggers a write) — a non-positive\n * cadence has no other sensible meaning, and clamping keeps `record` from\n * never checkpointing on a misconfigured `0`.\n */\n readonly every: number;\n /** Store key the rolling checkpoint is written under. Defaults to `\"@@snapshot\"`. */\n readonly key?: string;\n}\n\nconst DEFAULT_SNAPSHOT_KEY = \"@@snapshot\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Slice — the Model field this knob owns.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The snapshot bookkeeping slice. Plain data — JSON-serializable so it rides\n * inside the consumer's `Store<S>` blob and survives DO eviction / reload (the\n * \"durable\" non-negotiable). It carries NO payload: the thing being\n * checkpointed lives elsewhere in the machine's state; this slice only tracks\n * *when* to checkpoint and *what has been acknowledged durable*.\n *\n * - `sinceLast` — progress units accumulated since the last checkpoint\n * DECISION (reset to 0 the moment `record` decides to write, not when the\n * write lands). The cadence counter.\n * - `seq` — monotonic checkpoint sequence. Bumped each time a write is\n * emitted, so the write Cmd and its later acknowledgement can be matched\n * even if several are in flight. The \"version\" of the latest emitted\n * checkpoint.\n * - `lastSavedSeq` — the highest `seq` whose write the host has confirmed\n * durable (via `confirm`). `null` until the first confirmed write. The gap\n * `seq - lastSavedSeq` is the \"writes emitted but not yet acknowledged\"\n * depth — observable for backpressure / health.\n * - `lastSavedAt` — the `at` of the most recent confirmed write (epoch ms),\n * `null` until the first. Carried for observability, never compared inside\n * a verb (time is injected, never read here).\n */\nexport interface SnapshotState {\n readonly sinceLast: number;\n readonly seq: number;\n readonly lastSavedSeq: number | null;\n readonly lastSavedAt: number | null;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cmds + Msgs — the write effect, the read effect, and their settled Msgs.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The checkpoint-write Cmd a `record` / `force` decision emits. Plain data\n * (invariant 1 — Cmds are data, not closures): it carries the `payload` to\n * write, the `key` to write it under, the `seq` it represents, and the `at` it\n * was decided.\n *\n * `payload` is `V` — the consumer's serializable checkpoint value (the run\n * state, the cursor, whatever the machine wants restartable). The verb takes it\n * as an argument so the snapshot slice never has to hold a copy of the\n * machine's domain state.\n *\n * `Cmd.define`d (ADR 0014, 0021), minted per `V` by a factory the way\n * `workflowActivityDef<A>()` is, since the payload type is the knob's own\n * parameter. The handler you write puts the payload in your store and returns\n * `ok(undefined)`, or `err({ _tag: \"snapshot_write_failed\", … })`; the engine\n * mints `snapshot_write_ok` / `snapshot_write_err`.\n */\nexport function snapshotWriteDef<V>() {\n return Cmd.define(\"snapshot_write\", {\n input: unchecked<{\n readonly key: string;\n readonly seq: number;\n readonly at: number;\n readonly payload: V;\n }>(),\n ok: undefinedOnly,\n err: [\"snapshot_write_failed\"],\n });\n}\n/**\n * The Cmd a snapshot decision emits to write a checkpoint: `payload` under\n * `key`, at sequence `seq`.\n */\nexport type SnapshotWriteCmd<V> = CmdOf<ReturnType<typeof snapshotWriteDef<V>>>;\n\n/**\n * The checkpoint-READ Cmd a `requestLoad` decision emits — the recovery half of\n * the module. Plain data (invariant 1): it carries only the `key` to read. The\n * handler you write reads the store and returns `ok(payload)` — `null` when\n * the store has no checkpoint under `key` — or\n * `err({ _tag: \"snapshot_load_failed\", … })`.\n *\n * Distinct from `snapshot_write`: a write carries a payload OUT to the store; a\n * load asks the store to hand a payload BACK. The consumer dispatches the\n * triggering Msg once on resume (a `boot` Msg, never `init`'s rehydrate branch —\n * invariant 2), folds the recovered payload into its run slice, then continues.\n */\nexport function snapshotLoadDef<V>() {\n return Cmd.define(\"snapshot_load\", {\n input: unchecked<{ readonly key: string }>(),\n ok: unchecked<V | null>(),\n err: [\"snapshot_load_failed\"],\n });\n}\n/** The Cmd `requestLoad` emits to read the checkpoint saved under `key`. */\nexport type SnapshotLoadCmd<V = unknown> = CmdOf<\n ReturnType<typeof snapshotLoadDef<V>>\n>;\n\n/**\n * The Msg the engine mints when a write lands. Fold it through\n * `confirm(state, msg)` to advance `lastSavedSeq` / `lastSavedAt`. It carries\n * the write Cmd, so `msg.cmd.seq` matches the emitting decision and a late\n * acknowledgement of an older write can never regress a newer confirmed one.\n */\nexport type SnapshotSavedMsg<V = unknown> = SettledOk<\n \"snapshot_write\",\n SnapshotWriteCmd<V>,\n undefined\n>;\n\n/**\n * The Msg the engine mints when a write fails. Errors are data (never\n * swallowed): the consumer's reducer decides whether a failed checkpoint is\n * fatal, retryable (compose `@demlik/tea/retry-backoff`), or merely logged.\n * `msg.cmd.seq` identifies which checkpoint failed.\n */\nexport type SnapshotFailedMsg<V = unknown> = SettledErr<\n \"snapshot_write\",\n SnapshotWriteCmd<V>,\n TaggedError<\"snapshot_write_failed\">\n>;\n\n/**\n * The Msg the engine mints when a read resolves. `value` is the recovered\n * checkpoint, or `null` when the store has no checkpoint under `key` (fresh\n * boot — never checkpointed, or a wiped store). `null` is NOT an error — \"no\n * checkpoint yet\" is a normal first-run state, distinct from \"the read failed\"\n * (`snapshot_load_err`).\n */\nexport type SnapshotLoadedMsg<V> = SettledOk<\n \"snapshot_load\",\n SnapshotLoadCmd<V>,\n V | null\n>;\n\n/**\n * The Msg the engine mints when a read fails. Errors are data: a failed\n * recovery read is a visible signal the consumer's reducer decides on\n * (fatal-abort, retry, or boot-fresh-and-log). Kept apart from a `null` load\n * so \"the store has no checkpoint\" can never masquerade as \"the read failed\".\n */\nexport type SnapshotLoadFailedMsg<V = unknown> = SettledErr<\n \"snapshot_load\",\n SnapshotLoadCmd<V>,\n TaggedError<\"snapshot_load_failed\">\n>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The combinator.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The bundle `createSnapshot` returns: plain functions plus the two Cmd defs to\n * list in `cmds`. There are no subs: a checkpoint is driven by progress (a\n * `record` call from the consumer's reducer), not by a timer.\n */\nexport interface SnapshotKnob<V> {\n /** The `snapshot_write` Cmd def — list it in the machine's `cmds`. */\n readonly write: ReturnType<typeof snapshotWriteDef<V>>;\n /** The `snapshot_load` Cmd def — list it in the machine's `cmds`. */\n readonly load: ReturnType<typeof snapshotLoadDef<V>>;\n /** The initial slice — zero progress, no checkpoint emitted or confirmed yet. */\n init(): SnapshotState;\n /**\n * Account one unit of progress. PURE. Increments `sinceLast`; once the\n * accumulated count reaches the configured cadence, DECIDES to checkpoint:\n * resets `sinceLast` to 0, bumps `seq`, and emits a single `snapshot_write`\n * Cmd carrying `payload` + `key` + the new `seq` + `at`. Below the cadence it\n * returns the bumped slice and NO Cmd.\n *\n * `payload` is the checkpoint value to persist (the consumer's domain state);\n * `at` is the caller-stamped time the progress occurred (epoch ms) — injected,\n * never read from the clock here.\n */\n record(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly SnapshotWriteCmd<V>[]];\n /**\n * Force a checkpoint regardless of the cadence counter. PURE. Always bumps\n * `seq`, resets `sinceLast`, and emits the write Cmd. Use on a phase boundary\n * the cadence would otherwise miss — e.g. \"the run just finished, checkpoint\n * the terminal state now\" — so the last durable checkpoint is the final state,\n * not whatever the modulo happened to land on.\n */\n force(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly SnapshotWriteCmd<V>[]];\n /**\n * Record a confirmed durable write. PURE. Advances `lastSavedSeq` /\n * `lastSavedAt` to the acknowledged write's `seq` / `at`, but only forward:\n * an out-of-order acknowledgement of an OLDER `seq` (a slow write landing\n * after a newer one) is ignored, so the confirmed watermark is monotonic.\n * Fold the `snapshot_write_ok` Msg here.\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — an ack emits nothing) for uniformity with every sibling\n * knob, so it drops straight into a `snapshot_write_ok` cell.\n */\n confirm(\n state: SnapshotState,\n msg: SnapshotSavedMsg<V>,\n ): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Resume after a reload. PURE. Resets `sinceLast` to 0 because any\n * un-checkpointed progress accumulated before the crash is moot — recovery\n * resumes from the last DURABLE checkpoint, so the cadence counter starts\n * fresh from there. `seq` / `lastSavedSeq` / `lastSavedAt` are preserved\n * (they describe what is durable, which a reload does not change). Call from\n * a `boot` Msg the host dispatches once after `run(...)`, never from `init`'s\n * rehydrate branch (invariant 2).\n *\n * Returns the reducer-cell shape `readonly [SnapshotState, readonly Cmd[]]`\n * (with `Cmd.none` — a resume emits nothing) so it drops straight into a\n * `snapshot_load_ok` cell.\n */\n boot(state: SnapshotState): readonly [SnapshotState, readonly Cmd[]];\n /**\n * Ask the store for the durable checkpoint. PURE. Emits a single\n * `snapshot_load` Cmd carrying the configured `key`; returns the slice\n * UNCHANGED (a read decides nothing about the cadence bookkeeping — only the\n * later `snapshot_load_ok` fold, via `boot`, touches the slice). This is the\n * entry point of the recovery loop: the consumer dispatches a `boot` Msg once\n * after `run(...)` and folds `requestLoad` there.\n */\n requestLoad(\n state: SnapshotState,\n ): readonly [SnapshotState, readonly SnapshotLoadCmd<V>[]];\n}\n\n/**\n * Create a snapshot knob from a `SnapshotConfig`. The returned bundle is spliced\n * into a consumer machine: `init()` seeds the slice, `record` / `force` are\n * folded in reducer cells, `confirm` / `boot` handle the acknowledgement +\n * resume Msgs, and `write` / `load` are the Cmd defs whose handlers you write.\n */\nexport function createSnapshot<V>(config: SnapshotConfig): SnapshotKnob<V> {\n // Clamp the cadence to >= 1 at construction so `record`'s threshold check is\n // branchless and a misconfigured `0` / negative `every` checkpoints on every\n // progress unit instead of never.\n const every = config.every >= 1 ? config.every : 1;\n const key = config.key ?? DEFAULT_SNAPSHOT_KEY;\n const snapshotWrite = snapshotWriteDef<V>();\n const snapshotLoad = snapshotLoadDef<V>();\n\n /**\n * Emit the write Cmd for a checkpoint decision. Shared by `record`'s\n * cadence-hit branch and `force`. Bumps `seq`, resets `sinceLast`, returns the\n * new slice + the single `snapshot_write` Cmd. The input slice is never\n * mutated (invariant 1).\n */\n function emitWrite(\n state: SnapshotState,\n payload: V,\n at: number,\n ): readonly [SnapshotState, readonly SnapshotWriteCmd<V>[]] {\n const seq = state.seq + 1;\n const next: SnapshotState = { ...state, sinceLast: 0, seq };\n return [next, [snapshotWrite({ key, seq, at, payload })]];\n }\n\n return {\n write: snapshotWrite,\n load: snapshotLoad,\n\n init(): SnapshotState {\n return { sinceLast: 0, seq: 0, lastSavedSeq: null, lastSavedAt: null };\n },\n\n record(state, payload, at) {\n const sinceLast = state.sinceLast + 1;\n // Below the cadence: accumulate, emit nothing.\n if (sinceLast < every) {\n return [{ ...state, sinceLast }, []];\n }\n // Cadence reached: checkpoint. `emitWrite` resets the counter + bumps seq.\n return emitWrite(state, payload, at);\n },\n\n force(state, payload, at) {\n return emitWrite(state, payload, at);\n },\n\n confirm(state, msg) {\n const { seq, at } = msg.cmd;\n // Forward-only watermark: ignore a stale ack of an older seq so a slow\n // write landing after a newer one can't regress `lastSavedSeq`.\n if (state.lastSavedSeq !== null && seq <= state.lastSavedSeq) {\n return [state, Cmd.none];\n }\n return [{ ...state, lastSavedSeq: seq, lastSavedAt: at }, Cmd.none];\n },\n\n boot(state) {\n return [{ ...state, sinceLast: 0 }, Cmd.none];\n },\n\n requestLoad(state) {\n // A read decides nothing about the cadence bookkeeping — return the slice\n // unchanged and emit the single read Cmd. The recovered payload folds back\n // through `snapshot_load_ok` (the consumer's reducer), not here.\n return [state, [snapshotLoad({ key })]];\n },\n };\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { subscribeDeadline, deadlineExceeded, deadlineSub } from './chunk-
|
|
1
|
+
import { subscribeDeadline, deadlineExceeded, deadlineSub } from './chunk-TAKPC6SD.js';
|
|
2
2
|
|
|
3
3
|
// src/internal/flow/batch-window/index.ts
|
|
4
4
|
function initBatchWindow() {
|
|
@@ -42,5 +42,5 @@ function createBatchWindow(config) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export { addItem, batchWindowExpired, createBatchWindow, initBatchWindow, onWindow, subsFor, subscribeBatchWindow, timerFor };
|
|
45
|
-
//# sourceMappingURL=chunk-
|
|
46
|
-
//# sourceMappingURL=chunk-
|
|
45
|
+
//# sourceMappingURL=chunk-5HVKDHY6.js.map
|
|
46
|
+
//# sourceMappingURL=chunk-5HVKDHY6.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal/flow/batch-window/index.ts"],"names":[],"mappings":";;;AA0IO,SAAS,eAAA,GAAqC;AACnD,EAAA,OAAO,EAAE,MAAA,EAAQ,EAAC,EAAG,UAAU,IAAA,EAAK;AACtC;AAqBO,SAAS,kBAAA,CACd,IACA,IAAA,EACoB;AACpB,EAAA,OAAO,gBAAA,CAAiB,IAAI,IAAI,CAAA;AAClC;AAmEO,SAAS,OAAA,CACd,MAAA,EACA,KAAA,EACA,IAAA,EACA,EAAA,EACyC;AACzC,EAAA,MAAM,MAAA,GAAS,CAAC,GAAG,KAAA,CAAM,QAAQ,IAAI,CAAA;AAGrC,EAAA,MAAM,QAAA,GAAW,MAAM,QAAA,IAAY,EAAA;AAInC,EAAA,IAAI,OAAO,MAAA,IAAU,IAAA,CAAK,IAAI,CAAA,EAAG,MAAA,CAAO,QAAQ,CAAA,EAAG;AACjD,IAAA,OAAO,CAAC,iBAAmB,EAAG,CAAC,OAAO,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA;AAAA,EACtD;AAEA,EAAA,OAAO,CAAC,EAAE,MAAA,EAAQ,QAAA,EAAS,EAAG,EAAE,CAAA;AAClC;AAuBO,SAAS,QAAA,CACd,MAAA,EACA,KAAA,EACA,GAAA,EACyC;AAGzC,EAAA,IAAI,KAAA,CAAM,OAAO,MAAA,KAAW,CAAA,SAAU,CAAC,KAAA,EAAO,EAAE,CAAA;AAChD,EAAA,OAAO,CAAC,iBAAmB,EAAG,CAAC,OAAO,KAAA,CAAM,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA;AAC5D;AAqBO,SAAS,OAAA,CACd,MAAA,EACA,KAAA,EACA,EAAA,GAAK,cAAA,EACsB;AAK3B,EAAA,IAAI,KAAA,CAAM,QAAA,KAAa,IAAA,EAAM,OAAO,EAAC;AACrC,EAAA,OAAO,CAAC,WAAA,CAAY,EAAA,EAAI,MAAM,QAAA,GAAW,MAAA,CAAO,KAAK,CAAC,CAAA;AACxD;AAgBO,SAAS,QAAA,CACd,MAAA,EACA,KAAA,EACA,EAAA,GAAK,cAAA,EACiC;AACtC,EAAA,IAAI,KAAA,CAAM,QAAA,KAAa,IAAA,EAAM,OAAO,IAAA;AACpC,EAAA,OAAO;AAAA,IACL,IAAI,MAAA,CAAO,KAAA;AAAA,IACX,KAAK,kBAAA,CAAmB,EAAA,EAAI,KAAA,CAAM,QAAA,GAAW,OAAO,KAAK;AAAA,GAC3D;AACF;AASO,IAAM,oBAAA,GAIG;AAYT,SAAS,kBACd,MAAA,EACuB;AACvB,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,MAAM,eAAA,EAAmB;AAAA,IAC/B,GAAA,EAAK,CAAC,KAAA,EAAO,IAAA,EAAM,OAAO,OAAA,CAAQ,MAAA,EAAQ,KAAA,EAAO,IAAA,EAAM,EAAE,CAAA;AAAA,IACzD,UAAU,CAAC,KAAA,EAAO,OAAO,QAAA,CAAS,MAAA,EAAQ,KAAS,CAAA;AAAA,IACnD,OAAO,CAAC,KAAA,EAAO,OAAO,QAAA,CAAS,MAAA,EAAQ,OAAO,EAAE,CAAA;AAAA,IAChD,MAAM,CAAC,KAAA,EAAO,OAAO,OAAA,CAAQ,MAAA,EAAQ,OAAO,EAAE;AAAA,GAChD;AACF","file":"chunk-PQSDUX6B.js","sourcesContent":["/**\n * internal/flow/batch-window — coalesce a stream of items into size- or\n * time-bounded BATCHES, then flush each batch as a single Cmd.\n *\n * What this module actually DELEGATES to, vs. what it merely RE-EXPORTS:\n *\n * - `../deadline` — REAL delegation (the only one). The window timer's Msg\n * is `deadlineExceeded`, keyed on the window's identity. `timerFor` arms it\n * through the engine's built-in `timer` Sub: `maxMs` counted from the\n * transition that opened the window, so a machine needs no runner. The\n * absolute-instant form is still here too — `subsFor` lists the same\n * deadline for `deadlinesSub` + `subscribeDeadline`, which re-arms for the\n * REMAINING time after a `Store` rehydrate rather than a fresh `maxMs`.\n * - `../work-queue` — TYPE re-export only, NOT a sink this module calls. A\n * flush emits a Cmd; the CONSUMER's interpret handler is what enqueues onto a\n * `createQueue(store)`. This module never touches queue status — it has no\n * queue state and no status flip to delegate. It re-exports `EnqueueInput<I>`\n * purely so the consumer's flush Cmd and queue agree on the item shape; any\n * actual status flip belongs in the consumer and is delegated to\n * `../work-queue/ops` there, not here.\n * - `../debounce` — VALUE/TYPE re-export only, NOT wired into `update`. A\n * bursty source (keystrokes, scroll, websocket frames) can be `debounce`d at\n * the host BEFORE it ever becomes an `add` Msg, so the batch window only sees\n * settled items. `debounce` is a sink transformer OUTSIDE `update`;\n * batch-window is the durable, replayable half inside it. Re-exported for\n * one-import ergonomics — this module does not call it.\n *\n * Why this is a knob and not hand-wired logic: the managed state (the buffer +\n * the window-open instant) lives in the Model, so it survives DO eviction /\n * reload (durable) and every transition is still a Msg through `update`\n * (replayable). A closure holding a `setTimeout` and a mutable array would break\n * both — banned. `add` / `onWindow` are PURE: time arrives as an `at`\n * parameter, never `Date.now()`; they return NEW slice values, never mutate.\n *\n * Typical wiring (combinator form):\n *\n * const bw = createBatchWindow<LogLine, Msg>({\n * maxItems: 100,\n * maxMs: 5_000,\n * flush: (items) => ({ type: \"FlushBatch\", items }),\n * });\n *\n * // Model: { batch: BatchWindow<LogLine>, ... }\n * // init: (loaded) => [loaded ?? { batch: bw.init(), ... }, []]\n * // update cells:\n * // LogArrived: (s, m) => mapBatch(s, bw.add(s.batch, m.line, m.at))\n * // deadline_exceeded: (s, m) => mapBatch(s, bw.onWindow(s.batch, m.atMs))\n * // subs: [{ type: \"timer\", deps: (s: Model) => bw.timer(s.batch) }]\n * // at run: run(machine, { interpret }) // `timer` is built in\n *\n * where `mapBatch(s, [batch, cmds]) => [{ ...s, batch }, cmds]` lifts the slice\n * transition into the consumer's Model. The `FlushBatch` Cmd's interpret handler\n * is the consumer's — it is where the batch meets I/O (an `enqueue`, a network\n * POST, an R2 write).\n *\n * Internal since #48 — not published on any subpath; reached from inside the\n * package as `internal/flow/batch-window`. Same one-shape-per-package rule as `deadline`, `cache`, and\n * `work-queue`.\n */\n\nimport type { Cmd, Subscribe, TimerDeps } from \"../../../index\";\n\n// `../debounce` re-export (value + type) for one-import ergonomics — see file\n// header. This module does NOT call debounce; a consumer that wants to debounce\n// a bursty source BEFORE it becomes an `add` Msg reaches it from the same import\n// as the batch window.\nexport { type Debounced, debounce } from \"../../timing/debounce\";\n\nimport {\n type DeadlineExceeded,\n type DeadlineSub,\n type DeadlinesSub,\n deadlineExceeded,\n deadlineSub,\n subscribeDeadline,\n} from \"../../resilience/deadline\";\n\n// `../work-queue` TYPE re-export only (see file header). This module emits a\n// flush Cmd; the CONSUMER's interpret handler enqueues onto a\n// `createQueue(store)`. Re-export the caller-facing enqueue payload type so the\n// consumer's flush Cmd and queue agree on the item shape.\nexport type { EnqueueInput } from \"../../work-queue\";\n\n/**\n * Configuration for a batch window — the knob. Pure config, no mutable state\n * (the `BatchWindow<I>` slice carries that). One config object is shared across\n * every window of one logical stream (often a module-scope const).\n *\n * `flush` is a pure Cmd CONSTRUCTOR, not a closure-with-state: it maps the\n * buffered items to the single Cmd the runtime performs when a window closes.\n * Keeping it a `(items) => Cmd` (not a `(items) => void`) is what keeps the\n * trigger replayable — the flush is a Cmd in the transition's output, never a\n * side effect inside the verb.\n */\nexport interface BatchWindowConfig<I, C extends Cmd> {\n /**\n * Size trigger. A batch flushes the instant its buffer reaches `maxItems`,\n * WITHOUT waiting for the time window — the classic \"send as soon as the\n * page is full\" cutoff. Must be `>= 1`; a config of `maxItems: 0` would flush\n * an empty batch on every `add`, which is never useful, so callers treat `1`\n * as the floor (the verb still behaves: with `maxItems <= 1` every item\n * flushes on arrival, a degenerate but legal \"no batching\" window).\n */\n readonly maxItems: number;\n /**\n * Time trigger, in milliseconds. A batch flushes no later than `openedAt +\n * maxMs` — `openedAt` being the `at` of the item that opened the window. This\n * is the latency ceiling: an item never waits longer than `maxMs` to ship,\n * even if the buffer never reaches `maxItems`.\n */\n readonly maxMs: number;\n /**\n * Map a flushed batch to the single Cmd the runtime performs. Called with the\n * buffered items in arrival order; the returned Cmd is what `add` / `onWindow`\n * emit when a window closes. PURE constructor — no I/O, no clock; the I/O\n * happens later in the consumer's interpret handler for the returned Cmd.\n */\n readonly flush: (items: readonly I[]) => C;\n}\n\n/**\n * The Model slice a batch window owns. `buffer` is the items accumulated for the\n * current open window; `openedAt` is the `at` of the item that OPENED it (the\n * anchor the time trigger measures against), or `null` when the window is\n * closed (buffer empty). The pair is the single source of truth for \"is a\n * window open, and when must it flush by?\" — both plain data so the whole slice\n * is trivially JSON-serializable into a consumer's `Store<S>`.\n *\n * Invariant the verbs preserve: `buffer.length === 0` IFF `openedAt === null`.\n * A non-empty buffer always has an `openedAt`; an empty buffer never does. The\n * window timer Sub keys its arm/disarm off exactly this invariant.\n */\nexport interface BatchWindow<I> {\n readonly buffer: readonly I[];\n readonly openedAt: number | null;\n}\n\n/** The starting slice: closed window, empty buffer. */\nexport function initBatchWindow<I>(): BatchWindow<I> {\n return { buffer: [], openedAt: null };\n}\n\n/**\n * The deadline a batch window's open timer lists: it IS `../deadline`'s\n * `DeadlineSub`, not a re-tagged copy. `atMs` is the absolute flush-by instant\n * (`openedAt + maxMs`). Aliasing the deadline type (rather than redeclaring an\n * identical shape) is what lets `subsFor` return `deadlineSub(...)` with no\n * cast — the value's static type already satisfies this name. Consumers running\n * several windows route each one by the deadline's `id`, not by a distinct type.\n */\nexport type BatchWindowSub = DeadlineSub;\n\n/** The Msg a closed-by-time window dispatches. Alias of `deadline`'s Msg. */\nexport type BatchWindowExpired = DeadlineExceeded;\n\n/**\n * Construct the window-expired Msg for the window identified by `id`, flushing\n * by `atMs`. Re-exported alias of `deadlineExceeded` so the runner and\n * the consumer's reducer share one constructor rather than two literals that\n * can drift. The consumer handles it in a reducer cell by calling `onWindow`.\n */\nexport function batchWindowExpired(\n id: string,\n atMs: number,\n): BatchWindowExpired {\n return deadlineExceeded(id, atMs);\n}\n\n/**\n * The bound knob returned by `createBatchWindow`. `init` + the two pure verbs +\n * `timer` + `subs`, all closing over one `BatchWindowConfig`. The verbs\n * return `readonly [BatchWindow<I>, readonly C[]]`: the next slice and the Cmds\n * to emit (one `flush` Cmd when a window closes, none otherwise).\n */\nexport interface BatchWindowKnob<I, C extends Cmd> {\n /** The starting slice — `initBatchWindow()`. */\n init(): BatchWindow<I>;\n /**\n * Buffer `item` (arrival-stamped `at`), flushing immediately if that brings\n * the buffer to `maxItems`. See `addItem`.\n */\n add(\n state: BatchWindow<I>,\n item: I,\n at: number,\n ): readonly [BatchWindow<I>, readonly C[]];\n /**\n * Flush whatever is buffered because the time window closed. See `onWindow`.\n */\n onWindow(\n state: BatchWindow<I>,\n at: number,\n ): readonly [BatchWindow<I>, readonly C[]];\n /**\n * The built-in `timer` Sub's deps while a window is open, `null` while it is\n * closed. See `timerFor`. `id` keys the Msg so several windows on one\n * machine route distinctly.\n */\n timer(\n state: BatchWindow<I>,\n id?: string,\n ): TimerDeps<BatchWindowExpired> | null;\n /**\n * The window timer as an absolute deadline, listed only while a window is\n * open — for `deadlinesSub` + `subscribeBatchWindow`. See `subsFor`.\n */\n subs(state: BatchWindow<I>, id?: string): readonly BatchWindowSub[];\n}\n\n/**\n * Buffer `item` and decide whether the size trigger fires. PURE.\n *\n * - Empty (closed) window → `item` OPENS it: `openedAt` becomes `at`, buffer\n * becomes `[item]`. This is the anchor the time window measures against.\n * - Open window → `item` appends; `openedAt` is left untouched (the window's\n * latency clock keeps running from the FIRST item, not this one).\n *\n * After buffering, if the buffer has reached `maxItems` the window flushes\n * immediately: emit `config.flush(buffer)` and RESET to the closed slice. The\n * size trigger pre-empts the time trigger — a full page ships now, the window\n * Sub for the (now closed) window disarms on the next reconcile because `subs`\n * returns `[]` for an empty buffer.\n *\n * `maxItems <= 1` is the degenerate \"no batching\" case: every item flushes on\n * arrival (buffer reaches 1 immediately). Legal and useful as a feature flag to\n * disable batching without restructuring the wiring.\n *\n * @param config the knob (maxItems / flush constructor)\n * @param state the current slice\n * @param item the item to buffer\n * @param at the arrival instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function addItem<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n item: I,\n at: number,\n): readonly [BatchWindow<I>, readonly C[]] {\n const buffer = [...state.buffer, item];\n // A closed window opens at THIS item's `at`; an already-open window keeps its\n // original `openedAt` (the latency clock runs from the first item).\n const openedAt = state.openedAt ?? at;\n\n // Size trigger: a full page ships now, regardless of the time window. The\n // floor of 1 means `maxItems <= 1` flushes every item on arrival.\n if (buffer.length >= Math.max(1, config.maxItems)) {\n return [initBatchWindow<I>(), [config.flush(buffer)]];\n }\n\n return [{ buffer, openedAt }, []];\n}\n\n/**\n * Flush the open window because its time bound was reached. PURE.\n *\n * - Open window (buffer non-empty) → emit `config.flush(buffer)` and RESET to\n * the closed slice.\n * - Closed window (buffer empty) → no-op: return the same slice and no Cmds.\n * This guards the race where the size trigger already flushed and reset the\n * window before the time Sub's Msg lands — a stale `onWindow` after a\n * size-flush must NOT emit an empty batch.\n *\n * `at` is accepted for symmetry with `add` and so the consumer's reducer cell\n * has a uniform `(state, at)` shape; the verb doesn't need it for the flush\n * decision (the buffer's emptiness is the whole signal), but taking it keeps a\n * future \"flush only if the window is genuinely past `openedAt + maxMs`\"\n * tightening a non-breaking change.\n *\n * @param config the knob (flush constructor)\n * @param state the current slice\n * @param at the firing instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function onWindow<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n _at: number,\n): readonly [BatchWindow<I>, readonly C[]] {\n // Empty buffer = the size trigger already flushed (or nothing arrived). A\n // stale timer Msg must not ship an empty batch — return the slice untouched.\n if (state.buffer.length === 0) return [state, []];\n return [initBatchWindow<I>(), [config.flush(state.buffer)]];\n}\n\n/**\n * The window timer deadline, derived from the slice. PURE.\n *\n * Returns the single deadline targeting `openedAt + maxMs` while a window is\n * open (buffer non-empty), or `[]` while it is closed. Fed through\n * `deadlinesSub`, the engine arms the timer when the list gains the deadline\n * and disarms it (clearing the pending `setTimeout`) when the list loses it —\n * so a size-flush that empties the buffer auto-cancels the now-irrelevant time\n * trigger on the next transition.\n *\n * `id` defaults to `\"batch-window\"`; pass a distinct id per window when a\n * machine runs several so the dispatched `BatchWindowExpired` Msg routes to the\n * right one.\n *\n * @param config the knob (maxMs)\n * @param state the current slice\n * @param id stable identity for this window's timer (default\n * `\"batch-window\"`)\n */\nexport function subsFor<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n id = \"batch-window\",\n): readonly BatchWindowSub[] {\n // Closed window → no timer. The invariant (empty buffer IFF openedAt null)\n // means either check suffices; we read `openedAt` so the `atMs` arithmetic\n // below is non-null without a `!`. `deadlineSub` already returns a\n // `DeadlineSub`, which IS `BatchWindowSub` — no cast.\n if (state.openedAt === null) return [];\n return [deadlineSub(id, state.openedAt + config.maxMs)];\n}\n\n/**\n * The built-in `timer` Sub's deps for a batch window. PURE.\n *\n * While a window is open it counts `maxMs` down from the transition that\n * opened it — the deps are fixed for the window's life (`openedAt` never moves\n * while items append), so the engine starts the countdown exactly once — and\n * fires `batchWindowExpired(id, openedAt + maxMs)`. A closed window is `null`,\n * so a size-flush that empties the buffer cancels the countdown on the next\n * transition.\n *\n * Relative by construction: a process that rehydrates an open window counts a\n * fresh `maxMs`. Reach for `subsFor` + `subscribeBatchWindow` when the flush\n * must land at the original absolute instant.\n */\nexport function timerFor<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n id = \"batch-window\",\n): TimerDeps<BatchWindowExpired> | null {\n if (state.openedAt === null) return null;\n return {\n ms: config.maxMs,\n msg: batchWindowExpired(id, state.openedAt + config.maxMs),\n };\n}\n\n/**\n * The `deadline` runner for a batch window's absolute timer — the exact\n * `../deadline` runner. Arms a one-shot timer per listed deadline for\n * `max(0, atMs - now)` and dispatches `batchWindowExpired(...)`; returns a\n * cleanup that clears them. Hand it to `run` as `subscribe.deadline` when the\n * machine declares `deadlinesSub((s) => bw.subs(s.batch))`.\n */\nexport const subscribeBatchWindow: Subscribe<\n BatchWindowExpired,\n DeadlinesSub,\n unknown\n>[\"deadline\"] = subscribeDeadline;\n\n/**\n * Build a batch window knob from a config. Combinator form (the default L2\n * ergonomic): hand it the config, splice the ~4 returned hooks into your\n * machine. The pure verbs (`add` / `onWindow`) close over the config so the\n * consumer's reducer cells stay a uniform `(state, ...args)` shape; `timer`\n * arms the window timer.\n *\n * @typeParam I the item type buffered into each batch\n * @typeParam C the Cmd type `config.flush` returns\n */\nexport function createBatchWindow<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n): BatchWindowKnob<I, C> {\n return {\n init: () => initBatchWindow<I>(),\n add: (state, item, at) => addItem(config, state, item, at),\n onWindow: (state, at) => onWindow(config, state, at),\n timer: (state, id) => timerFor(config, state, id),\n subs: (state, id) => subsFor(config, state, id),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/internal/flow/batch-window/index.ts"],"names":[],"mappings":";;;AA0IO,SAAS,eAAA,GAAqC;AACnD,EAAA,OAAO,EAAE,MAAA,EAAQ,EAAC,EAAG,UAAU,IAAA,EAAK;AACtC;AAqBO,SAAS,kBAAA,CACd,IACA,IAAA,EACoB;AACpB,EAAA,OAAO,gBAAA,CAAiB,IAAI,IAAI,CAAA;AAClC;AAmEO,SAAS,OAAA,CACd,MAAA,EACA,KAAA,EACA,IAAA,EACA,EAAA,EACyC;AACzC,EAAA,MAAM,MAAA,GAAS,CAAC,GAAG,KAAA,CAAM,QAAQ,IAAI,CAAA;AAGrC,EAAA,MAAM,QAAA,GAAW,MAAM,QAAA,IAAY,EAAA;AAInC,EAAA,IAAI,OAAO,MAAA,IAAU,IAAA,CAAK,IAAI,CAAA,EAAG,MAAA,CAAO,QAAQ,CAAA,EAAG;AACjD,IAAA,OAAO,CAAC,iBAAmB,EAAG,CAAC,OAAO,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA;AAAA,EACtD;AAEA,EAAA,OAAO,CAAC,EAAE,MAAA,EAAQ,QAAA,EAAS,EAAG,EAAE,CAAA;AAClC;AAuBO,SAAS,QAAA,CACd,MAAA,EACA,KAAA,EACA,GAAA,EACyC;AAGzC,EAAA,IAAI,KAAA,CAAM,OAAO,MAAA,KAAW,CAAA,SAAU,CAAC,KAAA,EAAO,EAAE,CAAA;AAChD,EAAA,OAAO,CAAC,iBAAmB,EAAG,CAAC,OAAO,KAAA,CAAM,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA;AAC5D;AAqBO,SAAS,OAAA,CACd,MAAA,EACA,KAAA,EACA,EAAA,GAAK,cAAA,EACsB;AAK3B,EAAA,IAAI,KAAA,CAAM,QAAA,KAAa,IAAA,EAAM,OAAO,EAAC;AACrC,EAAA,OAAO,CAAC,WAAA,CAAY,EAAA,EAAI,MAAM,QAAA,GAAW,MAAA,CAAO,KAAK,CAAC,CAAA;AACxD;AAgBO,SAAS,QAAA,CACd,MAAA,EACA,KAAA,EACA,EAAA,GAAK,cAAA,EACiC;AACtC,EAAA,IAAI,KAAA,CAAM,QAAA,KAAa,IAAA,EAAM,OAAO,IAAA;AACpC,EAAA,OAAO;AAAA,IACL,IAAI,MAAA,CAAO,KAAA;AAAA,IACX,KAAK,kBAAA,CAAmB,EAAA,EAAI,KAAA,CAAM,QAAA,GAAW,OAAO,KAAK;AAAA,GAC3D;AACF;AASO,IAAM,oBAAA,GAIG;AAYT,SAAS,kBACd,MAAA,EACuB;AACvB,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,MAAM,eAAA,EAAmB;AAAA,IAC/B,GAAA,EAAK,CAAC,KAAA,EAAO,IAAA,EAAM,OAAO,OAAA,CAAQ,MAAA,EAAQ,KAAA,EAAO,IAAA,EAAM,EAAE,CAAA;AAAA,IACzD,UAAU,CAAC,KAAA,EAAO,OAAO,QAAA,CAAS,MAAA,EAAQ,KAAS,CAAA;AAAA,IACnD,OAAO,CAAC,KAAA,EAAO,OAAO,QAAA,CAAS,MAAA,EAAQ,OAAO,EAAE,CAAA;AAAA,IAChD,MAAM,CAAC,KAAA,EAAO,OAAO,OAAA,CAAQ,MAAA,EAAQ,OAAO,EAAE;AAAA,GAChD;AACF","file":"chunk-5HVKDHY6.js","sourcesContent":["/**\n * internal/flow/batch-window — coalesce a stream of items into size- or\n * time-bounded BATCHES, then flush each batch as a single Cmd.\n *\n * What this module actually DELEGATES to, vs. what it merely RE-EXPORTS:\n *\n * - `../deadline` — REAL delegation (the only one). The window timer's Msg\n * is `deadlineExceeded`, keyed on the window's identity. `timerFor` arms it\n * through the engine's built-in `timer` Sub: `maxMs` counted from the\n * transition that opened the window, so a machine needs no runner. The\n * absolute-instant form is still here too — `subsFor` lists the same\n * deadline for `deadlinesSub` + `subscribeDeadline`, which re-arms for the\n * REMAINING time after a `Store` rehydrate rather than a fresh `maxMs`.\n * - `../work-queue` — TYPE re-export only, NOT a sink this module calls. A\n * flush emits a Cmd; the CONSUMER's interpret handler is what enqueues onto a\n * `createQueue(store)`. This module never touches queue status — it has no\n * queue state and no status flip to delegate. It re-exports `EnqueueInput<I>`\n * purely so the consumer's flush Cmd and queue agree on the item shape; any\n * actual status flip belongs in the consumer and is delegated to\n * `../work-queue/ops` there, not here.\n * - `../debounce` — VALUE/TYPE re-export only, NOT wired into `update`. A\n * bursty source (keystrokes, scroll, websocket frames) can be `debounce`d at\n * the host BEFORE it ever becomes an `add` Msg, so the batch window only sees\n * settled items. `debounce` is a sink transformer OUTSIDE `update`;\n * batch-window is the durable, replayable half inside it. Re-exported for\n * one-import ergonomics — this module does not call it.\n *\n * Why this is a knob and not hand-wired logic: the managed state (the buffer +\n * the window-open instant) lives in the Model, so it survives DO eviction /\n * reload (durable) and every transition is still a Msg through `update`\n * (replayable). A closure holding a `setTimeout` and a mutable array would break\n * both — banned. `add` / `onWindow` are PURE: time arrives as an `at`\n * parameter, never `Date.now()`; they return NEW slice values, never mutate.\n *\n * Typical wiring (combinator form):\n *\n * const bw = createBatchWindow<LogLine, Msg>({\n * maxItems: 100,\n * maxMs: 5_000,\n * flush: (items) => ({ type: \"FlushBatch\", items }),\n * });\n *\n * // Model: { batch: BatchWindow<LogLine>, ... }\n * // init: (loaded) => [loaded ?? { batch: bw.init(), ... }, []]\n * // update cells:\n * // LogArrived: (s, m) => mapBatch(s, bw.add(s.batch, m.line, m.at))\n * // deadline_exceeded: (s, m) => mapBatch(s, bw.onWindow(s.batch, m.atMs))\n * // subs: [{ type: \"timer\", deps: (s: Model) => bw.timer(s.batch) }]\n * // at run: run(machine, { interpret }) // `timer` is built in\n *\n * where `mapBatch(s, [batch, cmds]) => [{ ...s, batch }, cmds]` lifts the slice\n * transition into the consumer's Model. The `FlushBatch` Cmd's interpret handler\n * is the consumer's — it is where the batch meets I/O (an `enqueue`, a network\n * POST, an R2 write).\n *\n * Internal since #48 — not published on any subpath; reached from inside the\n * package as `internal/flow/batch-window`. Same one-shape-per-package rule as `deadline`, `cache`, and\n * `work-queue`.\n */\n\nimport type { Cmd, Subscribe, TimerDeps } from \"../../../index\";\n\n// `../debounce` re-export (value + type) for one-import ergonomics — see file\n// header. This module does NOT call debounce; a consumer that wants to debounce\n// a bursty source BEFORE it becomes an `add` Msg reaches it from the same import\n// as the batch window.\nexport { type Debounced, debounce } from \"../../timing/debounce\";\n\nimport {\n type DeadlineExceeded,\n type DeadlineSub,\n type DeadlinesSub,\n deadlineExceeded,\n deadlineSub,\n subscribeDeadline,\n} from \"../../resilience/deadline\";\n\n// `../work-queue` TYPE re-export only (see file header). This module emits a\n// flush Cmd; the CONSUMER's interpret handler enqueues onto a\n// `createQueue(store)`. Re-export the caller-facing enqueue payload type so the\n// consumer's flush Cmd and queue agree on the item shape.\nexport type { EnqueueInput } from \"../../work-queue\";\n\n/**\n * Configuration for a batch window — the knob. Pure config, no mutable state\n * (the `BatchWindow<I>` slice carries that). One config object is shared across\n * every window of one logical stream (often a module-scope const).\n *\n * `flush` is a pure Cmd CONSTRUCTOR, not a closure-with-state: it maps the\n * buffered items to the single Cmd the runtime performs when a window closes.\n * Keeping it a `(items) => Cmd` (not a `(items) => void`) is what keeps the\n * trigger replayable — the flush is a Cmd in the transition's output, never a\n * side effect inside the verb.\n */\nexport interface BatchWindowConfig<I, C extends Cmd> {\n /**\n * Size trigger. A batch flushes the instant its buffer reaches `maxItems`,\n * WITHOUT waiting for the time window — the classic \"send as soon as the\n * page is full\" cutoff. Must be `>= 1`; a config of `maxItems: 0` would flush\n * an empty batch on every `add`, which is never useful, so callers treat `1`\n * as the floor (the verb still behaves: with `maxItems <= 1` every item\n * flushes on arrival, a degenerate but legal \"no batching\" window).\n */\n readonly maxItems: number;\n /**\n * Time trigger, in milliseconds. A batch flushes no later than `openedAt +\n * maxMs` — `openedAt` being the `at` of the item that opened the window. This\n * is the latency ceiling: an item never waits longer than `maxMs` to ship,\n * even if the buffer never reaches `maxItems`.\n */\n readonly maxMs: number;\n /**\n * Map a flushed batch to the single Cmd the runtime performs. Called with the\n * buffered items in arrival order; the returned Cmd is what `add` / `onWindow`\n * emit when a window closes. PURE constructor — no I/O, no clock; the I/O\n * happens later in the consumer's interpret handler for the returned Cmd.\n */\n readonly flush: (items: readonly I[]) => C;\n}\n\n/**\n * The Model slice a batch window owns. `buffer` is the items accumulated for the\n * current open window; `openedAt` is the `at` of the item that OPENED it (the\n * anchor the time trigger measures against), or `null` when the window is\n * closed (buffer empty). The pair is the single source of truth for \"is a\n * window open, and when must it flush by?\" — both plain data so the whole slice\n * is trivially JSON-serializable into a consumer's `Store<S>`.\n *\n * Invariant the verbs preserve: `buffer.length === 0` IFF `openedAt === null`.\n * A non-empty buffer always has an `openedAt`; an empty buffer never does. The\n * window timer Sub keys its arm/disarm off exactly this invariant.\n */\nexport interface BatchWindow<I> {\n readonly buffer: readonly I[];\n readonly openedAt: number | null;\n}\n\n/** The starting slice: closed window, empty buffer. */\nexport function initBatchWindow<I>(): BatchWindow<I> {\n return { buffer: [], openedAt: null };\n}\n\n/**\n * The deadline a batch window's open timer lists: it IS `../deadline`'s\n * `DeadlineSub`, not a re-tagged copy. `atMs` is the absolute flush-by instant\n * (`openedAt + maxMs`). Aliasing the deadline type (rather than redeclaring an\n * identical shape) is what lets `subsFor` return `deadlineSub(...)` with no\n * cast — the value's static type already satisfies this name. Consumers running\n * several windows route each one by the deadline's `id`, not by a distinct type.\n */\nexport type BatchWindowSub = DeadlineSub;\n\n/** The Msg a closed-by-time window dispatches. Alias of `deadline`'s Msg. */\nexport type BatchWindowExpired = DeadlineExceeded;\n\n/**\n * Construct the window-expired Msg for the window identified by `id`, flushing\n * by `atMs`. Re-exported alias of `deadlineExceeded` so the runner and\n * the consumer's reducer share one constructor rather than two literals that\n * can drift. The consumer handles it in a reducer cell by calling `onWindow`.\n */\nexport function batchWindowExpired(\n id: string,\n atMs: number,\n): BatchWindowExpired {\n return deadlineExceeded(id, atMs);\n}\n\n/**\n * The bound knob returned by `createBatchWindow`. `init` + the two pure verbs +\n * `timer` + `subs`, all closing over one `BatchWindowConfig`. The verbs\n * return `readonly [BatchWindow<I>, readonly C[]]`: the next slice and the Cmds\n * to emit (one `flush` Cmd when a window closes, none otherwise).\n */\nexport interface BatchWindowKnob<I, C extends Cmd> {\n /** The starting slice — `initBatchWindow()`. */\n init(): BatchWindow<I>;\n /**\n * Buffer `item` (arrival-stamped `at`), flushing immediately if that brings\n * the buffer to `maxItems`. See `addItem`.\n */\n add(\n state: BatchWindow<I>,\n item: I,\n at: number,\n ): readonly [BatchWindow<I>, readonly C[]];\n /**\n * Flush whatever is buffered because the time window closed. See `onWindow`.\n */\n onWindow(\n state: BatchWindow<I>,\n at: number,\n ): readonly [BatchWindow<I>, readonly C[]];\n /**\n * The built-in `timer` Sub's deps while a window is open, `null` while it is\n * closed. See `timerFor`. `id` keys the Msg so several windows on one\n * machine route distinctly.\n */\n timer(\n state: BatchWindow<I>,\n id?: string,\n ): TimerDeps<BatchWindowExpired> | null;\n /**\n * The window timer as an absolute deadline, listed only while a window is\n * open — for `deadlinesSub` + `subscribeBatchWindow`. See `subsFor`.\n */\n subs(state: BatchWindow<I>, id?: string): readonly BatchWindowSub[];\n}\n\n/**\n * Buffer `item` and decide whether the size trigger fires. PURE.\n *\n * - Empty (closed) window → `item` OPENS it: `openedAt` becomes `at`, buffer\n * becomes `[item]`. This is the anchor the time window measures against.\n * - Open window → `item` appends; `openedAt` is left untouched (the window's\n * latency clock keeps running from the FIRST item, not this one).\n *\n * After buffering, if the buffer has reached `maxItems` the window flushes\n * immediately: emit `config.flush(buffer)` and RESET to the closed slice. The\n * size trigger pre-empts the time trigger — a full page ships now, the window\n * Sub for the (now closed) window disarms on the next reconcile because `subs`\n * returns `[]` for an empty buffer.\n *\n * `maxItems <= 1` is the degenerate \"no batching\" case: every item flushes on\n * arrival (buffer reaches 1 immediately). Legal and useful as a feature flag to\n * disable batching without restructuring the wiring.\n *\n * @param config the knob (maxItems / flush constructor)\n * @param state the current slice\n * @param item the item to buffer\n * @param at the arrival instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function addItem<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n item: I,\n at: number,\n): readonly [BatchWindow<I>, readonly C[]] {\n const buffer = [...state.buffer, item];\n // A closed window opens at THIS item's `at`; an already-open window keeps its\n // original `openedAt` (the latency clock runs from the first item).\n const openedAt = state.openedAt ?? at;\n\n // Size trigger: a full page ships now, regardless of the time window. The\n // floor of 1 means `maxItems <= 1` flushes every item on arrival.\n if (buffer.length >= Math.max(1, config.maxItems)) {\n return [initBatchWindow<I>(), [config.flush(buffer)]];\n }\n\n return [{ buffer, openedAt }, []];\n}\n\n/**\n * Flush the open window because its time bound was reached. PURE.\n *\n * - Open window (buffer non-empty) → emit `config.flush(buffer)` and RESET to\n * the closed slice.\n * - Closed window (buffer empty) → no-op: return the same slice and no Cmds.\n * This guards the race where the size trigger already flushed and reset the\n * window before the time Sub's Msg lands — a stale `onWindow` after a\n * size-flush must NOT emit an empty batch.\n *\n * `at` is accepted for symmetry with `add` and so the consumer's reducer cell\n * has a uniform `(state, at)` shape; the verb doesn't need it for the flush\n * decision (the buffer's emptiness is the whole signal), but taking it keeps a\n * future \"flush only if the window is genuinely past `openedAt + maxMs`\"\n * tightening a non-breaking change.\n *\n * @param config the knob (flush constructor)\n * @param state the current slice\n * @param at the firing instant (epoch ms) — the caller stamps it; the verb\n * never reads the clock\n */\nexport function onWindow<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n _at: number,\n): readonly [BatchWindow<I>, readonly C[]] {\n // Empty buffer = the size trigger already flushed (or nothing arrived). A\n // stale timer Msg must not ship an empty batch — return the slice untouched.\n if (state.buffer.length === 0) return [state, []];\n return [initBatchWindow<I>(), [config.flush(state.buffer)]];\n}\n\n/**\n * The window timer deadline, derived from the slice. PURE.\n *\n * Returns the single deadline targeting `openedAt + maxMs` while a window is\n * open (buffer non-empty), or `[]` while it is closed. Fed through\n * `deadlinesSub`, the engine arms the timer when the list gains the deadline\n * and disarms it (clearing the pending `setTimeout`) when the list loses it —\n * so a size-flush that empties the buffer auto-cancels the now-irrelevant time\n * trigger on the next transition.\n *\n * `id` defaults to `\"batch-window\"`; pass a distinct id per window when a\n * machine runs several so the dispatched `BatchWindowExpired` Msg routes to the\n * right one.\n *\n * @param config the knob (maxMs)\n * @param state the current slice\n * @param id stable identity for this window's timer (default\n * `\"batch-window\"`)\n */\nexport function subsFor<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n id = \"batch-window\",\n): readonly BatchWindowSub[] {\n // Closed window → no timer. The invariant (empty buffer IFF openedAt null)\n // means either check suffices; we read `openedAt` so the `atMs` arithmetic\n // below is non-null without a `!`. `deadlineSub` already returns a\n // `DeadlineSub`, which IS `BatchWindowSub` — no cast.\n if (state.openedAt === null) return [];\n return [deadlineSub(id, state.openedAt + config.maxMs)];\n}\n\n/**\n * The built-in `timer` Sub's deps for a batch window. PURE.\n *\n * While a window is open it counts `maxMs` down from the transition that\n * opened it — the deps are fixed for the window's life (`openedAt` never moves\n * while items append), so the engine starts the countdown exactly once — and\n * fires `batchWindowExpired(id, openedAt + maxMs)`. A closed window is `null`,\n * so a size-flush that empties the buffer cancels the countdown on the next\n * transition.\n *\n * Relative by construction: a process that rehydrates an open window counts a\n * fresh `maxMs`. Reach for `subsFor` + `subscribeBatchWindow` when the flush\n * must land at the original absolute instant.\n */\nexport function timerFor<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n state: BatchWindow<I>,\n id = \"batch-window\",\n): TimerDeps<BatchWindowExpired> | null {\n if (state.openedAt === null) return null;\n return {\n ms: config.maxMs,\n msg: batchWindowExpired(id, state.openedAt + config.maxMs),\n };\n}\n\n/**\n * The `deadline` runner for a batch window's absolute timer — the exact\n * `../deadline` runner. Arms a one-shot timer per listed deadline for\n * `max(0, atMs - now)` and dispatches `batchWindowExpired(...)`; returns a\n * cleanup that clears them. Hand it to `run` as `subscribe.deadline` when the\n * machine declares `deadlinesSub((s) => bw.subs(s.batch))`.\n */\nexport const subscribeBatchWindow: Subscribe<\n BatchWindowExpired,\n DeadlinesSub,\n unknown\n>[\"deadline\"] = subscribeDeadline;\n\n/**\n * Build a batch window knob from a config. Combinator form (the default L2\n * ergonomic): hand it the config, splice the ~4 returned hooks into your\n * machine. The pure verbs (`add` / `onWindow`) close over the config so the\n * consumer's reducer cells stay a uniform `(state, ...args)` shape; `timer`\n * arms the window timer.\n *\n * @typeParam I the item type buffered into each batch\n * @typeParam C the Cmd type `config.flush` returns\n */\nexport function createBatchWindow<I, C extends Cmd>(\n config: BatchWindowConfig<I, C>,\n): BatchWindowKnob<I, C> {\n return {\n init: () => initBatchWindow<I>(),\n add: (state, item, at) => addItem(config, state, item, at),\n onWindow: (state, at) => onWindow(config, state, at),\n timer: (state, id) => timerFor(config, state, id),\n subs: (state, id) => subsFor(config, state, id),\n };\n}\n"]}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { initCache, deadlineSub, nextTimer, get, set } from './chunk-
|
|
1
|
+
import { initCache, deadlineSub, nextTimer, get, set } from './chunk-TAKPC6SD.js';
|
|
2
2
|
import { asRng, recordFailure, shouldRetry, nextDelayMs, initRetry } from './chunk-VBDU4JL6.js';
|
|
3
3
|
import { unchecked } from './chunk-4LPY67E4.js';
|
|
4
|
-
import { liftSlice } from './chunk-
|
|
5
|
-
import { Cmd, without } from './chunk-
|
|
4
|
+
import { liftSlice } from './chunk-DVNXEB7V.js';
|
|
5
|
+
import { Cmd, without } from './chunk-RKVUVALV.js';
|
|
6
6
|
|
|
7
7
|
// src/internal/resilience/circuit-breaker/index.ts
|
|
8
8
|
var defaultCircuitPolicy = {
|
|
@@ -391,5 +391,5 @@ function liftResilience(state, result) {
|
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
export { DEFAULT_RESILIENT_NAME, canPass, createResilientCall, defaultCircuitPolicy, initBucket, initCircuit, initWindow, liftResilience, onFailure, onSuccess, record, refill, remaining, runCmdDef, tryConsume };
|
|
394
|
-
//# sourceMappingURL=chunk-
|
|
395
|
-
//# sourceMappingURL=chunk-
|
|
394
|
+
//# sourceMappingURL=chunk-72CN3VPY.js.map
|
|
395
|
+
//# sourceMappingURL=chunk-72CN3VPY.js.map
|