@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.
- package/README.md +49 -0
- package/dist/agent/index.d.ts +16 -770
- package/dist/agent/index.js +18 -16
- package/dist/authed-call/index.d.ts +3 -3
- package/dist/authed-call/index.js +22 -36
- package/dist/authed-call/index.js.map +1 -1
- package/dist/await-terminal/index.d.ts +2 -2
- package/dist/await-terminal/index.js +2 -2
- package/dist/batch-window/index.d.ts +3 -3
- package/dist/batch-window/index.js +3 -3
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +5 -2
- package/dist/{chunk-3545BOYL.js → chunk-25LBDCBC.js} +6 -5
- package/dist/chunk-25LBDCBC.js.map +1 -0
- package/dist/{chunk-IK4S5DJN.js → chunk-2AHAR7CX.js} +7 -14
- package/dist/chunk-2AHAR7CX.js.map +1 -0
- package/dist/{chunk-IN33Q4VJ.js → chunk-2KNLKQVN.js} +3 -3
- package/dist/{chunk-IN33Q4VJ.js.map → chunk-2KNLKQVN.js.map} +1 -1
- package/dist/{chunk-VFGKTGGC.js → chunk-5W7CNAHB.js} +191 -178
- package/dist/chunk-5W7CNAHB.js.map +1 -0
- package/dist/{chunk-6GIGQT6C.js → chunk-B5RAZVXW.js} +12 -11
- package/dist/chunk-B5RAZVXW.js.map +1 -0
- package/dist/{chunk-6VDCTR7N.js → chunk-D4NJBQAH.js} +258 -151
- package/dist/chunk-D4NJBQAH.js.map +1 -0
- package/dist/{chunk-UV6UWGEO.js → chunk-D736QUGF.js} +6 -5
- package/dist/chunk-D736QUGF.js.map +1 -0
- package/dist/{chunk-EGWTT5XJ.js → chunk-ET6QQNSQ.js} +76 -95
- package/dist/chunk-ET6QQNSQ.js.map +1 -0
- package/dist/chunk-EWJP3YYP.js +21 -0
- package/dist/chunk-EWJP3YYP.js.map +1 -0
- package/dist/{chunk-M6UZT7HS.js → chunk-FCKP6N7Y.js} +6 -5
- package/dist/chunk-FCKP6N7Y.js.map +1 -0
- package/dist/{chunk-TGG26XMK.js → chunk-FV3Z5MBE.js} +10 -5
- package/dist/chunk-FV3Z5MBE.js.map +1 -0
- package/dist/chunk-G5E74IJB.js +8 -0
- package/dist/chunk-G5E74IJB.js.map +1 -0
- package/dist/{chunk-G2U7DJHL.js → chunk-JFFVBEDI.js} +3 -3
- package/dist/{chunk-G2U7DJHL.js.map → chunk-JFFVBEDI.js.map} +1 -1
- package/dist/{chunk-FOGL3TC7.js → chunk-JXYYRKD6.js} +8 -10
- package/dist/chunk-JXYYRKD6.js.map +1 -0
- package/dist/{chunk-ZDRN3YCE.js → chunk-LJJV6ZD5.js} +17 -24
- package/dist/chunk-LJJV6ZD5.js.map +1 -0
- package/dist/chunk-LYLDCIPH.js +365 -0
- package/dist/chunk-LYLDCIPH.js.map +1 -0
- package/dist/{chunk-HJO6VNQI.js → chunk-MU6MCIAC.js} +3 -3
- package/dist/{chunk-HJO6VNQI.js.map → chunk-MU6MCIAC.js.map} +1 -1
- package/dist/{chunk-UYWUVEP6.js → chunk-MUUROGIH.js} +2 -2
- package/dist/chunk-MUUROGIH.js.map +1 -0
- package/dist/{chunk-DYRKDCJI.js → chunk-PBDYW2TU.js} +41 -7
- package/dist/chunk-PBDYW2TU.js.map +1 -0
- package/dist/{chunk-5VNWZADP.js → chunk-Q2UM7BXU.js} +18 -7
- package/dist/chunk-Q2UM7BXU.js.map +1 -0
- package/dist/chunk-RMX4EOQK.js +26 -0
- package/dist/chunk-RMX4EOQK.js.map +1 -0
- package/dist/{chunk-2YOE5PIS.js → chunk-S2EGRPCM.js} +8 -6
- package/dist/chunk-S2EGRPCM.js.map +1 -0
- package/dist/{chunk-5KVHID6A.js → chunk-SPWVPIFQ.js} +4 -4
- package/dist/{chunk-5KVHID6A.js.map → chunk-SPWVPIFQ.js.map} +1 -1
- package/dist/chunk-SSHDBOXF.js +71 -0
- package/dist/chunk-SSHDBOXF.js.map +1 -0
- package/dist/{chunk-6OUDPSSG.js → chunk-TWDHNIY6.js} +3 -3
- package/dist/{chunk-6OUDPSSG.js.map → chunk-TWDHNIY6.js.map} +1 -1
- package/dist/{chunk-SC3FAFZN.js → chunk-UF4MZSLT.js} +2 -2
- package/dist/chunk-UF4MZSLT.js.map +1 -0
- package/dist/chunk-ZJ5AOYBH.js +8 -0
- package/dist/chunk-ZJ5AOYBH.js.map +1 -0
- package/dist/{chunk-LEC4U6CX.js → chunk-ZVJZ72HY.js} +3 -3
- package/dist/{chunk-LEC4U6CX.js.map → chunk-ZVJZ72HY.js.map} +1 -1
- package/dist/circuit-breaker/index.d.ts +1 -0
- package/dist/circuit-breaker/index.js +1 -1
- package/dist/{core-BmlJOvxM.d.ts → core-42kapW0n.d.ts} +96 -6
- package/dist/deadline/index.d.ts +77 -11
- package/dist/deadline/index.js +3 -3
- package/dist/devtools/index.js +8 -28
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +52 -24
- package/dist/do/index.js +94 -92
- package/dist/do/index.js.map +1 -1
- package/dist/{durable-effects-9LxFpmh3.d.ts → durable-effects-CFRngTNA.d.ts} +3 -3
- package/dist/extension/index.d.ts +25 -7
- package/dist/extension/index.js +4 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/react.js +4 -3
- package/dist/extension/react.js.map +1 -1
- package/dist/extension/subs/index.d.ts +2 -2
- package/dist/extension/subs/index.js +7 -10
- package/dist/extension/subs/index.js.map +1 -1
- package/dist/fan-out/index.d.ts +10 -10
- package/dist/fan-out/index.js +3 -3
- package/dist/idempotent-intake/index.d.ts +2 -2
- package/dist/idempotent-intake/index.js +3 -3
- package/dist/index.d.ts +50 -489
- package/dist/index.js +2 -2
- package/dist/llm-call/index.d.ts +3 -2
- package/dist/llm-call/index.js +12 -10
- package/dist/machine-B_BF4jij.d.ts +804 -0
- package/dist/machine-viz/index.d.ts +1 -1
- package/dist/machine-viz/index.js +2 -2
- package/dist/mem/index.d.ts +3 -2
- package/dist/mem/index.js.map +1 -1
- package/dist/monitored-run/index.d.ts +54 -30
- package/dist/monitored-run/index.js +7 -7
- package/dist/node/index.d.ts +3 -2
- package/dist/node/index.js +2 -3
- package/dist/node/index.js.map +1 -1
- package/dist/paginated-walk/index.d.ts +2 -2
- package/dist/paginated-walk/index.js +11 -10
- package/dist/parity/index.d.ts +6 -5
- package/dist/parity/index.js +5 -4
- package/dist/parity/index.js.map +1 -1
- package/dist/pbt/arbitraries/index.d.ts +1 -1
- package/dist/pbt/index.d.ts +1 -1
- package/dist/pbt/index.js +3 -3
- package/dist/pbt/index.js.map +1 -1
- package/dist/pbt/runners/index.d.ts +1 -1
- package/dist/pbt/runners/index.js +3 -3
- package/dist/poller/index.d.ts +59 -17
- package/dist/poller/index.js +56 -20
- package/dist/poller/index.js.map +1 -1
- package/dist/prediction/index.d.ts +1 -1
- package/dist/prediction/index.js +2 -2
- package/dist/pure/index.d.ts +1 -1
- package/dist/pure/index.js +2 -2
- package/dist/rate-limit/index.d.ts +2 -2
- package/dist/rate-limit/index.js +1 -1
- package/dist/react/index.d.ts +10 -3
- package/dist/react/index.js +2 -2
- package/dist/react/index.js.map +1 -1
- package/dist/reconciler/index.d.ts +2 -2
- package/dist/reconciler/index.js +13 -15
- package/dist/reconciler/index.js.map +1 -1
- package/dist/recorder/index.d.ts +20 -20
- package/dist/recorder/index.js +1 -1
- package/dist/resilient-call/index.d.ts +31 -6
- package/dist/resilient-call/index.js +10 -9
- package/dist/retry-backoff/index.d.ts +191 -14
- package/dist/retry-backoff/index.js +1 -1
- package/dist/retry-to-success/index.js +1 -1
- package/dist/runtime-types-CukV8kMX.d.ts +406 -0
- package/dist/saga/index.d.ts +3 -2
- package/dist/saga/index.js +2 -2
- package/dist/saga/index.js.map +1 -1
- package/dist/snapshot/index.d.ts +1 -1
- package/dist/snapshot/index.js +3 -3
- package/dist/subs/index.d.ts +372 -4
- package/dist/subs/index.js +4 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/throttled-input/index.d.ts +2 -2
- package/dist/throttled-input/index.js +14 -13
- package/dist/throttled-input/index.js.map +1 -1
- package/dist/token-refresh/index.d.ts +1 -1
- package/dist/token-refresh/index.js +3 -3
- package/dist/trace-replay/index.d.ts +12 -7
- package/dist/trace-replay/index.js +4 -3
- package/dist/{types-BoUWgwSm.d.ts → types-KbL3W1mj.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +1 -1
- package/dist/with-deadline/index.js +2 -2
- package/dist/with-resilience/index.d.ts +21 -2
- package/dist/with-resilience/index.js +12 -23
- package/dist/with-resilience/index.js.map +1 -1
- package/dist/with-telemetry/index.d.ts +1 -1
- package/dist/with-telemetry/index.js +2 -2
- package/dist/work-queue/adapter.d.ts +2 -2
- package/dist/work-queue/adapter.js +2 -2
- package/dist/work-queue/index.d.ts +3 -2
- package/dist/work-queue/index.js +1 -1
- package/dist/work-queue/index.js.map +1 -1
- package/dist/work-queue/ops.d.ts +2 -2
- package/dist/work-queue/ops.js +1 -1
- package/dist/workflow/index.d.ts +51 -19
- package/dist/workflow/index.js +71 -52
- package/dist/workflow/index.js.map +1 -1
- package/package.json +4 -1
- package/dist/chunk-2YOE5PIS.js.map +0 -1
- package/dist/chunk-3545BOYL.js.map +0 -1
- package/dist/chunk-5VNWZADP.js.map +0 -1
- package/dist/chunk-6GIGQT6C.js.map +0 -1
- package/dist/chunk-6VDCTR7N.js.map +0 -1
- package/dist/chunk-DYRKDCJI.js.map +0 -1
- package/dist/chunk-EGWTT5XJ.js.map +0 -1
- package/dist/chunk-EMJTPHI7.js +0 -91
- package/dist/chunk-EMJTPHI7.js.map +0 -1
- package/dist/chunk-FOGL3TC7.js.map +0 -1
- package/dist/chunk-IK4S5DJN.js.map +0 -1
- package/dist/chunk-M6UZT7HS.js.map +0 -1
- package/dist/chunk-NSDFA6M6.js +0 -198
- package/dist/chunk-NSDFA6M6.js.map +0 -1
- package/dist/chunk-SC3FAFZN.js.map +0 -1
- package/dist/chunk-SKAMZKTE.js +0 -20
- package/dist/chunk-SKAMZKTE.js.map +0 -1
- package/dist/chunk-TGG26XMK.js.map +0 -1
- package/dist/chunk-UV6UWGEO.js.map +0 -1
- package/dist/chunk-UYWUVEP6.js.map +0 -1
- package/dist/chunk-VFGKTGGC.js.map +0 -1
- package/dist/chunk-ZDRN3YCE.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/subs/types.ts"],"names":[],"mappings":";AA2CO,SAAS,iBAAA,CACd,UACA,GAAA,EACM;AACN,EAAA,IAAI,GAAA,KAAQ,IAAA,EAAM,QAAA,CAAS,GAAG,CAAA;AAChC","file":"chunk-ZJ5AOYBH.js","sourcesContent":["// ---------------------------------------------------------------------------\n// SubscribeHandler<S, M, Ctx> — the structural shape of a single entry in\n// `Machine.subscribe`. Pulled out as a named type so factory return types\n// in this subpath can match the substrate's expected `subscribe` cell shape\n// without re-deriving it from `Machine`'s mapped type.\n//\n// A Sub factory in this subpath returns a `SubscribeHandler<S, M, Ctx>` —\n// the same `(sub, ctx, dispatch) => cleanup` shape the substrate expects in\n// `machine.subscribe[type]`. Callers assign the factory's return value\n// directly to a subscribe cell:\n//\n// subscribe: {\n// \"audit-idle\": fromPort(\n// (ctx) => ctx.auditRuntime,\n// auditIdlePort,\n// (idle) => ({ type: \"audit:idle\", idle }),\n// ),\n// }\n//\n// The factory absorbs the lifecycle (subscribe + cleanup); the caller keeps\n// the intent (which port, which msg). Strengthens invariant 9 (the surface\n// for cross-cutting Sub topologies is named, small, and exported from one\n// subpath instead of redrawn at every call site).\n// ---------------------------------------------------------------------------\n\nimport type { Sub } from \"../index\";\n\nexport type SubscribeHandler<S extends Sub, M, Ctx> = (\n sub: S,\n ctx: Ctx,\n dispatch: (msg: M) => void,\n) => () => void;\n\n// ---------------------------------------------------------------------------\n// dispatchIfPresent — the null-drop dispatch idiom every Sub factory in this\n// subpath (and the chrome/node factories) shares. A factory's callback maps a\n// platform event to `M | null`; `null` is the explicit \"drop this emission\"\n// signal — the event fired but the caller decided it isn't interesting given\n// current state, so no dispatch is made and the listener stays armed. Hoisted\n// here so the one drop-on-null rule lives once instead of re-inlined as\n// `const msg = fn(...); if (msg !== null) dispatch(msg);` at every listener.\n// ---------------------------------------------------------------------------\n\nexport function dispatchIfPresent<M>(\n dispatch: (msg: M) => void,\n msg: M | null,\n): void {\n if (msg !== null) dispatch(msg);\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { idempotencyMemory } from './chunk-BYQUGDD3.js';
|
|
2
|
-
import { queueAdapter } from './chunk-
|
|
2
|
+
import { queueAdapter } from './chunk-MU6MCIAC.js';
|
|
3
3
|
|
|
4
4
|
// src/idempotent-intake/index.ts
|
|
5
5
|
function createIntake(config) {
|
|
@@ -87,5 +87,5 @@ function createIntake(config) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export { createIntake };
|
|
90
|
-
//# sourceMappingURL=chunk-
|
|
91
|
-
//# sourceMappingURL=chunk-
|
|
90
|
+
//# sourceMappingURL=chunk-ZVJZ72HY.js.map
|
|
91
|
+
//# sourceMappingURL=chunk-ZVJZ72HY.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/idempotent-intake/index.ts"],"names":[],"mappings":"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","file":"chunk-LEC4U6CX.js","sourcesContent":["/**\n * @demlik/tea/idempotent-intake — receive-once intake for webhooks / queue\n * messages: dedupe by key, enqueue the new ones, replay the cached result to\n * the duplicates. The \"process this payload exactly once even though it\n * arrives N times\" knob.\n *\n * **The recipe.** This is a Level-2 composition — it owns no primitive\n * queue/cache logic of its own, it *splices two L1 bricks into one Model slice*\n * and DELEGATES every transition to their VERB SEAMS — `IdempotencyMemory`\n * (`../idempotency/adapter`) and `QueueAdapter` (`../work-queue/adapter`) —\n * so the store ops and the `QueueItem` / `IdempotencyStore` shapes stay\n * insulated behind verbs the intake calls:\n *\n * - `../idempotency/adapter` — the dedupe-by-key + last-result cache. The\n * `seen` half of the slice is its `IdempotencyStore`, keyed by\n * `keyOf(payload)`. A key is recorded once `receive` calls `memory.remember`;\n * `complete` calls `memory.remember` again to swap its cached value from\n * `pending` to the finished `result`. The cache write is the brick's, never\n * re-rolled here.\n * - `../work-queue/adapter` — the pending-work list. The `queue` half of the\n * slice is its `QueueItem<P>[]`. Every status flip routes through the\n * adapter verbs: `receive` enqueues via `queue.enqueue`, `claimNext` stamps\n * the `running` claim via `queue.patch`, `boot` re-arms a dead worker's\n * claim via `queue.patch`. Intake owns only the SELECTION (which item,\n * gated on the done-cache reconcile below) — the array transition is the\n * brick's. The consumer drains it (claim → process → `complete`) at its own\n * pace.\n *\n * Both bricks are pure-state + pure-ops, host- and clock-agnostic by\n * construction, so the combined slice inherits TEA's two non-negotiables:\n *\n * - **Durable** — the slice is a plain serializable value (a record-backed\n * idempotency store + an array of queue items, no `Map`, no closures), so\n * it survives a Durable-Object eviction / reload and rehydrates whole.\n * - **Replayable** — every verb is a pure `(state, …) => [state, Cmd[]]`\n * transition; no `Date.now()`, no `Math.random()`, no I/O inside a verb.\n * `at` (the clock) and `id` (the queue-item id) are *injected* at the call\n * site, so `trace-replay` reconstructs each transition exactly.\n *\n * **The decision lives in a Cmd, not in a return shape.** `receive` cannot\n * \"return\" what it decided (the knob contract pins the shape to\n * `[State, Cmd[]]`), so the decision is *reified as a Cmd* the runtime\n * performs:\n *\n * - first sighting of a key → mark it `pending`, enqueue the payload, emit\n * `IntakeProcess` (go run the work).\n * - a key already `pending` → drop silently (the original is in flight);\n * emit nothing.\n * - a key already `done` → emit `IntakeReplay` carrying the cached\n * result so the duplicate caller gets the SAME response the first one\n * produced, with zero re-execution of the side effect.\n *\n * **Two crash-safety guarantees the verbs enforce (and the unit specs alone\n * missed — see the wired-machine test):**\n *\n * - *A slow worker is never double-enqueued.* The TTL clock starts at\n * `complete`, not at `receive`: a `pending` entry NEVER ages out, so a\n * duplicate of an in-flight key is always dropped no matter how long the\n * worker takes. Only a `done` entry expires (see `liveEntry`).\n * - *A complete-then-crash never re-runs.* `boot` and `claimNext` reconcile\n * the queue against the done cache: a queue item whose key already resolved\n * to `done` in `seen` is skipped (never reset to pending, never claimed),\n * so a worker that finished and cached its result before the DO evicted is\n * not re-executed on resume.\n *\n * Typical wiring (the combinator form — splice ~3 hooks into a host machine):\n *\n * const intake = createIntake<StripeEvent, ChargeResult>({\n * keyOf: (e) => e.id, // Stripe's idempotency key\n * ttlMs: 24 * 60 * 60 * 1000, // a replayed webhook 24h after it COMPLETED is re-run\n * });\n *\n * // in `init`:\n * intake: intake.init(),\n * // in `update`, on the inbound-webhook Msg:\n * const [next, cmds] = intake.receive(state.intake, payload, at, id);\n * // …route `IntakeProcess` to your worker, `IntakeReplay` to the responder.\n * // on the worker's done Msg:\n * const [next, _] = intake.complete(state.intake, key, result, at);\n * // once, after boot (DO reload), before resuming the drain:\n * const [next, _] = intake.boot(state.intake);\n *\n * Escape hatch: reach the raw L1 ops via `@demlik/tea/idempotency` and\n * `@demlik/tea/work-queue` and thread them by hand.\n */\n\nimport type { IdempotencyStore } from \"../idempotency\";\nimport { idempotencyMemory } from \"../idempotency/adapter\";\nimport type { Cmd } from \"../index\";\nimport type { QueueItem } from \"../work-queue\";\nimport { queueAdapter } from \"../work-queue/adapter\";\n\n/**\n * A cached intake entry: either a key whose work is still in flight\n * (`pending`) or one whose work has finished and whose `result` is held for\n * replay to later duplicates (`done`). Discriminated on `phase` — `pending`\n * carries no payload, `done` carries the result `R`.\n *\n * Lives inside the `seen` half of the slice (`IdempotencyStore<IntakeEntry<R>>`).\n * It is the dedupe ledger's *value*; the ledger's *key* is `keyOf(payload)`.\n */\nexport type IntakeEntry<R> =\n | { readonly phase: \"pending\" }\n | { readonly phase: \"done\"; readonly result: R };\n\n/**\n * The intake knob's config — `keyOf` derives the idempotency key from a\n * payload, `ttlMs` (optional) ages a *completed* cached key out so a webhook\n * replayed long after the work finished is treated as new and re-processed.\n *\n * **The TTL clock starts at `complete`, not at `receive`.** A `pending` entry\n * (work still in flight) NEVER ages out, no matter how slow the worker is —\n * ageing a pending key would let the duplicate re-enqueue the in-flight\n * payload and double-process it. Only a `done` entry expires, measured from\n * the `at` passed to `complete` (the moment the result was cached). Inside the\n * window a duplicate replays the cached result; at/after the window it is\n * re-accepted and re-processed.\n *\n * Omit `ttlMs` for a cache whose `done` entries never expire by age (keys live\n * forever — fine for a bounded universe of ids, unbounded growth otherwise).\n *\n * The TTL is owned HERE, not delegated to the composed `IdempotencyStore`: the\n * store's uniform per-entry TTL cannot tell `pending` from `done`, so intake\n * keeps the store TTL-free and applies the phase-aware rule itself (see\n * `liveEntry`).\n */\nexport interface IntakeConfig<P> {\n /** Derive the idempotency key from a payload. Must be pure + deterministic. */\n readonly keyOf: (payload: P) => string;\n /**\n * Age a *completed* key out after this many ms, measured from the `at`\n * passed to `complete`. Pending keys never age out.\n */\n readonly ttlMs?: number;\n}\n\n/**\n * The Model slice this knob owns. `seen` is the dedupe + last-result cache\n * (the `../idempotency` brick); `queue` is the pending-work list (the\n * `../work-queue` brick). Both are plain serializable values — the whole slice\n * round-trips through `JSON` so it survives eviction and replays exactly.\n */\nexport interface IntakeState<P, R> {\n readonly seen: IdempotencyStore<IntakeEntry<R>>;\n readonly queue: readonly QueueItem<P>[];\n}\n\n// === Cmds the verbs reify ===\n//\n// The knob contract pins verbs to `(state, …) => [State, Cmd[]]`, so a verb's\n// *decision* (new vs. duplicate-pending vs. duplicate-done) cannot be a return\n// value — it is emitted as a Cmd the runtime's interpret handler performs.\n// Each Cmd is plain data (invariant 1): the payload / key / result it carries\n// are values, never closures.\n\n/** A genuinely-new payload was accepted and enqueued — go run the work. */\nexport interface IntakeProcessCmd<P> extends Cmd<\"intake:process\"> {\n /** The dedupe key the payload was filed under. */\n readonly key: string;\n /** The enqueued queue-item id (the same `id` passed to `receive`). */\n readonly itemId: string;\n /** The original payload, for the worker to process. */\n readonly payload: P;\n}\n\n/**\n * A duplicate of an already-completed key arrived — replay the cached result\n * to the caller instead of re-running the side effect.\n */\nexport interface IntakeReplayCmd<R> extends Cmd<\"intake:replay\"> {\n /** The dedupe key whose cached result is being replayed. */\n readonly key: string;\n /** The result the ORIGINAL request produced, recalled from the cache. */\n readonly result: R;\n}\n\n/** The Cmd union the intake verbs emit. */\nexport type IntakeCmd<P, R> = IntakeProcessCmd<P> | IntakeReplayCmd<R>;\n\n/**\n * Build an idempotent-intake knob from `config`. Returns the uniform L2\n * contract — `init()` + the verbs (`receive`, `complete`, `boot`, `claimNext`).\n * No `subs` / `handlers`: intake owns no timers and does no I/O of its own (the\n * composed bricks are pure-ops; the worker that runs `IntakeProcess` and the\n * responder that runs `IntakeReplay` live in the host's interpret map).\n */\nexport function createIntake<P, R>(config: IntakeConfig<P>) {\n const { keyOf, ttlMs } = config;\n\n // The two store seams, bound once for this knob's `P`/`R`. The intake\n // delegates every queue status flip to `queue.*` and every dedupe-cache\n // transition to `memory.*`, so the `QueueItem<P>` / `IdempotencyStore` shapes\n // are insulated behind the verbs rather than reached by raw ops.\n const queue = queueAdapter<P>();\n const memory = idempotencyMemory<IntakeEntry<R>>();\n\n /**\n * The empty slice: a TTL-free idempotency store (intake owns the TTL itself —\n * see `liveEntry`, because the store cannot tell `pending` from `done`) plus\n * an empty work queue. The rehydrate path is the host's `init(loaded)` — it\n * passes the persisted slice straight back, this is only the fresh-boot value.\n */\n function init(): IntakeState<P, R> {\n return {\n seen: memory.init(),\n queue: [],\n };\n }\n\n /**\n * The live cached entry for `key` at clock reading `at`, or `undefined` if\n * the key is absent or its `done` window has elapsed. This is intake's\n * phase-aware expiry rule, applied HERE rather than by the store's uniform\n * per-entry TTL:\n *\n * - **`pending`** — work is in flight; NEVER expires. Ageing a pending key\n * would let a slow worker's payload be re-enqueued and double-processed.\n * - **`done`** — expires `ttlMs` after the `atMs` the result was cached at\n * (the `at` passed to `complete`). Half-open, matching the store /\n * rate-limit window edge: `at - atMs >= ttlMs` reads as expired. No\n * `ttlMs` ⇒ a `done` entry never expires by age.\n *\n * PURE — reads `state.seen.entries` directly (no `Date.now`), never mutates.\n */\n function liveEntry(\n store: IdempotencyStore<IntakeEntry<R>>,\n key: string,\n at: number,\n ): IntakeEntry<R> | undefined {\n const cached = store.entries[key];\n if (cached === undefined) return undefined;\n const entry = cached.value;\n if (entry.phase === \"pending\") return entry; // in flight — never ages out\n if (ttlMs !== undefined && at - cached.atMs >= ttlMs) return undefined;\n return entry;\n }\n\n /**\n * Whether the queue item `it`'s key has already resolved to a live `done`\n * entry in `seen` (the work completed and its result is cached). Such an\n * item must NOT be re-armed by `boot` or claimed by `claimNext` — doing so\n * re-runs work whose result is already known (the complete-then-crash bug).\n *\n * `at = +Infinity` is the right reading for the reconcile check: a `done`\n * entry counts as done for reconciliation regardless of how far past its\n * replay TTL it is. (TTL governs whether a *duplicate receive* re-processes,\n * not whether a *queued, already-completed* item should re-run — that one\n * never should.)\n */\n function isAlreadyDone(state: IntakeState<P, R>, it: QueueItem<P>): boolean {\n return state.seen.entries[keyOf(it.input)]?.value.phase === \"done\";\n }\n\n /**\n * Receive a payload at clock reading `at`, filing it under the queue-item\n * id `id`. PURE — returns a NEW slice; the input is untouched.\n *\n * Three outcomes, each reified as a Cmd (see the Cmd union above):\n *\n * 1. **New key** (`liveEntry` is absent at `at`) — `remember` the key as\n * `pending`, `enqueue` the payload onto the work queue, and emit\n * `intake:process`. The only path that grows either half of the slice.\n * 2. **Duplicate, still pending** — the original is in flight. Drop\n * silently: the slice is returned unchanged and no Cmd is emitted, so\n * the duplicate neither re-enqueues nor double-processes. A pending\n * entry never ages out, so this holds no matter how slow the worker is.\n * 3. **Duplicate, already done (within TTL)** — emit `intake:replay`\n * carrying the cached `result` so the caller gets the original response.\n * The slice is unchanged (a replay touches neither cache nor queue). A\n * `done` key past its `ttlMs` window reads as absent and falls to (1).\n *\n * `id` is injected (work-queue's `genId` reads `Date.now()` / `crypto`, so\n * it cannot be called inside a pure verb). Generate it at the effect\n * boundary and thread it in; it becomes the enqueued item's id AND the\n * `itemId` on the emitted `intake:process` Cmd, so the worker can address the\n * exact queue item it must `complete`.\n */\n function receive(\n state: IntakeState<P, R>,\n payload: P,\n at: number,\n id: string,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const key = keyOf(payload);\n\n // Duplicate path — the key has a live entry at `at` (pending never ages;\n // done ages only past `ttlMs` measured from `complete`).\n const entry = liveEntry(state.seen, key, at);\n if (entry !== undefined) {\n if (entry.phase === \"done\") {\n // Replay the original result; touch nothing.\n return [state, [{ type: \"intake:replay\", key, result: entry.result }]];\n }\n // phase === \"pending\": original is in flight — drop silently.\n return [state, []];\n }\n\n // New path — record the key as pending, enqueue the payload, process it.\n const seenNext = memory.remember(state.seen, key, { phase: \"pending\" }, at);\n const { next: queueNext, item } = queue.enqueue(\n state.queue,\n payload,\n at,\n id,\n );\n const nextState: IntakeState<P, R> = { seen: seenNext, queue: queueNext };\n return [\n nextState,\n [{ type: \"intake:process\", key, itemId: item.id, payload }],\n ];\n }\n\n /**\n * Record the finished `result` for `key` at clock reading `at`. PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds (completion is\n * a pure cache write; the host marks the queue item done via the work-queue\n * adapter at the effect boundary).\n *\n * Swaps the key's cached entry from `pending` to `done` so every subsequent\n * `receive` of the same key replays this `result`. `remember` stamps the\n * entry's `atMs` to `at`, which is the moment intake's `done`-TTL clock\n * starts (see `liveEntry`): a completed key stays replayable for `ttlMs`\n * past completion, not past first receipt.\n *\n * `complete` does NOT remove the queue item — the queue is the host's drain\n * ledger, and which items leave it (and when) is the host's call via the\n * work-queue adapter. `complete` owns the cache half only.\n */\n function complete(\n state: IntakeState<P, R>,\n key: string,\n result: R,\n at: number,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const seenNext = memory.remember(\n state.seen,\n key,\n { phase: \"done\", result },\n at,\n );\n return [{ ...state, seen: seenNext }, []];\n }\n\n /**\n * Resume after a boot (Durable-Object reload / process restart). PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds.\n *\n * Resets every `running` queue item back to `pending` so the host's drain\n * re-claims it — EXCEPT an item whose key already resolved to `done` in\n * `seen`. That item's worker finished and cached its result before the crash;\n * re-arming it would re-run completed work (the complete-then-crash bug). It\n * is left as-is (and `claimNext` will likewise skip it), so the host drains\n * it via `markDone` without ever re-executing the side effect.\n *\n * A `running` item still `pending` in the cache is one a now-dead worker\n * claimed but never finished — that one IS reset, so it gets re-claimed.\n *\n * Returns the same reference when nothing changed (no running item needed\n * re-arming) so the host can skip a redundant save.\n *\n * Call once from a `boot` Msg the host dispatches after `run(...)` returns —\n * never from `init`'s rehydrate branch (that branch must be a pure\n * passthrough; see the substrate's `init` contract).\n */\n function boot(\n state: IntakeState<P, R>,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n // Selection lives HERE (the done-cache reconcile is intake's own rule); the\n // status FLIP is delegated to the `QueueAdapter`. A blanket reset would\n // re-arm completed-then-crashed items (it cannot see the done cache), so\n // intake picks the items and `queue.patch` performs each transition — the\n // same spread the work-queue lifecycle uses, not a re-rolled one.\n let items = state.queue;\n let changed = false;\n for (const it of state.queue) {\n if (it.status !== \"running\") continue;\n // Reconcile against the done cache: a completed-then-crashed item must\n // NOT be re-armed (it would re-run already-cached work).\n if (isAlreadyDone(state, it)) continue;\n const flip = queue.patch(items, it.id, (item) => ({\n ...item,\n status: \"pending\",\n startedAt: undefined,\n }));\n items = flip.next;\n changed = changed || flip.changed;\n }\n if (!changed) return [state, []];\n return [{ ...state, queue: items }, []];\n }\n\n /**\n * Claim the next pending queue item, flipping it `running` and stamping\n * `startedAt` to `at`. PURE — returns a NEW slice plus the claimed item, or\n * `null` if nothing claimable remains.\n *\n * \"Claimable\" is a pending item WHOSE KEY IS NOT ALREADY `done` in `seen`.\n * An item whose key completed before a crash (its `intake:process` ran, work\n * finished, result cached, but the item never left the queue) is skipped —\n * claiming it would re-run already-cached work. Such items are inert here;\n * the host drains them via `markDone`.\n *\n * This is the drain side of the intake, the mirror of `receive`'s enqueue.\n * It is a pure helper (not a verb that emits Cmds) because \"what to do with\n * the claimed item\" is the host's decision — typically: dispatch the worker,\n * await it, then call `complete`. Threading it through the slice keeps the\n * claim durable: the `running` stamp survives eviction, and `boot` re-arms\n * it if the worker died.\n */\n function claimNext(\n state: IntakeState<P, R>,\n at: number,\n ): {\n readonly state: IntakeState<P, R>;\n readonly claimed: QueueItem<P>;\n } | null {\n // Selection lives HERE (skip an item whose key already resolved `done`);\n // the status FLIP is delegated to the `QueueAdapter`. A blind `queue.claim`\n // would take the first PENDING item regardless of the done cache, which\n // would re-run already-cached work — so intake selects the eligible item and\n // `queue.patch` performs the running-stamp transition (the work-queue\n // lifecycle's own spread, not a re-rolled one).\n const target = state.queue.find(\n (it) => it.status === \"pending\" && !isAlreadyDone(state, it),\n );\n if (target === undefined) return null;\n // The patch closure is the single place the claimed item is materialized;\n // capture it here so the result needs no re-scan of the new queue.\n let claimed: QueueItem<P> = target;\n const { next } = queue.patch(state.queue, target.id, (item) => {\n claimed = { ...item, status: \"running\", startedAt: at };\n return claimed;\n });\n return { state: { ...state, queue: next }, claimed };\n }\n\n return { init, receive, complete, boot, claimNext };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/idempotent-intake/index.ts"],"names":[],"mappings":"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","file":"chunk-ZVJZ72HY.js","sourcesContent":["/**\n * @demlik/tea/idempotent-intake — receive-once intake for webhooks / queue\n * messages: dedupe by key, enqueue the new ones, replay the cached result to\n * the duplicates. The \"process this payload exactly once even though it\n * arrives N times\" knob.\n *\n * **The recipe.** This is a Level-2 composition — it owns no primitive\n * queue/cache logic of its own, it *splices two L1 bricks into one Model slice*\n * and DELEGATES every transition to their VERB SEAMS — `IdempotencyMemory`\n * (`../idempotency/adapter`) and `QueueAdapter` (`../work-queue/adapter`) —\n * so the store ops and the `QueueItem` / `IdempotencyStore` shapes stay\n * insulated behind verbs the intake calls:\n *\n * - `../idempotency/adapter` — the dedupe-by-key + last-result cache. The\n * `seen` half of the slice is its `IdempotencyStore`, keyed by\n * `keyOf(payload)`. A key is recorded once `receive` calls `memory.remember`;\n * `complete` calls `memory.remember` again to swap its cached value from\n * `pending` to the finished `result`. The cache write is the brick's, never\n * re-rolled here.\n * - `../work-queue/adapter` — the pending-work list. The `queue` half of the\n * slice is its `QueueItem<P>[]`. Every status flip routes through the\n * adapter verbs: `receive` enqueues via `queue.enqueue`, `claimNext` stamps\n * the `running` claim via `queue.patch`, `boot` re-arms a dead worker's\n * claim via `queue.patch`. Intake owns only the SELECTION (which item,\n * gated on the done-cache reconcile below) — the array transition is the\n * brick's. The consumer drains it (claim → process → `complete`) at its own\n * pace.\n *\n * Both bricks are pure-state + pure-ops, host- and clock-agnostic by\n * construction, so the combined slice inherits TEA's two non-negotiables:\n *\n * - **Durable** — the slice is a plain serializable value (a record-backed\n * idempotency store + an array of queue items, no `Map`, no closures), so\n * it survives a Durable-Object eviction / reload and rehydrates whole.\n * - **Replayable** — every verb is a pure `(state, …) => [state, Cmd[]]`\n * transition; no `Date.now()`, no `Math.random()`, no I/O inside a verb.\n * `at` (the clock) and `id` (the queue-item id) are *injected* at the call\n * site, so `trace-replay` reconstructs each transition exactly.\n *\n * **The decision lives in a Cmd, not in a return shape.** `receive` cannot\n * \"return\" what it decided (the knob contract pins the shape to\n * `[State, Cmd[]]`), so the decision is *reified as a Cmd* the runtime\n * performs:\n *\n * - first sighting of a key → mark it `pending`, enqueue the payload, emit\n * `IntakeProcess` (go run the work).\n * - a key already `pending` → drop silently (the original is in flight);\n * emit nothing.\n * - a key already `done` → emit `IntakeReplay` carrying the cached\n * result so the duplicate caller gets the SAME response the first one\n * produced, with zero re-execution of the side effect.\n *\n * **Two crash-safety guarantees the verbs enforce (and the unit specs alone\n * missed — see the wired-machine test):**\n *\n * - *A slow worker is never double-enqueued.* The TTL clock starts at\n * `complete`, not at `receive`: a `pending` entry NEVER ages out, so a\n * duplicate of an in-flight key is always dropped no matter how long the\n * worker takes. Only a `done` entry expires (see `liveEntry`).\n * - *A complete-then-crash never re-runs.* `boot` and `claimNext` reconcile\n * the queue against the done cache: a queue item whose key already resolved\n * to `done` in `seen` is skipped (never reset to pending, never claimed),\n * so a worker that finished and cached its result before the DO evicted is\n * not re-executed on resume.\n *\n * Typical wiring (the combinator form — splice ~3 hooks into a host machine):\n *\n * const intake = createIntake<StripeEvent, ChargeResult>({\n * keyOf: (e) => e.id, // Stripe's idempotency key\n * ttlMs: 24 * 60 * 60 * 1000, // a replayed webhook 24h after it COMPLETED is re-run\n * });\n *\n * // in `init`:\n * intake: intake.init(),\n * // in `update`, on the inbound-webhook Msg:\n * const [next, cmds] = intake.receive(state.intake, payload, at, id);\n * // …route `IntakeProcess` to your worker, `IntakeReplay` to the responder.\n * // on the worker's done Msg:\n * const [next, _] = intake.complete(state.intake, key, result, at);\n * // once, after boot (DO reload), before resuming the drain:\n * const [next, _] = intake.boot(state.intake);\n *\n * Escape hatch: reach the raw L1 ops via `@demlik/tea/idempotency` and\n * `@demlik/tea/work-queue` and thread them by hand.\n */\n\nimport type { IdempotencyStore } from \"../idempotency\";\nimport { idempotencyMemory } from \"../idempotency/adapter\";\nimport type { Cmd } from \"../index\";\nimport type { QueueItem } from \"../work-queue\";\nimport { queueAdapter } from \"../work-queue/adapter\";\n\n/**\n * A cached intake entry: either a key whose work is still in flight\n * (`pending`) or one whose work has finished and whose `result` is held for\n * replay to later duplicates (`done`). Discriminated on `phase` — `pending`\n * carries no payload, `done` carries the result `R`.\n *\n * Lives inside the `seen` half of the slice (`IdempotencyStore<IntakeEntry<R>>`).\n * It is the dedupe ledger's *value*; the ledger's *key* is `keyOf(payload)`.\n */\nexport type IntakeEntry<R> =\n | { readonly phase: \"pending\" }\n | { readonly phase: \"done\"; readonly result: R };\n\n/**\n * The intake knob's config — `keyOf` derives the idempotency key from a\n * payload, `ttlMs` (optional) ages a *completed* cached key out so a webhook\n * replayed long after the work finished is treated as new and re-processed.\n *\n * **The TTL clock starts at `complete`, not at `receive`.** A `pending` entry\n * (work still in flight) NEVER ages out, no matter how slow the worker is —\n * ageing a pending key would let the duplicate re-enqueue the in-flight\n * payload and double-process it. Only a `done` entry expires, measured from\n * the `at` passed to `complete` (the moment the result was cached). Inside the\n * window a duplicate replays the cached result; at/after the window it is\n * re-accepted and re-processed.\n *\n * Omit `ttlMs` for a cache whose `done` entries never expire by age (keys live\n * forever — fine for a bounded universe of ids, unbounded growth otherwise).\n *\n * The TTL is owned HERE, not delegated to the composed `IdempotencyStore`: the\n * store's uniform per-entry TTL cannot tell `pending` from `done`, so intake\n * keeps the store TTL-free and applies the phase-aware rule itself (see\n * `liveEntry`).\n */\nexport interface IntakeConfig<P> {\n /** Derive the idempotency key from a payload. Must be pure + deterministic. */\n readonly keyOf: (payload: P) => string;\n /**\n * Age a *completed* key out after this many ms, measured from the `at`\n * passed to `complete`. Pending keys never age out.\n */\n readonly ttlMs?: number;\n}\n\n/**\n * The Model slice this knob owns. `seen` is the dedupe + last-result cache\n * (the `../idempotency` brick); `queue` is the pending-work list (the\n * `../work-queue` brick). Both are plain serializable values — the whole slice\n * round-trips through `JSON` so it survives eviction and replays exactly.\n */\nexport interface IntakeState<P, R> {\n readonly seen: IdempotencyStore<IntakeEntry<R>>;\n readonly queue: readonly QueueItem<P>[];\n}\n\n// === Cmds the verbs reify ===\n//\n// The knob contract pins verbs to `(state, …) => [State, Cmd[]]`, so a verb's\n// *decision* (new vs. duplicate-pending vs. duplicate-done) cannot be a return\n// value — it is emitted as a Cmd the runtime's interpret handler performs.\n// Each Cmd is plain data (invariant 1): the payload / key / result it carries\n// are values, never closures.\n\n/** A genuinely-new payload was accepted and enqueued — go run the work. */\nexport interface IntakeProcessCmd<P> extends Cmd<\"intake:process\"> {\n /** The dedupe key the payload was filed under. */\n readonly key: string;\n /** The enqueued queue-item id (the same `id` passed to `receive`). */\n readonly itemId: string;\n /** The original payload, for the worker to process. */\n readonly payload: P;\n}\n\n/**\n * A duplicate of an already-completed key arrived — replay the cached result\n * to the caller instead of re-running the side effect.\n */\nexport interface IntakeReplayCmd<R> extends Cmd<\"intake:replay\"> {\n /** The dedupe key whose cached result is being replayed. */\n readonly key: string;\n /** The result the ORIGINAL request produced, recalled from the cache. */\n readonly result: R;\n}\n\n/** The Cmd union the intake verbs emit. */\nexport type IntakeCmd<P, R> = IntakeProcessCmd<P> | IntakeReplayCmd<R>;\n\n/**\n * Build an idempotent-intake knob from `config`. Returns the uniform L2\n * contract — `init()` + the verbs (`receive`, `complete`, `boot`, `claimNext`).\n * No `subs` / `handlers`: intake owns no timers and does no I/O of its own (the\n * composed bricks are pure-ops; the worker that runs `IntakeProcess` and the\n * responder that runs `IntakeReplay` live in the host's interpret map).\n */\nexport function createIntake<P, R>(config: IntakeConfig<P>) {\n const { keyOf, ttlMs } = config;\n\n // The two store seams, bound once for this knob's `P`/`R`. The intake\n // delegates every queue status flip to `queue.*` and every dedupe-cache\n // transition to `memory.*`, so the `QueueItem<P>` / `IdempotencyStore` shapes\n // are insulated behind the verbs rather than reached by raw ops.\n const queue = queueAdapter<P>();\n const memory = idempotencyMemory<IntakeEntry<R>>();\n\n /**\n * The empty slice: a TTL-free idempotency store (intake owns the TTL itself —\n * see `liveEntry`, because the store cannot tell `pending` from `done`) plus\n * an empty work queue. The rehydrate path is the host's `init(loaded)` — it\n * passes the persisted slice straight back, this is only the fresh-boot value.\n */\n function init(): IntakeState<P, R> {\n return {\n seen: memory.init(),\n queue: [],\n };\n }\n\n /**\n * The live cached entry for `key` at clock reading `at`, or `undefined` if\n * the key is absent or its `done` window has elapsed. This is intake's\n * phase-aware expiry rule, applied HERE rather than by the store's uniform\n * per-entry TTL:\n *\n * - **`pending`** — work is in flight; NEVER expires. Ageing a pending key\n * would let a slow worker's payload be re-enqueued and double-processed.\n * - **`done`** — expires `ttlMs` after the `atMs` the result was cached at\n * (the `at` passed to `complete`). Half-open, matching the store /\n * rate-limit window edge: `at - atMs >= ttlMs` reads as expired. No\n * `ttlMs` ⇒ a `done` entry never expires by age.\n *\n * PURE — reads `state.seen.entries` directly (no `Date.now`), never mutates.\n */\n function liveEntry(\n store: IdempotencyStore<IntakeEntry<R>>,\n key: string,\n at: number,\n ): IntakeEntry<R> | undefined {\n const cached = store.entries[key];\n if (cached === undefined) return undefined;\n const entry = cached.value;\n if (entry.phase === \"pending\") return entry; // in flight — never ages out\n if (ttlMs !== undefined && at - cached.atMs >= ttlMs) return undefined;\n return entry;\n }\n\n /**\n * Whether the queue item `it`'s key has already resolved to a live `done`\n * entry in `seen` (the work completed and its result is cached). Such an\n * item must NOT be re-armed by `boot` or claimed by `claimNext` — doing so\n * re-runs work whose result is already known (the complete-then-crash bug).\n *\n * `at = +Infinity` is the right reading for the reconcile check: a `done`\n * entry counts as done for reconciliation regardless of how far past its\n * replay TTL it is. (TTL governs whether a *duplicate receive* re-processes,\n * not whether a *queued, already-completed* item should re-run — that one\n * never should.)\n */\n function isAlreadyDone(state: IntakeState<P, R>, it: QueueItem<P>): boolean {\n return state.seen.entries[keyOf(it.input)]?.value.phase === \"done\";\n }\n\n /**\n * Receive a payload at clock reading `at`, filing it under the queue-item\n * id `id`. PURE — returns a NEW slice; the input is untouched.\n *\n * Three outcomes, each reified as a Cmd (see the Cmd union above):\n *\n * 1. **New key** (`liveEntry` is absent at `at`) — `remember` the key as\n * `pending`, `enqueue` the payload onto the work queue, and emit\n * `intake:process`. The only path that grows either half of the slice.\n * 2. **Duplicate, still pending** — the original is in flight. Drop\n * silently: the slice is returned unchanged and no Cmd is emitted, so\n * the duplicate neither re-enqueues nor double-processes. A pending\n * entry never ages out, so this holds no matter how slow the worker is.\n * 3. **Duplicate, already done (within TTL)** — emit `intake:replay`\n * carrying the cached `result` so the caller gets the original response.\n * The slice is unchanged (a replay touches neither cache nor queue). A\n * `done` key past its `ttlMs` window reads as absent and falls to (1).\n *\n * `id` is injected (work-queue's `genId` reads `Date.now()` / `crypto`, so\n * it cannot be called inside a pure verb). Generate it at the effect\n * boundary and thread it in; it becomes the enqueued item's id AND the\n * `itemId` on the emitted `intake:process` Cmd, so the worker can address the\n * exact queue item it must `complete`.\n */\n function receive(\n state: IntakeState<P, R>,\n payload: P,\n at: number,\n id: string,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const key = keyOf(payload);\n\n // Duplicate path — the key has a live entry at `at` (pending never ages;\n // done ages only past `ttlMs` measured from `complete`).\n const entry = liveEntry(state.seen, key, at);\n if (entry !== undefined) {\n if (entry.phase === \"done\") {\n // Replay the original result; touch nothing.\n return [state, [{ type: \"intake:replay\", key, result: entry.result }]];\n }\n // phase === \"pending\": original is in flight — drop silently.\n return [state, []];\n }\n\n // New path — record the key as pending, enqueue the payload, process it.\n const seenNext = memory.remember(state.seen, key, { phase: \"pending\" }, at);\n const { next: queueNext, item } = queue.enqueue(\n state.queue,\n payload,\n at,\n id,\n );\n const nextState: IntakeState<P, R> = { seen: seenNext, queue: queueNext };\n return [\n nextState,\n [{ type: \"intake:process\", key, itemId: item.id, payload }],\n ];\n }\n\n /**\n * Record the finished `result` for `key` at clock reading `at`. PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds (completion is\n * a pure cache write; the host marks the queue item done via the work-queue\n * adapter at the effect boundary).\n *\n * Swaps the key's cached entry from `pending` to `done` so every subsequent\n * `receive` of the same key replays this `result`. `remember` stamps the\n * entry's `atMs` to `at`, which is the moment intake's `done`-TTL clock\n * starts (see `liveEntry`): a completed key stays replayable for `ttlMs`\n * past completion, not past first receipt.\n *\n * `complete` does NOT remove the queue item — the queue is the host's drain\n * ledger, and which items leave it (and when) is the host's call via the\n * work-queue adapter. `complete` owns the cache half only.\n */\n function complete(\n state: IntakeState<P, R>,\n key: string,\n result: R,\n at: number,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n const seenNext = memory.remember(\n state.seen,\n key,\n { phase: \"done\", result },\n at,\n );\n return [{ ...state, seen: seenNext }, []];\n }\n\n /**\n * Resume after a boot (Durable-Object reload / process restart). PURE —\n * returns a NEW slice; the input is untouched. Emits no Cmds.\n *\n * Resets every `running` queue item back to `pending` so the host's drain\n * re-claims it — EXCEPT an item whose key already resolved to `done` in\n * `seen`. That item's worker finished and cached its result before the crash;\n * re-arming it would re-run completed work (the complete-then-crash bug). It\n * is left as-is (and `claimNext` will likewise skip it), so the host drains\n * it via `markDone` without ever re-executing the side effect.\n *\n * A `running` item still `pending` in the cache is one a now-dead worker\n * claimed but never finished — that one IS reset, so it gets re-claimed.\n *\n * Returns the same reference when nothing changed (no running item needed\n * re-arming) so the host can skip a redundant save.\n *\n * Call once from a `boot` Msg the host dispatches after `run(...)` returns —\n * never from `init`'s rehydrate branch (that branch must be a pure\n * passthrough; see the substrate's `init` contract).\n */\n function boot(\n state: IntakeState<P, R>,\n ): readonly [IntakeState<P, R>, readonly IntakeCmd<P, R>[]] {\n // Selection lives HERE (the done-cache reconcile is intake's own rule); the\n // status FLIP is delegated to the `QueueAdapter`. A blanket reset would\n // re-arm completed-then-crashed items (it cannot see the done cache), so\n // intake picks the items and `queue.patch` performs each transition — the\n // same spread the work-queue lifecycle uses, not a re-rolled one.\n let items = state.queue;\n let changed = false;\n for (const it of state.queue) {\n if (it.status !== \"running\") continue;\n // Reconcile against the done cache: a completed-then-crashed item must\n // NOT be re-armed (it would re-run already-cached work).\n if (isAlreadyDone(state, it)) continue;\n const flip = queue.patch(items, it.id, (item) => ({\n ...item,\n status: \"pending\",\n startedAt: undefined,\n }));\n items = flip.next;\n changed = changed || flip.changed;\n }\n if (!changed) return [state, []];\n return [{ ...state, queue: items }, []];\n }\n\n /**\n * Claim the next pending queue item, flipping it `running` and stamping\n * `startedAt` to `at`. PURE — returns a NEW slice plus the claimed item, or\n * `null` if nothing claimable remains.\n *\n * \"Claimable\" is a pending item WHOSE KEY IS NOT ALREADY `done` in `seen`.\n * An item whose key completed before a crash (its `intake:process` ran, work\n * finished, result cached, but the item never left the queue) is skipped —\n * claiming it would re-run already-cached work. Such items are inert here;\n * the host drains them via `markDone`.\n *\n * This is the drain side of the intake, the mirror of `receive`'s enqueue.\n * It is a pure helper (not a verb that emits Cmds) because \"what to do with\n * the claimed item\" is the host's decision — typically: dispatch the worker,\n * await it, then call `complete`. Threading it through the slice keeps the\n * claim durable: the `running` stamp survives eviction, and `boot` re-arms\n * it if the worker died.\n */\n function claimNext(\n state: IntakeState<P, R>,\n at: number,\n ): {\n readonly state: IntakeState<P, R>;\n readonly claimed: QueueItem<P>;\n } | null {\n // Selection lives HERE (skip an item whose key already resolved `done`);\n // the status FLIP is delegated to the `QueueAdapter`. A blind `queue.claim`\n // would take the first PENDING item regardless of the done cache, which\n // would re-run already-cached work — so intake selects the eligible item and\n // `queue.patch` performs the running-stamp transition (the work-queue\n // lifecycle's own spread, not a re-rolled one).\n const target = state.queue.find(\n (it) => it.status === \"pending\" && !isAlreadyDone(state, it),\n );\n if (target === undefined) return null;\n // The patch closure is the single place the claimed item is materialized;\n // capture it here so the result needs no re-scan of the new queue.\n let claimed: QueueItem<P> = target;\n const { next } = queue.patch(state.queue, target.id, (item) => {\n claimed = { ...item, status: \"running\", startedAt: at };\n return claimed;\n });\n return { state: { ...state, queue: next }, claimed };\n }\n\n return { init, receive, complete, boot, claimNext };\n}\n"]}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { canPass, defaultCircuitPolicy, initCircuit, onFailure, onSuccess } from '../chunk-
|
|
1
|
+
export { canPass, defaultCircuitPolicy, initCircuit, onFailure, onSuccess } from '../chunk-UF4MZSLT.js';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
@@ -11,6 +11,7 @@ declare class NoCellError extends Error {
|
|
|
11
11
|
readonly msgType: string;
|
|
12
12
|
readonly stateName: string;
|
|
13
13
|
readonly name = "NoCellError";
|
|
14
|
+
readonly _tag: "NoCellError";
|
|
14
15
|
constructor(msgType: string, stateName: string);
|
|
15
16
|
}
|
|
16
17
|
declare function applyCell<S, M extends {
|
|
@@ -44,7 +45,7 @@ type Transitions<S extends {
|
|
|
44
45
|
};
|
|
45
46
|
};
|
|
46
47
|
type Cmd<T extends string = string> = {
|
|
47
|
-
type: T;
|
|
48
|
+
readonly type: T;
|
|
48
49
|
};
|
|
49
50
|
declare const Cmd: {
|
|
50
51
|
/**
|
|
@@ -113,9 +114,46 @@ type SubId = string & {
|
|
|
113
114
|
*/
|
|
114
115
|
declare function subId(s: string): SubId;
|
|
115
116
|
type Sub<T extends string = string> = {
|
|
116
|
-
id: SubId;
|
|
117
|
-
type: T;
|
|
117
|
+
readonly id: SubId;
|
|
118
|
+
readonly type: T;
|
|
118
119
|
};
|
|
120
|
+
type Dispose = () => void;
|
|
121
|
+
declare function structuralHash(deps: unknown): string;
|
|
122
|
+
interface DepKeyedSub<S, M, Ctx = unknown> {
|
|
123
|
+
/**
|
|
124
|
+
* The slice of state this Sub depends on. `null` ⇒ the Sub is inactive in
|
|
125
|
+
* this state (the substrate disposes it if it was running). Non-null ⇒ the
|
|
126
|
+
* Sub is active, keyed on `structuralHash(deps)`.
|
|
127
|
+
*
|
|
128
|
+
* Pure (invariant 2). Plain JSON-compatible data only (invariant 1) — the
|
|
129
|
+
* structural hash walks it deterministically.
|
|
130
|
+
*/
|
|
131
|
+
readonly deps: (state: S) => unknown;
|
|
132
|
+
/**
|
|
133
|
+
* Open the resource for the current `state` (whose `deps` the kernel just
|
|
134
|
+
* found non-null). Receives `dispatch` to fire follow-up Msgs (a timer's
|
|
135
|
+
* fire, an inbound frame) and the machine's `ctx` (the host's transport
|
|
136
|
+
* factory, storage handles). Returns the `Dispose` the substrate runs on
|
|
137
|
+
* teardown / re-arm. Same role as a `subscribe[type]` handler's returned
|
|
138
|
+
* cleanup.
|
|
139
|
+
*
|
|
140
|
+
* `source` takes `state` (not the deps slice) so the slice type `D` never
|
|
141
|
+
* escapes the entry — each battery's `.depKeyed` closes over its own typed
|
|
142
|
+
* deps internally (cast-free existential: the entry that produced the deps
|
|
143
|
+
* is the entry that consumes them).
|
|
144
|
+
*/
|
|
145
|
+
readonly source: (state: S, dispatch: (msg: M) => void, ctx: Ctx) => Dispose;
|
|
146
|
+
}
|
|
147
|
+
interface Identity<S, M> {
|
|
148
|
+
/** The identity THIS instance owns. Pure (invariant 2); plain value. */
|
|
149
|
+
readonly ofState: (state: S) => unknown;
|
|
150
|
+
/**
|
|
151
|
+
* The identity a message is addressed to, or `undefined` when the message
|
|
152
|
+
* carries no identity (lifecycle / pre-identity messages — never dropped).
|
|
153
|
+
* Pure (invariant 2); plain value when defined.
|
|
154
|
+
*/
|
|
155
|
+
readonly ofMsg: (msg: M) => unknown;
|
|
156
|
+
}
|
|
119
157
|
interface Port<T> {
|
|
120
158
|
readonly __brand: "port";
|
|
121
159
|
readonly name: string;
|
|
@@ -133,15 +171,25 @@ interface PortEmitter {
|
|
|
133
171
|
emit<T>(port: Port<T>, value: T): void;
|
|
134
172
|
}
|
|
135
173
|
type NoCtx = Readonly<Record<never, never>>;
|
|
136
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Spelled-out alias for `NoCtx`.
|
|
176
|
+
*
|
|
177
|
+
* @deprecated Use {@link NoCtx}. One Way to Do Each Thing — `NoCtx` is the
|
|
178
|
+
* single non-deprecated name for the context-free-ctx marker (this alias has
|
|
179
|
+
* no in-tree uses). Kept as a re-export for one minor per the deprecate-don't-
|
|
180
|
+
* delete policy in `MAINTAINING.md`, then removed.
|
|
181
|
+
*/
|
|
137
182
|
type ContextFree = NoCtx;
|
|
138
183
|
type Interpret<M extends {
|
|
139
184
|
type: string;
|
|
140
185
|
}, C extends Cmd, Ctx> = {
|
|
141
186
|
[K in C["type"]]: (cmd: Extract<C, {
|
|
142
187
|
type: K;
|
|
143
|
-
}>, ctx: Ctx & PortEmitter) => Promise<M | void>;
|
|
188
|
+
}>, ctx: Ctx & PortEmitter, dispatch?: (msg: M) => void) => Promise<M | void>;
|
|
144
189
|
};
|
|
190
|
+
type InterpretDetached<C extends Cmd, Allowed extends {
|
|
191
|
+
type: string;
|
|
192
|
+
}, Ctx> = (cmd: C, ctx: Ctx & PortEmitter, dispatch: (msg: Allowed) => void) => Promise<void>;
|
|
145
193
|
type Subscribe<M extends {
|
|
146
194
|
type: string;
|
|
147
195
|
}, U extends Sub, Ctx> = {
|
|
@@ -172,6 +220,48 @@ type Machine<S, M extends {
|
|
|
172
220
|
update: Reducer<S, M, C> | ([S] extends [{
|
|
173
221
|
type: string;
|
|
174
222
|
}] ? Transitions<S, M, C> : never);
|
|
223
|
+
/**
|
|
224
|
+
* Dep-keyed Subs. Each declares the state slice it depends on; the substrate
|
|
225
|
+
* derives the id (`structuralHash(deps)`) and the gate (`deps` non-null) and
|
|
226
|
+
* folds them into the SAME reconcile loop `subscriptions` feeds. The author
|
|
227
|
+
* never writes `subId(...)` and never lists Subs in a central
|
|
228
|
+
* `subscriptions(state)` aggregate — a per-Sub gate travels with the Sub, so
|
|
229
|
+
* a new phase can't silently forget to arm it.
|
|
230
|
+
*
|
|
231
|
+
* The deps slice type is erased to `unknown` per entry — each Sub's slice
|
|
232
|
+
* differs, but the slice type never crosses this field (the kernel only needs
|
|
233
|
+
* to hash it and pass `state` back to that same entry's `source`).
|
|
234
|
+
*
|
|
235
|
+
* Optional and independent of `subscriptions` / `subscribe`: a machine that
|
|
236
|
+
* omits `subs` reconciles exactly as before.
|
|
237
|
+
*
|
|
238
|
+
* Strengthens invariant 4 (lifecycle owned by the substrate) and invariant 7
|
|
239
|
+
* (identity derived, not hand-authored).
|
|
240
|
+
*/
|
|
241
|
+
subs?: ReadonlyArray<DepKeyedSub<S, M, Ctx>>;
|
|
242
|
+
/**
|
|
243
|
+
* Instance-identity filter. Declares THIS instance's identity once; the
|
|
244
|
+
* substrate drops any message addressed to a DIFFERENT identity before it
|
|
245
|
+
* reaches `update`. Replaces the per-cell `if (msg.runId !== state.runId)`
|
|
246
|
+
* guard entirely — the reducer never sees a foreign-instance message, so no
|
|
247
|
+
* cell needs to check.
|
|
248
|
+
*
|
|
249
|
+
* Opt-in: a machine that omits `identity` skips the filter (every message
|
|
250
|
+
* reaches `update`, exactly as before). A message whose `ofMsg` returns
|
|
251
|
+
* `undefined` is identity-agnostic and always reaches `update` (lifecycle /
|
|
252
|
+
* pre-identity messages).
|
|
253
|
+
*
|
|
254
|
+
* Strengthens invariant 7 (identity is explicit — declared once, enforced by
|
|
255
|
+
* the substrate) and invariant 6 (mis-addressed messages dropped at one
|
|
256
|
+
* observable point, not per cell).
|
|
257
|
+
*/
|
|
258
|
+
identity?: Identity<S, M>;
|
|
259
|
+
/**
|
|
260
|
+
* Manual Sub aggregate — the documented escape hatch, KEPT alongside `subs`.
|
|
261
|
+
* The machine that needs cross-Sub logic the per-Sub `deps` fold can't
|
|
262
|
+
* express lists Subs here; the substrate reconciles them via `subscribe`.
|
|
263
|
+
* Both paths feed ONE reconcile pass.
|
|
264
|
+
*/
|
|
175
265
|
subscriptions?: (state: S) => readonly U[];
|
|
176
266
|
subscribe?: Subscribe<M, U, Ctx>;
|
|
177
267
|
/**
|
|
@@ -194,4 +284,4 @@ declare function foldMsgs<S, M extends {
|
|
|
194
284
|
type: string;
|
|
195
285
|
}, C extends Cmd, U extends Sub, Ctx>(machine: Machine<S, M, C, U, Ctx>, base: S, msgs: readonly M[]): S;
|
|
196
286
|
|
|
197
|
-
export { Cmd as C, type
|
|
287
|
+
export { Cmd as C, type DepKeyedSub as D, type InterpretDetached as I, type Machine as M, type NoCtx as N, type Port as P, type Reducer as R, type Sub as S, type Transitions as T, type UpdateForm as U, type SubId as a, type PortEmitter as b, type Interpret as c, type ContextFree as d, type Dispose as e, type Identity as f, type Subscribe as g, type SyncReturn as h, applyCell as i, detectUpdateForm as j, foldMsgs as k, formOf as l, msgKeysOf as m, NoCellError as n, subId as o, structuralHash as s };
|
package/dist/deadline/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as Sub } from '../core-
|
|
2
|
-
import { S as SubscribeHandler } from '../types-
|
|
1
|
+
import { S as Sub, a as SubId } from '../core-42kapW0n.js';
|
|
2
|
+
import { S as SubscribeHandler } from '../types-KbL3W1mj.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @demlik/tea/deadline — a one-shot Sub that fires when an ABSOLUTE wall-clock
|
|
@@ -29,6 +29,21 @@ import { S as SubscribeHandler } from '../types-BoUWgwSm.js';
|
|
|
29
29
|
* (invariant 2) — and is exercised in tests via vitest fake timers, matching
|
|
30
30
|
* the no-injection convention of the `fromTimeout` / `fromInterval` exemplars.
|
|
31
31
|
*
|
|
32
|
+
* WHAT BACKS THE TIMER IS THE HOST'S CHOICE. `setTimeout` is the universal
|
|
33
|
+
* default, but it is not universal: a Durable Object that hibernates has no
|
|
34
|
+
* live `setTimeout` to wake it, so its deadline must be backed by a `do_alarm`
|
|
35
|
+
* registered on the alarm registry; a test host wants a fake-timer schedule.
|
|
36
|
+
* That backing is the `ArmTimer` seam — `subscribeWith(armTimer)` builds the
|
|
37
|
+
* `subscribe.deadline` cell from a host-plugged timer, and `subscribeDeadline`
|
|
38
|
+
* IS `subscribeWith(setTimeoutArmTimer())`. One deadline surface, three hosts;
|
|
39
|
+
* there is no second Sub type and no second `atMs` anchor to keep in sync.
|
|
40
|
+
*
|
|
41
|
+
* The anchor is what makes the seam safe. `atMs` is ABSOLUTE, so whatever backs
|
|
42
|
+
* it arms to a fixed wall-clock instant: the host computes the gap itself
|
|
43
|
+
* (`atMs - Date.now()`), and on a host that just rehydrated after hibernation
|
|
44
|
+
* that gap is the SHRUNKEN remainder — the deadline honours the original target
|
|
45
|
+
* rather than resetting to a fresh full-length window.
|
|
46
|
+
*
|
|
32
47
|
* Strengthens invariant 4 (external time is a subscription — the deadline is a
|
|
33
48
|
* `Sub` the runtime reconciles, never a `setTimeout` leaked into a reducer) and
|
|
34
49
|
* invariant 7 (identity is explicit — the Sub carries a stable `SubId`, so the
|
|
@@ -105,15 +120,66 @@ type DeadlineExceeded = {
|
|
|
105
120
|
*/
|
|
106
121
|
declare function deadlineSub(id: string, atMs: number, opts?: DeadlineOpts): DeadlineSub;
|
|
107
122
|
/**
|
|
108
|
-
* The
|
|
109
|
-
*
|
|
110
|
-
*
|
|
123
|
+
* The host-plugged timer backing. Given the deadline's stable `id`, its
|
|
124
|
+
* absolute `atMs`, and the Msg to fire, arm the platform-appropriate timer
|
|
125
|
+
* (DO alarm registry, `setTimeout`, fake timer) and return a cleanup that
|
|
126
|
+
* cancels the pending fire.
|
|
127
|
+
*
|
|
128
|
+
* `id` is the Sub's reconcile id — a registry-backed host (the DO alarm slot)
|
|
129
|
+
* keys its entry on `id` so the cleanup deletes the EXACT entry the substrate
|
|
130
|
+
* reconciled. A `setTimeout`-backed host ignores `id` (the closure holds the
|
|
131
|
+
* handle).
|
|
132
|
+
*
|
|
133
|
+
* The host computes the gap itself: `atMs - Date.now()` is the REMAINING time.
|
|
134
|
+
* On a host that just rehydrated after hibernation that gap is the shrunken
|
|
135
|
+
* remainder — the deadline is NOT reset to its full length. That recomputation
|
|
136
|
+
* is the reason `atMs` rides on the Sub as an absolute instant rather than a
|
|
137
|
+
* delay: the anchor is what every backing agrees on.
|
|
138
|
+
*/
|
|
139
|
+
type ArmTimer<M> = (id: SubId, atMs: number, msg: M, dispatch: (msg: M) => void) => () => void;
|
|
140
|
+
/**
|
|
141
|
+
* Build the `subscribe["deadline"]` cell from a host-plugged `armTimer`. This
|
|
142
|
+
* module owns the Sub shape, the anchor, and the Msg; the HOST owns what backs
|
|
143
|
+
* the timer. The returned handler reads `id` + `atMs` off the Sub, builds the
|
|
144
|
+
* `deadlineExceeded(...)` Msg, and hands all three to `armTimer`.
|
|
145
|
+
*
|
|
146
|
+
* Use it when `setTimeout` is the wrong backing — most concretely a Durable
|
|
147
|
+
* Object, which hibernates and must register a `do_alarm` instead:
|
|
148
|
+
*
|
|
149
|
+
* subscribe: {
|
|
150
|
+
* deadline: subscribeWith((id, atMs, msg, dispatch) =>
|
|
151
|
+
* alarms.register(id, atMs, () => dispatch(msg)),
|
|
152
|
+
* ),
|
|
153
|
+
* }
|
|
154
|
+
*
|
|
155
|
+
* For the `setTimeout` default, use {@link subscribeDeadline} — it is exactly
|
|
156
|
+
* `subscribeWith(setTimeoutArmTimer())`, so there is one deadline surface and
|
|
157
|
+
* one anchor, not a second one per host.
|
|
158
|
+
*/
|
|
159
|
+
declare function subscribeWith(armTimer: ArmTimer<DeadlineExceeded>): SubscribeHandler<DeadlineSub, DeadlineExceeded, unknown>;
|
|
160
|
+
/**
|
|
161
|
+
* The `setTimeout` timer backing — for node / browser / any host whose timer is
|
|
162
|
+
* a plain `setTimeout`. Arms for the REMAINING time (`max(0, atMs - Date.now())`,
|
|
163
|
+
* floored at 0 so an already-past deadline fires on the NEXT tick rather than
|
|
164
|
+
* synchronously inside the reconcile pass), so a deadline re-derived after a
|
|
165
|
+
* rehydrate fires at the original instant, not a fresh full window.
|
|
166
|
+
*
|
|
167
|
+
* Composes on `fromTimeout` rather than redrawing `setTimeout` /
|
|
168
|
+
* `clearTimeout`: this package has ONE timer lifecycle, and the arm-timer seam
|
|
169
|
+
* plugs into it instead of forking it. A hibernating host does NOT use this —
|
|
170
|
+
* it plugs its own `armTimer` (a `do_alarm` registration) into
|
|
171
|
+
* {@link subscribeWith}.
|
|
172
|
+
*/
|
|
173
|
+
declare function setTimeoutArmTimer(): ArmTimer<DeadlineExceeded>;
|
|
174
|
+
/**
|
|
175
|
+
* The `subscribe["deadline"]` cell for the DEFAULT `setTimeout` backing. Arms a
|
|
176
|
+
* one-shot timer for the remaining delay `max(0, atMs - Date.now())` and
|
|
177
|
+
* dispatches `deadlineExceeded(...)` when it fires; returns a cleanup that
|
|
178
|
+
* clears the pending timer.
|
|
111
179
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* to `delayMs: 0`, so `fromTimeout` fires on the next tick — asynchronously,
|
|
116
|
-
* never inside the reconcile pass.
|
|
180
|
+
* It is `subscribeWith(setTimeoutArmTimer())` — the default backing named, not
|
|
181
|
+
* a separate implementation, so the host-plugged path and the default path can
|
|
182
|
+
* never disagree about the anchor.
|
|
117
183
|
*
|
|
118
184
|
* Assign directly to a `Subscribe` cell:
|
|
119
185
|
*
|
|
@@ -129,4 +195,4 @@ declare const subscribeDeadline: SubscribeHandler<DeadlineSub, DeadlineExceeded,
|
|
|
129
195
|
*/
|
|
130
196
|
declare function deadlineExceeded(id: string, atMs: number): DeadlineExceeded;
|
|
131
197
|
|
|
132
|
-
export { type DeadlineExceeded, type DeadlineOpts, type DeadlineSub, deadlineExceeded, deadlineSub, subscribeDeadline };
|
|
198
|
+
export { type ArmTimer, type DeadlineExceeded, type DeadlineOpts, type DeadlineSub, deadlineExceeded, deadlineSub, setTimeoutArmTimer, subscribeDeadline, subscribeWith };
|
package/dist/deadline/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { deadlineExceeded, deadlineSub, subscribeDeadline } from '../chunk-
|
|
1
|
+
export { deadlineExceeded, deadlineSub, setTimeoutArmTimer, subscribeDeadline, subscribeWith } from '../chunk-RMX4EOQK.js';
|
|
2
2
|
import '../chunk-2YFLOCWX.js';
|
|
3
|
-
import '../chunk-
|
|
4
|
-
import '../chunk-
|
|
3
|
+
import '../chunk-D4NJBQAH.js';
|
|
4
|
+
import '../chunk-PBDYW2TU.js';
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
|
6
6
|
//# sourceMappingURL=index.js.map
|
package/dist/devtools/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { typeOf, indexPath, unionKeys, keyPath } from '../chunk-EWJP3YYP.js';
|
|
1
2
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
3
|
import { useRef, useEffect, useState, useCallback } from 'react';
|
|
3
4
|
|
|
4
|
-
// src/devtools/msg-log.tsx
|
|
5
5
|
function MsgLog({ history, className }) {
|
|
6
6
|
return /* @__PURE__ */ jsx("div", { className: `tea-dt-log${className ? ` ${className}` : ""}`, children: history.map((r, i) => /* @__PURE__ */ jsxs(
|
|
7
7
|
"div",
|
|
@@ -17,13 +17,6 @@ function MsgLog({ history, className }) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// src/devtools/state-diff.ts
|
|
20
|
-
function typeOf(v) {
|
|
21
|
-
if (v === null) return "null";
|
|
22
|
-
if (v === void 0) return "undefined";
|
|
23
|
-
if (Array.isArray(v)) return "array";
|
|
24
|
-
if (typeof v === "object") return "object";
|
|
25
|
-
return "primitive";
|
|
26
|
-
}
|
|
27
20
|
function diffState(expected, actual) {
|
|
28
21
|
const out = [];
|
|
29
22
|
walk(expected, actual, "state", out);
|
|
@@ -50,16 +43,15 @@ function walk(expected, actual, path, out) {
|
|
|
50
43
|
const aa = actual;
|
|
51
44
|
const len = Math.max(ae.length, aa.length);
|
|
52
45
|
for (let i = 0; i < len; i++) {
|
|
53
|
-
walk(ae[i], aa[i],
|
|
46
|
+
walk(ae[i], aa[i], indexPath(path, i), out);
|
|
54
47
|
}
|
|
55
48
|
return;
|
|
56
49
|
}
|
|
57
50
|
if (te === "object") {
|
|
58
51
|
const oe = expected;
|
|
59
52
|
const oa = actual;
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
walk(oe[key], oa[key], `${path}.${key}`, out);
|
|
53
|
+
for (const key of unionKeys(oe, oa)) {
|
|
54
|
+
walk(oe[key], oa[key], keyPath(path, key), out);
|
|
63
55
|
}
|
|
64
56
|
return;
|
|
65
57
|
}
|
|
@@ -96,36 +88,24 @@ function changeRow(c) {
|
|
|
96
88
|
return /* @__PURE__ */ jsxs("div", { className: "tea-dt-diff-row tea-dt-diff-changed", children: [
|
|
97
89
|
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-mark", children: "~" }),
|
|
98
90
|
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-path", children: c.path }),
|
|
99
|
-
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-old", children:
|
|
91
|
+
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-old", children: fmtValue(c.expected) }),
|
|
100
92
|
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-arrow", children: "\u2192" }),
|
|
101
|
-
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-new", children:
|
|
93
|
+
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-new", children: fmtValue(c.actual) })
|
|
102
94
|
] }, c.path);
|
|
103
95
|
case "removed":
|
|
104
96
|
return /* @__PURE__ */ jsxs("div", { className: "tea-dt-diff-row tea-dt-diff-removed", children: [
|
|
105
97
|
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-mark", children: "-" }),
|
|
106
98
|
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-path", children: c.path }),
|
|
107
|
-
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-old", children:
|
|
99
|
+
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-old", children: fmtValue(c.expected) })
|
|
108
100
|
] }, c.path);
|
|
109
101
|
case "added":
|
|
110
102
|
return /* @__PURE__ */ jsxs("div", { className: "tea-dt-diff-row tea-dt-diff-added", children: [
|
|
111
103
|
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-mark", children: "+" }),
|
|
112
104
|
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-path", children: c.path }),
|
|
113
|
-
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-new", children:
|
|
105
|
+
/* @__PURE__ */ jsx("span", { className: "tea-dt-diff-new", children: fmtValue(c.actual) })
|
|
114
106
|
] }, c.path);
|
|
115
107
|
}
|
|
116
108
|
}
|
|
117
|
-
function fmt(v) {
|
|
118
|
-
if (v === void 0) return "undefined";
|
|
119
|
-
if (typeof v === "number" && Number.isNaN(v)) return "NaN";
|
|
120
|
-
if (typeof v === "bigint") return `${v}n`;
|
|
121
|
-
if (typeof v === "string") return JSON.stringify(v);
|
|
122
|
-
if (typeof v === "function" || typeof v === "symbol") return String(v);
|
|
123
|
-
try {
|
|
124
|
-
return JSON.stringify(v) ?? String(v);
|
|
125
|
-
} catch {
|
|
126
|
-
return String(v);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
109
|
function StateDiff({ expected, actual, className }) {
|
|
130
110
|
const changes = diffState(expected, actual);
|
|
131
111
|
return /* @__PURE__ */ jsx(
|