@demlik/tea 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/agent/index.d.ts +907 -0
- package/dist/agent/index.js +20 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/authed-call/index.d.ts +119 -0
- package/dist/authed-call/index.js +148 -0
- package/dist/authed-call/index.js.map +1 -0
- package/dist/await-terminal/index.d.ts +99 -0
- package/dist/await-terminal/index.js +75 -0
- package/dist/await-terminal/index.js.map +1 -0
- package/dist/batch-window/index.d.ts +259 -0
- package/dist/batch-window/index.js +43 -0
- package/dist/batch-window/index.js.map +1 -0
- package/dist/cache/index.d.ts +2 -2
- package/dist/cache/index.js +2 -50
- package/dist/cache/index.js.map +1 -1
- package/dist/{chunk-CMG2KMWX.js → chunk-2WZHS4P2.js} +220 -177
- package/dist/chunk-2WZHS4P2.js.map +1 -0
- package/dist/chunk-2YOE5PIS.js +45 -0
- package/dist/chunk-2YOE5PIS.js.map +1 -0
- package/dist/chunk-3545BOYL.js +65 -0
- package/dist/chunk-3545BOYL.js.map +1 -0
- package/dist/chunk-5VNWZADP.js +49 -0
- package/dist/chunk-5VNWZADP.js.map +1 -0
- package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
- package/dist/chunk-7FYQGDMF.js.map +1 -0
- package/dist/chunk-7VEG4KHB.js +202 -0
- package/dist/chunk-7VEG4KHB.js.map +1 -0
- package/dist/{chunk-PYBQREFU.js → chunk-A56GNPI7.js} +20 -6
- package/dist/chunk-A56GNPI7.js.map +1 -0
- package/dist/chunk-BNJYE4AU.js +157 -0
- package/dist/chunk-BNJYE4AU.js.map +1 -0
- package/dist/chunk-BYQUGDD3.js +16 -0
- package/dist/chunk-BYQUGDD3.js.map +1 -0
- package/dist/chunk-D5EQDLDV.js +186 -0
- package/dist/chunk-D5EQDLDV.js.map +1 -0
- package/dist/chunk-EDGDR5XX.js +60 -0
- package/dist/chunk-EDGDR5XX.js.map +1 -0
- package/dist/chunk-EIZJTYKR.js +93 -0
- package/dist/chunk-EIZJTYKR.js.map +1 -0
- package/dist/chunk-EQRUAP2A.js +56 -0
- package/dist/chunk-EQRUAP2A.js.map +1 -0
- package/dist/chunk-FH5RUIIL.js +61 -0
- package/dist/chunk-FH5RUIIL.js.map +1 -0
- package/dist/chunk-HJO6VNQI.js +15 -0
- package/dist/chunk-HJO6VNQI.js.map +1 -0
- package/dist/chunk-IFBQJYGI.js +132 -0
- package/dist/chunk-IFBQJYGI.js.map +1 -0
- package/dist/chunk-IN33Q4VJ.js +52 -0
- package/dist/chunk-IN33Q4VJ.js.map +1 -0
- package/dist/chunk-JELSZN25.js +47 -0
- package/dist/chunk-JELSZN25.js.map +1 -0
- package/dist/chunk-K575PT4S.js +58 -0
- package/dist/chunk-K575PT4S.js.map +1 -0
- package/dist/chunk-LEC4U6CX.js +91 -0
- package/dist/chunk-LEC4U6CX.js.map +1 -0
- package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
- package/dist/chunk-NSDFA6M6.js.map +1 -0
- package/dist/chunk-QGZW4W3E.js +20 -0
- package/dist/chunk-QGZW4W3E.js.map +1 -0
- package/dist/{chunk-PYOCUBQJ.js → chunk-RGTPPF4U.js} +22 -15
- package/dist/chunk-RGTPPF4U.js.map +1 -0
- package/dist/chunk-RVJFGRGK.js +222 -0
- package/dist/chunk-RVJFGRGK.js.map +1 -0
- package/dist/chunk-RVU3RBE6.js +31 -0
- package/dist/chunk-RVU3RBE6.js.map +1 -0
- package/dist/chunk-SC3FAFZN.js +53 -0
- package/dist/chunk-SC3FAFZN.js.map +1 -0
- package/dist/chunk-SINXE47Y.js +55 -0
- package/dist/chunk-SINXE47Y.js.map +1 -0
- package/dist/chunk-TAO4YOLO.js +87 -0
- package/dist/chunk-TAO4YOLO.js.map +1 -0
- package/dist/chunk-TC4HWABA.js +506 -0
- package/dist/chunk-TC4HWABA.js.map +1 -0
- package/dist/chunk-TGG26XMK.js +52 -0
- package/dist/chunk-TGG26XMK.js.map +1 -0
- package/dist/chunk-UV6UWGEO.js +147 -0
- package/dist/chunk-UV6UWGEO.js.map +1 -0
- package/dist/chunk-UYWUVEP6.js +158 -0
- package/dist/chunk-UYWUVEP6.js.map +1 -0
- package/dist/chunk-WUV3PSCZ.js +54 -0
- package/dist/chunk-WUV3PSCZ.js.map +1 -0
- package/dist/chunk-XRXKH7KA.js +268 -0
- package/dist/chunk-XRXKH7KA.js.map +1 -0
- package/dist/circuit-breaker/index.js +1 -51
- package/dist/circuit-breaker/index.js.map +1 -1
- package/dist/core-CymXZCeY.d.ts +178 -0
- package/dist/deadline/index.d.ts +24 -5
- package/dist/deadline/index.js +4 -18
- package/dist/deadline/index.js.map +1 -1
- package/dist/debounce/index.js +1 -54
- package/dist/debounce/index.js.map +1 -1
- package/dist/devtools/index.d.ts +1 -1
- package/dist/devtools/index.js +18 -6
- package/dist/devtools/index.js.map +1 -1
- package/dist/do/index.d.ts +1732 -92
- package/dist/do/index.js +677 -31
- package/dist/do/index.js.map +1 -1
- package/dist/durable-effects-2MDxDs45.d.ts +180 -0
- package/dist/extension/index.d.ts +31 -13
- package/dist/extension/index.js +5 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/react.js +8 -3
- package/dist/extension/react.js.map +1 -1
- package/dist/extension/subs/index.d.ts +2 -2
- package/dist/extension/subs/index.js.map +1 -1
- package/dist/extension/test-utils.js +11 -3
- package/dist/extension/test-utils.js.map +1 -1
- package/dist/fan-out/index.d.ts +239 -0
- package/dist/fan-out/index.js +5 -0
- package/dist/fan-out/index.js.map +1 -0
- package/dist/idempotency/adapter.d.ts +62 -0
- package/dist/idempotency/adapter.js +4 -0
- package/dist/idempotency/adapter.js.map +1 -0
- package/dist/idempotency/index.js +1 -56
- package/dist/idempotency/index.js.map +1 -1
- package/dist/idempotent-intake/index.d.ts +186 -0
- package/dist/idempotent-intake/index.js +7 -0
- package/dist/idempotent-intake/index.js.map +1 -0
- package/dist/index.d.ts +341 -191
- package/dist/index.js +2 -1
- package/dist/llm-call/index.d.ts +309 -0
- package/dist/llm-call/index.js +14 -0
- package/dist/llm-call/index.js.map +1 -0
- package/dist/machine-viz/index.d.ts +1 -1
- package/dist/machine-viz/index.js +1 -1
- package/dist/machine-viz/index.js.map +1 -1
- package/dist/mem/index.d.ts +1 -0
- package/dist/monitored-run/index.d.ts +253 -0
- package/dist/monitored-run/index.js +10 -0
- package/dist/monitored-run/index.js.map +1 -0
- package/dist/node/index.d.ts +5 -5
- package/dist/node/index.js.map +1 -1
- package/dist/paginated-walk/index.d.ts +234 -0
- package/dist/paginated-walk/index.js +15 -0
- package/dist/paginated-walk/index.js.map +1 -0
- package/dist/paginator/index.d.ts +271 -0
- package/dist/paginator/index.js +3 -0
- package/dist/paginator/index.js.map +1 -0
- package/dist/parity/index.d.ts +86 -0
- package/dist/parity/index.js +77 -0
- package/dist/parity/index.js.map +1 -0
- package/dist/pbt/arbitraries/index.d.ts +1 -1
- package/dist/pbt/arbitraries/index.js +1 -1
- package/dist/pbt/index.d.ts +1 -1
- package/dist/pbt/index.js +5 -4
- package/dist/pbt/index.js.map +1 -1
- package/dist/pbt/runners/index.d.ts +1 -1
- package/dist/pbt/runners/index.js +3 -2
- package/dist/poller/index.d.ts +288 -0
- package/dist/poller/index.js +79 -0
- package/dist/poller/index.js.map +1 -0
- package/dist/prediction/index.d.ts +109 -0
- package/dist/prediction/index.js +4 -0
- package/dist/prediction/index.js.map +1 -0
- package/dist/protocol-B5YmJr0d.d.ts +45 -0
- package/dist/pure/index.d.ts +2 -0
- package/dist/pure/index.js +4 -0
- package/dist/pure/index.js.map +1 -0
- package/dist/raft/demo.d.ts +105 -0
- package/dist/raft/demo.js +172 -0
- package/dist/raft/demo.js.map +1 -0
- package/dist/raft/index.d.ts +548 -0
- package/dist/raft/index.js +7 -0
- package/dist/raft/index.js.map +1 -0
- package/dist/raft/sim.d.ts +222 -0
- package/dist/raft/sim.js +8 -0
- package/dist/raft/sim.js.map +1 -0
- package/dist/raft/timeline.d.ts +46 -0
- package/dist/raft/timeline.js +62 -0
- package/dist/raft/timeline.js.map +1 -0
- package/dist/rate-limit/index.js +1 -43
- package/dist/rate-limit/index.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +39 -20
- package/dist/react/index.js.map +1 -1
- package/dist/reconciler/index.d.ts +192 -0
- package/dist/reconciler/index.js +166 -0
- package/dist/reconciler/index.js.map +1 -0
- package/dist/recorder/index.d.ts +7 -3
- package/dist/recorder/index.js +1 -155
- package/dist/recorder/index.js.map +1 -1
- package/dist/resilient-call/index.d.ts +251 -0
- package/dist/resilient-call/index.js +13 -0
- package/dist/resilient-call/index.js.map +1 -0
- package/dist/retry-backoff/index.d.ts +42 -5
- package/dist/retry-backoff/index.js +1 -35
- package/dist/retry-backoff/index.js.map +1 -1
- package/dist/saga/index.d.ts +225 -0
- package/dist/saga/index.js +150 -0
- package/dist/saga/index.js.map +1 -0
- package/dist/snapshot/index.d.ts +334 -0
- package/dist/snapshot/index.js +5 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/subs/index.d.ts +48 -10
- package/dist/subs/index.js +1 -1
- package/dist/testing/index.d.ts +107 -2
- package/dist/testing/index.js +204 -6
- package/dist/testing/index.js.map +1 -1
- package/dist/throttle/index.js +1 -52
- package/dist/throttle/index.js.map +1 -1
- package/dist/throttled-input/index.d.ts +372 -0
- package/dist/throttled-input/index.js +88 -0
- package/dist/throttled-input/index.js.map +1 -0
- package/dist/token-refresh/index.d.ts +215 -0
- package/dist/token-refresh/index.js +6 -0
- package/dist/token-refresh/index.js.map +1 -0
- package/dist/trace-replay/index.d.ts +11 -2
- package/dist/trace-replay/index.js +3 -55
- package/dist/trace-replay/index.js.map +1 -1
- package/dist/{types-CwBC5wMn.d.ts → types-N9LZbdnr.d.ts} +1 -1
- package/dist/with-deadline/index.d.ts +196 -0
- package/dist/with-deadline/index.js +130 -0
- package/dist/with-deadline/index.js.map +1 -0
- package/dist/with-resilience/index.d.ts +205 -0
- package/dist/with-resilience/index.js +274 -0
- package/dist/with-resilience/index.js.map +1 -0
- package/dist/with-telemetry/index.d.ts +149 -0
- package/dist/with-telemetry/index.js +95 -0
- package/dist/with-telemetry/index.js.map +1 -0
- package/dist/work-queue/adapter.d.ts +82 -0
- package/dist/work-queue/adapter.js +4 -0
- package/dist/work-queue/adapter.js.map +1 -0
- package/dist/work-queue/index.d.ts +25 -11
- package/dist/work-queue/index.js +1 -46
- package/dist/work-queue/index.js.map +1 -1
- package/dist/work-queue/ops.d.ts +69 -0
- package/dist/work-queue/ops.js +3 -0
- package/dist/work-queue/ops.js.map +1 -0
- package/dist/workflow/index.d.ts +444 -0
- package/dist/workflow/index.js +225 -0
- package/dist/workflow/index.js.map +1 -0
- package/package.json +162 -25
- package/dist/chunk-CMG2KMWX.js.map +0 -1
- package/dist/chunk-PYBQREFU.js.map +0 -1
- package/dist/chunk-PYOCUBQJ.js.map +0 -1
- package/dist/chunk-Y5JM3UUH.js.map +0 -1
- package/dist/chunk-ZRUST6BK.js.map +0 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { C as Cmd } from './core-CymXZCeY.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @demlik/tea/do — the durable pending-effects ledger (substrate primitive #1).
|
|
5
|
+
*
|
|
6
|
+
* Durable *state* and durable *effects* are different problems. An
|
|
7
|
+
* event-sourced actor rebuilds its STATE by folding the event log
|
|
8
|
+
* (`doEventSourcedStore`, sibling file). But an outbound side effect — a tool
|
|
9
|
+
* round-trip, a message to a sibling grain — is fire-and-forget: if the actor
|
|
10
|
+
* interprets the `Cmd` inside an activation that hibernates mid-flight, the
|
|
11
|
+
* effect is lost, yet nothing in the replayed state records that it is still
|
|
12
|
+
* OWED. This is the one real tax of the event-sourced-virtual-actor target
|
|
13
|
+
* (ADR 0003 primitive #1): "I owe this effect" must itself be part of the
|
|
14
|
+
* folded state.
|
|
15
|
+
*
|
|
16
|
+
* Akka's classic `AtLeastOnceDelivery` solves it with a ledger of unconfirmed
|
|
17
|
+
* deliveries kept AS PART OF the persisted state: each `deliver` adds an entry
|
|
18
|
+
* keyed by a monotonic `deliveryId`; each `confirmDelivery` removes it. On
|
|
19
|
+
* recovery the ledger is rebuilt from the SAME events, and any surviving entry
|
|
20
|
+
* is re-sent. At-least-once ⇒ a confirmed-but-not-yet-persisted delivery WILL
|
|
21
|
+
* be re-sent after a crash, so the receiver must deduplicate by id.
|
|
22
|
+
*
|
|
23
|
+
* ## The load-bearing rule (do not violate)
|
|
24
|
+
*
|
|
25
|
+
* The ledger is a PURE FOLD over the same kind of event stream the actor
|
|
26
|
+
* already replays — it is **NOT** a separately-persisted side table:
|
|
27
|
+
*
|
|
28
|
+
* - `effect_owed` — `{ id, effect }` ADDS an entry to the ledger.
|
|
29
|
+
* - `effect_confirmed`— `{ id }` REMOVES the entry.
|
|
30
|
+
*
|
|
31
|
+
* `foldLedger(events)` is `events.reduce(applyEffectEvent, empty())` — the same
|
|
32
|
+
* fold shape `replay` / `foldEvents` / the recorder use. Persist these two
|
|
33
|
+
* events into the SAME log `doEventSourcedStore` already appends (they are just
|
|
34
|
+
* Msg variants), and the ledger is rebuilt for free on the next activation:
|
|
35
|
+
* `foldLedger(log)` after eviction equals `foldLedger(log)` never-evicted, by
|
|
36
|
+
* construction (a pure fold over identical input).
|
|
37
|
+
*
|
|
38
|
+
* On activation (replay completion), `survivingEffects(ledger)` returns the
|
|
39
|
+
* owed-but-unconfirmed entries to re-fire, ordered by their monotonic id. The
|
|
40
|
+
* re-fire is a SIDE EFFECT DERIVED from the surviving ledger — never stored
|
|
41
|
+
* separately, never fired during replay (recovery.md: add to the ledger during
|
|
42
|
+
* recovery; let recovery-END re-emit the survivors).
|
|
43
|
+
*
|
|
44
|
+
* Anti-patterns this file refuses (mirroring durable-effects.md):
|
|
45
|
+
* - A side-table ledger persisted apart from the events. The events ARE the
|
|
46
|
+
* ledger; the Map is rebuilt by folding them.
|
|
47
|
+
* - Re-firing every historical effect during replay. Only `survivingEffects`
|
|
48
|
+
* (post-fold) re-emits.
|
|
49
|
+
* - Assuming the receiver gets each effect exactly once. At-least-once: dedup
|
|
50
|
+
* by `id` at the receiver (`hasOwed` / `isConfirmed` here are the sender's
|
|
51
|
+
* view; the receiver owns its own dedup).
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/** Monotonic, gap-free delivery id — the single correlation + dedup key. */
|
|
55
|
+
type DeliveryId = number;
|
|
56
|
+
/**
|
|
57
|
+
* An effect is now OWED: it has been decided but its delivery is not yet
|
|
58
|
+
* confirmed. Adds `{ id → effect }` to the ledger when folded.
|
|
59
|
+
*
|
|
60
|
+
* Persist this BEFORE interpreting the effect (durable-effects.md rule:
|
|
61
|
+
* "persist the intent before deliver"). A crash between persist and deliver is
|
|
62
|
+
* fine — the surviving entry re-fires on activation. A crash before the persist
|
|
63
|
+
* means the effect was never owed, so nothing is lost that the actor promised.
|
|
64
|
+
*/
|
|
65
|
+
interface EffectOwed<E> extends Cmd<"effect_owed"> {
|
|
66
|
+
readonly id: DeliveryId;
|
|
67
|
+
readonly effect: E;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* An owed effect has been CONFIRMED delivered. Removes its entry from the
|
|
71
|
+
* ledger when folded. Persist this BEFORE acting on the confirmation
|
|
72
|
+
* ("persist the confirmation before confirmDelivery").
|
|
73
|
+
*/
|
|
74
|
+
interface EffectConfirmed extends Cmd<"effect_confirmed"> {
|
|
75
|
+
readonly id: DeliveryId;
|
|
76
|
+
}
|
|
77
|
+
/** The event union the ledger folds over. `E` is the consumer's effect type. */
|
|
78
|
+
type EffectLedgerEvent<E> = EffectOwed<E> | EffectConfirmed;
|
|
79
|
+
/**
|
|
80
|
+
* The pending-effects ledger: owed-but-unconfirmed effects keyed by their
|
|
81
|
+
* monotonic delivery id. A pure value reconstructed by `foldLedger` — it is
|
|
82
|
+
* never persisted on its own; the events are.
|
|
83
|
+
*
|
|
84
|
+
* `ReadonlyMap` preserves insertion order, which (because ids are appended
|
|
85
|
+
* monotonically) is exactly id order — so iteration yields the survivors
|
|
86
|
+
* oldest-first with no sort.
|
|
87
|
+
*/
|
|
88
|
+
type PendingEffectsLedger<E> = ReadonlyMap<DeliveryId, E>;
|
|
89
|
+
/** A single surviving entry to re-emit: its id and the effect it owes. */
|
|
90
|
+
interface OwedEffect<E> {
|
|
91
|
+
readonly id: DeliveryId;
|
|
92
|
+
readonly effect: E;
|
|
93
|
+
}
|
|
94
|
+
/** The empty ledger — the fold's seed (a fresh actor owes nothing). */
|
|
95
|
+
declare function emptyLedger<E>(): PendingEffectsLedger<E>;
|
|
96
|
+
/**
|
|
97
|
+
* Apply ONE ledger event — the reducer at the heart of the fold.
|
|
98
|
+
*
|
|
99
|
+
* - `effect_owed` → add `{ id → effect }`.
|
|
100
|
+
* - `effect_confirmed` → remove `id` (no-op if already absent: a duplicate
|
|
101
|
+
* confirm, or a confirm for an id evicted with the actor — at-least-once
|
|
102
|
+
* tolerates both).
|
|
103
|
+
*
|
|
104
|
+
* Pure: returns a NEW ledger, never mutates the input — so folding the same
|
|
105
|
+
* events twice (live and post-eviction) yields equal results, and a caller may
|
|
106
|
+
* keep the prior ledger value.
|
|
107
|
+
*/
|
|
108
|
+
declare function applyEffectEvent<E>(ledger: PendingEffectsLedger<E>, event: EffectLedgerEvent<E>): PendingEffectsLedger<E>;
|
|
109
|
+
/**
|
|
110
|
+
* THE FOLD. Rebuild the ledger from an event stream — the same fold shape
|
|
111
|
+
* `replay` uses for state. After a simulated eviction this is called over the
|
|
112
|
+
* persisted log and yields a ledger byte-for-byte equal to the never-evicted
|
|
113
|
+
* one, because a pure fold over identical input is identical.
|
|
114
|
+
*
|
|
115
|
+
* This is the function that proves "no side table": the ledger is `events`
|
|
116
|
+
* reduced, nothing more.
|
|
117
|
+
*/
|
|
118
|
+
declare function foldLedger<E>(events: Iterable<EffectLedgerEvent<E>>): PendingEffectsLedger<E>;
|
|
119
|
+
/**
|
|
120
|
+
* RE-EMIT ON ACTIVATION. Given the rebuilt ledger, the owed-but-unconfirmed
|
|
121
|
+
* effects to re-fire — ordered oldest-first by monotonic id. On a fresh /
|
|
122
|
+
* fully-confirmed actor this is empty (nothing re-emitted).
|
|
123
|
+
*
|
|
124
|
+
* Idempotency is the RECEIVER's job: it must dedup by `id`, because
|
|
125
|
+
* at-least-once means a re-emitted already-handled id WILL arrive again
|
|
126
|
+
* (e.g. an effect confirmed in the activation that hibernated before the
|
|
127
|
+
* `effect_confirmed` event was persisted). Re-firing the same id is therefore
|
|
128
|
+
* a no-op AT THE RECEIVER, not here.
|
|
129
|
+
*/
|
|
130
|
+
declare function survivingEffects<E>(ledger: PendingEffectsLedger<E>): readonly OwedEffect<E>[];
|
|
131
|
+
/** Is this id still owed (unconfirmed) in the ledger? Sender-side view. */
|
|
132
|
+
declare function isOwed<E>(ledger: PendingEffectsLedger<E>, id: DeliveryId): boolean;
|
|
133
|
+
/**
|
|
134
|
+
* A live, monotonic-id-issuing ledger recorder.
|
|
135
|
+
*
|
|
136
|
+
* - `owe(effect)` — allocate the next `deliveryId`, return the
|
|
137
|
+
* `effect_owed` event to PERSIST plus the allocated `id`. Folding the event
|
|
138
|
+
* into the recorder's mirror is done for you; the caller persists it.
|
|
139
|
+
* - `confirm(id)` — return the `effect_confirmed` event to PERSIST, and `true`
|
|
140
|
+
* iff the id was owed (a duplicate confirm returns `false` — the boolean is
|
|
141
|
+
* itself a dedup signal, exactly Akka `confirmDelivery`).
|
|
142
|
+
* - `surviving()` — the current owed-but-unconfirmed effects (for re-emit on
|
|
143
|
+
* activation).
|
|
144
|
+
* - `ledger()` — the current folded ledger value.
|
|
145
|
+
* - `lastId()` — the highest id issued (so a host can seed the counter from a
|
|
146
|
+
* rebuilt ledger after eviction; see `restore`).
|
|
147
|
+
*/
|
|
148
|
+
interface PendingEffectsRecorder<E> {
|
|
149
|
+
owe(effect: E): {
|
|
150
|
+
id: DeliveryId;
|
|
151
|
+
event: EffectOwed<E>;
|
|
152
|
+
};
|
|
153
|
+
confirm(id: DeliveryId): {
|
|
154
|
+
confirmed: boolean;
|
|
155
|
+
event: EffectConfirmed;
|
|
156
|
+
};
|
|
157
|
+
surviving(): readonly OwedEffect<E>[];
|
|
158
|
+
ledger(): PendingEffectsLedger<E>;
|
|
159
|
+
lastId(): DeliveryId;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Build a live recorder. `restore` rehydrates it after an eviction: pass the
|
|
163
|
+
* events read back from the log (or the rebuilt ledger's survivors) so the
|
|
164
|
+
* monotonic counter resumes WITHOUT gaps and the mirror reflects the surviving
|
|
165
|
+
* owed set. Omit `restore` for a fresh actor.
|
|
166
|
+
*
|
|
167
|
+
* Counter seeding rule: the next id is `max(seen ids) + 1`. Restoring only the
|
|
168
|
+
* survivors is sufficient for correctness of re-emit, but to keep ids gap-free
|
|
169
|
+
* across confirmed-and-pruned entries the host should pass `lastId` explicitly
|
|
170
|
+
* when it has it (e.g. persisted alongside the snapshot). When only survivors
|
|
171
|
+
* are known, the counter resumes from the highest surviving id — a confirmed id
|
|
172
|
+
* higher than every survivor could in principle be reissued, so a host that
|
|
173
|
+
* needs strict global monotonicity persists `lastId`.
|
|
174
|
+
*/
|
|
175
|
+
declare function pendingEffectsLedger<E>(restore?: {
|
|
176
|
+
readonly events?: Iterable<EffectLedgerEvent<E>>;
|
|
177
|
+
readonly lastId?: DeliveryId;
|
|
178
|
+
}): PendingEffectsRecorder<E>;
|
|
179
|
+
|
|
180
|
+
export { type DeliveryId as D, type EffectLedgerEvent as E, type OwedEffect as O, type PendingEffectsRecorder as P, type EffectConfirmed as a, type EffectOwed as b, applyEffectEvent as c, type PendingEffectsLedger as d, emptyLedger as e, foldLedger as f, isOwed as i, pendingEffectsLedger as p, survivingEffects as s };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Runtime, Store } from '../index.js';
|
|
2
|
+
import '../core-CymXZCeY.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Runtime bridge — expose a `@demlik/tea` runtime running in one chrome context
|
|
@@ -92,6 +93,18 @@ interface BridgeRuntimeOpts<S, V = S> {
|
|
|
92
93
|
declare function bridgeRuntime<S, M extends {
|
|
93
94
|
type: string;
|
|
94
95
|
}, V = S>(runtime: Runtime<S, M>, { channel, serialize, historySize }: BridgeRuntimeOpts<S, V>): () => void;
|
|
96
|
+
/**
|
|
97
|
+
* The explicit opt-out of inbound msg parsing: an identity pass-through that
|
|
98
|
+
* casts the raw `unknown` to `M`. Surfaces that gate only on state and treat
|
|
99
|
+
* the broadcast msg as advisory pass this as `parseMsg` to acknowledge — at
|
|
100
|
+
* the call site, in the types — that they are NOT parsing the msg boundary.
|
|
101
|
+
*
|
|
102
|
+
* This is the same behavior the old optional-`parseMsg` default had, but the
|
|
103
|
+
* unsound cast now lives at one named, greppable, opt-in seam instead of being
|
|
104
|
+
* the silent default for everyone. (Inv 8: the boundary parses; this helper is
|
|
105
|
+
* the documented exception, chosen explicitly.)
|
|
106
|
+
*/
|
|
107
|
+
declare function passThroughMsg<M>(raw: unknown): M | null;
|
|
95
108
|
interface BridgeClient<S, M> {
|
|
96
109
|
/** Send the hydrate request; resolves with the initial snapshot (or null
|
|
97
110
|
* if the host is still booting OR the response failed to parse). Also
|
|
@@ -101,9 +114,9 @@ interface BridgeClient<S, M> {
|
|
|
101
114
|
/** Send a dispatch envelope; resolves once the host's dispatch settles. */
|
|
102
115
|
dispatch(msg: M): Promise<void>;
|
|
103
116
|
/** Subscribe to broadcasts. Returns a cleanup function. Broadcasts whose
|
|
104
|
-
* payload fails `parseState`
|
|
105
|
-
*
|
|
106
|
-
*
|
|
117
|
+
* payload fails `parseState` or `parseMsg` are dropped silently — the
|
|
118
|
+
* listener is not called. Each successful broadcast updates
|
|
119
|
+
* `getSnapshot()`. */
|
|
107
120
|
subscribe(listener: (msg: M | null, state: S) => void): () => void;
|
|
108
121
|
/** Synchronous accessor for the latest known state, or `null` until the
|
|
109
122
|
* first successful hydrate or broadcast lands. Required reference stability
|
|
@@ -123,13 +136,17 @@ interface BridgeClientOpts<S, M> {
|
|
|
123
136
|
*/
|
|
124
137
|
parseState: (raw: unknown) => S | null;
|
|
125
138
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* the msg
|
|
129
|
-
*
|
|
130
|
-
*
|
|
139
|
+
* REQUIRED parse for inbound `msg` payloads on broadcasts. Chrome runtime
|
|
140
|
+
* messages are an `unknown` boundary (Inv 8) just like the `state` payload,
|
|
141
|
+
* so the msg crosses the same JSON serialization seam and must be parsed —
|
|
142
|
+
* never cast. Returning `null` drops the broadcast silently.
|
|
143
|
+
*
|
|
144
|
+
* Surfaces that treat the msg union as advisory and only gate on state opt
|
|
145
|
+
* out EXPLICITLY by passing the {@link passThroughMsg} helper — the opt-out
|
|
146
|
+
* is now a visible call-site decision, not a hidden default that lets an
|
|
147
|
+
* unparsed `unknown` reach listeners typed as `M`.
|
|
131
148
|
*/
|
|
132
|
-
parseMsg
|
|
149
|
+
parseMsg: (raw: unknown) => M | null;
|
|
133
150
|
}
|
|
134
151
|
declare function bridgeClient<S, M extends {
|
|
135
152
|
type: string;
|
|
@@ -144,10 +161,11 @@ interface BridgeTabClientOpts<S, M> {
|
|
|
144
161
|
*/
|
|
145
162
|
parseState: (raw: unknown) => S | null;
|
|
146
163
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
164
|
+
* REQUIRED parse for inbound `msg` payloads. See `BridgeClientOpts.parseMsg`
|
|
165
|
+
* — same contract, same rationale. Opt out of msg parsing with
|
|
166
|
+
* {@link passThroughMsg}.
|
|
149
167
|
*/
|
|
150
|
-
parseMsg
|
|
168
|
+
parseMsg: (raw: unknown) => M | null;
|
|
151
169
|
}
|
|
152
170
|
/**
|
|
153
171
|
* SW-side client of a `bridgeRuntime` hosted in a content script.
|
|
@@ -204,4 +222,4 @@ declare function bridgeTabClient<S, M extends {
|
|
|
204
222
|
*/
|
|
205
223
|
declare function chromeStorageStore<S>(key: string, parse: (raw: unknown) => S | null, area?: chrome.storage.StorageArea): Store<S>;
|
|
206
224
|
|
|
207
|
-
export { type BridgeClient, type BridgeClientOpts, type BridgeRuntimeOpts, type BridgeTabClientOpts, bridgeClient, bridgeRuntime, bridgeTabClient, chromeStorageStore };
|
|
225
|
+
export { type BridgeClient, type BridgeClientOpts, type BridgeRuntimeOpts, type BridgeTabClientOpts, bridgeClient, bridgeRuntime, bridgeTabClient, chromeStorageStore, passThroughMsg };
|
package/dist/extension/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { bridgeClient, bridgeRuntime, bridgeTabClient } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
1
|
+
export { bridgeClient, bridgeRuntime, bridgeTabClient, passThroughMsg } from '../chunk-RGTPPF4U.js';
|
|
2
|
+
import '../chunk-2WZHS4P2.js';
|
|
3
|
+
import '../chunk-BNJYE4AU.js';
|
|
3
4
|
|
|
4
5
|
// src/extension/index.ts
|
|
5
6
|
var MALFORMED = "chromeStorageStore: stored value is not a string";
|
|
@@ -9,7 +10,8 @@ function chromeStorageStore(key, parse, area = chrome.storage.local) {
|
|
|
9
10
|
const result = await area.get(key);
|
|
10
11
|
const raw = result[key];
|
|
11
12
|
if (raw === void 0 || raw === null) return null;
|
|
12
|
-
if (typeof raw !== "string")
|
|
13
|
+
if (typeof raw !== "string")
|
|
14
|
+
throw new Error(`${MALFORMED} at key "${key}"`);
|
|
13
15
|
return JSON.parse(raw);
|
|
14
16
|
},
|
|
15
17
|
async save(state) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/extension/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/extension/index.ts"],"names":[],"mappings":";;;;;AAkCA,IAAM,SAAA,GAAY,kDAAA;AAqBX,SAAS,mBACd,GAAA,EACA,KAAA,EACA,IAAA,GAAmC,MAAA,CAAO,QAAQ,KAAA,EACxC;AACV,EAAA,OAAO;AAAA,IACL,MAAM,IAAA,GAAyB;AAC7B,MAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA;AACjC,MAAA,MAAM,GAAA,GAAO,OAAmC,GAAG,CAAA;AACnD,MAAA,IAAI,GAAA,KAAQ,MAAA,IAAa,GAAA,KAAQ,IAAA,EAAM,OAAO,IAAA;AAC9C,MAAA,IAAI,OAAO,GAAA,KAAQ,QAAA;AACjB,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,EAAG,SAAS,CAAA,SAAA,EAAY,GAAG,CAAA,CAAA,CAAG,CAAA;AAKhD,MAAA,OAAO,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,IACvB,CAAA;AAAA,IACA,MAAM,KAAK,KAAA,EAAyB;AAClC,MAAA,MAAM,IAAA,CAAK,GAAA,CAAI,EAAE,CAAC,GAAG,GAAG,IAAA,CAAK,SAAA,CAAU,KAAK,CAAA,EAAG,CAAA;AAAA,IACjD,CAAA;AAAA,IACA,QAAQ,GAAA,EAAwB;AAC9B,MAAA,OAAO,MAAM,GAAG,CAAA;AAAA,IAClB;AAAA,GACF;AACF","file":"index.js","sourcesContent":["/**\n * @demlik/tea/extension — Chrome service-worker host adapter for @demlik/tea.\n *\n * Substrate-level companion to @demlik/tea/do (Durable Objects) and\n * @demlik/tea/react (React). Lets a chrome service worker run a @demlik/tea\n * machine with persistence backed by chrome.storage.\n *\n * Single export: chromeStorageStore<S>(key, area?). Apps register chrome.*\n * listeners themselves and call runtime.dispatch from inside the handler —\n * those wiring patterns are app-specific (different extensions register\n * different listener sets, with different Msg adapters), so we don't\n * abstract them here until the shape stabilizes across consumers.\n */\n\nimport type { Store } from \"../index\";\n\nexport type {\n BridgeClient,\n BridgeClientOpts,\n BridgeRuntimeOpts,\n BridgeTabClientOpts,\n} from \"./bridge\";\n// Re-export the wire-envelope types — surfaces that test against the\n// bridge can pin the shapes without round-tripping through the runtime.\n// Re-export the runtime bridge primitives. The React adapter lives at\n// `@demlik/tea/extension/react` so the main entry stays react-free for the\n// background service-worker side.\nexport {\n bridgeClient,\n bridgeRuntime,\n bridgeTabClient,\n passThroughMsg,\n} from \"./bridge\";\n\nconst MALFORMED = \"chromeStorageStore: stored value is not a string\";\n\n/**\n * `Store<S>` backed by `chrome.storage.local` (or any compatible\n * `StorageArea`). JSON.stringify on save, JSON.parse on load. Returns `null`\n * (typed as `unknown` per `Store<S>.load` contract) when the key is absent.\n *\n * Throws on the structural layer if the stored value is malformed (non-string\n * at the key, or unparseable JSON) — matches @demlik/tea/do's \"throw at boot via\n * store.load\" contract so tea's `run()` surfaces it predictably. The\n * `parse` callback handles the semantic layer (shape mismatch → return\n * `null`); it must NOT throw, per the `Store<S>.migrate` contract.\n *\n * `parse` is REQUIRED because chrome.storage is a real serialization boundary\n * — JSON to disk, then back — and the structural type of what comes back is\n * `unknown`. The caller (extension code that knows `S`) is the only party\n * with enough information to write the parse. There is no honest default.\n *\n * `area` defaults to `chrome.storage.local` for production. Tests pass a\n * `fakeChrome().storage.local` in.\n */\nexport function chromeStorageStore<S>(\n key: string,\n parse: (raw: unknown) => S | null,\n area: chrome.storage.StorageArea = chrome.storage.local,\n): Store<S> {\n return {\n async load(): Promise<unknown> {\n const result = await area.get(key);\n const raw = (result as Record<string, unknown>)[key];\n if (raw === undefined || raw === null) return null;\n if (typeof raw !== \"string\")\n throw new Error(`${MALFORMED} at key \"${key}\"`);\n // JSON.parse throws on malformed — propagate per @demlik/tea/do parity.\n // The decoded value is intentionally returned as `unknown`; the\n // substrate's `migrate` callback (forwarded from `parse`) is the\n // boundary parse that turns it into `S | null`.\n return JSON.parse(raw);\n },\n async save(state: S): Promise<void> {\n await area.set({ [key]: JSON.stringify(state) });\n },\n migrate(raw: unknown): S | null {\n return parse(raw);\n },\n };\n}\n"]}
|
package/dist/extension/react.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { bridgeClient } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
1
|
+
import { bridgeClient, passThroughMsg } from '../chunk-RGTPPF4U.js';
|
|
2
|
+
import '../chunk-2WZHS4P2.js';
|
|
3
|
+
import '../chunk-BNJYE4AU.js';
|
|
3
4
|
import { useRef, useSyncExternalStore, useEffect, useCallback, createContext, useMemo, useContext } from 'react';
|
|
4
5
|
import { jsx } from 'react/jsx-runtime';
|
|
5
6
|
|
|
@@ -9,7 +10,11 @@ function useBackgroundRuntime({
|
|
|
9
10
|
}) {
|
|
10
11
|
const clientRef = useRef(null);
|
|
11
12
|
if (clientRef.current === null) {
|
|
12
|
-
clientRef.current = bridgeClient({
|
|
13
|
+
clientRef.current = bridgeClient({
|
|
14
|
+
channel,
|
|
15
|
+
parseState,
|
|
16
|
+
parseMsg: passThroughMsg
|
|
17
|
+
});
|
|
13
18
|
}
|
|
14
19
|
const client = clientRef.current;
|
|
15
20
|
const state = useSyncExternalStore(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/extension/react.tsx"],"names":[],"mappings":";;;;;AAoFO,SAAS,oBAAA,CAAoD;AAAA,EAClE,OAAA;AAAA,EACA;AACF,CAAA,EAAkE;AAIhE,EAAA,MAAM,SAAA,GAAY,OAAqD,IAAI,CAAA;AAC3E,EAAA,IAAI,SAAA,CAAU,YAAY,IAAA,EAAM;AAC9B,IAAA,SAAA,CAAU,OAAA,GAAU,YAAA,CAAmB,EAAE,OAAA,EAAS,YAAY,CAAA;AAAA,EAChE;AACA,EAAA,MAAM,SAAS,SAAA,CAAU,OAAA;AAEzB,EAAA,MAAM,KAAA,GAAQ,oBAAA;AAAA,IACZ,MAAA,CAAO,SAAA;AAAA,IACP,MAAA,CAAO,WAAA;AAAA,IACP,MAAA,CAAO;AAAA,GACT;AAQA,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAA,CAAO,OAAA,EAAQ,CAAE,KAAA,CAAM,MAAM;AAAA,IAE7B,CAAC,CAAA;AAAA,EACH,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEX,EAAA,MAAM,QAAA,GAAW,WAAA;AAAA,IACf,OAAO,GAAA,KAA0B;AAC/B,MAAA,MAAM,MAAA,CAAO,SAAS,GAAG,CAAA;AAAA,IAC3B,CAAA;AAAA,IACA,CAAC,MAAM;AAAA,GACT;AAEA,EAAA,OAAO,EAAE,OAAO,QAAA,EAAS;AAC3B;AA0DA,IAAM,cAAA,GACJ,+HAAA;AAEK,SAAS,8BAAA,GAGoB;AAIlC,EAAA,MAAM,GAAA,GAAM,cAAuD,IAAI,CAAA;AAEvE,EAAA,SAAS,QAAA,CAAS;AAAA,IAChB,IAAA;AAAA,IACA;AAAA,GACF,EAGc;AACZ,IAAA,MAAM,MAAA,GAAS,qBAA2B,IAAI,CAAA;AAO9C,IAAA,MAAM,KAAA,GAAQ,OAAA;AAAA,MACZ,OAAO,EAAE,KAAA,EAAO,OAAO,KAAA,EAAO,QAAA,EAAU,OAAO,QAAA,EAAS,CAAA;AAAA,MACxD,CAAC,MAAA,CAAO,KAAA,EAAO,MAAA,CAAO,QAAQ;AAAA,KAChC;AACA,IAAA,uBAAO,GAAA,CAAC,GAAA,CAAI,QAAA,EAAJ,EAAa,OAAe,QAAA,EAAS,CAAA;AAAA,EAC/C;AAEA,EAAA,SAAS,UAAA,GAA+C;AACtD,IAAA,MAAM,GAAA,GAAM,WAAW,GAAG,CAAA;AAC1B,IAAA,IAAI,GAAA,KAAQ,IAAA,EAAM,MAAM,IAAI,MAAM,cAAc,CAAA;AAChD,IAAA,OAAO,GAAA;AAAA,EACT;AAEA,EAAA,SAAS,WAAA,GAAyC;AAChD,IAAA,OAAO,YAAW,CAAE,QAAA;AAAA,EACtB;AAOA,EAAA,SAAS,aAAgB,QAAA,EAAmD;AAC1E,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,UAAA,EAAW;AAC7B,IAAA,IAAI,QAAA,KAAa,QAAW,OAAO,KAAA;AACnC,IAAA,OAAO,SAAS,KAAK,CAAA;AAAA,EACvB;AAEA,EAAA,OAAO;AAAA,IACL,QAAA;AAAA,IACA,WAAA;AAAA,IACA,QAAA,EAAU,YAAA;AAAA,IACV;AAAA,GACF;AACF","file":"react.js","sourcesContent":["/**\n * React adapters for the background TEA runtime.\n *\n * Two layers, both built on the same `bridgeClient`:\n *\n * 1. `useBackgroundRuntime(opts)` — direct hook. One bridge client per\n * call. Surfaces that only have a single consumer mount it inline.\n *\n * 2. `createBackgroundRuntimeContext<S, M>()` → `{ Provider, useDispatch,\n * useState, useRuntime }` — factory-style React context. Mounts the\n * bridge client ONCE inside the Provider; child hooks read `state` /\n * `dispatch` from context. Surfaces that drill `dispatch` through\n * multiple component layers, or mount `useBackgroundRuntime` more\n * than once for the same channel, should use this instead.\n *\n * Why a factory (not a single global context):\n * - Type safety: each app types it against its own `S` / `M`.\n * - Tests can mount fresh contexts per test (no module-level state to\n * reset).\n * - Multiple-runtimes case: an app COULD theoretically have two\n * background-shaped runtimes (e.g. one per channel); the factory\n * pattern doesn't preclude it.\n *\n * Hook lifecycle (`useBackgroundRuntime`) — preserved from the prior\n * single-export module:\n * 1. `useSyncExternalStore` subscribes to broadcasts via\n * `bridgeClient.subscribe`, reading the latest state via\n * `bridgeClient.getSnapshot`. Reference stability is owned by the\n * bridge — `getSnapshot` returns the SAME `S` until a new broadcast\n * overwrites it, so React doesn't loop.\n * 2. A one-shot `hydrate()` call seeds `latestState` before the first\n * broadcast arrives. The hydrate side-effect runs in an effect so the\n * first render shows `null` and the subsequent render flips to the\n * hydrated snapshot (or stays null if the host is still booting).\n * 3. `dispatch(msg)` sends `:dispatch` envelopes.\n *\n * On unmount: `useSyncExternalStore` removes the listener; the host's\n * bridge stays installed.\n *\n * Canon §3.1: React adapters use `useSyncExternalStore`, not\n * `useState + useEffect`. `useMachine` and `useRuntime` already follow this\n * — `useBackgroundRuntime` (and by extension the Provider) does too. One\n * Way to Do Each Thing.\n *\n * NOTE: This is a separate entry from the main `index.ts` so apps that\n * never need React don't drag react into their type graph. The React\n * adapter peer-depends on react (you must have it installed in the app).\n */\n\n\"use client\";\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport { bridgeClient } from \"./bridge\";\n\nexport interface UseBackgroundRuntimeOpts<S> {\n channel: string;\n /**\n * REQUIRED parse for inbound `state` payloads. Forwarded to\n * `bridgeClient` — see `BridgeClientOpts.parseState`. Surfaces own this\n * because the chrome.runtime boundary is `unknown` (Inv 8). Returning\n * `null` drops the payload silently.\n */\n parseState: (raw: unknown) => S | null;\n}\n\nexport interface UseBackgroundRuntimeResult<S, M> {\n /** Latest snapshot from the host. `null` until the first hydrate response\n * or the first broadcast arrives. Surfaces SHOULD render a placeholder\n * for the null window. */\n state: S | null;\n /** Send a msg to the host runtime. Resolves once the host's dispatch\n * settles. */\n dispatch(msg: M): Promise<void>;\n}\n\nexport function useBackgroundRuntime<S, M extends { type: string }>({\n channel,\n parseState,\n}: UseBackgroundRuntimeOpts<S>): UseBackgroundRuntimeResult<S, M> {\n // Pin the client across renders. Identity must be stable because\n // `useSyncExternalStore` deduplicates subscribe by reference; rebuilding\n // the client every render would re-register listeners every render too.\n const clientRef = useRef<ReturnType<typeof bridgeClient<S, M>> | null>(null);\n if (clientRef.current === null) {\n clientRef.current = bridgeClient<S, M>({ channel, parseState });\n }\n const client = clientRef.current;\n\n const state = useSyncExternalStore<S | null>(\n client.subscribe,\n client.getSnapshot,\n client.getSnapshot,\n );\n\n // One-shot hydrate on mount. The hydrate response primes `getSnapshot`\n // before the first broadcast lands so newly-opened surfaces show the\n // right view immediately. We don't read its return value — the hook\n // re-renders when `useSyncExternalStore` notices the snapshot change.\n // Hydrate failures (host not present yet) are swallowed; surfaces wait\n // for the next broadcast.\n useEffect(() => {\n client.hydrate().catch(() => {\n // No host yet. Surfaces fall back to broadcasts.\n });\n }, [client]);\n\n const dispatch = useCallback(\n async (msg: M): Promise<void> => {\n await client.dispatch(msg);\n },\n [client],\n );\n\n return { state, dispatch };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Provider + consumer hooks — factory-style React context.\n//\n// `createBackgroundRuntimeContext<S, M>()` returns a `{ Provider, useDispatch,\n// useState, useRuntime }` tuple bound to the caller's `S` / `M`. The\n// Provider mounts `useBackgroundRuntime(opts)` ONCE internally and shares\n// the `{ state, dispatch }` pair via context. Child consumers (anywhere in\n// the tree) read the slice they need — no prop drilling, single bridge\n// client per channel per Provider mount.\n//\n// The factory is generic; the returned hooks throw a clear error if used\n// outside the Provider so the failure mode is \"loud and obvious\" rather\n// than \"silently returns null state forever\".\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface BackgroundRuntimeContext<S, M> {\n /**\n * Wrap a subtree to share a single background-runtime bridge client.\n * `opts` is forwarded verbatim to `useBackgroundRuntime` — same channel,\n * same parse contract.\n */\n Provider: (props: { opts: UseBackgroundRuntimeOpts<S>; children: ReactNode }) => ReactNode;\n /**\n * Returns the dispatch function bound to this Provider's bridge client.\n * Reference is stable across renders (per `useBackgroundRuntime`'s\n * `useCallback`), so passing it to memoized children won't churn them.\n */\n useDispatch: () => (msg: M) => Promise<void>;\n /**\n * Returns a slice of the latest state. `selector` defaults to identity\n * (returns the whole `S | null` snapshot). Selectors run on every state\n * change — surfaces with expensive transforms should memoize the\n * selector at call site.\n *\n * Returns `null` until the first hydrate / broadcast lands when the\n * selector is identity; for non-identity selectors the return type is\n * `T` and the caller's selector decides how to handle the pre-hydrate\n * `null` snapshot.\n */\n useState: {\n (): S | null;\n <T>(selector: (state: S | null) => T): T;\n };\n /**\n * Escape hatch — returns the full `{ state, dispatch }` pair as-is.\n * Useful for consumers that want both the snapshot and the dispatch\n * fn in a single hook call (e.g. effects that read state THEN\n * dispatch in response).\n *\n * Not a `@demlik/tea` `Runtime<S, M>` — the bridge surface is a thinner\n * client. This hook returns the same shape as `useBackgroundRuntime`,\n * just sourced from context instead of mounting a fresh client.\n */\n useRuntime: () => UseBackgroundRuntimeResult<S, M>;\n}\n\nconst PROVIDER_ERROR =\n \"createBackgroundRuntimeContext: hook used outside its Provider. Wrap the consumer in <BackgroundRuntime.Provider opts={...}>.\";\n\nexport function createBackgroundRuntimeContext<\n S,\n M extends { type: string },\n>(): BackgroundRuntimeContext<S, M> {\n // `null` is the \"not inside a Provider\" sentinel — the hooks below\n // throw on read. Distinct from `state: null` (pre-hydrate), which IS a\n // valid context value.\n const Ctx = createContext<UseBackgroundRuntimeResult<S, M> | null>(null);\n\n function Provider({\n opts,\n children,\n }: {\n opts: UseBackgroundRuntimeOpts<S>;\n children: ReactNode;\n }): ReactNode {\n const result = useBackgroundRuntime<S, M>(opts);\n // Stabilize the context value reference so consumers that subscribe via\n // `useContext` don't re-render unless `state` or `dispatch` actually\n // changes. `state` is reference-stable (bridge's `getSnapshot` returns\n // the same `S` until a new broadcast). `dispatch` is stable per\n // `useBackgroundRuntime`'s `useCallback`. So `useMemo` keyed on both\n // gives a stable value object across renders.\n const value = useMemo<UseBackgroundRuntimeResult<S, M>>(\n () => ({ state: result.state, dispatch: result.dispatch }),\n [result.state, result.dispatch],\n );\n return <Ctx.Provider value={value}>{children}</Ctx.Provider>;\n }\n\n function useRuntime(): UseBackgroundRuntimeResult<S, M> {\n const ctx = useContext(Ctx);\n if (ctx === null) throw new Error(PROVIDER_ERROR);\n return ctx;\n }\n\n function useDispatch(): (msg: M) => Promise<void> {\n return useRuntime().dispatch;\n }\n\n // Two-overload `useState` so callers see `S | null` without passing a\n // selector and `T` when they do. The runtime body collapses them — if\n // `selector` is omitted we pass identity through.\n function useStateHook(): S | null;\n function useStateHook<T>(selector: (state: S | null) => T): T;\n function useStateHook<T>(selector?: (state: S | null) => T): T | (S | null) {\n const { state } = useRuntime();\n if (selector === undefined) return state;\n return selector(state);\n }\n\n return {\n Provider,\n useDispatch,\n useState: useStateHook,\n useRuntime,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/extension/react.tsx"],"names":[],"mappings":";;;;;;AAoFO,SAAS,oBAAA,CAAoD;AAAA,EAClE,OAAA;AAAA,EACA;AACF,CAAA,EAAkE;AAIhE,EAAA,MAAM,SAAA,GAAY,OAAqD,IAAI,CAAA;AAC3E,EAAA,IAAI,SAAA,CAAU,YAAY,IAAA,EAAM;AAI9B,IAAA,SAAA,CAAU,UAAU,YAAA,CAAmB;AAAA,MACrC,OAAA;AAAA,MACA,UAAA;AAAA,MACA,QAAA,EAAU;AAAA,KACX,CAAA;AAAA,EACH;AACA,EAAA,MAAM,SAAS,SAAA,CAAU,OAAA;AAEzB,EAAA,MAAM,KAAA,GAAQ,oBAAA;AAAA,IACZ,MAAA,CAAO,SAAA;AAAA,IACP,MAAA,CAAO,WAAA;AAAA,IACP,MAAA,CAAO;AAAA,GACT;AAQA,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAA,CAAO,OAAA,EAAQ,CAAE,KAAA,CAAM,MAAM;AAAA,IAE7B,CAAC,CAAA;AAAA,EACH,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEX,EAAA,MAAM,QAAA,GAAW,WAAA;AAAA,IACf,OAAO,GAAA,KAA0B;AAC/B,MAAA,MAAM,MAAA,CAAO,SAAS,GAAG,CAAA;AAAA,IAC3B,CAAA;AAAA,IACA,CAAC,MAAM;AAAA,GACT;AAEA,EAAA,OAAO,EAAE,OAAO,QAAA,EAAS;AAC3B;AA6DA,IAAM,cAAA,GACJ,+HAAA;AAEK,SAAS,8BAAA,GAGoB;AAIlC,EAAA,MAAM,GAAA,GAAM,cAAuD,IAAI,CAAA;AAEvE,EAAA,SAAS,QAAA,CAAS;AAAA,IAChB,IAAA;AAAA,IACA;AAAA,GACF,EAGc;AACZ,IAAA,MAAM,MAAA,GAAS,qBAA2B,IAAI,CAAA;AAO9C,IAAA,MAAM,KAAA,GAAQ,OAAA;AAAA,MACZ,OAAO,EAAE,KAAA,EAAO,OAAO,KAAA,EAAO,QAAA,EAAU,OAAO,QAAA,EAAS,CAAA;AAAA,MACxD,CAAC,MAAA,CAAO,KAAA,EAAO,MAAA,CAAO,QAAQ;AAAA,KAChC;AACA,IAAA,uBAAO,GAAA,CAAC,GAAA,CAAI,QAAA,EAAJ,EAAa,OAAe,QAAA,EAAS,CAAA;AAAA,EAC/C;AAEA,EAAA,SAAS,UAAA,GAA+C;AACtD,IAAA,MAAM,GAAA,GAAM,WAAW,GAAG,CAAA;AAC1B,IAAA,IAAI,GAAA,KAAQ,IAAA,EAAM,MAAM,IAAI,MAAM,cAAc,CAAA;AAChD,IAAA,OAAO,GAAA;AAAA,EACT;AAEA,EAAA,SAAS,WAAA,GAAyC;AAChD,IAAA,OAAO,YAAW,CAAE,QAAA;AAAA,EACtB;AAOA,EAAA,SAAS,aAAgB,QAAA,EAAmD;AAC1E,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,UAAA,EAAW;AAC7B,IAAA,IAAI,QAAA,KAAa,QAAW,OAAO,KAAA;AACnC,IAAA,OAAO,SAAS,KAAK,CAAA;AAAA,EACvB;AAEA,EAAA,OAAO;AAAA,IACL,QAAA;AAAA,IACA,WAAA;AAAA,IACA,QAAA,EAAU,YAAA;AAAA,IACV;AAAA,GACF;AACF","file":"react.js","sourcesContent":["/**\n * React adapters for the background TEA runtime.\n *\n * Two layers, both built on the same `bridgeClient`:\n *\n * 1. `useBackgroundRuntime(opts)` — direct hook. One bridge client per\n * call. Surfaces that only have a single consumer mount it inline.\n *\n * 2. `createBackgroundRuntimeContext<S, M>()` → `{ Provider, useDispatch,\n * useState, useRuntime }` — factory-style React context. Mounts the\n * bridge client ONCE inside the Provider; child hooks read `state` /\n * `dispatch` from context. Surfaces that drill `dispatch` through\n * multiple component layers, or mount `useBackgroundRuntime` more\n * than once for the same channel, should use this instead.\n *\n * Why a factory (not a single global context):\n * - Type safety: each app types it against its own `S` / `M`.\n * - Tests can mount fresh contexts per test (no module-level state to\n * reset).\n * - Multiple-runtimes case: an app COULD theoretically have two\n * background-shaped runtimes (e.g. one per channel); the factory\n * pattern doesn't preclude it.\n *\n * Hook lifecycle (`useBackgroundRuntime`) — preserved from the prior\n * single-export module:\n * 1. `useSyncExternalStore` subscribes to broadcasts via\n * `bridgeClient.subscribe`, reading the latest state via\n * `bridgeClient.getSnapshot`. Reference stability is owned by the\n * bridge — `getSnapshot` returns the SAME `S` until a new broadcast\n * overwrites it, so React doesn't loop.\n * 2. A one-shot `hydrate()` call seeds `latestState` before the first\n * broadcast arrives. The hydrate side-effect runs in an effect so the\n * first render shows `null` and the subsequent render flips to the\n * hydrated snapshot (or stays null if the host is still booting).\n * 3. `dispatch(msg)` sends `:dispatch` envelopes.\n *\n * On unmount: `useSyncExternalStore` removes the listener; the host's\n * bridge stays installed.\n *\n * Canon §3.1: React adapters use `useSyncExternalStore`, not\n * `useState + useEffect`. `useMachine` and `useRuntime` already follow this\n * — `useBackgroundRuntime` (and by extension the Provider) does too. One\n * Way to Do Each Thing.\n *\n * NOTE: This is a separate entry from the main `index.ts` so apps that\n * never need React don't drag react into their type graph. The React\n * adapter peer-depends on react (you must have it installed in the app).\n */\n\n\"use client\";\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport { bridgeClient, passThroughMsg } from \"./bridge\";\n\nexport interface UseBackgroundRuntimeOpts<S> {\n channel: string;\n /**\n * REQUIRED parse for inbound `state` payloads. Forwarded to\n * `bridgeClient` — see `BridgeClientOpts.parseState`. Surfaces own this\n * because the chrome.runtime boundary is `unknown` (Inv 8). Returning\n * `null` drops the payload silently.\n */\n parseState: (raw: unknown) => S | null;\n}\n\nexport interface UseBackgroundRuntimeResult<S, M> {\n /** Latest snapshot from the host. `null` until the first hydrate response\n * or the first broadcast arrives. Surfaces SHOULD render a placeholder\n * for the null window. */\n state: S | null;\n /** Send a msg to the host runtime. Resolves once the host's dispatch\n * settles. */\n dispatch(msg: M): Promise<void>;\n}\n\nexport function useBackgroundRuntime<S, M extends { type: string }>({\n channel,\n parseState,\n}: UseBackgroundRuntimeOpts<S>): UseBackgroundRuntimeResult<S, M> {\n // Pin the client across renders. Identity must be stable because\n // `useSyncExternalStore` deduplicates subscribe by reference; rebuilding\n // the client every render would re-register listeners every render too.\n const clientRef = useRef<ReturnType<typeof bridgeClient<S, M>> | null>(null);\n if (clientRef.current === null) {\n // This hook surfaces only `state`; the broadcast msg is unused, so it\n // explicitly opts out of msg parsing via `passThroughMsg`. Same behavior\n // as the former optional-`parseMsg` default, now a visible decision.\n clientRef.current = bridgeClient<S, M>({\n channel,\n parseState,\n parseMsg: passThroughMsg,\n });\n }\n const client = clientRef.current;\n\n const state = useSyncExternalStore<S | null>(\n client.subscribe,\n client.getSnapshot,\n client.getSnapshot,\n );\n\n // One-shot hydrate on mount. The hydrate response primes `getSnapshot`\n // before the first broadcast lands so newly-opened surfaces show the\n // right view immediately. We don't read its return value — the hook\n // re-renders when `useSyncExternalStore` notices the snapshot change.\n // Hydrate failures (host not present yet) are swallowed; surfaces wait\n // for the next broadcast.\n useEffect(() => {\n client.hydrate().catch(() => {\n // No host yet. Surfaces fall back to broadcasts.\n });\n }, [client]);\n\n const dispatch = useCallback(\n async (msg: M): Promise<void> => {\n await client.dispatch(msg);\n },\n [client],\n );\n\n return { state, dispatch };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Provider + consumer hooks — factory-style React context.\n//\n// `createBackgroundRuntimeContext<S, M>()` returns a `{ Provider, useDispatch,\n// useState, useRuntime }` tuple bound to the caller's `S` / `M`. The\n// Provider mounts `useBackgroundRuntime(opts)` ONCE internally and shares\n// the `{ state, dispatch }` pair via context. Child consumers (anywhere in\n// the tree) read the slice they need — no prop drilling, single bridge\n// client per channel per Provider mount.\n//\n// The factory is generic; the returned hooks throw a clear error if used\n// outside the Provider so the failure mode is \"loud and obvious\" rather\n// than \"silently returns null state forever\".\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface BackgroundRuntimeContext<S, M> {\n /**\n * Wrap a subtree to share a single background-runtime bridge client.\n * `opts` is forwarded verbatim to `useBackgroundRuntime` — same channel,\n * same parse contract.\n */\n Provider: (props: {\n opts: UseBackgroundRuntimeOpts<S>;\n children: ReactNode;\n }) => ReactNode;\n /**\n * Returns the dispatch function bound to this Provider's bridge client.\n * Reference is stable across renders (per `useBackgroundRuntime`'s\n * `useCallback`), so passing it to memoized children won't churn them.\n */\n useDispatch: () => (msg: M) => Promise<void>;\n /**\n * Returns a slice of the latest state. `selector` defaults to identity\n * (returns the whole `S | null` snapshot). Selectors run on every state\n * change — surfaces with expensive transforms should memoize the\n * selector at call site.\n *\n * Returns `null` until the first hydrate / broadcast lands when the\n * selector is identity; for non-identity selectors the return type is\n * `T` and the caller's selector decides how to handle the pre-hydrate\n * `null` snapshot.\n */\n useState: {\n (): S | null;\n <T>(selector: (state: S | null) => T): T;\n };\n /**\n * Escape hatch — returns the full `{ state, dispatch }` pair as-is.\n * Useful for consumers that want both the snapshot and the dispatch\n * fn in a single hook call (e.g. effects that read state THEN\n * dispatch in response).\n *\n * Not a `@demlik/tea` `Runtime<S, M>` — the bridge surface is a thinner\n * client. This hook returns the same shape as `useBackgroundRuntime`,\n * just sourced from context instead of mounting a fresh client.\n */\n useRuntime: () => UseBackgroundRuntimeResult<S, M>;\n}\n\nconst PROVIDER_ERROR =\n \"createBackgroundRuntimeContext: hook used outside its Provider. Wrap the consumer in <BackgroundRuntime.Provider opts={...}>.\";\n\nexport function createBackgroundRuntimeContext<\n S,\n M extends { type: string },\n>(): BackgroundRuntimeContext<S, M> {\n // `null` is the \"not inside a Provider\" sentinel — the hooks below\n // throw on read. Distinct from `state: null` (pre-hydrate), which IS a\n // valid context value.\n const Ctx = createContext<UseBackgroundRuntimeResult<S, M> | null>(null);\n\n function Provider({\n opts,\n children,\n }: {\n opts: UseBackgroundRuntimeOpts<S>;\n children: ReactNode;\n }): ReactNode {\n const result = useBackgroundRuntime<S, M>(opts);\n // Stabilize the context value reference so consumers that subscribe via\n // `useContext` don't re-render unless `state` or `dispatch` actually\n // changes. `state` is reference-stable (bridge's `getSnapshot` returns\n // the same `S` until a new broadcast). `dispatch` is stable per\n // `useBackgroundRuntime`'s `useCallback`. So `useMemo` keyed on both\n // gives a stable value object across renders.\n const value = useMemo<UseBackgroundRuntimeResult<S, M>>(\n () => ({ state: result.state, dispatch: result.dispatch }),\n [result.state, result.dispatch],\n );\n return <Ctx.Provider value={value}>{children}</Ctx.Provider>;\n }\n\n function useRuntime(): UseBackgroundRuntimeResult<S, M> {\n const ctx = useContext(Ctx);\n if (ctx === null) throw new Error(PROVIDER_ERROR);\n return ctx;\n }\n\n function useDispatch(): (msg: M) => Promise<void> {\n return useRuntime().dispatch;\n }\n\n // Two-overload `useState` so callers see `S | null` without passing a\n // selector and `T` when they do. The runtime body collapses them — if\n // `selector` is omitted we pass identity through.\n function useStateHook(): S | null;\n function useStateHook<T>(selector: (state: S | null) => T): T;\n function useStateHook<T>(selector?: (state: S | null) => T): T | (S | null) {\n const { state } = useRuntime();\n if (selector === undefined) return state;\n return selector(state);\n }\n\n return {\n Provider,\n useDispatch,\n useState: useStateHook,\n useRuntime,\n };\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Sub } from '../../
|
|
2
|
-
import { S as SubscribeHandler } from '../../types-
|
|
1
|
+
import { S as Sub } from '../../core-CymXZCeY.js';
|
|
2
|
+
import { S as SubscribeHandler } from '../../types-N9LZbdnr.js';
|
|
3
3
|
|
|
4
4
|
type AlarmSubData = {
|
|
5
5
|
alarmName: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/extension/subs/from-chrome-alarm.ts","../../../src/extension/subs/from-chrome-message.ts","../../../src/extension/subs/from-chrome-storage-change.ts","../../../src/extension/subs/from-chrome-tabs-event.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// fromChromeAlarm — pure observer for `chrome.alarms.onAlarm`.\n//\n// chrome.alarms is a PERSISTENT resource (the alarm registry survives MV3\n// service-worker restarts — that's the whole reason it exists). Modeling\n// alarm CREATION inside `Sub.subscribe` conflates two concerns:\n//\n// - Persistent-resource lifecycle (create/clear) — caller's responsibility,\n// expressed as Cmds (typically `ensure_alarm` on boot, `clear_alarm`\n// when retiring). Cmds run through `interpret`, where async is the\n// contract.\n// - Event observation — the Sub's responsibility. `SubscribeHandler` is\n// SYNC: `(sub, ctx, dispatch) => () => void`. Mixing in a check-then-\n// create await chain forces a `void (async () => …)()` wrapper that\n// hides errors and breaks test ordering.\n//\n// World B: the Sub is pure listener. It carries `alarmName` only,\n// addListener + filter on mount, removeListener on cleanup. The Cmd\n// vocabulary in the consumer machine (`ensure_alarm`, `clear_alarm`) owns\n// the create/clear lifecycle.\n//\n// Cites invariant 3 (effects describe via Cmd, not perform inside Sub) and\n// invariant 4 (external events enter as Subs — observation only).\n// ---------------------------------------------------------------------------\n\nimport type { Sub } from \"../../index\";\nimport type { SubscribeHandler } from \"../../subs/index\";\n\ntype AlarmSubData = { alarmName: string };\n\nexport function fromChromeAlarm<S extends Sub & AlarmSubData, M>(\n msgFn: (alarm: chrome.alarms.Alarm, sub: S) => M | null,\n): SubscribeHandler<S, M, unknown> {\n return (sub, _ctx, dispatch) => {\n const listener = (alarm: chrome.alarms.Alarm): void => {\n // Filter by name — onAlarm fires for EVERY alarm registered in the\n // extension, not just ours. Without this filter, two Subs with\n // different alarmName values would cross-dispatch.\n if (alarm.name !== sub.alarmName) return;\n const msg = msgFn(alarm, sub);\n if (msg !== null) dispatch(msg);\n };\n chrome.alarms.onAlarm.addListener(listener);\n return () => chrome.alarms.onAlarm.removeListener(listener);\n };\n}\n","// ---------------------------------------------------------------------------\n// fromChromeMessage — universal `chrome.runtime.onMessage`-shaped Sub\n// factory.\n//\n// `chrome.runtime.onMessage` is the extension's intra-process message bus:\n// the service worker receives broadcasts from popup, options, content\n// scripts, and the WebApp's `chrome.runtime.sendMessage` calls. Every\n// listener wakes for every message; consumers filter by shape (the message\n// is `unknown` to chrome — only the participants share a schema).\n//\n// Shape choices:\n//\n// - `filter?: (message: unknown) => boolean` — optional pre-filter that\n// runs BEFORE `msgFn`. The factory drops the dispatch when `filter`\n// returns false. Use this for cheap discriminant checks (e.g.\n// `message.type === \"ping\"`) that short-circuit before the more\n// expensive Msg construction.\n//\n// - `msgFn(message, sender, sub)` — both the message AND the sender\n// are forwarded. `sender` carries `tab.id` for content-script sends\n// and `id` for cross-extension sends; consumers that route by sender\n// (e.g. \"only accept from this tab\") need it. Returns `M | null`;\n// null drops the dispatch.\n//\n// What this factory deliberately does NOT do:\n//\n// - It does not call `sendResponse` or return `true` from the listener\n// — that's the request/response shape, not the broadcast shape. A\n// factory that supports both would have to commit to a Promise<M>\n// return path, which couples to async-Cmd plumbing that's not in\n// scope for a Sub. Use a one-shot Cmd handler for request/response.\n//\n// Strengthens invariant 9 (named, small surface) and invariant 2 (the\n// reducer never sees chrome.runtime.onMessage).\n// ---------------------------------------------------------------------------\n\nimport type { Sub } from \"../../index\";\nimport type { SubscribeHandler } from \"../../subs/index\";\n\nexport interface ChromeMessageOpts<S extends Sub, M> {\n /**\n * Pre-filter — when provided, the factory only dispatches when this\n * returns `true`. Cheap discriminant checks belong here.\n */\n filter?: (message: unknown) => boolean;\n msgFn: (message: unknown, sender: chrome.runtime.MessageSender, sub: S) => M | null;\n}\n\nexport function fromChromeMessage<S extends Sub, M>(\n opts: ChromeMessageOpts<S, M>,\n): SubscribeHandler<S, M, unknown> {\n return (sub, _ctx, dispatch) => {\n const listener = (\n message: unknown,\n sender: chrome.runtime.MessageSender,\n _sendResponse: (response: unknown) => void,\n ): void => {\n if (opts.filter !== undefined && !opts.filter(message)) return;\n const msg = opts.msgFn(message, sender, sub);\n if (msg !== null) dispatch(msg);\n // We intentionally do not return `true` here — that's the\n // request/response posture, not the broadcast posture. See the\n // module comment for the rationale.\n };\n chrome.runtime.onMessage.addListener(listener);\n return () => {\n chrome.runtime.onMessage.removeListener(listener);\n };\n };\n}\n","// ---------------------------------------------------------------------------\n// fromChromeStorageChange — universal `chrome.storage.onChanged`-shaped Sub\n// factory.\n//\n// `chrome.storage.onChanged` is the top-level event that fires for every\n// area (`local`, `sync`, `session`, `managed`) — the listener receives\n// `(changes, areaName)`. Consumers typically care about ONE area and often\n// only a subset of keys; without a filter, every listener wakes for every\n// change anywhere in the extension's storage. The factory absorbs the\n// addListener/removeListener pair PLUS the standard area/key filter.\n//\n// Shape choices:\n//\n// - `area: chrome.storage.AreaName` — required. Chrome's typing covers\n// \"local\" | \"sync\" | \"session\" | \"managed\". The factory drops every\n// fire where `areaName !== opts.area`.\n//\n// - `keys?: readonly string[]` — optional intersection filter. When\n// provided, the factory drops fires where none of the changed keys\n// overlaps with the requested set. Omitting means \"any key in the\n// area\". The filter is `O(keys × changes)` per fire — acceptable for\n// small key lists (typical: 1-5 keys per Sub).\n//\n// - `msgFn` may return `null` — drops the dispatch even after the\n// area/keys filters pass. Gives the caller a final hook for\n// state-aware filtering (e.g. \"only when the new value is\n// truthy\").\n//\n// Strengthens invariant 9 (named, small surface) and invariant 2 (the\n// reducer never sees chrome.storage; the factory owns the wiring).\n// ---------------------------------------------------------------------------\n\nimport type { Sub } from \"../../index\";\nimport type { SubscribeHandler } from \"../../subs/index\";\n\nexport interface ChromeStorageChangeOpts<S extends Sub, M> {\n area: chrome.storage.AreaName;\n /**\n * If provided, only dispatch when at least one of the changed keys\n * overlaps with this set. Omitting means \"any key in the area\".\n */\n keys?: readonly string[];\n msgFn: (changes: Record<string, chrome.storage.StorageChange>, sub: S) => M | null;\n}\n\nexport function fromChromeStorageChange<S extends Sub, M>(\n opts: ChromeStorageChangeOpts<S, M>,\n): SubscribeHandler<S, M, unknown> {\n return (sub, _ctx, dispatch) => {\n const listener = (\n changes: Record<string, chrome.storage.StorageChange>,\n areaName: chrome.storage.AreaName,\n ): void => {\n if (areaName !== opts.area) return;\n if (opts.keys !== undefined) {\n // Intersect — at least one changed key must be in opts.keys.\n // Using a small list scan over Object.keys(changes) avoids\n // allocating a Set per fire for the typical 1-5 key case.\n const changedKeys = Object.keys(changes);\n let overlap = false;\n for (const k of opts.keys) {\n if (changedKeys.includes(k)) {\n overlap = true;\n break;\n }\n }\n if (!overlap) return;\n }\n const msg = opts.msgFn(changes, sub);\n if (msg !== null) dispatch(msg);\n };\n chrome.storage.onChanged.addListener(listener);\n return () => {\n chrome.storage.onChanged.removeListener(listener);\n };\n };\n}\n","// ---------------------------------------------------------------------------\n// fromChromeTabsEvent — universal `chrome.tabs.on*`-shaped Sub factory.\n//\n// chrome.tabs exposes a small set of lifecycle events (`onActivated`,\n// `onUpdated`, `onRemoved`) with different listener signatures but the\n// same wiring shape: register listener, dispatch on fire, remove on\n// cleanup. Most consumers subscribe to a SET of these events and treat\n// every fire the same way (e.g. \"tabs changed, please re-query\"). The\n// recurring ~20-line `addListener` × N + `removeListener` × N + optional\n// mount-time refresh is what this factory absorbs.\n//\n// Three shape choices worth flagging:\n//\n// - `events: readonly TabsEventName[]` — a SET (semantically), passed\n// as a tuple so the caller can be terse. The factory registers a\n// single listener per event name. The listener fires `msgFn(sub)`;\n// event-specific payloads (`activeInfo`, `tabId`, `changeInfo`,\n// `removeInfo`) are NOT forwarded — consumers that care about which\n// event fired model that as separate Subs with different ids.\n//\n// - `fireOnMount?: boolean` — synthetic immediate dispatch at subscribe\n// time. Most \"watch tabs\" subscriptions need the first emission to\n// populate state without waiting for the first chrome event; this is\n// the named, opt-in form of the otherwise-recurring `void refresh()`\n// line at the top of every chrome-tabs subscribe cell. Default\n// `false` matches the surprise principle (no synthetic dispatch\n// unless the caller asks for one).\n//\n// - `msgFn` may return `null` — drops the dispatch. The event fired,\n// the factory called `msgFn`, the caller decided \"this event is not\n// interesting given current state.\" No dispatch is made; listeners\n// stay armed for the next fire.\n//\n// Cleanup removes listeners from every event the factory installed —\n// not from the full `events` list, since a future edit could expand the\n// list mid-Sub. Captured the resolved set at subscribe time.\n//\n// Strengthens invariant 9 (named, small surface) and invariant 2 (the\n// reducer never sees chrome.tabs; the factory owns the wiring).\n// ---------------------------------------------------------------------------\n\nimport type { Sub } from \"../../index\";\nimport type { SubscribeHandler } from \"../../subs/index\";\n\nexport type TabsEventName = \"onActivated\" | \"onUpdated\" | \"onRemoved\";\n\nexport interface ChromeTabsEventOpts<S extends Sub, M> {\n events: readonly TabsEventName[];\n /**\n * If `true`, the factory dispatches a synthetic `msgFn(sub)` once at\n * subscribe time — useful for \"populate tab list on mount\" patterns\n * where the first emission must not wait for chrome to fire an event.\n * Default `false`.\n */\n fireOnMount?: boolean;\n msgFn: (sub: S) => M | null;\n}\n\n// The chrome.tabs.on* events have different listener signatures\n// (`activeInfo`, `(tabId, changeInfo, tab)`, `(tabId, removeInfo)`). We\n// type the listener as accepting unknown args because the factory does\n// not forward the event payload — every event collapses to `msgFn(sub)`.\ntype AnyTabsListener = (...args: unknown[]) => void;\n\nexport function fromChromeTabsEvent<S extends Sub, M>(\n opts: ChromeTabsEventOpts<S, M>,\n): SubscribeHandler<S, M, unknown> {\n return (sub, _ctx, dispatch) => {\n const installed: TabsEventName[] = [];\n const listener: AnyTabsListener = () => {\n const msg = opts.msgFn(sub);\n if (msg !== null) dispatch(msg);\n };\n\n for (const event of opts.events) {\n // Each chrome.tabs.onX is a typed EventEmitter — addListener takes\n // a callback whose signature matches the event. Cast at the chrome\n // boundary since the factory collapses every payload shape into a\n // single `msgFn(sub)` call.\n const emitter = chrome.tabs[event] as chrome.events.Event<AnyTabsListener>;\n emitter.addListener(listener);\n installed.push(event);\n }\n\n if (opts.fireOnMount === true) {\n const msg = opts.msgFn(sub);\n if (msg !== null) dispatch(msg);\n }\n\n return () => {\n for (const event of installed) {\n const emitter = chrome.tabs[event] as chrome.events.Event<AnyTabsListener>;\n emitter.removeListener(listener);\n }\n };\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/extension/subs/from-chrome-alarm.ts","../../../src/extension/subs/from-chrome-message.ts","../../../src/extension/subs/from-chrome-storage-change.ts","../../../src/extension/subs/from-chrome-tabs-event.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// ---------------------------------------------------------------------------\n// fromChromeAlarm — pure observer for `chrome.alarms.onAlarm`.\n//\n// chrome.alarms is a PERSISTENT resource (the alarm registry survives MV3\n// service-worker restarts — that's the whole reason it exists). Modeling\n// alarm CREATION inside `Sub.subscribe` conflates two concerns:\n//\n// - Persistent-resource lifecycle (create/clear) — caller's responsibility,\n// expressed as Cmds (typically `ensure_alarm` on boot, `clear_alarm`\n// when retiring). Cmds run through `interpret`, where async is the\n// contract.\n// - Event observation — the Sub's responsibility. `SubscribeHandler` is\n// SYNC: `(sub, ctx, dispatch) => () => void`. Mixing in a check-then-\n// create await chain forces a `void (async () => …)()` wrapper that\n// hides errors and breaks test ordering.\n//\n// World B: the Sub is pure listener. It carries `alarmName` only,\n// addListener + filter on mount, removeListener on cleanup. The Cmd\n// vocabulary in the consumer machine (`ensure_alarm`, `clear_alarm`) owns\n// the create/clear lifecycle.\n//\n// Cites invariant 3 (effects describe via Cmd, not perform inside Sub) and\n// invariant 4 (external events enter as Subs — observation only).\n// ---------------------------------------------------------------------------\n\nimport type { Sub } from \"../../index\";\nimport type { SubscribeHandler } from \"../../subs/index\";\n\ntype AlarmSubData = { alarmName: string };\n\nexport function fromChromeAlarm<S extends Sub & AlarmSubData, M>(\n msgFn: (alarm: chrome.alarms.Alarm, sub: S) => M | null,\n): SubscribeHandler<S, M, unknown> {\n return (sub, _ctx, dispatch) => {\n const listener = (alarm: chrome.alarms.Alarm): void => {\n // Filter by name — onAlarm fires for EVERY alarm registered in the\n // extension, not just ours. Without this filter, two Subs with\n // different alarmName values would cross-dispatch.\n if (alarm.name !== sub.alarmName) return;\n const msg = msgFn(alarm, sub);\n if (msg !== null) dispatch(msg);\n };\n chrome.alarms.onAlarm.addListener(listener);\n return () => chrome.alarms.onAlarm.removeListener(listener);\n };\n}\n","// ---------------------------------------------------------------------------\n// fromChromeMessage — universal `chrome.runtime.onMessage`-shaped Sub\n// factory.\n//\n// `chrome.runtime.onMessage` is the extension's intra-process message bus:\n// the service worker receives broadcasts from popup, options, content\n// scripts, and the WebApp's `chrome.runtime.sendMessage` calls. Every\n// listener wakes for every message; consumers filter by shape (the message\n// is `unknown` to chrome — only the participants share a schema).\n//\n// Shape choices:\n//\n// - `filter?: (message: unknown) => boolean` — optional pre-filter that\n// runs BEFORE `msgFn`. The factory drops the dispatch when `filter`\n// returns false. Use this for cheap discriminant checks (e.g.\n// `message.type === \"ping\"`) that short-circuit before the more\n// expensive Msg construction.\n//\n// - `msgFn(message, sender, sub)` — both the message AND the sender\n// are forwarded. `sender` carries `tab.id` for content-script sends\n// and `id` for cross-extension sends; consumers that route by sender\n// (e.g. \"only accept from this tab\") need it. Returns `M | null`;\n// null drops the dispatch.\n//\n// What this factory deliberately does NOT do:\n//\n// - It does not call `sendResponse` or return `true` from the listener\n// — that's the request/response shape, not the broadcast shape. A\n// factory that supports both would have to commit to a Promise<M>\n// return path, which couples to async-Cmd plumbing that's not in\n// scope for a Sub. Use a one-shot Cmd handler for request/response.\n//\n// Strengthens invariant 9 (named, small surface) and invariant 2 (the\n// reducer never sees chrome.runtime.onMessage).\n// ---------------------------------------------------------------------------\n\nimport type { Sub } from \"../../index\";\nimport type { SubscribeHandler } from \"../../subs/index\";\n\nexport interface ChromeMessageOpts<S extends Sub, M> {\n /**\n * Pre-filter — when provided, the factory only dispatches when this\n * returns `true`. Cheap discriminant checks belong here.\n */\n filter?: (message: unknown) => boolean;\n msgFn: (\n message: unknown,\n sender: chrome.runtime.MessageSender,\n sub: S,\n ) => M | null;\n}\n\nexport function fromChromeMessage<S extends Sub, M>(\n opts: ChromeMessageOpts<S, M>,\n): SubscribeHandler<S, M, unknown> {\n return (sub, _ctx, dispatch) => {\n const listener = (\n message: unknown,\n sender: chrome.runtime.MessageSender,\n _sendResponse: (response: unknown) => void,\n ): void => {\n if (opts.filter !== undefined && !opts.filter(message)) return;\n const msg = opts.msgFn(message, sender, sub);\n if (msg !== null) dispatch(msg);\n // We intentionally do not return `true` here — that's the\n // request/response posture, not the broadcast posture. See the\n // module comment for the rationale.\n };\n chrome.runtime.onMessage.addListener(listener);\n return () => {\n chrome.runtime.onMessage.removeListener(listener);\n };\n };\n}\n","// ---------------------------------------------------------------------------\n// fromChromeStorageChange — universal `chrome.storage.onChanged`-shaped Sub\n// factory.\n//\n// `chrome.storage.onChanged` is the top-level event that fires for every\n// area (`local`, `sync`, `session`, `managed`) — the listener receives\n// `(changes, areaName)`. Consumers typically care about ONE area and often\n// only a subset of keys; without a filter, every listener wakes for every\n// change anywhere in the extension's storage. The factory absorbs the\n// addListener/removeListener pair PLUS the standard area/key filter.\n//\n// Shape choices:\n//\n// - `area: chrome.storage.AreaName` — required. Chrome's typing covers\n// \"local\" | \"sync\" | \"session\" | \"managed\". The factory drops every\n// fire where `areaName !== opts.area`.\n//\n// - `keys?: readonly string[]` — optional intersection filter. When\n// provided, the factory drops fires where none of the changed keys\n// overlaps with the requested set. Omitting means \"any key in the\n// area\". The filter is `O(keys × changes)` per fire — acceptable for\n// small key lists (typical: 1-5 keys per Sub).\n//\n// - `msgFn` may return `null` — drops the dispatch even after the\n// area/keys filters pass. Gives the caller a final hook for\n// state-aware filtering (e.g. \"only when the new value is\n// truthy\").\n//\n// Strengthens invariant 9 (named, small surface) and invariant 2 (the\n// reducer never sees chrome.storage; the factory owns the wiring).\n// ---------------------------------------------------------------------------\n\nimport type { Sub } from \"../../index\";\nimport type { SubscribeHandler } from \"../../subs/index\";\n\nexport interface ChromeStorageChangeOpts<S extends Sub, M> {\n area: chrome.storage.AreaName;\n /**\n * If provided, only dispatch when at least one of the changed keys\n * overlaps with this set. Omitting means \"any key in the area\".\n */\n keys?: readonly string[];\n msgFn: (\n changes: Record<string, chrome.storage.StorageChange>,\n sub: S,\n ) => M | null;\n}\n\nexport function fromChromeStorageChange<S extends Sub, M>(\n opts: ChromeStorageChangeOpts<S, M>,\n): SubscribeHandler<S, M, unknown> {\n return (sub, _ctx, dispatch) => {\n const listener = (\n changes: Record<string, chrome.storage.StorageChange>,\n areaName: chrome.storage.AreaName,\n ): void => {\n if (areaName !== opts.area) return;\n if (opts.keys !== undefined) {\n // Intersect — at least one changed key must be in opts.keys.\n // Using a small list scan over Object.keys(changes) avoids\n // allocating a Set per fire for the typical 1-5 key case.\n const changedKeys = Object.keys(changes);\n let overlap = false;\n for (const k of opts.keys) {\n if (changedKeys.includes(k)) {\n overlap = true;\n break;\n }\n }\n if (!overlap) return;\n }\n const msg = opts.msgFn(changes, sub);\n if (msg !== null) dispatch(msg);\n };\n chrome.storage.onChanged.addListener(listener);\n return () => {\n chrome.storage.onChanged.removeListener(listener);\n };\n };\n}\n","// ---------------------------------------------------------------------------\n// fromChromeTabsEvent — universal `chrome.tabs.on*`-shaped Sub factory.\n//\n// chrome.tabs exposes a small set of lifecycle events (`onActivated`,\n// `onUpdated`, `onRemoved`) with different listener signatures but the\n// same wiring shape: register listener, dispatch on fire, remove on\n// cleanup. Most consumers subscribe to a SET of these events and treat\n// every fire the same way (e.g. \"tabs changed, please re-query\"). The\n// recurring ~20-line `addListener` × N + `removeListener` × N + optional\n// mount-time refresh is what this factory absorbs.\n//\n// Three shape choices worth flagging:\n//\n// - `events: readonly TabsEventName[]` — a SET (semantically), passed\n// as a tuple so the caller can be terse. The factory registers a\n// single listener per event name. The listener fires `msgFn(sub)`;\n// event-specific payloads (`activeInfo`, `tabId`, `changeInfo`,\n// `removeInfo`) are NOT forwarded — consumers that care about which\n// event fired model that as separate Subs with different ids.\n//\n// - `fireOnMount?: boolean` — synthetic immediate dispatch at subscribe\n// time. Most \"watch tabs\" subscriptions need the first emission to\n// populate state without waiting for the first chrome event; this is\n// the named, opt-in form of the otherwise-recurring `void refresh()`\n// line at the top of every chrome-tabs subscribe cell. Default\n// `false` matches the surprise principle (no synthetic dispatch\n// unless the caller asks for one).\n//\n// - `msgFn` may return `null` — drops the dispatch. The event fired,\n// the factory called `msgFn`, the caller decided \"this event is not\n// interesting given current state.\" No dispatch is made; listeners\n// stay armed for the next fire.\n//\n// Cleanup removes listeners from every event the factory installed —\n// not from the full `events` list, since a future edit could expand the\n// list mid-Sub. Captured the resolved set at subscribe time.\n//\n// Strengthens invariant 9 (named, small surface) and invariant 2 (the\n// reducer never sees chrome.tabs; the factory owns the wiring).\n// ---------------------------------------------------------------------------\n\nimport type { Sub } from \"../../index\";\nimport type { SubscribeHandler } from \"../../subs/index\";\n\nexport type TabsEventName = \"onActivated\" | \"onUpdated\" | \"onRemoved\";\n\nexport interface ChromeTabsEventOpts<S extends Sub, M> {\n events: readonly TabsEventName[];\n /**\n * If `true`, the factory dispatches a synthetic `msgFn(sub)` once at\n * subscribe time — useful for \"populate tab list on mount\" patterns\n * where the first emission must not wait for chrome to fire an event.\n * Default `false`.\n */\n fireOnMount?: boolean;\n msgFn: (sub: S) => M | null;\n}\n\n// The chrome.tabs.on* events have different listener signatures\n// (`activeInfo`, `(tabId, changeInfo, tab)`, `(tabId, removeInfo)`). We\n// type the listener as accepting unknown args because the factory does\n// not forward the event payload — every event collapses to `msgFn(sub)`.\ntype AnyTabsListener = (...args: unknown[]) => void;\n\nexport function fromChromeTabsEvent<S extends Sub, M>(\n opts: ChromeTabsEventOpts<S, M>,\n): SubscribeHandler<S, M, unknown> {\n return (sub, _ctx, dispatch) => {\n const installed: TabsEventName[] = [];\n const listener: AnyTabsListener = () => {\n const msg = opts.msgFn(sub);\n if (msg !== null) dispatch(msg);\n };\n\n for (const event of opts.events) {\n // Each chrome.tabs.onX is a typed EventEmitter — addListener takes\n // a callback whose signature matches the event. Cast at the chrome\n // boundary since the factory collapses every payload shape into a\n // single `msgFn(sub)` call.\n const emitter = chrome.tabs[\n event\n ] as chrome.events.Event<AnyTabsListener>;\n emitter.addListener(listener);\n installed.push(event);\n }\n\n if (opts.fireOnMount === true) {\n const msg = opts.msgFn(sub);\n if (msg !== null) dispatch(msg);\n }\n\n return () => {\n for (const event of installed) {\n const emitter = chrome.tabs[\n event\n ] as chrome.events.Event<AnyTabsListener>;\n emitter.removeListener(listener);\n }\n };\n };\n}\n"]}
|
|
@@ -24,7 +24,9 @@ function fakeChrome() {
|
|
|
24
24
|
return v === void 0 ? {} : { [keys]: v };
|
|
25
25
|
}
|
|
26
26
|
if (Array.isArray(keys)) {
|
|
27
|
-
return Object.fromEntries(
|
|
27
|
+
return Object.fromEntries(
|
|
28
|
+
keys.flatMap((k) => data.has(k) ? [[k, data.get(k)]] : [])
|
|
29
|
+
);
|
|
28
30
|
}
|
|
29
31
|
const out = {};
|
|
30
32
|
for (const [k, def] of Object.entries(keys)) {
|
|
@@ -80,7 +82,11 @@ function fakeChrome() {
|
|
|
80
82
|
if (responded) return;
|
|
81
83
|
}
|
|
82
84
|
if (willRespondAsync) return;
|
|
83
|
-
reject(
|
|
85
|
+
reject(
|
|
86
|
+
new Error(
|
|
87
|
+
"Could not establish connection. Receiving end does not exist."
|
|
88
|
+
)
|
|
89
|
+
);
|
|
84
90
|
});
|
|
85
91
|
};
|
|
86
92
|
const extensionSender = {};
|
|
@@ -221,7 +227,9 @@ function fakeChrome() {
|
|
|
221
227
|
alarms,
|
|
222
228
|
__test: {
|
|
223
229
|
sendFromTab(tabId, message) {
|
|
224
|
-
const taggedSender = {
|
|
230
|
+
const taggedSender = {
|
|
231
|
+
tab: { id: tabId }
|
|
232
|
+
};
|
|
225
233
|
return dispatch(message, taggedSender);
|
|
226
234
|
},
|
|
227
235
|
removeTab(tabId) {
|