@demlik/tea 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/cache/index.d.ts +151 -0
  2. package/dist/cache/index.js +53 -0
  3. package/dist/cache/index.js.map +1 -0
  4. package/dist/chunk-2YFLOCWX.js +15 -0
  5. package/dist/chunk-2YFLOCWX.js.map +1 -0
  6. package/dist/chunk-CMG2KMWX.js +458 -0
  7. package/dist/chunk-CMG2KMWX.js.map +1 -0
  8. package/dist/chunk-PYBQREFU.js +95 -0
  9. package/dist/chunk-PYBQREFU.js.map +1 -0
  10. package/dist/chunk-PYOCUBQJ.js +196 -0
  11. package/dist/chunk-PYOCUBQJ.js.map +1 -0
  12. package/dist/chunk-Y5JM3UUH.js +126 -0
  13. package/dist/chunk-Y5JM3UUH.js.map +1 -0
  14. package/dist/chunk-ZRUST6BK.js +62 -0
  15. package/dist/chunk-ZRUST6BK.js.map +1 -0
  16. package/dist/circuit-breaker/index.d.ts +196 -0
  17. package/dist/circuit-breaker/index.js +53 -0
  18. package/dist/circuit-breaker/index.js.map +1 -0
  19. package/dist/deadline/index.d.ts +113 -0
  20. package/dist/deadline/index.js +20 -0
  21. package/dist/deadline/index.js.map +1 -0
  22. package/dist/debounce/index.d.ts +114 -0
  23. package/dist/debounce/index.js +56 -0
  24. package/dist/debounce/index.js.map +1 -0
  25. package/dist/devtools/index.d.ts +137 -0
  26. package/dist/devtools/index.js +217 -0
  27. package/dist/devtools/index.js.map +1 -0
  28. package/dist/devtools/styles.css +218 -0
  29. package/dist/do/index.d.ts +135 -0
  30. package/dist/do/index.js +54 -0
  31. package/dist/do/index.js.map +1 -0
  32. package/dist/extension/index.d.ts +207 -0
  33. package/dist/extension/index.js +26 -0
  34. package/dist/extension/index.js.map +1 -0
  35. package/dist/extension/react.d.ts +121 -0
  36. package/dist/extension/react.js +69 -0
  37. package/dist/extension/react.js.map +1 -0
  38. package/dist/extension/subs/index.d.ts +44 -0
  39. package/dist/extension/subs/index.js +83 -0
  40. package/dist/extension/subs/index.js.map +1 -0
  41. package/dist/extension/test-utils.d.ts +128 -0
  42. package/dist/extension/test-utils.js +269 -0
  43. package/dist/extension/test-utils.js.map +1 -0
  44. package/dist/idempotency/index.d.ts +131 -0
  45. package/dist/idempotency/index.js +58 -0
  46. package/dist/idempotency/index.js.map +1 -0
  47. package/dist/index.d.ts +364 -0
  48. package/dist/index.js +3 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/machine-viz/index.d.ts +78 -0
  51. package/dist/machine-viz/index.js +139 -0
  52. package/dist/machine-viz/index.js.map +1 -0
  53. package/dist/mem/index.d.ts +45 -0
  54. package/dist/mem/index.js +19 -0
  55. package/dist/mem/index.js.map +1 -0
  56. package/dist/node/index.d.ts +134 -0
  57. package/dist/node/index.js +89 -0
  58. package/dist/node/index.js.map +1 -0
  59. package/dist/pbt/arbitraries/index.d.ts +135 -0
  60. package/dist/pbt/arbitraries/index.js +3 -0
  61. package/dist/pbt/arbitraries/index.js.map +1 -0
  62. package/dist/pbt/index.d.ts +28 -0
  63. package/dist/pbt/index.js +18 -0
  64. package/dist/pbt/index.js.map +1 -0
  65. package/dist/pbt/runners/index.d.ts +117 -0
  66. package/dist/pbt/runners/index.js +4 -0
  67. package/dist/pbt/runners/index.js.map +1 -0
  68. package/dist/rate-limit/index.d.ts +115 -0
  69. package/dist/rate-limit/index.js +45 -0
  70. package/dist/rate-limit/index.js.map +1 -0
  71. package/dist/react/index.d.ts +72 -0
  72. package/dist/react/index.js +33 -0
  73. package/dist/react/index.js.map +1 -0
  74. package/dist/recorder/index.d.ts +287 -0
  75. package/dist/recorder/index.js +157 -0
  76. package/dist/recorder/index.js.map +1 -0
  77. package/dist/retry-backoff/index.d.ts +131 -0
  78. package/dist/retry-backoff/index.js +37 -0
  79. package/dist/retry-backoff/index.js.map +1 -0
  80. package/dist/subs/index.d.ts +57 -0
  81. package/dist/subs/index.js +4 -0
  82. package/dist/subs/index.js.map +1 -0
  83. package/dist/testing/index.d.ts +220 -0
  84. package/dist/testing/index.js +105 -0
  85. package/dist/testing/index.js.map +1 -0
  86. package/dist/throttle/index.d.ts +125 -0
  87. package/dist/throttle/index.js +54 -0
  88. package/dist/throttle/index.js.map +1 -0
  89. package/dist/trace-replay/index.d.ts +52 -0
  90. package/dist/trace-replay/index.js +57 -0
  91. package/dist/trace-replay/index.js.map +1 -0
  92. package/dist/types-CwBC5wMn.d.ts +5 -0
  93. package/dist/work-queue/index.d.ts +56 -0
  94. package/dist/work-queue/index.js +143 -0
  95. package/dist/work-queue/index.js.map +1 -0
  96. package/package.json +184 -0
