@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.
Files changed (238) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/index.d.ts +907 -0
  3. package/dist/agent/index.js +20 -0
  4. package/dist/agent/index.js.map +1 -0
  5. package/dist/authed-call/index.d.ts +119 -0
  6. package/dist/authed-call/index.js +148 -0
  7. package/dist/authed-call/index.js.map +1 -0
  8. package/dist/await-terminal/index.d.ts +99 -0
  9. package/dist/await-terminal/index.js +75 -0
  10. package/dist/await-terminal/index.js.map +1 -0
  11. package/dist/batch-window/index.d.ts +259 -0
  12. package/dist/batch-window/index.js +43 -0
  13. package/dist/batch-window/index.js.map +1 -0
  14. package/dist/cache/index.d.ts +2 -2
  15. package/dist/cache/index.js +2 -50
  16. package/dist/cache/index.js.map +1 -1
  17. package/dist/{chunk-CMG2KMWX.js → chunk-2WZHS4P2.js} +220 -177
  18. package/dist/chunk-2WZHS4P2.js.map +1 -0
  19. package/dist/chunk-2YOE5PIS.js +45 -0
  20. package/dist/chunk-2YOE5PIS.js.map +1 -0
  21. package/dist/chunk-3545BOYL.js +65 -0
  22. package/dist/chunk-3545BOYL.js.map +1 -0
  23. package/dist/chunk-5VNWZADP.js +49 -0
  24. package/dist/chunk-5VNWZADP.js.map +1 -0
  25. package/dist/{chunk-ZRUST6BK.js → chunk-7FYQGDMF.js} +2 -2
  26. package/dist/chunk-7FYQGDMF.js.map +1 -0
  27. package/dist/chunk-7VEG4KHB.js +202 -0
  28. package/dist/chunk-7VEG4KHB.js.map +1 -0
  29. package/dist/{chunk-PYBQREFU.js → chunk-A56GNPI7.js} +20 -6
  30. package/dist/chunk-A56GNPI7.js.map +1 -0
  31. package/dist/chunk-BNJYE4AU.js +157 -0
  32. package/dist/chunk-BNJYE4AU.js.map +1 -0
  33. package/dist/chunk-BYQUGDD3.js +16 -0
  34. package/dist/chunk-BYQUGDD3.js.map +1 -0
  35. package/dist/chunk-D5EQDLDV.js +186 -0
  36. package/dist/chunk-D5EQDLDV.js.map +1 -0
  37. package/dist/chunk-EDGDR5XX.js +60 -0
  38. package/dist/chunk-EDGDR5XX.js.map +1 -0
  39. package/dist/chunk-EIZJTYKR.js +93 -0
  40. package/dist/chunk-EIZJTYKR.js.map +1 -0
  41. package/dist/chunk-EQRUAP2A.js +56 -0
  42. package/dist/chunk-EQRUAP2A.js.map +1 -0
  43. package/dist/chunk-FH5RUIIL.js +61 -0
  44. package/dist/chunk-FH5RUIIL.js.map +1 -0
  45. package/dist/chunk-HJO6VNQI.js +15 -0
  46. package/dist/chunk-HJO6VNQI.js.map +1 -0
  47. package/dist/chunk-IFBQJYGI.js +132 -0
  48. package/dist/chunk-IFBQJYGI.js.map +1 -0
  49. package/dist/chunk-IN33Q4VJ.js +52 -0
  50. package/dist/chunk-IN33Q4VJ.js.map +1 -0
  51. package/dist/chunk-JELSZN25.js +47 -0
  52. package/dist/chunk-JELSZN25.js.map +1 -0
  53. package/dist/chunk-K575PT4S.js +58 -0
  54. package/dist/chunk-K575PT4S.js.map +1 -0
  55. package/dist/chunk-LEC4U6CX.js +91 -0
  56. package/dist/chunk-LEC4U6CX.js.map +1 -0
  57. package/dist/{chunk-Y5JM3UUH.js → chunk-NSDFA6M6.js} +75 -3
  58. package/dist/chunk-NSDFA6M6.js.map +1 -0
  59. package/dist/chunk-QGZW4W3E.js +20 -0
  60. package/dist/chunk-QGZW4W3E.js.map +1 -0
  61. package/dist/{chunk-PYOCUBQJ.js → chunk-RGTPPF4U.js} +22 -15
  62. package/dist/chunk-RGTPPF4U.js.map +1 -0
  63. package/dist/chunk-RVJFGRGK.js +222 -0
  64. package/dist/chunk-RVJFGRGK.js.map +1 -0
  65. package/dist/chunk-RVU3RBE6.js +31 -0
  66. package/dist/chunk-RVU3RBE6.js.map +1 -0
  67. package/dist/chunk-SC3FAFZN.js +53 -0
  68. package/dist/chunk-SC3FAFZN.js.map +1 -0
  69. package/dist/chunk-SINXE47Y.js +55 -0
  70. package/dist/chunk-SINXE47Y.js.map +1 -0
  71. package/dist/chunk-TAO4YOLO.js +87 -0
  72. package/dist/chunk-TAO4YOLO.js.map +1 -0
  73. package/dist/chunk-TC4HWABA.js +506 -0
  74. package/dist/chunk-TC4HWABA.js.map +1 -0
  75. package/dist/chunk-TGG26XMK.js +52 -0
  76. package/dist/chunk-TGG26XMK.js.map +1 -0
  77. package/dist/chunk-UV6UWGEO.js +147 -0
  78. package/dist/chunk-UV6UWGEO.js.map +1 -0
  79. package/dist/chunk-UYWUVEP6.js +158 -0
  80. package/dist/chunk-UYWUVEP6.js.map +1 -0
  81. package/dist/chunk-WUV3PSCZ.js +54 -0
  82. package/dist/chunk-WUV3PSCZ.js.map +1 -0
  83. package/dist/chunk-XRXKH7KA.js +268 -0
  84. package/dist/chunk-XRXKH7KA.js.map +1 -0
  85. package/dist/circuit-breaker/index.js +1 -51
  86. package/dist/circuit-breaker/index.js.map +1 -1
  87. package/dist/core-CymXZCeY.d.ts +178 -0
  88. package/dist/deadline/index.d.ts +24 -5
  89. package/dist/deadline/index.js +4 -18
  90. package/dist/deadline/index.js.map +1 -1
  91. package/dist/debounce/index.js +1 -54
  92. package/dist/debounce/index.js.map +1 -1
  93. package/dist/devtools/index.d.ts +1 -1
  94. package/dist/devtools/index.js +18 -6
  95. package/dist/devtools/index.js.map +1 -1
  96. package/dist/do/index.d.ts +1732 -92
  97. package/dist/do/index.js +677 -31
  98. package/dist/do/index.js.map +1 -1
  99. package/dist/durable-effects-2MDxDs45.d.ts +180 -0
  100. package/dist/extension/index.d.ts +31 -13
  101. package/dist/extension/index.js +5 -3
  102. package/dist/extension/index.js.map +1 -1
  103. package/dist/extension/react.js +8 -3
  104. package/dist/extension/react.js.map +1 -1
  105. package/dist/extension/subs/index.d.ts +2 -2
  106. package/dist/extension/subs/index.js.map +1 -1
  107. package/dist/extension/test-utils.js +11 -3
  108. package/dist/extension/test-utils.js.map +1 -1
  109. package/dist/fan-out/index.d.ts +239 -0
  110. package/dist/fan-out/index.js +5 -0
  111. package/dist/fan-out/index.js.map +1 -0
  112. package/dist/idempotency/adapter.d.ts +62 -0
  113. package/dist/idempotency/adapter.js +4 -0
  114. package/dist/idempotency/adapter.js.map +1 -0
  115. package/dist/idempotency/index.js +1 -56
  116. package/dist/idempotency/index.js.map +1 -1
  117. package/dist/idempotent-intake/index.d.ts +186 -0
  118. package/dist/idempotent-intake/index.js +7 -0
  119. package/dist/idempotent-intake/index.js.map +1 -0
  120. package/dist/index.d.ts +341 -191
  121. package/dist/index.js +2 -1
  122. package/dist/llm-call/index.d.ts +309 -0
  123. package/dist/llm-call/index.js +14 -0
  124. package/dist/llm-call/index.js.map +1 -0
  125. package/dist/machine-viz/index.d.ts +1 -1
  126. package/dist/machine-viz/index.js +1 -1
  127. package/dist/machine-viz/index.js.map +1 -1
  128. package/dist/mem/index.d.ts +1 -0
  129. package/dist/monitored-run/index.d.ts +253 -0
  130. package/dist/monitored-run/index.js +10 -0
  131. package/dist/monitored-run/index.js.map +1 -0
  132. package/dist/node/index.d.ts +5 -5
  133. package/dist/node/index.js.map +1 -1
  134. package/dist/paginated-walk/index.d.ts +234 -0
  135. package/dist/paginated-walk/index.js +15 -0
  136. package/dist/paginated-walk/index.js.map +1 -0
  137. package/dist/paginator/index.d.ts +271 -0
  138. package/dist/paginator/index.js +3 -0
  139. package/dist/paginator/index.js.map +1 -0
  140. package/dist/parity/index.d.ts +86 -0
  141. package/dist/parity/index.js +77 -0
  142. package/dist/parity/index.js.map +1 -0
  143. package/dist/pbt/arbitraries/index.d.ts +1 -1
  144. package/dist/pbt/arbitraries/index.js +1 -1
  145. package/dist/pbt/index.d.ts +1 -1
  146. package/dist/pbt/index.js +5 -4
  147. package/dist/pbt/index.js.map +1 -1
  148. package/dist/pbt/runners/index.d.ts +1 -1
  149. package/dist/pbt/runners/index.js +3 -2
  150. package/dist/poller/index.d.ts +288 -0
  151. package/dist/poller/index.js +79 -0
  152. package/dist/poller/index.js.map +1 -0
  153. package/dist/prediction/index.d.ts +109 -0
  154. package/dist/prediction/index.js +4 -0
  155. package/dist/prediction/index.js.map +1 -0
  156. package/dist/protocol-B5YmJr0d.d.ts +45 -0
  157. package/dist/pure/index.d.ts +2 -0
  158. package/dist/pure/index.js +4 -0
  159. package/dist/pure/index.js.map +1 -0
  160. package/dist/raft/demo.d.ts +105 -0
  161. package/dist/raft/demo.js +172 -0
  162. package/dist/raft/demo.js.map +1 -0
  163. package/dist/raft/index.d.ts +548 -0
  164. package/dist/raft/index.js +7 -0
  165. package/dist/raft/index.js.map +1 -0
  166. package/dist/raft/sim.d.ts +222 -0
  167. package/dist/raft/sim.js +8 -0
  168. package/dist/raft/sim.js.map +1 -0
  169. package/dist/raft/timeline.d.ts +46 -0
  170. package/dist/raft/timeline.js +62 -0
  171. package/dist/raft/timeline.js.map +1 -0
  172. package/dist/rate-limit/index.js +1 -43
  173. package/dist/rate-limit/index.js.map +1 -1
  174. package/dist/react/index.d.ts +2 -1
  175. package/dist/react/index.js +39 -20
  176. package/dist/react/index.js.map +1 -1
  177. package/dist/reconciler/index.d.ts +192 -0
  178. package/dist/reconciler/index.js +166 -0
  179. package/dist/reconciler/index.js.map +1 -0
  180. package/dist/recorder/index.d.ts +7 -3
  181. package/dist/recorder/index.js +1 -155
  182. package/dist/recorder/index.js.map +1 -1
  183. package/dist/resilient-call/index.d.ts +251 -0
  184. package/dist/resilient-call/index.js +13 -0
  185. package/dist/resilient-call/index.js.map +1 -0
  186. package/dist/retry-backoff/index.d.ts +42 -5
  187. package/dist/retry-backoff/index.js +1 -35
  188. package/dist/retry-backoff/index.js.map +1 -1
  189. package/dist/saga/index.d.ts +225 -0
  190. package/dist/saga/index.js +150 -0
  191. package/dist/saga/index.js.map +1 -0
  192. package/dist/snapshot/index.d.ts +334 -0
  193. package/dist/snapshot/index.js +5 -0
  194. package/dist/snapshot/index.js.map +1 -0
  195. package/dist/subs/index.d.ts +48 -10
  196. package/dist/subs/index.js +1 -1
  197. package/dist/testing/index.d.ts +107 -2
  198. package/dist/testing/index.js +204 -6
  199. package/dist/testing/index.js.map +1 -1
  200. package/dist/throttle/index.js +1 -52
  201. package/dist/throttle/index.js.map +1 -1
  202. package/dist/throttled-input/index.d.ts +372 -0
  203. package/dist/throttled-input/index.js +88 -0
  204. package/dist/throttled-input/index.js.map +1 -0
  205. package/dist/token-refresh/index.d.ts +215 -0
  206. package/dist/token-refresh/index.js +6 -0
  207. package/dist/token-refresh/index.js.map +1 -0
  208. package/dist/trace-replay/index.d.ts +11 -2
  209. package/dist/trace-replay/index.js +3 -55
  210. package/dist/trace-replay/index.js.map +1 -1
  211. package/dist/{types-CwBC5wMn.d.ts → types-N9LZbdnr.d.ts} +1 -1
  212. package/dist/with-deadline/index.d.ts +196 -0
  213. package/dist/with-deadline/index.js +130 -0
  214. package/dist/with-deadline/index.js.map +1 -0
  215. package/dist/with-resilience/index.d.ts +205 -0
  216. package/dist/with-resilience/index.js +274 -0
  217. package/dist/with-resilience/index.js.map +1 -0
  218. package/dist/with-telemetry/index.d.ts +149 -0
  219. package/dist/with-telemetry/index.js +95 -0
  220. package/dist/with-telemetry/index.js.map +1 -0
  221. package/dist/work-queue/adapter.d.ts +82 -0
  222. package/dist/work-queue/adapter.js +4 -0
  223. package/dist/work-queue/adapter.js.map +1 -0
  224. package/dist/work-queue/index.d.ts +25 -11
  225. package/dist/work-queue/index.js +1 -46
  226. package/dist/work-queue/index.js.map +1 -1
  227. package/dist/work-queue/ops.d.ts +69 -0
  228. package/dist/work-queue/ops.js +3 -0
  229. package/dist/work-queue/ops.js.map +1 -0
  230. package/dist/workflow/index.d.ts +444 -0
  231. package/dist/workflow/index.js +225 -0
  232. package/dist/workflow/index.js.map +1 -0
  233. package/package.json +162 -25
  234. package/dist/chunk-CMG2KMWX.js.map +0 -1
  235. package/dist/chunk-PYBQREFU.js.map +0 -1
  236. package/dist/chunk-PYOCUBQJ.js.map +0 -1
  237. package/dist/chunk-Y5JM3UUH.js.map +0 -1
  238. package/dist/chunk-ZRUST6BK.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/extension/test-utils.ts"],"names":[],"mappings":"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","file":"test-utils.js","sourcesContent":["/**\n * In-memory `chrome.*` mock for tests in this package and any downstream\n * consumer that wants to test against `chromeStorageStore` behavior without\n * a real chrome environment.\n *\n * Currently exposes `chrome.storage.local`, `chrome.runtime` (sendMessage +\n * onMessage), and `chrome.tabs` (sendMessage). The `runtime` and `tabs`\n * surfaces are used by the runtime-bridge tests (`bridge.ts`) to round-trip\n * envelopes between a host runtime and a surface client (sw↔surface) and\n * between a sw client and a per-tab content-script runtime (sw↔content).\n *\n * Extend additional surfaces (alarms, windows, …) only when real consumer\n * tests reach for them — earn each surface through a concrete need.\n */\n\ntype RuntimeListener = (\n message: unknown,\n sender: chrome.runtime.MessageSender,\n sendResponse: (response: unknown) => void,\n) => boolean | undefined | void;\n\ntype StorageChangeListener = (\n changes: Record<string, chrome.storage.StorageChange>,\n areaName: chrome.storage.AreaName,\n) => void;\n\ntype AlarmListener = (alarm: chrome.alarms.Alarm) => void;\n\ntype TabActivatedListener = (info: chrome.tabs.OnActivatedInfo) => void;\ntype TabUpdatedListener = (\n tabId: number,\n changeInfo: chrome.tabs.OnUpdatedInfo,\n tab: chrome.tabs.Tab,\n) => void;\ntype TabRemovedListener = (tabId: number, removeInfo: chrome.tabs.OnRemovedInfo) => void;\n\nexport interface FakeChrome {\n storage: {\n local: chrome.storage.StorageArea;\n /**\n * Top-level `chrome.storage.onChanged` event. Fires for ANY area; consumer\n * filters by the `areaName` arg. Real chrome semantics: every `set` /\n * `remove` / `clear` that materially changes a key emits a single\n * `changes` map keyed by the changed keys.\n */\n onChanged: {\n addListener(listener: StorageChangeListener): void;\n removeListener(listener: StorageChangeListener): void;\n hasListener(listener: StorageChangeListener): boolean;\n hasListeners(): boolean;\n };\n };\n // Subset of `typeof chrome.runtime` covered by the fake — chrome's\n // ambient typings don't export a single `Runtime` interface, so we\n // declare the surface inline. Extend when tests reach for more.\n runtime: {\n sendMessage: typeof chrome.runtime.sendMessage;\n onMessage: typeof chrome.runtime.onMessage;\n };\n tabs: {\n sendMessage: typeof chrome.tabs.sendMessage;\n /**\n * `chrome.tabs.onActivated` — fires when the active tab in a window\n * changes. The fake only models the listener registry; tests drive\n * fires via `__test.fireTabActivated(info)`.\n */\n onActivated: {\n addListener(listener: TabActivatedListener): void;\n removeListener(listener: TabActivatedListener): void;\n hasListener(listener: TabActivatedListener): boolean;\n hasListeners(): boolean;\n };\n /**\n * `chrome.tabs.onUpdated` — fires when a tab is updated (url, title,\n * status, etc.). Tests drive fires via `__test.fireTabUpdated(...)`.\n */\n onUpdated: {\n addListener(listener: TabUpdatedListener): void;\n removeListener(listener: TabUpdatedListener): void;\n hasListener(listener: TabUpdatedListener): boolean;\n hasListeners(): boolean;\n };\n /**\n * `chrome.tabs.onRemoved` — fires when a tab is closed. Tests drive\n * fires via `__test.fireTabRemoved(...)`.\n */\n onRemoved: {\n addListener(listener: TabRemovedListener): void;\n removeListener(listener: TabRemovedListener): void;\n hasListener(listener: TabRemovedListener): boolean;\n hasListeners(): boolean;\n };\n };\n /**\n * `chrome.alarms` — the MV3 periodic timer primitive. The fake tracks\n * created alarms in a Map keyed by name, models `create` (dedupe by\n * name) + `clear` (remove from map), and exposes `onAlarm` as a\n * standard listener registry. Tests drive fires via\n * `__test.fireAlarm(name)`.\n */\n alarms: {\n create(name: string, alarmInfo: chrome.alarms.AlarmCreateInfo): void;\n /**\n * `chrome.alarms.get(name)` — resolves to the Alarm shape Chrome would\n * return (with `scheduledTime` synthesized at lookup time), or\n * undefined if no alarm with that name exists. Mirrors the contract\n * the `ensureQueueAlarm` SW-module-top-level helper checks (\"is this\n * alarm already present with the right period?\") — see\n * `apps/extension/extension/entrypoints/background.ts`.\n */\n get(name: string): Promise<chrome.alarms.Alarm | undefined>;\n clear(name: string): Promise<boolean>;\n onAlarm: {\n addListener(listener: AlarmListener): void;\n removeListener(listener: AlarmListener): void;\n hasListener(listener: AlarmListener): boolean;\n hasListeners(): boolean;\n };\n };\n /**\n * Test helpers — not part of the real chrome surface. Lets tests\n * simulate a content script issuing `chrome.runtime.sendMessage` from\n * inside a specific tab (chrome auto-tags `sender.tab.id` in that\n * direction) and remove a tab so `chrome.tabs.sendMessage` rejects\n * the way real chrome does when the receiver is gone.\n */\n __test: {\n /** Simulate a content-script broadcast tagged with `sender.tab.id`. */\n sendFromTab(tabId: number, message: unknown): Promise<unknown>;\n /** Mark a tab as gone — `chrome.tabs.sendMessage(tabId, …)` will reject. */\n removeTab(tabId: number): void;\n /** Mark a tab as alive (default for any tabId that hasn't been removed). */\n addTab(tabId: number): void;\n /** Fire an alarm — invokes every onAlarm listener with `{ name, ...}`. */\n fireAlarm(name: string): void;\n /** Snapshot the alarm registry — tests assert on created/cleared alarms. */\n getAlarms(): readonly string[];\n /** Fire `chrome.tabs.onActivated` — drives every listener. */\n fireTabActivated(info: chrome.tabs.OnActivatedInfo): void;\n /** Fire `chrome.tabs.onUpdated` — drives every listener. */\n fireTabUpdated(\n tabId: number,\n changeInfo: chrome.tabs.OnUpdatedInfo,\n tab: chrome.tabs.Tab,\n ): void;\n /** Fire `chrome.tabs.onRemoved` — drives every listener. */\n fireTabRemoved(tabId: number, removeInfo: chrome.tabs.OnRemovedInfo): void;\n };\n}\n\nexport function fakeChrome(): FakeChrome {\n const data = new Map<string, unknown>();\n\n // ─── chrome.storage.onChanged — top-level event ───────────────────────\n //\n // Real chrome emits `changes` keyed by every key that actually changed\n // (oldValue / newValue present). Unchanged keys aren't included. The\n // fake mirrors that: build the change map inside set/remove/clear, fire\n // listeners only if the map is non-empty. AreaName is \"local\" here —\n // the fake only models storage.local; if a future test reaches for\n // session/sync, add a per-area fake with its own onChanged.\n const storageListeners = new Set<StorageChangeListener>();\n function deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== typeof b) return false;\n if (typeof a !== \"object\" || a === null || b === null) return false;\n return JSON.stringify(a) === JSON.stringify(b);\n }\n function fireStorageChange(changes: Record<string, chrome.storage.StorageChange>): void {\n if (Object.keys(changes).length === 0) return;\n for (const listener of Array.from(storageListeners)) {\n listener(changes, \"local\");\n }\n }\n\n const partial = {\n async get(\n keys?: string | string[] | Record<string, unknown> | null,\n ): Promise<Record<string, unknown>> {\n // get() → all entries\n // get(\"k\") → { k: v } if present, {} otherwise\n // get([\"k1\",\"k2\"]) → { k: v } for present keys\n // get({ k: default}) → { k: v ?? default } per key\n if (keys === undefined || keys === null) {\n return Object.fromEntries(data);\n }\n if (typeof keys === \"string\") {\n const v = data.get(keys);\n return v === undefined ? {} : { [keys]: v };\n }\n if (Array.isArray(keys)) {\n return Object.fromEntries(keys.flatMap((k) => (data.has(k) ? [[k, data.get(k)]] : [])));\n }\n const out: Record<string, unknown> = {};\n for (const [k, def] of Object.entries(keys)) {\n out[k] = data.has(k) ? data.get(k) : def;\n }\n return out;\n },\n async set(items: Record<string, unknown>): Promise<void> {\n const changes: Record<string, chrome.storage.StorageChange> = {};\n for (const [k, v] of Object.entries(items)) {\n const prev = data.get(k);\n // Real chrome only fires onChanged when the value actually\n // changes — match that to avoid spurious re-renders in tests.\n if (deepEqual(prev, v)) continue;\n const change: chrome.storage.StorageChange = { newValue: v };\n if (data.has(k)) change.oldValue = prev;\n changes[k] = change;\n data.set(k, v);\n }\n fireStorageChange(changes);\n },\n async remove(keys: string | string[]): Promise<void> {\n const arr = Array.isArray(keys) ? keys : [keys];\n const changes: Record<string, chrome.storage.StorageChange> = {};\n for (const k of arr) {\n if (!data.has(k)) continue;\n changes[k] = { oldValue: data.get(k) };\n data.delete(k);\n }\n fireStorageChange(changes);\n },\n async clear(): Promise<void> {\n const changes: Record<string, chrome.storage.StorageChange> = {};\n for (const [k, v] of data) changes[k] = { oldValue: v };\n data.clear();\n fireStorageChange(changes);\n },\n };\n\n // The full `chrome.storage.StorageArea` contract has more surface\n // (getBytesInUse, getKeys, onChanged event, setAccessLevel) than real\n // consumer tests currently exercise. The cast is local to this test util;\n // src/index.ts never casts. If a test needs more surface, implement it\n // here instead of widening the cast.\n const local = partial as unknown as chrome.storage.StorageArea;\n\n // ─── chrome.runtime — sendMessage + onMessage round-trip ───────────────\n //\n // The fake mirrors MV3 semantics that matter for the bridge:\n // - sendMessage delivers to EVERY onMessage listener in arrival order.\n // - The first listener that returns `true` (or calls sendResponse\n // synchronously) \"wins\" — its sendResponse value resolves the\n // sendMessage promise. Listeners that return false/undefined are\n // passed over.\n // - If no listener handles the message, sendMessage rejects with the\n // same shape chrome uses (\"Could not establish connection.\n // Receiving end does not exist.\"). The bridge's broadcast path\n // swallows this.\n const runtimeListeners = new Set<RuntimeListener>();\n\n // Tracks tabs that are \"alive\" from `chrome.tabs.sendMessage`'s point of\n // view. Unknown tabIds are treated as alive by default (matches the\n // common test path: assign a number, send to it). Tests can `removeTab`\n // to simulate disconnection.\n const removedTabs = new Set<number>();\n\n /**\n * Internal dispatch helper. Real chrome distinguishes:\n * - `chrome.runtime.sendMessage(msg)` — sender is the extension origin,\n * `sender.tab` is undefined; all `chrome.runtime.onMessage` listeners\n * hear it.\n * - `chrome.tabs.sendMessage(tabId, msg)` — sender is the extension\n * origin (still no `sender.tab` on the receiving end's view here, but\n * the routing is by tab); only listeners IN that tab receive it. Our\n * fake collapses to the same listener set since tests run a single\n * \"tab\" of listeners at a time. The routing assertion is enforced by\n * callers reading the envelope/tabId pair they sent.\n * - Content-script `chrome.runtime.sendMessage(msg)` (the `sendFromTab`\n * helper) — chrome auto-tags `sender.tab.id`. SW-side listeners use\n * this to filter cross-tab broadcasts.\n *\n * `sender` controls which sender object the listeners see.\n */\n const dispatch = (message: unknown, sender: chrome.runtime.MessageSender): Promise<unknown> => {\n return new Promise((resolve, reject) => {\n let responded = false;\n const sendResponse = (response: unknown): void => {\n if (responded) return;\n responded = true;\n resolve(response);\n };\n const snapshot = Array.from(runtimeListeners);\n let willRespondAsync = false;\n for (const listener of snapshot) {\n const r = listener(message, sender, sendResponse);\n if (r === true) willRespondAsync = true;\n if (responded) return;\n }\n if (willRespondAsync) return;\n reject(new Error(\"Could not establish connection. Receiving end does not exist.\"));\n });\n };\n\n const extensionSender: chrome.runtime.MessageSender = {} as chrome.runtime.MessageSender;\n\n const runtimePartial = {\n sendMessage(message: unknown): Promise<unknown> {\n return dispatch(message, extensionSender);\n },\n onMessage: {\n addListener(listener: RuntimeListener): void {\n runtimeListeners.add(listener);\n },\n removeListener(listener: RuntimeListener): void {\n runtimeListeners.delete(listener);\n },\n hasListener(listener: RuntimeListener): boolean {\n return runtimeListeners.has(listener);\n },\n hasListeners(): boolean {\n return runtimeListeners.size > 0;\n },\n },\n };\n\n const runtime = runtimePartial as unknown as FakeChrome[\"runtime\"];\n\n // ─── chrome.tabs.sendMessage — SW → content-script routing ─────────────\n //\n // Real chrome routes by tabId; the receiving content script's\n // `chrome.runtime.onMessage` fires with `sender` representing the\n // extension (no `sender.tab`). The fake collapses to the shared listener\n // set since tests run one runtime per process, but enforces the\n // \"tab gone\" reject so consumers can verify their fire-and-forget\n // posture.\n const tabsPartial = {\n sendMessage(tabId: number, message: unknown): Promise<unknown> {\n if (removedTabs.has(tabId)) {\n return Promise.reject(\n new Error(\n `Could not establish connection. Receiving end does not exist. (tabId=${tabId})`,\n ),\n );\n }\n return dispatch(message, extensionSender);\n },\n };\n\n // ─── chrome.tabs.on{Activated,Updated,Removed} — listener registries ────\n //\n // Tests drive fires via the __test helpers below. Production-code Sub\n // factories register/unregister; the fake only models the registry +\n // dispatch, not the chrome events themselves (no real tabs to observe).\n const tabActivatedListeners = new Set<TabActivatedListener>();\n const tabUpdatedListeners = new Set<TabUpdatedListener>();\n const tabRemovedListeners = new Set<TabRemovedListener>();\n\n const tabsFull = {\n ...tabsPartial,\n onActivated: {\n addListener(listener: TabActivatedListener): void {\n tabActivatedListeners.add(listener);\n },\n removeListener(listener: TabActivatedListener): void {\n tabActivatedListeners.delete(listener);\n },\n hasListener(listener: TabActivatedListener): boolean {\n return tabActivatedListeners.has(listener);\n },\n hasListeners(): boolean {\n return tabActivatedListeners.size > 0;\n },\n },\n onUpdated: {\n addListener(listener: TabUpdatedListener): void {\n tabUpdatedListeners.add(listener);\n },\n removeListener(listener: TabUpdatedListener): void {\n tabUpdatedListeners.delete(listener);\n },\n hasListener(listener: TabUpdatedListener): boolean {\n return tabUpdatedListeners.has(listener);\n },\n hasListeners(): boolean {\n return tabUpdatedListeners.size > 0;\n },\n },\n onRemoved: {\n addListener(listener: TabRemovedListener): void {\n tabRemovedListeners.add(listener);\n },\n removeListener(listener: TabRemovedListener): void {\n tabRemovedListeners.delete(listener);\n },\n hasListener(listener: TabRemovedListener): boolean {\n return tabRemovedListeners.has(listener);\n },\n hasListeners(): boolean {\n return tabRemovedListeners.size > 0;\n },\n },\n };\n\n const tabs = tabsFull as unknown as FakeChrome[\"tabs\"];\n\n // ─── chrome.alarms — listener registry + name-keyed alarm map ─────────\n //\n // create(name, info) stores `info` keyed by name (dedupe on repeat —\n // matches real chrome's behavior of replacing the prior alarm). clear()\n // removes from the map and resolves to `true` if the alarm existed,\n // `false` otherwise (matches real chrome's contract). onAlarm is a\n // standard listener registry. The fake does NOT auto-fire alarms based\n // on `periodInMinutes` — tests drive fires deterministically via\n // `__test.fireAlarm(name)`.\n const alarmMap = new Map<string, chrome.alarms.AlarmCreateInfo>();\n const alarmListeners = new Set<AlarmListener>();\n\n const alarms: FakeChrome[\"alarms\"] = {\n create(name: string, alarmInfo: chrome.alarms.AlarmCreateInfo): void {\n alarmMap.set(name, alarmInfo);\n },\n async get(name: string): Promise<chrome.alarms.Alarm | undefined> {\n const info = alarmMap.get(name);\n if (info === undefined) return undefined;\n return {\n name,\n scheduledTime: Date.now(),\n periodInMinutes: info.periodInMinutes,\n };\n },\n async clear(name: string): Promise<boolean> {\n return alarmMap.delete(name);\n },\n onAlarm: {\n addListener(listener: AlarmListener): void {\n alarmListeners.add(listener);\n },\n removeListener(listener: AlarmListener): void {\n alarmListeners.delete(listener);\n },\n hasListener(listener: AlarmListener): boolean {\n return alarmListeners.has(listener);\n },\n hasListeners(): boolean {\n return alarmListeners.size > 0;\n },\n },\n };\n\n return {\n storage: {\n local,\n onChanged: {\n addListener(listener: StorageChangeListener): void {\n storageListeners.add(listener);\n },\n removeListener(listener: StorageChangeListener): void {\n storageListeners.delete(listener);\n },\n hasListener(listener: StorageChangeListener): boolean {\n return storageListeners.has(listener);\n },\n hasListeners(): boolean {\n return storageListeners.size > 0;\n },\n },\n },\n runtime,\n tabs,\n alarms,\n __test: {\n sendFromTab(tabId: number, message: unknown): Promise<unknown> {\n const taggedSender = { tab: { id: tabId } } as chrome.runtime.MessageSender;\n return dispatch(message, taggedSender);\n },\n removeTab(tabId: number): void {\n removedTabs.add(tabId);\n },\n addTab(tabId: number): void {\n removedTabs.delete(tabId);\n },\n fireAlarm(name: string): void {\n const info = alarmMap.get(name);\n if (info === undefined) return;\n // Real chrome's Alarm shape carries `name`, `scheduledTime`, and\n // `periodInMinutes`. The fake stamps `scheduledTime` at fire time\n // and forwards the period from the create call.\n const alarm: chrome.alarms.Alarm = {\n name,\n scheduledTime: Date.now(),\n periodInMinutes: info.periodInMinutes,\n };\n for (const listener of Array.from(alarmListeners)) {\n listener(alarm);\n }\n },\n getAlarms(): readonly string[] {\n return Array.from(alarmMap.keys());\n },\n fireTabActivated(info: chrome.tabs.OnActivatedInfo): void {\n for (const listener of Array.from(tabActivatedListeners)) {\n listener(info);\n }\n },\n fireTabUpdated(\n tabId: number,\n changeInfo: chrome.tabs.OnUpdatedInfo,\n tab: chrome.tabs.Tab,\n ): void {\n for (const listener of Array.from(tabUpdatedListeners)) {\n listener(tabId, changeInfo, tab);\n }\n },\n fireTabRemoved(tabId: number, removeInfo: chrome.tabs.OnRemovedInfo): void {\n for (const listener of Array.from(tabRemovedListeners)) {\n listener(tabId, removeInfo);\n }\n },\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/extension/test-utils.ts"],"names":[],"mappings":"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","file":"test-utils.js","sourcesContent":["/**\n * In-memory `chrome.*` mock for tests in this package and any downstream\n * consumer that wants to test against `chromeStorageStore` behavior without\n * a real chrome environment.\n *\n * Currently exposes `chrome.storage.local`, `chrome.runtime` (sendMessage +\n * onMessage), and `chrome.tabs` (sendMessage). The `runtime` and `tabs`\n * surfaces are used by the runtime-bridge tests (`bridge.ts`) to round-trip\n * envelopes between a host runtime and a surface client (sw↔surface) and\n * between a sw client and a per-tab content-script runtime (sw↔content).\n *\n * Extend additional surfaces (alarms, windows, …) only when real consumer\n * tests reach for them — earn each surface through a concrete need.\n */\n\ntype RuntimeListener = (\n message: unknown,\n sender: chrome.runtime.MessageSender,\n sendResponse: (response: unknown) => void,\n // biome-ignore lint/suspicious/noConfusingVoidType: mirrors the chrome.runtime.onMessage listener return type (boolean | undefined | void)\n) => boolean | undefined | void;\n\ntype StorageChangeListener = (\n changes: Record<string, chrome.storage.StorageChange>,\n areaName: chrome.storage.AreaName,\n) => void;\n\ntype AlarmListener = (alarm: chrome.alarms.Alarm) => void;\n\ntype TabActivatedListener = (info: chrome.tabs.OnActivatedInfo) => void;\ntype TabUpdatedListener = (\n tabId: number,\n changeInfo: chrome.tabs.OnUpdatedInfo,\n tab: chrome.tabs.Tab,\n) => void;\ntype TabRemovedListener = (\n tabId: number,\n removeInfo: chrome.tabs.OnRemovedInfo,\n) => void;\n\nexport interface FakeChrome {\n storage: {\n local: chrome.storage.StorageArea;\n /**\n * Top-level `chrome.storage.onChanged` event. Fires for ANY area; consumer\n * filters by the `areaName` arg. Real chrome semantics: every `set` /\n * `remove` / `clear` that materially changes a key emits a single\n * `changes` map keyed by the changed keys.\n */\n onChanged: {\n addListener(listener: StorageChangeListener): void;\n removeListener(listener: StorageChangeListener): void;\n hasListener(listener: StorageChangeListener): boolean;\n hasListeners(): boolean;\n };\n };\n // Subset of `typeof chrome.runtime` covered by the fake — chrome's\n // ambient typings don't export a single `Runtime` interface, so we\n // declare the surface inline. Extend when tests reach for more.\n runtime: {\n sendMessage: typeof chrome.runtime.sendMessage;\n onMessage: typeof chrome.runtime.onMessage;\n };\n tabs: {\n sendMessage: typeof chrome.tabs.sendMessage;\n /**\n * `chrome.tabs.onActivated` — fires when the active tab in a window\n * changes. The fake only models the listener registry; tests drive\n * fires via `__test.fireTabActivated(info)`.\n */\n onActivated: {\n addListener(listener: TabActivatedListener): void;\n removeListener(listener: TabActivatedListener): void;\n hasListener(listener: TabActivatedListener): boolean;\n hasListeners(): boolean;\n };\n /**\n * `chrome.tabs.onUpdated` — fires when a tab is updated (url, title,\n * status, etc.). Tests drive fires via `__test.fireTabUpdated(...)`.\n */\n onUpdated: {\n addListener(listener: TabUpdatedListener): void;\n removeListener(listener: TabUpdatedListener): void;\n hasListener(listener: TabUpdatedListener): boolean;\n hasListeners(): boolean;\n };\n /**\n * `chrome.tabs.onRemoved` — fires when a tab is closed. Tests drive\n * fires via `__test.fireTabRemoved(...)`.\n */\n onRemoved: {\n addListener(listener: TabRemovedListener): void;\n removeListener(listener: TabRemovedListener): void;\n hasListener(listener: TabRemovedListener): boolean;\n hasListeners(): boolean;\n };\n };\n /**\n * `chrome.alarms` — the MV3 periodic timer primitive. The fake tracks\n * created alarms in a Map keyed by name, models `create` (dedupe by\n * name) + `clear` (remove from map), and exposes `onAlarm` as a\n * standard listener registry. Tests drive fires via\n * `__test.fireAlarm(name)`.\n */\n alarms: {\n create(name: string, alarmInfo: chrome.alarms.AlarmCreateInfo): void;\n /**\n * `chrome.alarms.get(name)` — resolves to the Alarm shape Chrome would\n * return (with `scheduledTime` synthesized at lookup time), or\n * undefined if no alarm with that name exists. Mirrors the contract\n * the `ensureQueueAlarm` SW-module-top-level helper checks (\"is this\n * alarm already present with the right period?\") — see\n * `apps/extension/extension/entrypoints/background.ts`.\n */\n get(name: string): Promise<chrome.alarms.Alarm | undefined>;\n clear(name: string): Promise<boolean>;\n onAlarm: {\n addListener(listener: AlarmListener): void;\n removeListener(listener: AlarmListener): void;\n hasListener(listener: AlarmListener): boolean;\n hasListeners(): boolean;\n };\n };\n /**\n * Test helpers — not part of the real chrome surface. Lets tests\n * simulate a content script issuing `chrome.runtime.sendMessage` from\n * inside a specific tab (chrome auto-tags `sender.tab.id` in that\n * direction) and remove a tab so `chrome.tabs.sendMessage` rejects\n * the way real chrome does when the receiver is gone.\n */\n __test: {\n /** Simulate a content-script broadcast tagged with `sender.tab.id`. */\n sendFromTab(tabId: number, message: unknown): Promise<unknown>;\n /** Mark a tab as gone — `chrome.tabs.sendMessage(tabId, …)` will reject. */\n removeTab(tabId: number): void;\n /** Mark a tab as alive (default for any tabId that hasn't been removed). */\n addTab(tabId: number): void;\n /** Fire an alarm — invokes every onAlarm listener with `{ name, ...}`. */\n fireAlarm(name: string): void;\n /** Snapshot the alarm registry — tests assert on created/cleared alarms. */\n getAlarms(): readonly string[];\n /** Fire `chrome.tabs.onActivated` — drives every listener. */\n fireTabActivated(info: chrome.tabs.OnActivatedInfo): void;\n /** Fire `chrome.tabs.onUpdated` — drives every listener. */\n fireTabUpdated(\n tabId: number,\n changeInfo: chrome.tabs.OnUpdatedInfo,\n tab: chrome.tabs.Tab,\n ): void;\n /** Fire `chrome.tabs.onRemoved` — drives every listener. */\n fireTabRemoved(tabId: number, removeInfo: chrome.tabs.OnRemovedInfo): void;\n };\n}\n\nexport function fakeChrome(): FakeChrome {\n const data = new Map<string, unknown>();\n\n // ─── chrome.storage.onChanged — top-level event ───────────────────────\n //\n // Real chrome emits `changes` keyed by every key that actually changed\n // (oldValue / newValue present). Unchanged keys aren't included. The\n // fake mirrors that: build the change map inside set/remove/clear, fire\n // listeners only if the map is non-empty. AreaName is \"local\" here —\n // the fake only models storage.local; if a future test reaches for\n // session/sync, add a per-area fake with its own onChanged.\n const storageListeners = new Set<StorageChangeListener>();\n function deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== typeof b) return false;\n if (typeof a !== \"object\" || a === null || b === null) return false;\n return JSON.stringify(a) === JSON.stringify(b);\n }\n function fireStorageChange(\n changes: Record<string, chrome.storage.StorageChange>,\n ): void {\n if (Object.keys(changes).length === 0) return;\n for (const listener of Array.from(storageListeners)) {\n listener(changes, \"local\");\n }\n }\n\n const partial = {\n async get(\n keys?: string | string[] | Record<string, unknown> | null,\n ): Promise<Record<string, unknown>> {\n // get() → all entries\n // get(\"k\") → { k: v } if present, {} otherwise\n // get([\"k1\",\"k2\"]) → { k: v } for present keys\n // get({ k: default}) → { k: v ?? default } per key\n if (keys === undefined || keys === null) {\n return Object.fromEntries(data);\n }\n if (typeof keys === \"string\") {\n const v = data.get(keys);\n return v === undefined ? {} : { [keys]: v };\n }\n if (Array.isArray(keys)) {\n return Object.fromEntries(\n keys.flatMap((k) => (data.has(k) ? [[k, data.get(k)]] : [])),\n );\n }\n const out: Record<string, unknown> = {};\n for (const [k, def] of Object.entries(keys)) {\n out[k] = data.has(k) ? data.get(k) : def;\n }\n return out;\n },\n async set(items: Record<string, unknown>): Promise<void> {\n const changes: Record<string, chrome.storage.StorageChange> = {};\n for (const [k, v] of Object.entries(items)) {\n const prev = data.get(k);\n // Real chrome only fires onChanged when the value actually\n // changes — match that to avoid spurious re-renders in tests.\n if (deepEqual(prev, v)) continue;\n const change: chrome.storage.StorageChange = { newValue: v };\n if (data.has(k)) change.oldValue = prev;\n changes[k] = change;\n data.set(k, v);\n }\n fireStorageChange(changes);\n },\n async remove(keys: string | string[]): Promise<void> {\n const arr = Array.isArray(keys) ? keys : [keys];\n const changes: Record<string, chrome.storage.StorageChange> = {};\n for (const k of arr) {\n if (!data.has(k)) continue;\n changes[k] = { oldValue: data.get(k) };\n data.delete(k);\n }\n fireStorageChange(changes);\n },\n async clear(): Promise<void> {\n const changes: Record<string, chrome.storage.StorageChange> = {};\n for (const [k, v] of data) changes[k] = { oldValue: v };\n data.clear();\n fireStorageChange(changes);\n },\n };\n\n // The full `chrome.storage.StorageArea` contract has more surface\n // (getBytesInUse, getKeys, onChanged event, setAccessLevel) than real\n // consumer tests currently exercise. The cast is local to this test util;\n // src/index.ts never casts. If a test needs more surface, implement it\n // here instead of widening the cast.\n const local = partial as unknown as chrome.storage.StorageArea;\n\n // ─── chrome.runtime — sendMessage + onMessage round-trip ───────────────\n //\n // The fake mirrors MV3 semantics that matter for the bridge:\n // - sendMessage delivers to EVERY onMessage listener in arrival order.\n // - The first listener that returns `true` (or calls sendResponse\n // synchronously) \"wins\" — its sendResponse value resolves the\n // sendMessage promise. Listeners that return false/undefined are\n // passed over.\n // - If no listener handles the message, sendMessage rejects with the\n // same shape chrome uses (\"Could not establish connection.\n // Receiving end does not exist.\"). The bridge's broadcast path\n // swallows this.\n const runtimeListeners = new Set<RuntimeListener>();\n\n // Tracks tabs that are \"alive\" from `chrome.tabs.sendMessage`'s point of\n // view. Unknown tabIds are treated as alive by default (matches the\n // common test path: assign a number, send to it). Tests can `removeTab`\n // to simulate disconnection.\n const removedTabs = new Set<number>();\n\n /**\n * Internal dispatch helper. Real chrome distinguishes:\n * - `chrome.runtime.sendMessage(msg)` — sender is the extension origin,\n * `sender.tab` is undefined; all `chrome.runtime.onMessage` listeners\n * hear it.\n * - `chrome.tabs.sendMessage(tabId, msg)` — sender is the extension\n * origin (still no `sender.tab` on the receiving end's view here, but\n * the routing is by tab); only listeners IN that tab receive it. Our\n * fake collapses to the same listener set since tests run a single\n * \"tab\" of listeners at a time. The routing assertion is enforced by\n * callers reading the envelope/tabId pair they sent.\n * - Content-script `chrome.runtime.sendMessage(msg)` (the `sendFromTab`\n * helper) — chrome auto-tags `sender.tab.id`. SW-side listeners use\n * this to filter cross-tab broadcasts.\n *\n * `sender` controls which sender object the listeners see.\n */\n const dispatch = (\n message: unknown,\n sender: chrome.runtime.MessageSender,\n ): Promise<unknown> => {\n return new Promise((resolve, reject) => {\n let responded = false;\n const sendResponse = (response: unknown): void => {\n if (responded) return;\n responded = true;\n resolve(response);\n };\n const snapshot = Array.from(runtimeListeners);\n let willRespondAsync = false;\n for (const listener of snapshot) {\n const r = listener(message, sender, sendResponse);\n if (r === true) willRespondAsync = true;\n if (responded) return;\n }\n if (willRespondAsync) return;\n reject(\n new Error(\n \"Could not establish connection. Receiving end does not exist.\",\n ),\n );\n });\n };\n\n const extensionSender: chrome.runtime.MessageSender =\n {} as chrome.runtime.MessageSender;\n\n const runtimePartial = {\n sendMessage(message: unknown): Promise<unknown> {\n return dispatch(message, extensionSender);\n },\n onMessage: {\n addListener(listener: RuntimeListener): void {\n runtimeListeners.add(listener);\n },\n removeListener(listener: RuntimeListener): void {\n runtimeListeners.delete(listener);\n },\n hasListener(listener: RuntimeListener): boolean {\n return runtimeListeners.has(listener);\n },\n hasListeners(): boolean {\n return runtimeListeners.size > 0;\n },\n },\n };\n\n const runtime = runtimePartial as unknown as FakeChrome[\"runtime\"];\n\n // ─── chrome.tabs.sendMessage — SW → content-script routing ─────────────\n //\n // Real chrome routes by tabId; the receiving content script's\n // `chrome.runtime.onMessage` fires with `sender` representing the\n // extension (no `sender.tab`). The fake collapses to the shared listener\n // set since tests run one runtime per process, but enforces the\n // \"tab gone\" reject so consumers can verify their fire-and-forget\n // posture.\n const tabsPartial = {\n sendMessage(tabId: number, message: unknown): Promise<unknown> {\n if (removedTabs.has(tabId)) {\n return Promise.reject(\n new Error(\n `Could not establish connection. Receiving end does not exist. (tabId=${tabId})`,\n ),\n );\n }\n return dispatch(message, extensionSender);\n },\n };\n\n // ─── chrome.tabs.on{Activated,Updated,Removed} — listener registries ────\n //\n // Tests drive fires via the __test helpers below. Production-code Sub\n // factories register/unregister; the fake only models the registry +\n // dispatch, not the chrome events themselves (no real tabs to observe).\n const tabActivatedListeners = new Set<TabActivatedListener>();\n const tabUpdatedListeners = new Set<TabUpdatedListener>();\n const tabRemovedListeners = new Set<TabRemovedListener>();\n\n const tabsFull = {\n ...tabsPartial,\n onActivated: {\n addListener(listener: TabActivatedListener): void {\n tabActivatedListeners.add(listener);\n },\n removeListener(listener: TabActivatedListener): void {\n tabActivatedListeners.delete(listener);\n },\n hasListener(listener: TabActivatedListener): boolean {\n return tabActivatedListeners.has(listener);\n },\n hasListeners(): boolean {\n return tabActivatedListeners.size > 0;\n },\n },\n onUpdated: {\n addListener(listener: TabUpdatedListener): void {\n tabUpdatedListeners.add(listener);\n },\n removeListener(listener: TabUpdatedListener): void {\n tabUpdatedListeners.delete(listener);\n },\n hasListener(listener: TabUpdatedListener): boolean {\n return tabUpdatedListeners.has(listener);\n },\n hasListeners(): boolean {\n return tabUpdatedListeners.size > 0;\n },\n },\n onRemoved: {\n addListener(listener: TabRemovedListener): void {\n tabRemovedListeners.add(listener);\n },\n removeListener(listener: TabRemovedListener): void {\n tabRemovedListeners.delete(listener);\n },\n hasListener(listener: TabRemovedListener): boolean {\n return tabRemovedListeners.has(listener);\n },\n hasListeners(): boolean {\n return tabRemovedListeners.size > 0;\n },\n },\n };\n\n const tabs = tabsFull as unknown as FakeChrome[\"tabs\"];\n\n // ─── chrome.alarms — listener registry + name-keyed alarm map ─────────\n //\n // create(name, info) stores `info` keyed by name (dedupe on repeat —\n // matches real chrome's behavior of replacing the prior alarm). clear()\n // removes from the map and resolves to `true` if the alarm existed,\n // `false` otherwise (matches real chrome's contract). onAlarm is a\n // standard listener registry. The fake does NOT auto-fire alarms based\n // on `periodInMinutes` — tests drive fires deterministically via\n // `__test.fireAlarm(name)`.\n const alarmMap = new Map<string, chrome.alarms.AlarmCreateInfo>();\n const alarmListeners = new Set<AlarmListener>();\n\n const alarms: FakeChrome[\"alarms\"] = {\n create(name: string, alarmInfo: chrome.alarms.AlarmCreateInfo): void {\n alarmMap.set(name, alarmInfo);\n },\n async get(name: string): Promise<chrome.alarms.Alarm | undefined> {\n const info = alarmMap.get(name);\n if (info === undefined) return undefined;\n return {\n name,\n scheduledTime: Date.now(),\n periodInMinutes: info.periodInMinutes,\n };\n },\n async clear(name: string): Promise<boolean> {\n return alarmMap.delete(name);\n },\n onAlarm: {\n addListener(listener: AlarmListener): void {\n alarmListeners.add(listener);\n },\n removeListener(listener: AlarmListener): void {\n alarmListeners.delete(listener);\n },\n hasListener(listener: AlarmListener): boolean {\n return alarmListeners.has(listener);\n },\n hasListeners(): boolean {\n return alarmListeners.size > 0;\n },\n },\n };\n\n return {\n storage: {\n local,\n onChanged: {\n addListener(listener: StorageChangeListener): void {\n storageListeners.add(listener);\n },\n removeListener(listener: StorageChangeListener): void {\n storageListeners.delete(listener);\n },\n hasListener(listener: StorageChangeListener): boolean {\n return storageListeners.has(listener);\n },\n hasListeners(): boolean {\n return storageListeners.size > 0;\n },\n },\n },\n runtime,\n tabs,\n alarms,\n __test: {\n sendFromTab(tabId: number, message: unknown): Promise<unknown> {\n const taggedSender = {\n tab: { id: tabId },\n } as chrome.runtime.MessageSender;\n return dispatch(message, taggedSender);\n },\n removeTab(tabId: number): void {\n removedTabs.add(tabId);\n },\n addTab(tabId: number): void {\n removedTabs.delete(tabId);\n },\n fireAlarm(name: string): void {\n const info = alarmMap.get(name);\n if (info === undefined) return;\n // Real chrome's Alarm shape carries `name`, `scheduledTime`, and\n // `periodInMinutes`. The fake stamps `scheduledTime` at fire time\n // and forwards the period from the create call.\n const alarm: chrome.alarms.Alarm = {\n name,\n scheduledTime: Date.now(),\n periodInMinutes: info.periodInMinutes,\n };\n for (const listener of Array.from(alarmListeners)) {\n listener(alarm);\n }\n },\n getAlarms(): readonly string[] {\n return Array.from(alarmMap.keys());\n },\n fireTabActivated(info: chrome.tabs.OnActivatedInfo): void {\n for (const listener of Array.from(tabActivatedListeners)) {\n listener(info);\n }\n },\n fireTabUpdated(\n tabId: number,\n changeInfo: chrome.tabs.OnUpdatedInfo,\n tab: chrome.tabs.Tab,\n ): void {\n for (const listener of Array.from(tabUpdatedListeners)) {\n listener(tabId, changeInfo, tab);\n }\n },\n fireTabRemoved(\n tabId: number,\n removeInfo: chrome.tabs.OnRemovedInfo,\n ): void {\n for (const listener of Array.from(tabRemovedListeners)) {\n listener(tabId, removeInfo);\n }\n },\n },\n };\n}\n"]}
@@ -0,0 +1,239 @@
1
+ import { C as Cmd, P as Port } from '../core-CymXZCeY.js';
2
+ import { QueueItem } from '../work-queue/index.js';
3
+ import '../index.js';
4
+
5
+ /**
6
+ * @demlik/tea/fan-out — scatter-gather over a bounded-concurrency work list.
7
+ *
8
+ * The Level-2 combinator that fans a batch of items out across at most
9
+ * `concurrency` in-flight effects, gathers each item's result (or error), and
10
+ * — once every item has settled — fires an optional `join` over the collected
11
+ * results. It is the "map a list through an effect, but never run more than N
12
+ * at once" knob.
13
+ *
14
+ * The recipe (which bricks it composes):
15
+ * - `../work-queue` — fan-out's tracked items ARE work-queue items. Each
16
+ * item carries the same `{ id, status, input }` lifecycle vocabulary
17
+ * (`pending → running → done | failed`). The slice this knob owns is four
18
+ * partitions over that one lifecycle, so the work-queue's status taxonomy
19
+ * is the substrate rather than a parallel invention. We reuse its
20
+ * `QueueItem<I>` / `QueueItemStatus` TYPES and delegate the ledger
21
+ * transitions to its BLESSED pure ops (`@demlik/tea/work-queue/ops`):
22
+ * `scatter` mints each pending record with `enqueueOp`, and `launchUpTo`
23
+ * flips `pending → running` with `claimNextOp` (one positional claim per
24
+ * launch). The lone exception is `settleOne`'s `running → done|failed`
25
+ * flip — it must match "first id-match that is still `running`" to settle
26
+ * exactly one of a DUPLICATE id, a status predicate the ops surface can't
27
+ * express, so fan-out owns that one index (see `settleOne`). The async
28
+ * `createQueue(store)` adapter is NOT used here because its `load → mutate
29
+ * → save` reads the clock and lives at an I/O boundary, while a combinator's
30
+ * verbs must be pure (invariant 2) — fan-out reaches the underlying ops
31
+ * directly instead, the same route `monitored-run` / `idempotent-intake`
32
+ * take. The slice lives in the Model instead — durable + replayable, the
33
+ * two non-negotiables every knob preserves.
34
+ *
35
+ * Typical wiring (combinator form — splice the hooks into your machine):
36
+ *
37
+ * const fan = createFanOut<Url, Report>({
38
+ * concurrency: 4,
39
+ * idOf: (url) => url, // stable, pure identity
40
+ * of: (url) => ({ type: "crawl", url }), // the per-item effect, as data
41
+ * join: (results) => ({ type: "report_done", results }),
42
+ * });
43
+ *
44
+ * // in `update`:
45
+ * start_crawl: (s, m) => { const [fanOut, cmds] = fan.scatter(s.fanOut, m.urls);
46
+ * return [{ ...s, fanOut }, cmds]; },
47
+ * crawl_ok: (s, m) => { const [fanOut, cmds] = fan.itemOk(s.fanOut, m.id, m.report);
48
+ * return [{ ...s, fanOut }, cmds]; },
49
+ * crawl_err: (s, m) => { const [fanOut, cmds] = fan.itemErr(s.fanOut, m.id, m.error);
50
+ * return [{ ...s, fanOut }, cmds]; },
51
+ *
52
+ * Every verb is PURE: it returns a NEW slice plus the Cmds to perform (the
53
+ * `of(item)` effects for newly-launched items, and at most one `join` Cmd when
54
+ * the batch completes). The reducer never reads a clock or RNG — item identity
55
+ * is supplied by the caller's `idOf` so `scatter` is deterministic, and the
56
+ * launch effects are plain data the runtime performs (invariant 3).
57
+ *
58
+ * `handlers(ports)` is the I/O splice: it wires the runtime side that actually
59
+ * runs each launched `of(item)` Cmd. Because `of` already produces a fully
60
+ * caller-defined Cmd, fan-out does NOT own the interpret handler for it — the
61
+ * consumer's own `interpret` performs the effect and routes Ok/Err back to the
62
+ * `itemOk` / `itemErr` verbs (the same shape resilient-fetch uses with
63
+ * `tryInterpret`). `handlers` therefore exposes the one piece fan-out CAN
64
+ * pre-wire generically: emitting the gathered batch onto a typed Port when the
65
+ * batch completes, for consumers that observe completion out-of-band rather
66
+ * than through the `join` Cmd.
67
+ *
68
+ * NOT a substrate primitive: it depends only on `../work-queue`'s pure types +
69
+ * blessed ops and `../index`'s `Cmd` / `Port` types. Consumers reach it via the
70
+ * `@demlik/tea/fan-out` subpath.
71
+ */
72
+
73
+ /**
74
+ * One settled-OK item: the original `input` and the `result` its effect
75
+ * produced. Mirrors the spec slice's `done: {item, result}[]` — `item` is the
76
+ * caller's input value (not the internal `QueueItem` envelope), so a consumer
77
+ * reading `done` gets back exactly what it scattered.
78
+ */
79
+ interface FanOutDone<I, R> {
80
+ /** The original scattered input value. */
81
+ readonly item: I;
82
+ /** The result the item's effect produced, routed in via `itemOk`. */
83
+ readonly result: R;
84
+ }
85
+ /**
86
+ * One settled-error item: the original `input` and the `error` its effect
87
+ * surfaced. `error` is `unknown` — fan-out carries it forward verbatim (an
88
+ * `Error`, a tagged result, a string) for the consumer's reducer to read,
89
+ * never interpreting it (same discipline as `retry-backoff`'s `lastError`).
90
+ */
91
+ interface FanOutFailed<I> {
92
+ /** The original scattered input value. */
93
+ readonly item: I;
94
+ /** The error the item's effect surfaced, routed in via `itemErr`. Carried, never interpreted. */
95
+ readonly error: unknown;
96
+ }
97
+ /**
98
+ * The slice this knob owns — four partitions over the work-queue item
99
+ * lifecycle, plus the work-queue records the verbs thread the lifecycle
100
+ * through.
101
+ *
102
+ * Why both the spec's four arrays AND `items`:
103
+ * - The four arrays (`pending` / `running` / `done` / `failed`) are the
104
+ * SPEC's public read surface — what a consumer's view renders and what
105
+ * `isComplete` reads. They hold caller-facing values (`I`, or
106
+ * `FanOutDone` / `FanOutFailed`), not the internal id-bearing envelope.
107
+ * - `items` is the work-queue ledger: one `QueueItem<I>` per scattered item,
108
+ * carrying the stable `id` (from `config.idOf`) that `itemOk` / `itemErr`
109
+ * address and the `status` that decides launch-eligibility. It is the
110
+ * single source of truth; the four arrays are a derived projection kept in
111
+ * lockstep so consumers never re-scan `items` to render a partition.
112
+ *
113
+ * Every field is `readonly` and the whole slice is JSON-serializable (plain
114
+ * arrays + the work-queue's plain `QueueItem` records), so it round-trips
115
+ * through a consumer's `Store<S>` unchanged — durable across DO eviction.
116
+ */
117
+ interface FanOutState<I, R> {
118
+ /** Items accepted but not yet launched (queue back-pressure beyond `concurrency`). */
119
+ readonly pending: readonly I[];
120
+ /** Items whose `of(item)` effect is in flight (size never exceeds `concurrency`). */
121
+ readonly running: readonly I[];
122
+ /** Items whose effect resolved OK, with the result it produced. */
123
+ readonly done: readonly FanOutDone<I, R>[];
124
+ /** Items whose effect failed, with the error it surfaced. */
125
+ readonly failed: readonly FanOutFailed<I>[];
126
+ /**
127
+ * The work-queue ledger: one record per scattered item, keyed by the
128
+ * caller's `idOf`. The single source of truth the four arrays project from.
129
+ */
130
+ readonly items: readonly QueueItem<I>[];
131
+ /**
132
+ * Index into `done` where the CURRENT wave's OK results begin. `done` is
133
+ * append-only and accumulates across every wave (so a consumer reading
134
+ * `done` sees the full history), but `join` must fold only the wave that
135
+ * just drained — not the cumulative total. A wave opens when `scatter`
136
+ * enqueues into a slice with nothing in flight; this marks `done.length` at
137
+ * that moment so `join` receives exactly `done.slice(waveDoneFrom)`.
138
+ */
139
+ readonly waveDoneFrom: number;
140
+ /**
141
+ * Index into `failed` where the current wave's failures begin. Symmetric to
142
+ * `waveDoneFrom` — the wave boundary over the failure partition. Carried for
143
+ * a consumer that wants to read only the current wave's failures; `join`
144
+ * itself folds only OK results.
145
+ */
146
+ readonly waveFailedFrom: number;
147
+ /**
148
+ * Whether the current wave has already fired its `join`. `join` fires at most
149
+ * once per wave — set `true` at the completion edge so a stale settle that
150
+ * re-reaches `isComplete` cannot re-fire it. A new `scatter` that opens a wave
151
+ * resets it to `false`.
152
+ */
153
+ readonly joined: boolean;
154
+ }
155
+ /**
156
+ * The knob. `concurrency` caps how many `of(item)` effects are in flight at
157
+ * once; `idOf` mints a stable, PURE identity per item (so `scatter` is
158
+ * deterministic and `itemOk` / `itemErr` can address an item by id); `of`
159
+ * turns an item into the launch Cmd; `join` (optional) turns the gathered
160
+ * results into a single completion Cmd fired exactly once when every item has
161
+ * settled.
162
+ *
163
+ * Field optionality follows the resilient-call rule — omit `join`, omit the
164
+ * completion Cmd (a consumer that observes `isComplete` itself, or wires the
165
+ * `handlers` Port, doesn't need one).
166
+ */
167
+ interface FanOutConfig<I, R, C extends Cmd, J extends Cmd> {
168
+ /** Max in-flight `of(item)` effects. Clamped to `>= 1` (a 0/negative cap would deadlock). */
169
+ readonly concurrency: number;
170
+ /**
171
+ * Stable, deterministic identity for an item. PURE — no clock, no RNG. The
172
+ * id is what `itemOk` / `itemErr` address, so it must survive a round-trip
173
+ * through the durable slice and match across the launch → settle gap. For
174
+ * value-unique inputs (URLs, ids) this is often the identity function; for
175
+ * structural inputs, a content hash or a caller-assigned key.
176
+ */
177
+ readonly idOf: (item: I) => string;
178
+ /** The per-item effect, as data. Performed by the consumer's own `interpret`. */
179
+ readonly of: (item: I) => C;
180
+ /**
181
+ * Optional: fold the gathered done-results into a single completion Cmd,
182
+ * fired exactly once at the transition that empties `pending` + `running`.
183
+ * Omit to drive completion off `isComplete` (or the `handlers` Port) instead.
184
+ */
185
+ readonly join?: (results: readonly R[]) => J;
186
+ }
187
+ /** The starting slice: nothing scattered yet. */
188
+ declare function initFanOut<I, R>(): FanOutState<I, R>;
189
+ /**
190
+ * Whether every scattered item has settled (no `pending`, no `running`).
191
+ * Derived — reads the slice, never mutates. The verbs use it to decide when to
192
+ * fire `join`; consumers can read it directly to branch their own phase.
193
+ *
194
+ * An empty fan-out (nothing ever scattered) is NOT complete: completion is "a
195
+ * non-empty batch fully drained," so `isComplete(initFanOut())` is `false` and
196
+ * a `scatter([])` of zero items also reads `false` (there was no batch to
197
+ * complete). This keeps `join` from firing on a degenerate empty scatter.
198
+ */
199
+ declare function isComplete<I, R>(state: FanOutState<I, R>): boolean;
200
+ /**
201
+ * Optional Port for observing batch completion out-of-band. When a consumer
202
+ * passes `handlers(ports)` a `complete` Port, the runtime side can emit the
203
+ * gathered results onto it the moment `isComplete` flips true — distinct from
204
+ * the in-band `join` Cmd (which threads back through `update`). Use the Port
205
+ * for "tell the outside world the batch finished" without folding a completion
206
+ * signal into the consumer's Msg union.
207
+ */
208
+ interface FanOutPorts<R> {
209
+ /** Fires once with every gathered done-result when the batch completes. */
210
+ readonly complete: Port<readonly R[]>;
211
+ }
212
+ /**
213
+ * Build the fan-out knob from `config`. Returns the uniform combinator
214
+ * contract: `init` for the owned slice, the three settle verbs + the derived
215
+ * `isComplete`, and `handlers` for the optional out-of-band completion Port.
216
+ *
217
+ * No `subs` — fan-out runs no timers (the spec pins "Subs: none"). The
218
+ * per-item effect's lifetime is owned by the consumer's `interpret`, and
219
+ * completion is a pure function of the settle Msgs, so there is nothing
220
+ * continuous to subscribe to.
221
+ */
222
+ declare function createFanOut<I, R, C extends Cmd = Cmd, J extends Cmd = Cmd>(config: FanOutConfig<I, R, C, J>): {
223
+ init: () => FanOutState<I, R>;
224
+ scatter: (state: FanOutState<I, R>, items: readonly I[]) => readonly [FanOutState<I, R>, readonly C[]];
225
+ itemOk: (state: FanOutState<I, R>, id: string, result: R) => readonly [FanOutState<I, R>, readonly (C | J)[]];
226
+ itemErr: (state: FanOutState<I, R>, id: string, error: unknown) => readonly [FanOutState<I, R>, readonly (C | J)[]];
227
+ isComplete: typeof isComplete;
228
+ handlers: (ports: FanOutPorts<R>) => {
229
+ /**
230
+ * Emit the gathered done-results onto `ports.complete` iff `state` is a
231
+ * just-completed batch. Idempotency is the caller's concern (call it once
232
+ * per transition via `observe`); emitting to a Port with no subscribers
233
+ * is a no-op at the runtime, so an extra call is harmless.
234
+ */
235
+ emitOnComplete(emit: <T>(port: Port<T>, value: T) => void, state: FanOutState<I, R>): void;
236
+ };
237
+ };
238
+
239
+ export { type FanOutConfig, type FanOutDone, type FanOutFailed, type FanOutPorts, type FanOutState, createFanOut, initFanOut, isComplete };
@@ -0,0 +1,5 @@
1
+ export { createFanOut, initFanOut, isComplete } from '../chunk-UV6UWGEO.js';
2
+ import '../chunk-HJO6VNQI.js';
3
+ import '../chunk-TGG26XMK.js';
4
+ //# sourceMappingURL=index.js.map
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,62 @@
1
+ import { IdempotencyStore } from './index.js';
2
+
3
+ /**
4
+ * @demlik/tea/idempotency/adapter — the verb seam over the pure idempotency ops.
5
+ *
6
+ * The functions in `./index` (`seen` / `recall` / `remember` / `evictExpired`
7
+ * / `initStore`) are the BLESSED transitions, but their names and the
8
+ * `IdempotencyStore<V>` shape they read/return are the STORE's vocabulary. A
9
+ * consumer that owns an `IdempotencyStore<V>` slice (`idempotent-intake`,
10
+ * `poller`) used to import those functions by name and call them directly, so
11
+ * a change to the op surface — or to `IdempotencyStore`'s fields — cascaded to
12
+ * every call site.
13
+ *
14
+ * `IdempotencyMemory<V>` is the insulating interface: five VERBS (`isSeen` /
15
+ * `recall` / `remember` / `evict` / `init`) consumers depend on instead of the
16
+ * raw functions. The verbs keep the ops' pure contract — no I/O, no `Date.now`
17
+ * (the clock is injected as `now`) — so a consumer folding the adapter through
18
+ * its reducer stays inside invariant 2 (pure transitions). `isSeen` renames the
19
+ * blessed `seen` predicate (a noun-y `seen` reads ambiguously as a slice field;
20
+ * the verb form states the question it answers), and `evict` is the verb form
21
+ * of `evictExpired`.
22
+ *
23
+ * `idempotencyMemory<V>()` is the canonical implementation, a thin binding of
24
+ * the blessed ops. Because the verbs are the only surface consumers touch, an
25
+ * `IdempotencyStore` shape change is absorbed HERE rather than at the consumer
26
+ * call sites.
27
+ *
28
+ * INTERNAL seam (no public-API port): consumers import the adapter from this
29
+ * subpath the same way they imported the ops. The ops remain exported from
30
+ * `./index` for any call site that genuinely wants a function directly.
31
+ */
32
+
33
+ /**
34
+ * The verb interface over an `IdempotencyStore<V>`. Each verb mirrors a blessed
35
+ * op's pure contract but speaks the consumer's dedupe vocabulary, so the
36
+ * consumer never names a store function or reaches into an `IdempotencyStore`
37
+ * field it did not put there.
38
+ */
39
+ interface IdempotencyMemory<V> {
40
+ /** Create an empty store, optionally bounded by `capacity` and/or `ttlMs`. */
41
+ init(opts?: {
42
+ capacity?: number;
43
+ ttlMs?: number;
44
+ }): IdempotencyStore<V>;
45
+ /** True iff `key` is present AND not expired at `now`. Read-only. */
46
+ isSeen(store: IdempotencyStore<V>, key: string, now: number): boolean;
47
+ /** The cached value for `key` if present and unexpired at `now`. Read-only. */
48
+ recall(store: IdempotencyStore<V>, key: string, now: number): V | undefined;
49
+ /** Record `key → value` at `now`, enforcing the store's bounds. Returns a new store. */
50
+ remember(store: IdempotencyStore<V>, key: string, value: V, now: number): IdempotencyStore<V>;
51
+ /** Drop every entry expired at `now`. Returns a new store. */
52
+ evict(store: IdempotencyStore<V>, now: number): IdempotencyStore<V>;
53
+ }
54
+ /**
55
+ * The canonical `IdempotencyMemory<V>`, binding each verb to its blessed op.
56
+ * Pure and stateless — call once at module scope in a consumer and fold its
57
+ * verbs through the reducer. An `IdempotencyStore` shape change is absorbed by
58
+ * re-binding a verb here, not at the consumers.
59
+ */
60
+ declare function idempotencyMemory<V>(): IdempotencyMemory<V>;
61
+
62
+ export { type IdempotencyMemory, idempotencyMemory };
@@ -0,0 +1,4 @@
1
+ export { idempotencyMemory } from '../chunk-BYQUGDD3.js';
2
+ import '../chunk-K575PT4S.js';
3
+ //# sourceMappingURL=adapter.js.map
4
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"adapter.js"}
@@ -1,58 +1,3 @@
1
- // src/idempotency/index.ts
2
- function initStore(opts) {
3
- return {
4
- entries: {},
5
- order: [],
6
- capacity: opts?.capacity,
7
- ttlMs: opts?.ttlMs
8
- };
9
- }
10
- function isExpired(entry, nowMs, ttlMs) {
11
- if (ttlMs === void 0) return false;
12
- return nowMs - entry.atMs >= ttlMs;
13
- }
14
- function seen(store, key, nowMs) {
15
- const entry = store.entries[key];
16
- if (entry === void 0) return false;
17
- return !isExpired(entry, nowMs, store.ttlMs);
18
- }
19
- function recall(store, key, nowMs) {
20
- const entry = store.entries[key];
21
- if (entry === void 0 || isExpired(entry, nowMs, store.ttlMs))
22
- return void 0;
23
- return entry.value;
24
- }
25
- function evictExpired(store, nowMs) {
26
- if (store.ttlMs === void 0) return store;
27
- const nextEntries = {};
28
- const nextOrder = [];
29
- for (const key of store.order) {
30
- const entry = store.entries[key];
31
- if (entry === void 0) continue;
32
- if (isExpired(entry, nowMs, store.ttlMs)) continue;
33
- nextEntries[key] = entry;
34
- nextOrder.push(key);
35
- }
36
- return { ...store, entries: nextEntries, order: nextOrder };
37
- }
38
- function remember(store, key, value, nowMs) {
39
- const pruned = evictExpired(store, nowMs);
40
- const nextEntries = {
41
- ...pruned.entries
42
- };
43
- nextEntries[key] = { value, atMs: nowMs };
44
- const nextOrder = pruned.order.filter((k) => k !== key);
45
- nextOrder.push(key);
46
- if (store.capacity !== void 0) {
47
- while (nextOrder.length > store.capacity) {
48
- const oldest = nextOrder.shift();
49
- if (oldest === void 0) break;
50
- delete nextEntries[oldest];
51
- }
52
- }
53
- return { ...store, entries: nextEntries, order: nextOrder };
54
- }
55
-
56
- export { evictExpired, initStore, recall, remember, seen };
1
+ export { evictExpired, initStore, recall, remember, seen } from '../chunk-K575PT4S.js';
57
2
  //# sourceMappingURL=index.js.map
58
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/idempotency/index.ts"],"names":[],"mappings":";AAuFO,SAAS,UAAa,IAAA,EAGL;AACtB,EAAA,OAAO;AAAA,IACL,SAAS,EAAC;AAAA,IACV,OAAO,EAAC;AAAA,IACR,UAAU,IAAA,EAAM,QAAA;AAAA,IAChB,OAAO,IAAA,EAAM;AAAA,GACf;AACF;AAUA,SAAS,SAAA,CACP,KAAA,EACA,KAAA,EACA,KAAA,EACS;AACT,EAAA,IAAI,KAAA,KAAU,QAAW,OAAO,KAAA;AAChC,EAAA,OAAO,KAAA,GAAQ,MAAM,IAAA,IAAQ,KAAA;AAC/B;AAQO,SAAS,IAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACS;AACT,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,KAAA,KAAU,QAAW,OAAO,KAAA;AAChC,EAAA,OAAO,CAAC,SAAA,CAAU,KAAA,EAAO,KAAA,EAAO,MAAM,KAAK,CAAA;AAC7C;AASO,SAAS,MAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACe;AACf,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,EAAA,IAAI,UAAU,MAAA,IAAa,SAAA,CAAU,KAAA,EAAO,KAAA,EAAO,MAAM,KAAK,CAAA;AAC5D,IAAA,OAAO,MAAA;AACT,EAAA,OAAO,KAAA,CAAM,KAAA;AACf;AAQO,SAAS,YAAA,CACd,OACA,KAAA,EACqB;AACrB,EAAA,IAAI,KAAA,CAAM,KAAA,KAAU,MAAA,EAAW,OAAO,KAAA;AACtC,EAAA,MAAM,cAAmD,EAAC;AAC1D,EAAA,MAAM,YAAsB,EAAC;AAG7B,EAAA,KAAA,MAAW,GAAA,IAAO,MAAM,KAAA,EAAO;AAC7B,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AAC/B,IAAA,IAAI,UAAU,MAAA,EAAW;AACzB,IAAA,IAAI,SAAA,CAAU,KAAA,EAAO,KAAA,EAAO,KAAA,CAAM,KAAK,CAAA,EAAG;AAC1C,IAAA,WAAA,CAAY,GAAG,CAAA,GAAI,KAAA;AACnB,IAAA,SAAA,CAAU,KAAK,GAAG,CAAA;AAAA,EACpB;AACA,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAa,OAAO,SAAA,EAAU;AAC5D;AAoBO,SAAS,QAAA,CACd,KAAA,EACA,GAAA,EACA,KAAA,EACA,KAAA,EACqB;AAErB,EAAA,MAAM,MAAA,GAAS,YAAA,CAAa,KAAA,EAAO,KAAK,CAAA;AAKxC,EAAA,MAAM,WAAA,GAAmD;AAAA,IACvD,GAAG,MAAA,CAAO;AAAA,GACZ;AACA,EAAA,WAAA,CAAY,GAAG,CAAA,GAAI,EAAE,KAAA,EAAO,MAAM,KAAA,EAAM;AACxC,EAAA,MAAM,YAAY,MAAA,CAAO,KAAA,CAAM,OAAO,CAAC,CAAA,KAAM,MAAM,GAAG,CAAA;AACtD,EAAA,SAAA,CAAU,KAAK,GAAG,CAAA;AAKlB,EAAA,IAAI,KAAA,CAAM,aAAa,MAAA,EAAW;AAChC,IAAA,OAAO,SAAA,CAAU,MAAA,GAAS,KAAA,CAAM,QAAA,EAAU;AACxC,MAAA,MAAM,MAAA,GAAS,UAAU,KAAA,EAAM;AAC/B,MAAA,IAAI,WAAW,MAAA,EAAW;AAC1B,MAAA,OAAO,YAAY,MAAM,CAAA;AAAA,IAC3B;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,GAAG,KAAA,EAAO,OAAA,EAAS,WAAA,EAAa,OAAO,SAAA,EAAU;AAC5D","file":"index.js","sourcesContent":["/**\n * @demlik/tea/idempotency — dedupe-by-key + last-result cache as pure state + ops.\n *\n * Same shape as `@demlik/tea/work-queue`'s `ops.ts` and `@demlik/tea/rate-limit`:\n * a state type plus pure transition functions. Host-agnostic by construction —\n * no `Date.now()`, no timers, no I/O. The caller injects the clock (`nowMs`)\n * and wires the result into its own reducer / interpret. This keeps the store\n * inside TEA's invariant 2 (transitions are pure: same inputs → same outputs,\n * no `Date.now`) so a consumer can fold it through `update` and exercise it\n * with `replay` / the pbt fold without a fake clock.\n *\n * Every op returns a NEW store value and never mutates its input (invariant 1 —\n * state is a value, not a place). `seen` / `recall` are read-shaped predicates;\n * `remember` / `evictExpired` return a new store.\n *\n * The use case: a webhook receiver, a retried queue message, or a repeated user\n * action arrives carrying an idempotency key. `seen(store, key, now)` answers\n * \"did I already process this?\"; `recall(store, key, now)` returns the cached\n * result so the duplicate request gets the SAME response the original produced\n * rather than re-running the side effect. `remember(store, key, value, now)`\n * records the key + its result after the first run.\n *\n * Two eviction policies keep the store bounded, both injected at `initStore`:\n *\n * - **`ttlMs`** — an entry expires `ttlMs` after the `atMs` it was remembered\n * at. `seen` / `recall` treat an expired entry as absent; `evictExpired`\n * (and every `remember`) physically drops it. Without a TTL, entries live\n * until evicted by capacity. The cutoff is half-open: an entry exactly\n * `ttlMs` old is expired (`age >= ttlMs` drops it), matching `rate-limit`'s\n * window edge — a key remembered at `t` no longer counts at `t + ttlMs`.\n * - **`capacity`** — at most `capacity` entries are retained. When a\n * `remember` would push the count past `capacity` (after expiry eviction),\n * the OLDEST entries by insertion `order` are dropped until the count fits.\n * Without a capacity, the store grows unbounded (TTL alone bounds it).\n *\n * `order` is the insertion-order index that capacity eviction reads — the keys\n * oldest-first. It is the store's own LRU-by-insertion ledger, kept in lockstep\n * with `entries` so `entries` never needs to be sorted to find the oldest.\n *\n * **Overwrite semantics** (decided + pinned by tests): re-`remember`ing an\n * existing key is an UPDATE, not a no-op. It (1) overwrites `value`, (2)\n * refreshes `atMs` to the new `nowMs` — so the TTL clock restarts and a\n * frequently-seen key never expires — and (3) moves the key to the NEWEST\n * position in `order` — so capacity eviction treats a re-remembered key as\n * fresh, not as a stale candidate for eviction. This makes `remember` behave\n * like \"touch\": a key actively being re-seen is kept alive on both the TTL and\n * capacity axes.\n *\n * NOT exported from the package root — reached via the `@demlik/tea/idempotency`\n * subpath, same one-shape-per-package rule as `work-queue`, `rate-limit`, and\n * `do`.\n */\n\n/**\n * A cached entry: the result `value` the original request produced and the\n * clock reading `atMs` it was remembered at. `atMs` is the TTL anchor — expiry\n * is measured from here, and a re-`remember` refreshes it. Both fields are\n * plain data so the whole store is trivially JSON-serializable into a\n * consumer's `Store<S>`.\n */\nexport interface IdempotencyEntry<V> {\n readonly value: V;\n readonly atMs: number;\n}\n\n/**\n * The dedupe store: a map of `key → entry`, an insertion-`order` ledger\n * (oldest-first) that capacity eviction reads, and the two optional bounds.\n *\n * `entries` and `order` are kept in lockstep — every key in `order` has an\n * entry and vice versa. `capacity` / `ttlMs` are carried on the store (not\n * passed per-op) so a single value fully describes the policy; a consumer\n * persists the store and rehydrates the same bounds without re-deriving them.\n */\nexport interface IdempotencyStore<V> {\n readonly entries: Readonly<Record<string, IdempotencyEntry<V>>>;\n readonly order: readonly string[];\n readonly capacity?: number;\n readonly ttlMs?: number;\n}\n\n/**\n * Create an empty store. `capacity` and `ttlMs` are both optional — omit both\n * for an unbounded cache, supply either or both to bound by count, age, or\n * both. Stored on the returned value so every later op reads the policy off\n * the store rather than from a re-passed argument.\n */\nexport function initStore<V>(opts?: {\n capacity?: number;\n ttlMs?: number;\n}): IdempotencyStore<V> {\n return {\n entries: {},\n order: [],\n capacity: opts?.capacity,\n ttlMs: opts?.ttlMs,\n };\n}\n\n/**\n * Whether `entry` (remembered at `entry.atMs`) is expired at `nowMs` under\n * `ttlMs`. Half-open: an entry exactly `ttlMs` old is expired (`age >= ttlMs`),\n * matching `rate-limit`'s window-edge cutoff. A `nowMs` before `atMs` (clock\n * skewed backwards) yields a negative age — never expired, the safe direction\n * (a duplicate is still deduped rather than re-run). When `ttlMs` is undefined\n * the entry never expires by age.\n */\nfunction isExpired<V>(\n entry: IdempotencyEntry<V>,\n nowMs: number,\n ttlMs: number | undefined,\n): boolean {\n if (ttlMs === undefined) return false;\n return nowMs - entry.atMs >= ttlMs;\n}\n\n/**\n * True iff `key` is present AND not expired at `nowMs`. This is the dedupe\n * predicate: a webhook / retried message / repeated action whose key returns\n * `true` has already been processed and should be short-circuited (paired with\n * `recall` to return the original result). PURE — reads only, never mutates.\n */\nexport function seen<V>(\n store: IdempotencyStore<V>,\n key: string,\n nowMs: number,\n): boolean {\n const entry = store.entries[key];\n if (entry === undefined) return false;\n return !isExpired(entry, nowMs, store.ttlMs);\n}\n\n/**\n * The cached `value` for `key` if present and unexpired at `nowMs`, else\n * `undefined`. Use after `seen` returns `true` to replay the original result to\n * a duplicate request. An expired entry reads as absent (returns `undefined`)\n * even though it is physically still in `entries` until the next `remember` /\n * `evictExpired` drops it. PURE — reads only, never mutates.\n */\nexport function recall<V>(\n store: IdempotencyStore<V>,\n key: string,\n nowMs: number,\n): V | undefined {\n const entry = store.entries[key];\n if (entry === undefined || isExpired(entry, nowMs, store.ttlMs))\n return undefined;\n return entry.value;\n}\n\n/**\n * Drop every entry expired at `nowMs` (older than `ttlMs`). Returns a NEW store\n * with the survivors; `order` is rebuilt to drop the same keys, preserving the\n * relative insertion order of the survivors. A no-TTL store returns an\n * equivalent store (nothing ages out). PURE — the input store is never mutated.\n */\nexport function evictExpired<V>(\n store: IdempotencyStore<V>,\n nowMs: number,\n): IdempotencyStore<V> {\n if (store.ttlMs === undefined) return store;\n const nextEntries: Record<string, IdempotencyEntry<V>> = {};\n const nextOrder: string[] = [];\n // Walk `order` (not `Object.keys`) so the surviving order ledger keeps the\n // exact relative insertion order capacity eviction depends on.\n for (const key of store.order) {\n const entry = store.entries[key];\n if (entry === undefined) continue; // defensive: order/entries kept in lockstep\n if (isExpired(entry, nowMs, store.ttlMs)) continue;\n nextEntries[key] = entry;\n nextOrder.push(key);\n }\n return { ...store, entries: nextEntries, order: nextOrder };\n}\n\n/**\n * Record `key → value` at `nowMs`, then enforce both bounds. PURE — returns a\n * new store; the input is untouched.\n *\n * Order of operations (each step matters):\n * 1. **Expiry eviction** — drop everything older than `ttlMs` first, so an\n * expired key never occupies a capacity slot the new entry could take, and\n * a stale entry under the same `key` is cleared before the overwrite.\n * 2. **Insert / overwrite** — write the new entry. If `key` already exists,\n * this is an UPDATE: `value` is overwritten, `atMs` is refreshed to\n * `nowMs` (restarting the TTL clock), and the key is moved to the NEWEST\n * position in `order` (removed from wherever it sat, appended at the end).\n * See the module-level \"Overwrite semantics\" note.\n * 3. **Capacity eviction** — if the entry count now exceeds `capacity`, drop\n * the OLDEST keys by `order` until the count fits. Because step 2 moves a\n * re-remembered key to the newest position, an actively re-seen key is\n * never the eviction victim.\n */\nexport function remember<V>(\n store: IdempotencyStore<V>,\n key: string,\n value: V,\n nowMs: number,\n): IdempotencyStore<V> {\n // Step 1 — expire first so stale entries free capacity slots before insert.\n const pruned = evictExpired(store, nowMs);\n\n // Step 2 — insert/overwrite. Drop any prior position of `key` from order,\n // then append it as newest. `delete` on a fresh shallow copy keeps the input\n // store untouched (invariant 1).\n const nextEntries: Record<string, IdempotencyEntry<V>> = {\n ...pruned.entries,\n };\n nextEntries[key] = { value, atMs: nowMs };\n const nextOrder = pruned.order.filter((k) => k !== key);\n nextOrder.push(key);\n\n // Step 3 — capacity eviction. Drop oldest (front of `order`) until the count\n // fits. `capacity <= 0` evicts everything just inserted down to the bound;\n // `undefined` capacity skips eviction entirely.\n if (store.capacity !== undefined) {\n while (nextOrder.length > store.capacity) {\n const oldest = nextOrder.shift();\n if (oldest === undefined) break; // order emptied — nothing left to evict\n delete nextEntries[oldest];\n }\n }\n\n return { ...store, entries: nextEntries, order: nextOrder };\n}\n"]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}