@@ -0,0 +1,135 @@
1
+ import { Store, SubId, Sub } from '../index.js';
2
+
3
+ /**
4
+ * @demlik/tea/do — Durable Object adapter for `@demlik/tea`.
5
+ *
6
+ * Two exports drive the integration:
7
+ *
8
+ * 1. `doStore<S>(storage, parse, key?)` — `Store<S>` impl backed by
9
+ * `DurableObjectStorage`. JSON-stringifies on save, JSON-parses on load
10
+ * and returns `unknown` (invariant-8 boundary); the substrate then calls
11
+ * `migrate(raw)` (which forwards to `parse`) to turn it into `S | null`.
12
+ * Returns `null` (typed as `unknown`) when the key is absent. Structural
13
+ * JSON malformation throws at load — `run()`'s boot path surfaces that
14
+ * (PRD throw-semantics table: "store.load throw at boot → run() throws
15
+ * synchronously"). Shape mismatch is NOT a throw — `parse` returns
16
+ * `null` and the substrate boots fresh.
17
+ *
18
+ * 2. `doSubscribe<M, Ctx>()` — handler registry for the two DO-native sub
19
+ * types this package owns:
20
+ * - `do_alarm` — schedules a real DO alarm via `state.storage.setAlarm`;
21
+ * the cooperating DO's `alarm()` lifecycle method dispatches the sub's
22
+ * msg. Cleanup clears the alarm.
23
+ * - `do_ws` — registers a WebSocket-id'd listener in a per-DO registry
24
+ * that the cooperating DO's `webSocketMessage()` consults. Cleanup
25
+ * deregisters.
26
+ *
27
+ * BOTH `do_alarm` and `do_ws` rely on the host DO cooperating: alarm and
28
+ * webSocketMessage are DO lifecycle methods, not JS events the handler can
29
+ * subscribe to from the outside. The cooperating DO must:
30
+ *
31
+ * - Maintain a `Ctx` that exposes `state: DurableObjectState` AND the
32
+ * subscriber registries (`wsRegistry`, `alarmRegistry`). The handlers read
33
+ * from `ctx`.
34
+ * - Forward `alarm()` callbacks into the alarm registry by id, dispatching
35
+ * the registered msg.
36
+ * - Forward `webSocketMessage(ws, data)` callbacks into the ws registry,
37
+ * dispatching `sub.msg(data)` for matching `socketId`s.
38
+ *
39
+ * `src/test-worker.ts` is the canonical example of this cooperation. See
40
+ * `test/README.md` for the convention.
41
+ */
42
+
43
+ /**
44
+ * `Store<S>` over `DurableObjectStorage`.
45
+ *
46
+ * `parse` is REQUIRED because DO storage is a real serialization boundary —
47
+ * the bytes that come back through `JSON.parse` are structurally `unknown`,
48
+ * and the caller (the DO that owns `S`) is the only party with enough
49
+ * information to validate the shape. Returning `null` from `parse` means
50
+ * "no usable persisted state" — the substrate boots `init` with `loaded =
51
+ * null`, a known-good fresh-boot path. `parse` must NOT throw per the
52
+ * `Store<S>.migrate` contract; shape mismatch is a value (null), not a
53
+ * panic.
54
+ *
55
+ * Structural malformations (the stored cell isn't even JSON) still throw at
56
+ * `load()` — that's an infrastructure error, distinct from a schema
57
+ * mismatch. The substrate surfaces it via `runtime.ready` rejection.
58
+ *
59
+ * Concrete bite this closes: when a `State` variant is renamed between
60
+ * deploys, the DO's new code previously received a value typed as the new
61
+ * `S` that held a runtime string from the old `S` — silent undefined
62
+ * behavior in `init`/`update`. After this, the boundary parse rejects the
63
+ * old shape (returns null) and the DO boots fresh, picking up its new
64
+ * schema from the next dispatch onward.
65
+ */
66
+ declare function doStore<S>(storage: DurableObjectStorage, parse: (raw: unknown) => S | null, key?: string): Store<S>;
67
+ /**
68
+ * A DO alarm sub: fires `msg` at `firesAt` (epoch ms). The substrate diffs
69
+ * subs by `id`; same id across transitions = same alarm. To reschedule, emit
70
+ * a DIFFERENT id (e.g., `audit-timeout-v2`).
71
+ */
72
+ type DoAlarmSub<M> = {
73
+ id: SubId;
74
+ type: "do_alarm";
75
+ firesAt: number;
76
+ msg: M;
77
+ };
78
+ /**
79
+ * A DO WebSocket sub: routes incoming messages on `socketId` through
80
+ * `sub.msg(data)` to dispatch.
81
+ */
82
+ type DoWsSub<M> = {
83
+ id: SubId;
84
+ type: "do_ws";
85
+ socketId: string;
86
+ msg: (data: string) => M;
87
+ };
88
+ type DoSub<M> = DoAlarmSub<M> | DoWsSub<M>;
89
+ type AssertDoSubIsSub<T extends Sub = DoSub<unknown>> = T;
90
+ /**
91
+ * Registry of pending alarm dispatches keyed by sub `id`. The cooperating DO's
92
+ * `alarm()` lifecycle method drains this and dispatches each msg.
93
+ *
94
+ * One alarm-queue cell at a time per DO (DO has exactly one alarm slot). We
95
+ * still key by `id` so multiple alarm subs scheduled in the same reconcile
96
+ * pass don't clobber each other — the earliest `firesAt` wins (DO native
97
+ * behavior) and `alarm()` drains everything due by the fire time.
98
+ */
99
+ type DoAlarmRegistry<M> = Map<string, {
100
+ firesAt: number;
101
+ msg: M;
102
+ }>;
103
+ /**
104
+ * Registry of WebSocket message subscribers keyed by sub `id`. The cooperating
105
+ * DO's `webSocketMessage(ws, data)` consults this and dispatches
106
+ * `sub.msg(data)` for entries matching the incoming `socketId`.
107
+ */
108
+ type DoWsRegistry<M> = Map<string, {
109
+ socketId: string;
110
+ msg: (data: string) => M;
111
+ }>;
112
+ /**
113
+ * Minimum shape a cooperating DO must put on its `Ctx` for `doSubscribe`
114
+ * handlers to wire up. Users may extend with anything else they like.
115
+ */
116
+ interface DoSubscribeCtx<M> {
117
+ state: DurableObjectState;
118
+ alarmRegistry: DoAlarmRegistry<M>;
119
+ wsRegistry: DoWsRegistry<M>;
120
+ }
121
+ /**
122
+ * Build the `subscribe` handler registry for `do_alarm` and `do_ws` subs.
123
+ *
124
+ * Returned object shape matches `Machine#subscribe`'s `[K in U['type']]:
125
+ * (sub, ctx, dispatch) => () => void` contract for `U = DoSub<M>`.
126
+ *
127
+ * Generic `Ctx` is user-defined and must extend `DoSubscribeCtx<M>` so handlers
128
+ * can reach `state.storage`, `alarmRegistry`, and `wsRegistry`.
129
+ */
130
+ declare function doSubscribe<M, Ctx extends DoSubscribeCtx<M>>(): {
131
+ do_alarm: (sub: DoAlarmSub<M>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;
132
+ do_ws: (sub: DoWsSub<M>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;
133
+ };
134
+
135
+ export { type AssertDoSubIsSub, type DoAlarmRegistry, type DoAlarmSub, type DoSub, type DoSubscribeCtx, type DoWsRegistry, type DoWsSub, doStore, doSubscribe };
@@ -0,0 +1,54 @@
1
+ // src/do/index.ts
2
+ var DEFAULT_STATE_KEY = "@@state";
3
+ function doStore(storage, parse, key = DEFAULT_STATE_KEY) {
4
+ return {
5
+ async load() {
6
+ const raw = await storage.get(key);
7
+ if (raw === void 0 || raw === null) return null;
8
+ return JSON.parse(raw);
9
+ },
10
+ async save(state) {
11
+ await storage.put(key, JSON.stringify(state));
12
+ },
13
+ migrate(raw) {
14
+ return parse(raw);
15
+ }
16
+ };
17
+ }
18
+ function doSubscribe() {
19
+ return {
20
+ do_alarm: (sub, ctx, _dispatch) => {
21
+ ctx.alarmRegistry.set(sub.id, { firesAt: sub.firesAt, msg: sub.msg });
22
+ const earliest = earliestFiresAt(ctx.alarmRegistry);
23
+ if (earliest !== null) {
24
+ ctx.state.storage.setAlarm(earliest).catch((err) => console.error("[tea-do] setAlarm failed", err));
25
+ }
26
+ return () => {
27
+ ctx.alarmRegistry.delete(sub.id);
28
+ const nextEarliest = earliestFiresAt(ctx.alarmRegistry);
29
+ if (nextEarliest === null) {
30
+ ctx.state.storage.deleteAlarm().catch((err) => console.error("[tea-do] deleteAlarm failed", err));
31
+ } else {
32
+ ctx.state.storage.setAlarm(nextEarliest).catch((err) => console.error("[tea-do] setAlarm failed", err));
33
+ }
34
+ };
35
+ },
36
+ do_ws: (sub, ctx, _dispatch) => {
37
+ ctx.wsRegistry.set(sub.id, { socketId: sub.socketId, msg: sub.msg });
38
+ return () => {
39
+ ctx.wsRegistry.delete(sub.id);
40
+ };
41
+ }
42
+ };
43
+ }
44
+ function earliestFiresAt(registry) {
45
+ let earliest = null;
46
+ for (const { firesAt } of registry.values()) {
47
+ if (earliest === null || firesAt < earliest) earliest = firesAt;
48
+ }
49
+ return earliest;
50
+ }
51
+
52
+ export { doStore, doSubscribe };
53
+ //# sourceMappingURL=index.js.map
54
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/do/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/// <reference types=\"@cloudflare/workers-types\" />\n/**\n * @demlik/tea/do — Durable Object adapter for `@demlik/tea`.\n *\n * Two exports drive the integration:\n *\n * 1. `doStore<S>(storage, parse, key?)` — `Store<S>` impl backed by\n * `DurableObjectStorage`. JSON-stringifies on save, JSON-parses on load\n * and returns `unknown` (invariant-8 boundary); the substrate then calls\n * `migrate(raw)` (which forwards to `parse`) to turn it into `S | null`.\n * Returns `null` (typed as `unknown`) when the key is absent. Structural\n * JSON malformation throws at load — `run()`'s boot path surfaces that\n * (PRD throw-semantics table: \"store.load throw at boot → run() throws\n * synchronously\"). Shape mismatch is NOT a throw — `parse` returns\n * `null` and the substrate boots fresh.\n *\n * 2. `doSubscribe<M, Ctx>()` — handler registry for the two DO-native sub\n * types this package owns:\n * - `do_alarm` — schedules a real DO alarm via `state.storage.setAlarm`;\n * the cooperating DO's `alarm()` lifecycle method dispatches the sub's\n * msg. Cleanup clears the alarm.\n * - `do_ws` — registers a WebSocket-id'd listener in a per-DO registry\n * that the cooperating DO's `webSocketMessage()` consults. Cleanup\n * deregisters.\n *\n * BOTH `do_alarm` and `do_ws` rely on the host DO cooperating: alarm and\n * webSocketMessage are DO lifecycle methods, not JS events the handler can\n * subscribe to from the outside. The cooperating DO must:\n *\n * - Maintain a `Ctx` that exposes `state: DurableObjectState` AND the\n * subscriber registries (`wsRegistry`, `alarmRegistry`). The handlers read\n * from `ctx`.\n * - Forward `alarm()` callbacks into the alarm registry by id, dispatching\n * the registered msg.\n * - Forward `webSocketMessage(ws, data)` callbacks into the ws registry,\n * dispatching `sub.msg(data)` for matching `socketId`s.\n *\n * `src/test-worker.ts` is the canonical example of this cooperation. See\n * `test/README.md` for the convention.\n */\n\nimport type { Store, Sub, SubId } from \"../index\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// doStore — `Store<S>` over DurableObjectStorage.\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst DEFAULT_STATE_KEY = \"@@state\";\n\n/**\n * `Store<S>` over `DurableObjectStorage`.\n *\n * `parse` is REQUIRED because DO storage is a real serialization boundary —\n * the bytes that come back through `JSON.parse` are structurally `unknown`,\n * and the caller (the DO that owns `S`) is the only party with enough\n * information to validate the shape. Returning `null` from `parse` means\n * \"no usable persisted state\" — the substrate boots `init` with `loaded =\n * null`, a known-good fresh-boot path. `parse` must NOT throw per the\n * `Store<S>.migrate` contract; shape mismatch is a value (null), not a\n * panic.\n *\n * Structural malformations (the stored cell isn't even JSON) still throw at\n * `load()` — that's an infrastructure error, distinct from a schema\n * mismatch. The substrate surfaces it via `runtime.ready` rejection.\n *\n * Concrete bite this closes: when a `State` variant is renamed between\n * deploys, the DO's new code previously received a value typed as the new\n * `S` that held a runtime string from the old `S` — silent undefined\n * behavior in `init`/`update`. After this, the boundary parse rejects the\n * old shape (returns null) and the DO boots fresh, picking up its new\n * schema from the next dispatch onward.\n */\nexport function doStore<S>(\n storage: DurableObjectStorage,\n parse: (raw: unknown) => S | null,\n key: string = DEFAULT_STATE_KEY,\n): Store<S> {\n return {\n async load(): Promise<unknown> {\n const raw = await storage.get<string>(key);\n if (raw === undefined || raw === null) return null;\n // JSON.parse throws on malformed — propagate per PRD throw-semantics.\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 storage.put(key, JSON.stringify(state));\n },\n migrate(raw: unknown): S | null {\n return parse(raw);\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// DoSub — the two DO-native subscription variants, locked in the PRD.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * A DO alarm sub: fires `msg` at `firesAt` (epoch ms). The substrate diffs\n * subs by `id`; same id across transitions = same alarm. To reschedule, emit\n * a DIFFERENT id (e.g., `audit-timeout-v2`).\n */\nexport type DoAlarmSub<M> = { id: SubId; type: \"do_alarm\"; firesAt: number; msg: M };\n\n/**\n * A DO WebSocket sub: routes incoming messages on `socketId` through\n * `sub.msg(data)` to dispatch.\n */\nexport type DoWsSub<M> = {\n id: SubId;\n type: \"do_ws\";\n socketId: string;\n msg: (data: string) => M;\n};\n\nexport type DoSub<M> = DoAlarmSub<M> | DoWsSub<M>;\n\n// Compile-time guarantee: `DoSub<M>` is structurally a `Sub` (has `id`+`type`).\n// Erased at runtime — the `extends Sub` constraint on the alias's type\n// parameter fails to compile if `DoSub` ever drifts off the `Sub` shape.\nexport type AssertDoSubIsSub<T extends Sub = DoSub<unknown>> = T;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Ctx contract — what the cooperating DO must expose to `doSubscribe`.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Registry of pending alarm dispatches keyed by sub `id`. The cooperating DO's\n * `alarm()` lifecycle method drains this and dispatches each msg.\n *\n * One alarm-queue cell at a time per DO (DO has exactly one alarm slot). We\n * still key by `id` so multiple alarm subs scheduled in the same reconcile\n * pass don't clobber each other — the earliest `firesAt` wins (DO native\n * behavior) and `alarm()` drains everything due by the fire time.\n */\nexport type DoAlarmRegistry<M> = Map<string, { firesAt: number; msg: M }>;\n\n/**\n * Registry of WebSocket message subscribers keyed by sub `id`. The cooperating\n * DO's `webSocketMessage(ws, data)` consults this and dispatches\n * `sub.msg(data)` for entries matching the incoming `socketId`.\n */\nexport type DoWsRegistry<M> = Map<string, { socketId: string; msg: (data: string) => M }>;\n\n/**\n * Minimum shape a cooperating DO must put on its `Ctx` for `doSubscribe`\n * handlers to wire up. Users may extend with anything else they like.\n */\nexport interface DoSubscribeCtx<M> {\n state: DurableObjectState;\n alarmRegistry: DoAlarmRegistry<M>;\n wsRegistry: DoWsRegistry<M>;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// doSubscribe — the handler registry.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Build the `subscribe` handler registry for `do_alarm` and `do_ws` subs.\n *\n * Returned object shape matches `Machine#subscribe`'s `[K in U['type']]:\n * (sub, ctx, dispatch) => () => void` contract for `U = DoSub<M>`.\n *\n * Generic `Ctx` is user-defined and must extend `DoSubscribeCtx<M>` so handlers\n * can reach `state.storage`, `alarmRegistry`, and `wsRegistry`.\n */\nexport function doSubscribe<M, Ctx extends DoSubscribeCtx<M>>(): {\n do_alarm: (sub: DoAlarmSub<M>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;\n do_ws: (sub: DoWsSub<M>, ctx: Ctx, dispatch: (msg: M) => void) => () => void;\n} {\n return {\n do_alarm: (sub, ctx, _dispatch) => {\n // Register the msg so the DO's `alarm()` lifecycle method can find it.\n // `dispatch` itself is not captured here — the DO's alarm() drains the\n // registry and calls dispatch via the runtime it owns. That keeps the\n // cleanup race-free: clearing the registry entry is the only thing\n // cleanup needs to do.\n ctx.alarmRegistry.set(sub.id, { firesAt: sub.firesAt, msg: sub.msg });\n\n // Recompute the next alarm wall-clock target. DO supports exactly one\n // alarm slot; the earliest firesAt across all registered subs wins.\n const earliest = earliestFiresAt(ctx.alarmRegistry);\n // `earliestFiresAt` returns null only when the registry is empty, which\n // cannot happen here (we just inserted). The null branch is for cleanup.\n if (earliest !== null) {\n // `setAlarm` is a Promise; we don't await — the substrate's subscribe\n // signature is sync. Attach `.catch` so an unhandled rejection doesn't\n // tear down the DO isolate silently (DO isolates terminate on\n // unhandled rejections — silent failure in an alarm scheduler would\n // vanish without trace otherwise).\n ctx.state.storage\n .setAlarm(earliest)\n .catch((err) => console.error(\"[tea-do] setAlarm failed\", err));\n }\n\n return () => {\n ctx.alarmRegistry.delete(sub.id);\n const nextEarliest = earliestFiresAt(ctx.alarmRegistry);\n if (nextEarliest === null) {\n // No registered alarms left — clear the slot. `deleteAlarm` returns\n // a Promise we don't await for the same reason as above; `.catch`\n // for the same isolate-safety reason.\n ctx.state.storage\n .deleteAlarm()\n .catch((err) => console.error(\"[tea-do] deleteAlarm failed\", err));\n } else {\n ctx.state.storage\n .setAlarm(nextEarliest)\n .catch((err) => console.error(\"[tea-do] setAlarm failed\", err));\n }\n };\n },\n\n do_ws: (sub, ctx, _dispatch) => {\n ctx.wsRegistry.set(sub.id, { socketId: sub.socketId, msg: sub.msg });\n return () => {\n ctx.wsRegistry.delete(sub.id);\n };\n },\n };\n}\n\n// Pure utility: lowest `firesAt` in a non-empty alarm registry, or `null`.\nfunction earliestFiresAt<M>(registry: DoAlarmRegistry<M>): number | null {\n let earliest: number | null = null;\n for (const { firesAt } of registry.values()) {\n if (earliest === null || firesAt < earliest) earliest = firesAt;\n }\n return earliest;\n}\n"]}
@@ -0,0 +1,207 @@
1
+ import { Runtime, Store } from '../index.js';
2
+
3
+ /**
4
+ * Runtime bridge — expose a `@demlik/tea` runtime running in one chrome context
5
+ * (typically the background service worker) to consumers in other contexts
6
+ * (sidepanel, popup, devtools panel) over `chrome.runtime.sendMessage` /
7
+ * `chrome.runtime.onMessage`.
8
+ *
9
+ * Two halves, one channel name string.
10
+ *
11
+ * ┌──── background (host) ────┐ ┌──── react surface ────┐
12
+ * │ bridgeRuntime(rt, {ch}) │ chrome.* │ useBackgroundRuntime │
13
+ * │ ├ observe → broadcast │ ──────────▶ │ ├ hydrate (initial) │
14
+ * │ └ onMessage(":dispatch") │ ◀────────── │ └ dispatch (msgs) │
15
+ * │ └ onMessage(":hydrate") │ │ │
16
+ * └────────────────────────────┘ └───────────────────────┘
17
+ *
18
+ * Wire-shape (single `channel` string, three messages):
19
+ *
20
+ * { type: channel, msg, state } // background → surfaces (broadcast)
21
+ * { type: `${channel}:hydrate` } // surface → background (request)
22
+ * { type: `${channel}:dispatch`, msg } // surface → background (input)
23
+ *
24
+ * The `:hydrate` reply is the current `runtime.getState()` snapshot —
25
+ * lets a newly-opened surface render the right view BEFORE the next
26
+ * transition broadcast arrives.
27
+ *
28
+ * The bridge is intentionally agnostic to `chrome.runtime.sendMessage`
29
+ * failure modes (no receiver, sender gone, etc.) — every cross-context send
30
+ * is fire-and-forget with `.catch(() => {})`. Surfaces that need
31
+ * delivery guarantees should layer their own retry, not bake it here.
32
+ */
33
+
34
+ interface BridgeRuntimeOpts<S, V = S> {
35
+ channel: string;
36
+ /**
37
+ * Optional projection from the runtime's live state `S` to a JSON-safe
38
+ * view `V` that goes over the wire (broadcast + hydrate). Required when
39
+ * `S` carries non-serializable values (DOM refs, Map, Date, …).
40
+ *
41
+ * Identity passthrough by default — existing callers with POJO state pass
42
+ * `serialize` unset and `V = S`.
43
+ *
44
+ * Wire shape with `serialize`:
45
+ * { type: channel, msg, state: serialize(currentState) } // broadcast
46
+ * { type: hydrate } → { state: serialize(currentState) } // hydrate
47
+ *
48
+ * Dispatch is NOT affected — it carries `M` end-to-end. Surfaces parse the
49
+ * `V` they receive (they should pick a type parameter matching the
50
+ * projection).
51
+ */
52
+ serialize?: (state: S) => V;
53
+ /**
54
+ * If > 0, the bridge composes a `historyTracker(runtime, size)` over the
55
+ * runtime and returns the captured `(msg, state)` backlog in every
56
+ * `:hydrate` reply. Late subscribers receive recent traffic on connect —
57
+ * the inspector then sees prior tool calls / phase transitions instead
58
+ * of just the current state snapshot.
59
+ *
60
+ * Defaults to 0 (no tracker, empty backlog). Composition stays opt-in:
61
+ * the substrate is untouched, the dispatch loop is untouched, and the
62
+ * bridge attaches the tracker only when this opt is set. Cleanup is
63
+ * the bridge's responsibility — the disposer returned by `bridgeRuntime`
64
+ * stops the tracker along with the observer.
65
+ *
66
+ * Memory cost: ~size × (msg + state) bytes, in-memory only, dies with
67
+ * the bridge. Privacy: backlog entries flow to every `:hydrate` reply —
68
+ * any surface that can hydrate sees them. Do not enable on bridges
69
+ * carrying user secrets.
70
+ */
71
+ historySize?: number;
72
+ }
73
+ /**
74
+ * Install the bridge for a runtime. Returns a cleanup function that removes
75
+ * the observer and the chrome listener.
76
+ *
77
+ * - **Broadcast:** every (msg, state) transition emits
78
+ * `{ type: channel, msg, state }` via `chrome.runtime.sendMessage`. The
79
+ * send is fire-and-forget — when no surface is listening, chrome.runtime
80
+ * rejects with "Could not establish connection. Receiving end does not
81
+ * exist." which we swallow. If `serialize` is provided, `state` carries
82
+ * `serialize(state)` instead of the raw state.
83
+ * - **Hydrate:** `{ type: ":hydrate" }` is answered via `sendResponse` with
84
+ * the current (optionally projected) `runtime.getState()`. If the runtime
85
+ * is mid-boot (store load not yet resolved), `getState()` throws — we
86
+ * swallow and reply with `state: null`; surfaces should treat null as
87
+ * "not ready yet" and wait for the first broadcast.
88
+ * - **Dispatch:** `{ type: ":dispatch", msg }` routes to `runtime.dispatch`.
89
+ * Returns `true` from the listener so chrome keeps the message channel
90
+ * open until dispatch's promise settles (we ack with `{ ok: true }`).
91
+ */
92
+ declare function bridgeRuntime<S, M extends {
93
+ type: string;
94
+ }, V = S>(runtime: Runtime<S, M>, { channel, serialize, historySize }: BridgeRuntimeOpts<S, V>): () => void;
95
+ interface BridgeClient<S, M> {
96
+ /** Send the hydrate request; resolves with the initial snapshot (or null
97
+ * if the host is still booting OR the response failed to parse). Also
98
+ * primes `getSnapshot()` so React's `useSyncExternalStore` sees the
99
+ * hydrated value on the next render. */
100
+ hydrate(): Promise<S | null>;
101
+ /** Send a dispatch envelope; resolves once the host's dispatch settles. */
102
+ dispatch(msg: M): Promise<void>;
103
+ /** Subscribe to broadcasts. Returns a cleanup function. Broadcasts whose
104
+ * payload fails `parseState` (or, if provided, `parseMsg`) are dropped
105
+ * silently — the listener is not called. Each successful broadcast
106
+ * updates `getSnapshot()`. */
107
+ subscribe(listener: (msg: M | null, state: S) => void): () => void;
108
+ /** Synchronous accessor for the latest known state, or `null` until the
109
+ * first successful hydrate or broadcast lands. Required reference stability
110
+ * for `useSyncExternalStore`: returns the SAME object reference when the
111
+ * underlying state hasn't changed since the prior call. */
112
+ getSnapshot(): S | null;
113
+ }
114
+ interface BridgeClientOpts<S, M> {
115
+ channel: string;
116
+ /**
117
+ * REQUIRED parse for inbound `state` payloads (from `:hydrate` reply AND
118
+ * every broadcast). Chrome runtime messages are an `unknown` boundary
119
+ * (Inv 8) — the surface is the only party that knows the domain type `S`,
120
+ * so it owns the boundary parse. Returning `null` drops the payload
121
+ * silently (subscriber not called; hydrate resolves to `null`). The
122
+ * parser MUST NOT throw — model rejection as `null`.
123
+ */
124
+ parseState: (raw: unknown) => S | null;
125
+ /**
126
+ * Optional parse for inbound `msg` payloads on broadcasts. Defaults to a
127
+ * pass-through cast — most surfaces only need to gate on state and treat
128
+ * the msg union as advisory. Provide this when malformed msgs must be
129
+ * dropped (e.g. when the host's M is a real wire enum, not a local type).
130
+ * Returning `null` drops the broadcast silently.
131
+ */
132
+ parseMsg?: (raw: unknown) => M | null;
133
+ }
134
+ declare function bridgeClient<S, M extends {
135
+ type: string;
136
+ }>({ channel, parseState, parseMsg, }: BridgeClientOpts<S, M>): BridgeClient<S, M>;
137
+ interface BridgeTabClientOpts<S, M> {
138
+ tabId: number;
139
+ channel: string;
140
+ /**
141
+ * REQUIRED parse for inbound `state` payloads. See `BridgeClientOpts.parseState`
142
+ * — same contract, same rationale. Chrome runtime messages from a tab are
143
+ * an `unknown` boundary; the SW client owns the parse.
144
+ */
145
+ parseState: (raw: unknown) => S | null;
146
+ /**
147
+ * Optional parse for inbound `msg` payloads. See
148
+ * `BridgeClientOpts.parseMsg`.
149
+ */
150
+ parseMsg?: (raw: unknown) => M | null;
151
+ }
152
+ /**
153
+ * SW-side client of a `bridgeRuntime` hosted in a content script.
154
+ *
155
+ * Identical contract to `bridgeClient` (same `BridgeClient<S, M>` shape).
156
+ * Differences are confined to transport:
157
+ *
158
+ * - `hydrate()` / `dispatch(msg)` send via `chrome.tabs.sendMessage(tabId, …)`.
159
+ * If the tab is gone, `chrome.tabs.sendMessage` rejects (same shape as
160
+ * `chrome.runtime.sendMessage` with no receiver) — the promise propagates
161
+ * the rejection. Callers decide whether to retry, drop, or surface; the
162
+ * bridge has no opinion. Fire-and-forget posture matches `bridgeRuntime`.
163
+ * - `subscribe(listener)` registers a `chrome.runtime.onMessage` listener
164
+ * filtered by both `envelope.type === channel` AND `sender.tab?.id === tabId`,
165
+ * so broadcasts from other tabs are ignored (no cross-contamination across
166
+ * per-tab content-script runtimes).
167
+ */
168
+ declare function bridgeTabClient<S, M extends {
169
+ type: string;
170
+ }>({ tabId, channel, parseState, parseMsg, }: BridgeTabClientOpts<S, M>): BridgeClient<S, M>;
171
+
172
+ /**
173
+ * @demlik/tea/extension — Chrome service-worker host adapter for @demlik/tea.
174
+ *
175
+ * Substrate-level companion to @demlik/tea/do (Durable Objects) and
176
+ * @demlik/tea/react (React). Lets a chrome service worker run a @demlik/tea
177
+ * machine with persistence backed by chrome.storage.
178
+ *
179
+ * Single export: chromeStorageStore<S>(key, area?). Apps register chrome.*
180
+ * listeners themselves and call runtime.dispatch from inside the handler —
181
+ * those wiring patterns are app-specific (different extensions register
182
+ * different listener sets, with different Msg adapters), so we don't
183
+ * abstract them here until the shape stabilizes across consumers.
184
+ */
185
+
186
+ /**
187
+ * `Store<S>` backed by `chrome.storage.local` (or any compatible
188
+ * `StorageArea`). JSON.stringify on save, JSON.parse on load. Returns `null`
189
+ * (typed as `unknown` per `Store<S>.load` contract) when the key is absent.
190
+ *
191
+ * Throws on the structural layer if the stored value is malformed (non-string
192
+ * at the key, or unparseable JSON) — matches @demlik/tea/do's "throw at boot via
193
+ * store.load" contract so tea's `run()` surfaces it predictably. The
194
+ * `parse` callback handles the semantic layer (shape mismatch → return
195
+ * `null`); it must NOT throw, per the `Store<S>.migrate` contract.
196
+ *
197
+ * `parse` is REQUIRED because chrome.storage is a real serialization boundary
198
+ * — JSON to disk, then back — and the structural type of what comes back is
199
+ * `unknown`. The caller (extension code that knows `S`) is the only party
200
+ * with enough information to write the parse. There is no honest default.
201
+ *
202
+ * `area` defaults to `chrome.storage.local` for production. Tests pass a
203
+ * `fakeChrome().storage.local` in.
204
+ */
205
+ declare function chromeStorageStore<S>(key: string, parse: (raw: unknown) => S | null, area?: chrome.storage.StorageArea): Store<S>;
206
+
207
+ export { type BridgeClient, type BridgeClientOpts, type BridgeRuntimeOpts, type BridgeTabClientOpts, bridgeClient, bridgeRuntime, bridgeTabClient, chromeStorageStore };
@@ -0,0 +1,26 @@
1
+ export { bridgeClient, bridgeRuntime, bridgeTabClient } from '../chunk-PYOCUBQJ.js';
2
+ import '../chunk-CMG2KMWX.js';
3
+
4
+ // src/extension/index.ts
5
+ var MALFORMED = "chromeStorageStore: stored value is not a string";
6
+ function chromeStorageStore(key, parse, area = chrome.storage.local) {
7
+ return {
8
+ async load() {
9
+ const result = await area.get(key);
10
+ const raw = result[key];
11
+ if (raw === void 0 || raw === null) return null;
12
+ if (typeof raw !== "string") throw new Error(`${MALFORMED} at key "${key}"`);
13
+ return JSON.parse(raw);
14
+ },
15
+ async save(state) {
16
+ await area.set({ [key]: JSON.stringify(state) });
17
+ },
18
+ migrate(raw) {
19
+ return parse(raw);
20
+ }
21
+ };
22
+ }
23
+
24
+ export { chromeStorageStore };
25
+ //# sourceMappingURL=index.js.map
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/extension/index.ts"],"names":[],"mappings":";;;;AA6BA,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,EAAU,MAAM,IAAI,MAAM,CAAA,EAAG,SAAS,CAAA,SAAA,EAAY,GAAG,CAAA,CAAA,CAAG,CAAA;AAK3E,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 { bridgeClient, bridgeRuntime, bridgeTabClient } 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\") 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"]}
@@ -0,0 +1,121 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ /**
4
+ * React adapters for the background TEA runtime.
5
+ *
6
+ * Two layers, both built on the same `bridgeClient`:
7
+ *
8
+ * 1. `useBackgroundRuntime(opts)` — direct hook. One bridge client per
9
+ * call. Surfaces that only have a single consumer mount it inline.
10
+ *
11
+ * 2. `createBackgroundRuntimeContext<S, M>()` → `{ Provider, useDispatch,
12
+ * useState, useRuntime }` — factory-style React context. Mounts the
13
+ * bridge client ONCE inside the Provider; child hooks read `state` /
14
+ * `dispatch` from context. Surfaces that drill `dispatch` through
15
+ * multiple component layers, or mount `useBackgroundRuntime` more
16
+ * than once for the same channel, should use this instead.
17
+ *
18
+ * Why a factory (not a single global context):
19
+ * - Type safety: each app types it against its own `S` / `M`.
20
+ * - Tests can mount fresh contexts per test (no module-level state to
21
+ * reset).
22
+ * - Multiple-runtimes case: an app COULD theoretically have two
23
+ * background-shaped runtimes (e.g. one per channel); the factory
24
+ * pattern doesn't preclude it.
25
+ *
26
+ * Hook lifecycle (`useBackgroundRuntime`) — preserved from the prior
27
+ * single-export module:
28
+ * 1. `useSyncExternalStore` subscribes to broadcasts via
29
+ * `bridgeClient.subscribe`, reading the latest state via
30
+ * `bridgeClient.getSnapshot`. Reference stability is owned by the
31
+ * bridge — `getSnapshot` returns the SAME `S` until a new broadcast
32
+ * overwrites it, so React doesn't loop.
33
+ * 2. A one-shot `hydrate()` call seeds `latestState` before the first
34
+ * broadcast arrives. The hydrate side-effect runs in an effect so the
35
+ * first render shows `null` and the subsequent render flips to the
36
+ * hydrated snapshot (or stays null if the host is still booting).
37
+ * 3. `dispatch(msg)` sends `:dispatch` envelopes.
38
+ *
39
+ * On unmount: `useSyncExternalStore` removes the listener; the host's
40
+ * bridge stays installed.
41
+ *
42
+ * Canon §3.1: React adapters use `useSyncExternalStore`, not
43
+ * `useState + useEffect`. `useMachine` and `useRuntime` already follow this
44
+ * — `useBackgroundRuntime` (and by extension the Provider) does too. One
45
+ * Way to Do Each Thing.
46
+ *
47
+ * NOTE: This is a separate entry from the main `index.ts` so apps that
48
+ * never need React don't drag react into their type graph. The React
49
+ * adapter peer-depends on react (you must have it installed in the app).
50
+ */
51
+
52
+ interface UseBackgroundRuntimeOpts<S> {
53
+ channel: string;
54
+ /**
55
+ * REQUIRED parse for inbound `state` payloads. Forwarded to
56
+ * `bridgeClient` — see `BridgeClientOpts.parseState`. Surfaces own this
57
+ * because the chrome.runtime boundary is `unknown` (Inv 8). Returning
58
+ * `null` drops the payload silently.
59
+ */
60
+ parseState: (raw: unknown) => S | null;
61
+ }
62
+ interface UseBackgroundRuntimeResult<S, M> {
63
+ /** Latest snapshot from the host. `null` until the first hydrate response
64
+ * or the first broadcast arrives. Surfaces SHOULD render a placeholder
65
+ * for the null window. */
66
+ state: S | null;
67
+ /** Send a msg to the host runtime. Resolves once the host's dispatch
68
+ * settles. */
69
+ dispatch(msg: M): Promise<void>;
70
+ }
71
+ declare function useBackgroundRuntime<S, M extends {
72
+ type: string;
73
+ }>({ channel, parseState, }: UseBackgroundRuntimeOpts<S>): UseBackgroundRuntimeResult<S, M>;
74
+ interface BackgroundRuntimeContext<S, M> {
75
+ /**
76
+ * Wrap a subtree to share a single background-runtime bridge client.
77
+ * `opts` is forwarded verbatim to `useBackgroundRuntime` — same channel,
78
+ * same parse contract.
79
+ */
80
+ Provider: (props: {
81
+ opts: UseBackgroundRuntimeOpts<S>;
82
+ children: ReactNode;
83
+ }) => ReactNode;
84
+ /**
85
+ * Returns the dispatch function bound to this Provider's bridge client.
86
+ * Reference is stable across renders (per `useBackgroundRuntime`'s
87
+ * `useCallback`), so passing it to memoized children won't churn them.
88
+ */
89
+ useDispatch: () => (msg: M) => Promise<void>;
90
+ /**
91
+ * Returns a slice of the latest state. `selector` defaults to identity
92
+ * (returns the whole `S | null` snapshot). Selectors run on every state
93
+ * change — surfaces with expensive transforms should memoize the
94
+ * selector at call site.
95
+ *
96
+ * Returns `null` until the first hydrate / broadcast lands when the
97
+ * selector is identity; for non-identity selectors the return type is
98
+ * `T` and the caller's selector decides how to handle the pre-hydrate
99
+ * `null` snapshot.
100
+ */
101
+ useState: {
102
+ (): S | null;
103
+ <T>(selector: (state: S | null) => T): T;
104
+ };
105
+ /**
106
+ * Escape hatch — returns the full `{ state, dispatch }` pair as-is.
107
+ * Useful for consumers that want both the snapshot and the dispatch
108
+ * fn in a single hook call (e.g. effects that read state THEN
109
+ * dispatch in response).
110
+ *
111
+ * Not a `@demlik/tea` `Runtime<S, M>` — the bridge surface is a thinner
112
+ * client. This hook returns the same shape as `useBackgroundRuntime`,
113
+ * just sourced from context instead of mounting a fresh client.
114
+ */
115
+ useRuntime: () => UseBackgroundRuntimeResult<S, M>;
116
+ }
117
+ declare function createBackgroundRuntimeContext<S, M extends {
118
+ type: string;
119
+ }>(): BackgroundRuntimeContext<S, M>;
120
+
121
+ export { type BackgroundRuntimeContext, type UseBackgroundRuntimeOpts, type UseBackgroundRuntimeResult, createBackgroundRuntimeContext, useBackgroundRuntime };
@@ -0,0 +1,69 @@
1
+ import { bridgeClient } from '../chunk-PYOCUBQJ.js';
2
+ import '../chunk-CMG2KMWX.js';
3
+ import { useRef, useSyncExternalStore, useEffect, useCallback, createContext, useMemo, useContext } from 'react';
4
+ import { jsx } from 'react/jsx-runtime';
5
+
6
+ function useBackgroundRuntime({
7
+ channel,
8
+ parseState
9
+ }) {
10
+ const clientRef = useRef(null);
11
+ if (clientRef.current === null) {
12
+ clientRef.current = bridgeClient({ channel, parseState });
13
+ }
14
+ const client = clientRef.current;
15
+ const state = useSyncExternalStore(
16
+ client.subscribe,
17
+ client.getSnapshot,
18
+ client.getSnapshot
19
+ );
20
+ useEffect(() => {
21
+ client.hydrate().catch(() => {
22
+ });
23
+ }, [client]);
24
+ const dispatch = useCallback(
25
+ async (msg) => {
26
+ await client.dispatch(msg);
27
+ },
28
+ [client]
29
+ );
30
+ return { state, dispatch };
31
+ }
32
+ var PROVIDER_ERROR = "createBackgroundRuntimeContext: hook used outside its Provider. Wrap the consumer in <BackgroundRuntime.Provider opts={...}>.";
33
+ function createBackgroundRuntimeContext() {
34
+ const Ctx = createContext(null);
35
+ function Provider({
36
+ opts,
37
+ children
38
+ }) {
39
+ const result = useBackgroundRuntime(opts);
40
+ const value = useMemo(
41
+ () => ({ state: result.state, dispatch: result.dispatch }),
42
+ [result.state, result.dispatch]
43
+ );
44
+ return /* @__PURE__ */ jsx(Ctx.Provider, { value, children });
45
+ }
46
+ function useRuntime() {
47
+ const ctx = useContext(Ctx);
48
+ if (ctx === null) throw new Error(PROVIDER_ERROR);
49
+ return ctx;
50
+ }
51
+ function useDispatch() {
52
+ return useRuntime().dispatch;
53
+ }
54
+ function useStateHook(selector) {
55
+ const { state } = useRuntime();
56
+ if (selector === void 0) return state;
57
+ return selector(state);
58
+ }
59
+ return {
60
+ Provider,
61
+ useDispatch,
62
+ useState: useStateHook,
63
+ useRuntime
64
+ };
65
+ }
66
+
67
+ export { createBackgroundRuntimeContext, useBackgroundRuntime };
68
+ //# sourceMappingURL=react.js.map
69
+ //# sourceMappingURL=react.js.map