sygnal 5.4.0 → 6.0.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 (316) hide show
  1. package/CHANGELOG.md +646 -0
  2. package/README.md +24 -9
  3. package/dist/astro/client.cjs.js +16 -9
  4. package/dist/astro/client.cjs.js.map +1 -1
  5. package/dist/astro/client.mjs +16 -9
  6. package/dist/astro/client.mjs.map +1 -1
  7. package/dist/astro/index.cjs.js +362 -91
  8. package/dist/astro/index.cjs.js.map +1 -1
  9. package/dist/astro/index.mjs +362 -92
  10. package/dist/astro/index.mjs.map +1 -1
  11. package/dist/astro/server.cjs.js +3396 -165
  12. package/dist/astro/server.cjs.js.map +1 -1
  13. package/dist/astro/server.mjs +3396 -165
  14. package/dist/astro/server.mjs.map +1 -1
  15. package/dist/devtools.cjs.js +1431 -0
  16. package/dist/devtools.cjs.js.map +1 -0
  17. package/dist/devtools.esm.js +1419 -0
  18. package/dist/devtools.esm.js.map +1 -0
  19. package/dist/diagnostics.cjs.js +3032 -296
  20. package/dist/diagnostics.cjs.js.map +1 -1
  21. package/dist/diagnostics.esm.js +3032 -296
  22. package/dist/diagnostics.esm.js.map +1 -1
  23. package/dist/element.cjs.js +230 -0
  24. package/dist/element.cjs.js.map +1 -0
  25. package/dist/element.esm.js +228 -0
  26. package/dist/element.esm.js.map +1 -0
  27. package/dist/guide/accessibility.md +257 -0
  28. package/dist/guide/adapters.md +221 -0
  29. package/dist/guide/behaviors.md +312 -0
  30. package/dist/guide/browser-sources.md +203 -0
  31. package/dist/guide/drag-and-drop.md +342 -0
  32. package/dist/guide/element-commands.md +306 -0
  33. package/dist/guide/error-boundaries.md +169 -0
  34. package/dist/guide/forms-reference.md +381 -0
  35. package/dist/guide/forms.md +250 -0
  36. package/dist/guide/http.md +299 -0
  37. package/dist/guide/inputs.md +173 -0
  38. package/dist/guide/persistence.md +245 -0
  39. package/dist/guide/recipes/carousel.md +155 -0
  40. package/dist/guide/recipes/charts.md +177 -0
  41. package/dist/guide/recipes/code-editor.md +135 -0
  42. package/dist/guide/recipes/data-grid.md +155 -0
  43. package/dist/guide/recipes/data-table.md +196 -0
  44. package/dist/guide/recipes/i18n.md +222 -0
  45. package/dist/guide/recipes/icons.md +115 -0
  46. package/dist/guide/recipes/overview.md +33 -0
  47. package/dist/guide/recipes/rich-text.md +154 -0
  48. package/dist/guide/resources.md +471 -0
  49. package/dist/guide/ssr.md +255 -0
  50. package/dist/guide/timers.md +178 -0
  51. package/dist/guide/ui/accordion.md +93 -0
  52. package/dist/guide/ui/combobox.md +144 -0
  53. package/dist/guide/ui/dialog.md +124 -0
  54. package/dist/guide/ui/disclosure.md +60 -0
  55. package/dist/guide/ui/menu.md +125 -0
  56. package/dist/guide/ui/overview.md +166 -0
  57. package/dist/guide/ui/popover.md +101 -0
  58. package/dist/guide/ui/select.md +114 -0
  59. package/dist/guide/ui/tabs.md +161 -0
  60. package/dist/guide/ui/toaster.md +152 -0
  61. package/dist/guide/ui/tooltip.md +103 -0
  62. package/dist/guide/undo.md +178 -0
  63. package/dist/guide/virtual-collections.md +183 -0
  64. package/dist/guide/web-components.md +179 -0
  65. package/dist/guide/widgets.md +217 -0
  66. package/dist/index.cjs.js +13178 -5769
  67. package/dist/index.cjs.js.map +1 -1
  68. package/dist/index.d.ts +2908 -276
  69. package/dist/index.esm.js +13149 -5779
  70. package/dist/index.esm.js.map +1 -1
  71. package/dist/jsx-dev-runtime.cjs.js +26 -334
  72. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  73. package/dist/jsx-dev-runtime.esm.js +19 -327
  74. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  75. package/dist/jsx-runtime.cjs.js +26 -334
  76. package/dist/jsx-runtime.cjs.js.map +1 -1
  77. package/dist/jsx-runtime.esm.js +19 -327
  78. package/dist/jsx-runtime.esm.js.map +1 -1
  79. package/dist/jsx.cjs.js +10 -314
  80. package/dist/jsx.cjs.js.map +1 -1
  81. package/dist/jsx.esm.js +8 -315
  82. package/dist/jsx.esm.js.map +1 -1
  83. package/dist/react.cjs.js +117 -0
  84. package/dist/react.cjs.js.map +1 -0
  85. package/dist/react.esm.js +115 -0
  86. package/dist/react.esm.js.map +1 -0
  87. package/dist/shims/globalthis.cjs +20 -0
  88. package/dist/sygnal.min.js +1 -1
  89. package/dist/sygnal.min.js.map +1 -1
  90. package/dist/ui-combobox.cjs.js +169 -0
  91. package/dist/ui-combobox.cjs.js.map +1 -0
  92. package/dist/ui-combobox.esm.js +148 -0
  93. package/dist/ui-combobox.esm.js.map +1 -0
  94. package/dist/ui-menu.cjs.js +87 -0
  95. package/dist/ui-menu.cjs.js.map +1 -0
  96. package/dist/ui-menu.esm.js +66 -0
  97. package/dist/ui-menu.esm.js.map +1 -0
  98. package/dist/ui-select.cjs.js +104 -0
  99. package/dist/ui-select.cjs.js.map +1 -0
  100. package/dist/ui-select.esm.js +83 -0
  101. package/dist/ui-select.esm.js.map +1 -0
  102. package/dist/ui.cjs.js +738 -0
  103. package/dist/ui.cjs.js.map +1 -0
  104. package/dist/ui.esm.js +727 -0
  105. package/dist/ui.esm.js.map +1 -0
  106. package/dist/vike/ClientOnly.cjs.js +4 -14
  107. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  108. package/dist/vike/ClientOnly.mjs +4 -14
  109. package/dist/vike/ClientOnly.mjs.map +1 -1
  110. package/dist/vike/{+config.js → config/+config.js} +9 -1
  111. package/dist/vike/config/+config.js.map +1 -0
  112. package/dist/vike/config/package.json +6 -0
  113. package/dist/vike/onRenderClient.cjs.js +165 -95
  114. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  115. package/dist/vike/onRenderClient.mjs +165 -95
  116. package/dist/vike/onRenderClient.mjs.map +1 -1
  117. package/dist/vike/onRenderHtml.cjs.js +59 -24
  118. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  119. package/dist/vike/onRenderHtml.mjs +60 -25
  120. package/dist/vike/onRenderHtml.mjs.map +1 -1
  121. package/dist/vite/plugin.cjs.js +323 -88
  122. package/dist/vite/plugin.cjs.js.map +1 -1
  123. package/dist/vite/plugin.mjs +323 -89
  124. package/dist/vite/plugin.mjs.map +1 -1
  125. package/dist/zag.cjs.js +408 -0
  126. package/dist/zag.cjs.js.map +1 -0
  127. package/dist/zag.esm.js +405 -0
  128. package/dist/zag.esm.js.map +1 -0
  129. package/llms.txt +166 -103
  130. package/package.json +98 -14
  131. package/src/astro/client.ts +27 -16
  132. package/src/astro/index.d.ts +24 -0
  133. package/src/astro/index.ts +69 -3
  134. package/src/astro/server.ts +8 -2
  135. package/src/collection.ts +6 -93
  136. package/src/core/actions.ts +134 -0
  137. package/src/core/cell.ts +202 -0
  138. package/src/core/debug.ts +23 -0
  139. package/src/core/define.ts +242 -0
  140. package/src/core/hooks.ts +314 -0
  141. package/src/core/hosts/collection.ts +325 -0
  142. package/src/core/hosts/switchable.ts +146 -0
  143. package/src/core/instance.ts +592 -0
  144. package/src/core/markers/clientonly.ts +13 -0
  145. package/src/core/markers/lazy.ts +40 -0
  146. package/src/core/markers/portal.ts +96 -0
  147. package/src/core/markers/suspense.ts +41 -0
  148. package/src/core/markers/transition.ts +70 -0
  149. package/src/core/registry.ts +25 -0
  150. package/src/core/runtime.ts +573 -0
  151. package/src/core/statics.ts +104 -0
  152. package/src/core/teardown.ts +60 -0
  153. package/src/core/view.ts +37 -0
  154. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  155. package/src/cycle/dom/ElementFinder.ts +7 -9
  156. package/src/cycle/dom/EventDelegator.ts +174 -223
  157. package/src/cycle/dom/IsolateModule.ts +65 -35
  158. package/src/cycle/dom/MainDOMSource.ts +9 -2
  159. package/src/cycle/dom/PriorityQueue.ts +4 -2
  160. package/src/cycle/dom/SymbolTree.ts +18 -47
  161. package/src/cycle/dom/classNameModule.ts +9 -7
  162. package/src/cycle/dom/controlledInputModule.ts +23 -6
  163. package/src/cycle/dom/enrichEventStream.ts +25 -47
  164. package/src/cycle/dom/fragment.ts +7 -0
  165. package/src/cycle/dom/isolate.ts +30 -27
  166. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  167. package/src/cycle/dom/mockDOMSource.ts +21 -1
  168. package/src/cycle/dom/modules.ts +16 -3
  169. package/src/cycle/dom/propsModule.ts +65 -0
  170. package/src/cycle/dom/selectModule.ts +22 -17
  171. package/src/cycle/dom/snabbdom.ts +1 -6
  172. package/src/cycle/dom/thunk.ts +3 -0
  173. package/src/cycle/dom/utils.ts +98 -0
  174. package/src/cycle/dom/viewTransition.ts +43 -0
  175. package/src/cycle/state/StateSource.ts +19 -3
  176. package/src/cycle/state/objIsEqual.ts +50 -0
  177. package/src/cycle/state/types.ts +0 -10
  178. package/src/defineComponent.ts +34 -0
  179. package/src/devtools.d.ts +151 -0
  180. package/src/devtools.ts +34 -0
  181. package/src/element.d.ts +59 -0
  182. package/src/element.ts +235 -0
  183. package/src/extra/backoff.ts +9 -0
  184. package/src/extra/behaviors.ts +169 -0
  185. package/src/extra/browserSignals.ts +23 -0
  186. package/src/extra/browserSources.ts +326 -0
  187. package/src/extra/command.ts +3 -3
  188. package/src/extra/controls.ts +47 -0
  189. package/src/extra/copyAsTest.ts +286 -0
  190. package/src/extra/devtools.ts +101 -31
  191. package/src/extra/devtoolsActions.ts +379 -0
  192. package/src/extra/devtoolsHook.ts +9 -0
  193. package/src/extra/devtoolsNext.ts +83 -0
  194. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  195. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  196. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  197. package/src/extra/diagnostics/checks/collections.ts +1 -1
  198. package/src/extra/diagnostics/checks/controls.ts +148 -0
  199. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  200. package/src/extra/diagnostics/checks/dom.ts +20 -11
  201. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  202. package/src/extra/diagnostics/checks/events.ts +17 -2
  203. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  204. package/src/extra/diagnostics/checks/forms.ts +163 -0
  205. package/src/extra/diagnostics/checks/index.ts +97 -4
  206. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  207. package/src/extra/diagnostics/checks/next.ts +417 -0
  208. package/src/extra/diagnostics/checks/persist.ts +54 -0
  209. package/src/extra/diagnostics/checks/props.ts +10 -5
  210. package/src/extra/diagnostics/checks/public.d.ts +98 -7
  211. package/src/extra/diagnostics/checks/replies.ts +128 -0
  212. package/src/extra/diagnostics/checks/router.ts +100 -0
  213. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  214. package/src/extra/diagnostics/checks/shared.ts +86 -2
  215. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  216. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  217. package/src/extra/diagnostics/checks/state.ts +100 -7
  218. package/src/extra/diagnostics/checks/statics.ts +48 -0
  219. package/src/extra/diagnostics/checks/strict.ts +10 -67
  220. package/src/extra/diagnostics/checks/timers.ts +80 -0
  221. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  222. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  223. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  224. package/src/extra/diagnostics/checks/wiring.ts +66 -10
  225. package/src/extra/diagnostics/codes.ts +268 -23
  226. package/src/extra/diagnostics/index.ts +34 -9
  227. package/src/extra/diagnostics/legacy.ts +17 -0
  228. package/src/extra/driverFactories.ts +83 -19
  229. package/src/extra/elementCommands.ts +53 -0
  230. package/src/extra/fetchDriver.ts +519 -0
  231. package/src/extra/focusWithin.ts +27 -0
  232. package/src/extra/form.ts +268 -0
  233. package/src/extra/formHelpers.ts +139 -0
  234. package/src/extra/head.ts +106 -0
  235. package/src/extra/hmr.ts +2 -3
  236. package/src/extra/owned.ts +24 -0
  237. package/src/extra/pager.ts +50 -0
  238. package/src/extra/persist.ts +133 -0
  239. package/src/extra/pwa.ts +1 -1
  240. package/src/extra/queryCache.ts +107 -0
  241. package/src/extra/reducers.ts +1 -1
  242. package/src/extra/reduxDevtools.ts +92 -0
  243. package/src/extra/replies.ts +54 -0
  244. package/src/extra/router.ts +323 -0
  245. package/src/extra/run.ts +90 -217
  246. package/src/extra/selection.ts +76 -0
  247. package/src/extra/socketDriver.ts +265 -0
  248. package/src/extra/sortable.ts +377 -0
  249. package/src/extra/ssr.ts +427 -160
  250. package/src/extra/standardSchema.ts +27 -0
  251. package/src/extra/testing.ts +2433 -175
  252. package/src/extra/timers.ts +95 -0
  253. package/src/extra/undo.ts +261 -0
  254. package/src/extra/viewTransitions.ts +38 -0
  255. package/src/extra/virtual.ts +513 -0
  256. package/src/extra/widget.ts +201 -0
  257. package/src/index.d.ts +2631 -117
  258. package/src/index.ts +25 -4
  259. package/src/jsx-runtime.ts +15 -1
  260. package/src/jsx.ts +2 -1
  261. package/src/lazy.ts +50 -7
  262. package/src/portal.ts +3 -1
  263. package/src/pragma/index.ts +256 -133
  264. package/src/react-peers.d.ts +5 -0
  265. package/src/react.d.ts +43 -0
  266. package/src/react.ts +110 -0
  267. package/src/shared.ts +48 -0
  268. package/src/slot.ts +1 -1
  269. package/src/suspense.ts +3 -1
  270. package/src/switchable.ts +6 -121
  271. package/src/transition.ts +3 -1
  272. package/src/ui/accordion.ts +64 -0
  273. package/src/ui/dialog.ts +129 -0
  274. package/src/ui/disclosure.ts +35 -0
  275. package/src/ui/popover.ts +45 -0
  276. package/src/ui/shared.ts +94 -0
  277. package/src/ui/tabs.ts +82 -0
  278. package/src/ui/toaster.ts +198 -0
  279. package/src/ui/tooltip.ts +70 -0
  280. package/src/ui/zag/combobox.ts +115 -0
  281. package/src/ui/zag/menu.ts +40 -0
  282. package/src/ui/zag/select.ts +54 -0
  283. package/src/ui/zag/shared.ts +43 -0
  284. package/src/ui-combobox.d.ts +20 -0
  285. package/src/ui-combobox.ts +6 -0
  286. package/src/ui-menu.d.ts +30 -0
  287. package/src/ui-menu.ts +6 -0
  288. package/src/ui-select.d.ts +17 -0
  289. package/src/ui-select.ts +6 -0
  290. package/src/ui-zag-types.d.ts +42 -0
  291. package/src/ui.d.ts +208 -0
  292. package/src/ui.ts +15 -0
  293. package/src/vike/+config.ts +9 -1
  294. package/src/vike/ClientOnly.ts +1 -1
  295. package/src/vike/onRenderClient.ts +144 -96
  296. package/src/vike/onRenderHtml.ts +69 -26
  297. package/src/vike/types.ts +7 -1
  298. package/src/vite/globalthis-shim.ts +18 -0
  299. package/src/vite/globalthis.ts +56 -0
  300. package/src/vite/plugin.d.ts +22 -3
  301. package/src/vite/plugin.ts +222 -24
  302. package/src/zag.d.ts +64 -0
  303. package/src/zag.ts +238 -0
  304. package/dist/vike/+config.cjs.js +0 -66
  305. package/dist/vike/+config.cjs.js.map +0 -1
  306. package/dist/vike/+config.js.map +0 -1
  307. package/src/component.ts +0 -2301
  308. package/src/cycle/isolate/index.ts +0 -196
  309. package/src/cycle/run/index.ts +0 -151
  310. package/src/cycle/run/internals.ts +0 -143
  311. package/src/cycle/state/Collection.ts +0 -184
  312. package/src/cycle/state/index.ts +0 -5
  313. package/src/cycle/state/pickCombine.ts +0 -201
  314. package/src/cycle/state/pickMerge.ts +0 -122
  315. package/src/cycle/state/withState.ts +0 -47
  316. package/src/pragma/fn.ts +0 -55
@@ -0,0 +1,133 @@
1
+ import {warn} from './diagnostics/legacy';
2
+
3
+ /*
4
+ * PLAN-4 GS-5 (D114): state persistence for the root component, as a helper value:
5
+ *
6
+ * TodoApp.persist = persist({ key: 'todo-app', pick: ['todos', 'filter'], version: 2, migrate })
7
+ *
8
+ * The core has no persistence code: a root component calls `persist.setup(this)` once, in its
9
+ * root setup (src/core/runtime.ts rootShim); an app that never imports persist() pays 0 B.
10
+ *
11
+ * - Stored as JSON `{ version, state }` under `key`; `state` has the picked top-level keys
12
+ * (`pick`), or all but `omit` and the calculated fields. A `uses` key whose behavior is marked
13
+ * `persist: false` (sortable: drag state) is left out of both the save and the restore (the root's own
14
+ * `uses` only: a sub-component's or Collection item's behavior slice is part of its data, 3-L G-476). `format: 'plain'` (4-G1, D143) stores
15
+ * that object itself, with no envelope; `version` / `migrate` then don't apply (a type error;
16
+ * ignored here), and a stored value that isn't an object is ignored.
17
+ * - Restore: a synchronous read before INITIALIZE, merged into initialState, so the restore is
18
+ * part of the initial-state action. A stored `version` other than `version` (default 1) goes
19
+ * through `migrate(old, fromVersion)`; without migrate, or when it returns nothing, the entry
20
+ * is ignored. Hydrating server HTML, the restore is a RESTORE action after the first state
21
+ * instead, so the first render matches the server's. Hydrating: the `hydrate` option when given,
22
+ * else the integration's `__hydrate` source (Astro: a server-rendered island; Vike: a hydration),
23
+ * else whether run()'s mount point (its `__m` source) starts with renderToString's markup (the
24
+ * root element carries `data-sygnal-ssr`; 3-R).
25
+ * - Writes: debounced (`debounceMs`, default 100), skipped when the stored text is the same,
26
+ * flushed on `pagehide` and on dispose.
27
+ * - `PERSIST: { clear: true }` in any model entry's object form (a value or a function of
28
+ * (state, data)) removes the stored copy; the state that same action produces isn't written
29
+ * (later changes are). The entry's PERSIST sink is turned into an EFFECT here, so it needs no
30
+ * driver. An 'ACTION | PERSIST' key (non-canonical) isn't handled (3-B2).
31
+ * - `sync: true`: another tab's write is applied through RESTORE (the `storage` event, or an
32
+ * adapter's `subscribe(fn)`).
33
+ * - `storage`: 'local' (default), 'session', or a synchronous { getItem, setItem, removeItem }
34
+ * adapter (optionally with subscribe). renderComponent passes its fake stores as the
35
+ * `__storage` source ({ local, session, f: flush functions t.settle() calls }).
36
+ * - Failures (unreadable entry, migrate throws, quota) are SYG642 (warn, printed in production
37
+ * too), once per kind (restore / save / clear; a save that works again re-arms it); the app
38
+ * continues on initialState / unsaved, and a failed save is retried at the next change.
39
+ */
40
+ const g: any = globalThis;
41
+
42
+ export const setupPersist = (c: any, o: any): void => {
43
+ const {key, pick, omit, version = 1, migrate, sync, debounceMs = 100} = o, plain = o.format == 'plain', src = c.sources, m = src.__m;
44
+ const env = src.__storage, calc = c.calculated || {};
45
+ // hydrating: the option, else the integration's signal (__hydrate: Astro, Vike), else whether
46
+ // run()'s mount point starts with renderToString's markup (its root element is marked; a
47
+ // client-only app's loading placeholder isn't server markup)
48
+ const hydrate = o.hydrate ?? src.__hydrate ?? !!(typeof m == 'string' ? g.document?.querySelector(m) : m)?.firstElementChild?.hasAttribute('data-sygnal-ssr');
49
+ let S: any, t: any, last: any, raw: any, skip: any, off: any;
50
+ // (the docs link the message ends with explains the causes and fixes). Once per kind (restore,
51
+ // save, clear): a storage that throws on every write must not warn on every change; a save that
52
+ // works again re-arms the save report
53
+ const bad: any = {};
54
+ const fail = (what: string, e?: any) => bad[what] || (bad[what] = 1, warn('SYG642', c, `persist '${key}': ${what} failed`, undefined, e));
55
+ // a behavior's slice marked `persist: false` (UI state: sortable's drag) is never saved or restored (3-H G-452)
56
+ const uses = c.view?.uses || {};
57
+ const only = (s: any) => {
58
+ const out: any = {};
59
+ for (const k in s) if ((pick ? pick.includes(k) : !omit?.includes(k)) && !(k in calc) && uses[k]?.persist !== false) out[k] = s[k];
60
+ return out;
61
+ };
62
+ const read = (r: any) => {
63
+ try {
64
+ if (r == null) return;
65
+ const j = JSON.parse(r);
66
+ if (plain) return j && typeof j == 'object' && !Array.isArray(j) ? only(j) : undefined;
67
+ const {version: v = 1, state} = j, s = v === version ? state : migrate && migrate(state, v);
68
+ return s && only(s);
69
+ } catch (e) { fail('restore', e); }
70
+ };
71
+ const write = () => {
72
+ clearTimeout(t); t = 0;
73
+ const r = JSON.stringify(plain ? only(last) : {version, state: only(last)}), p = raw;
74
+ // a failed save is retried at the next change (raw stays the stored text)
75
+ if (r != raw) try { raw = r; S.setItem(key, r); bad.save = 0; } catch (e) { raw = p; fail('save', e); }
76
+ };
77
+ const clear = () => {
78
+ clearTimeout(t); t = 0; skip = 1; raw = null;
79
+ setTimeout(() => skip = 0);
80
+ try { S?.removeItem(key); } catch (e) { fail('clear', e); }
81
+ };
82
+ const send = (d: any) => d && c.action$.shamefullySendNext({type: 'RESTORE', data: d});
83
+ // PERSIST sinks become EFFECTs (no driver); a model RESTORE entry replaces the built-in one
84
+ const wrap = (p: any, e?: any) => (...a: any[]) => { const r = e?.(...a), v = typeof p == 'function' ? p(...a) : p; v?.clear && clear(); return r; };
85
+ const model: any = {RESTORE: (s: any, d: any) => ({...s, ...d})};
86
+ for (const k in c.model) {
87
+ let v = c.model[k];
88
+ if (v && typeof v == 'object' && 'PERSIST' in v) { const {PERSIST, ...rest} = v; v = {...rest, EFFECT: wrap(PERSIST, rest.EFFECT)}; }
89
+ model[k] = v;
90
+ }
91
+ c.model = model;
92
+ try { S = typeof o.storage == 'object' ? o.storage : (env || g)[(o.storage || 'local') + (env ? '' : 'Storage')]; } catch (_) {}
93
+ if (!S) return;
94
+ try {
95
+ raw = S.getItem(key);
96
+ if (!hydrate) { const r = read(raw); if (r) c.initialState = {...c.initialState, ...r}; }
97
+ } catch (e) { fail('restore', e); }
98
+ const state$ = c.sources[c.stateSourceName].stream;
99
+ let first = 1;
100
+ const L = {next: (s: any) => {
101
+ last = s;
102
+ // the initial state (restored or not) isn't written back. hydrate: it is the server's; restore
103
+ // once it has rendered (the DOM driver patches before this listener hears the vtree)
104
+ if (first) {
105
+ first = 0;
106
+ let once = hydrate;
107
+ const v$ = c.vdom$, l = {next: () => once && setTimeout(() => (v$.removeListener(l), send(read(raw))), once = 0)};
108
+ return hydrate && v$.addListener(l);
109
+ }
110
+ if (!skip) clearTimeout(t), t = setTimeout(write, debounceMs);
111
+ }};
112
+ state$.addListener(L);
113
+ const flush = () => t && write();
114
+ const onSync = (k: any, v: any) => { if (k == key && v != raw) send(read(raw = v)); };
115
+ const onStorage = (e: any) => (!e.storageArea || e.storageArea == S) && onSync(e.key, e.newValue);
116
+ if (sync) off = S.subscribe ? S.subscribe(onSync) : (g.addEventListener?.('storage', onStorage), () => g.removeEventListener?.('storage', onStorage));
117
+ g.addEventListener?.('pagehide', flush);
118
+ env?.f.add(flush);
119
+ c._dispose$.addListener({next: () => {
120
+ flush();
121
+ state$.removeListener(L);
122
+ off?.();
123
+ g.removeEventListener?.('pagehide', flush);
124
+ env?.f.delete(flush);
125
+ }});
126
+ };
127
+
128
+ /**
129
+ * `App.persist = persist({ key, pick | omit, version, migrate, format, storage, sync, hydrate, debounceMs })`:
130
+ * the root component's state, saved to localStorage (or sessionStorage, or an adapter) and
131
+ * restored at startup. Only an app that imports it pays for it.
132
+ */
133
+ export const persist = (o: any) => ({options: o, setup: (c: any) => setupPersist(c, o)});
package/src/extra/pwa.ts CHANGED
@@ -118,7 +118,7 @@ export function makeServiceWorkerDriver(
118
118
 
119
119
  let _onlineCleanup: (() => void) | undefined;
120
120
 
121
- export const onlineStatus$: Stream<boolean> = xs.create<boolean>({
121
+ export const onlineStatus$: Stream<boolean> = /*#__PURE__*/ xs.create<boolean>({
122
122
  start(listener: Listener<boolean>) {
123
123
  if (typeof window === 'undefined') {
124
124
  listener.next(true);
@@ -0,0 +1,107 @@
1
+ import {onBrowserSignals} from './browserSignals';
2
+ import {withQuery, hit} from './fetchDriver';
3
+
4
+ /*
5
+ * queryCache(options?) (PLAN-3 D88, 5-5): the opt-in query cache of makeFetchDriver, in its own
6
+ * export so apps that only make requests don't pay for it:
7
+ *
8
+ * makeFetchDriver({ cache: queryCache({ staleTime, gcTime, refetchOnFocus, refetchOnReconnect, initial }) })
9
+ *
10
+ * It owns the entries (key → { k, q (the request), v (data), at (fetched at), s (marked stale),
11
+ * n (resources using it), g (gc timer) }), gc, freshness (the stale-while-revalidate decision),
12
+ * the fetches identical requests share (de-duplication, `f`), entry invalidation, the focus /
13
+ * reconnect trigger and SSR seeding. The driver calls its hooks (key, fresh, get, put, use,
14
+ * inval, on, list) and keeps resources, polling, retries and validation.
15
+ *
16
+ * - Keys: method + URL as written (the driver's baseUrl left out, query sorted) + body + parse.
17
+ * Cacheable: a resource's GET/HEAD request, and any request with `cache: true` or `staleTime`
18
+ * (`cache: false` opts out; a `parse` function / 'response' or a non-string body never is).
19
+ * - SSR seeding (H-7): `dehydrate()` returns a JSON-safe snapshot `[{ key, data, updatedAt, tags }]`
20
+ * of the entries with data; `hydrate(snapshot)` / `queryCache({ initial })` write it back (an
21
+ * entry newer than the snapshot's is kept). `set(request, data)` writes one entry (a loader on
22
+ * the server). Entries keep their `updatedAt`, so a seeded entry is fresh for `staleTime`.
23
+ * - `prefetch(request)`: the attached driver (the last makeFetchDriver given this cache) fetches
24
+ * the request into the cache without a reply, like the `{ prefetch }` sink command.
25
+ */
26
+
27
+ const IDEMPOTENT = /^(GET|HEAD)$/i;
28
+
29
+ export function queryCache(o: any = {}): any {
30
+ const map = new Map<string, any>();
31
+ const now = () => Date.now();
32
+ const gc = (E: any) => {
33
+ clearTimeout(E.g);
34
+ const ms = o.gcTime ?? 3e5;
35
+ // unref: a server's seeded cache never keeps Node alive
36
+ if (ms < 1 / 0) (E.g = setTimeout(() => E.n || map.delete(E.k), ms) as any).unref?.();
37
+ };
38
+ const entry = (k: string, q: any) => {
39
+ let E = map.get(k);
40
+ if (!E) map.set(k, (E = {k, q, n: 0}));
41
+ return E;
42
+ };
43
+ // the key of a cacheable request (else undefined); `res`: a resource's
44
+ const key = (q: any, res?: any) => {
45
+ if (typeof q == 'string') q = {url: q};
46
+ const {method, json, body, parse, query} = q;
47
+ const m = (method || (json !== undefined || body !== undefined ? 'POST' : 'GET')).toUpperCase();
48
+ const b = json !== undefined ? JSON.stringify(json) : body ?? '';
49
+ if (q.cache === false || typeof b != 'string' || typeof parse == 'function' || parse == 'response' ||
50
+ !(q.cache || q.staleTime != null || (res && IDEMPOTENT.test(m)))) return;
51
+ return [m, withQuery(q.url ?? '', query, 1), b, parse].join(' ').trim();
52
+ };
53
+ const fresh = (k: any, q: any) => {
54
+ const E = map.get(k);
55
+ return E?.at > 0 && !E.s && now() - E.at < (q?.staleTime ?? o.staleTime ?? 0);
56
+ };
57
+ const put = (k: string, q: any, v: any, at = now()) => {
58
+ const E = entry(k, q);
59
+ E.v = v;
60
+ E.at = at;
61
+ E.s = 0;
62
+ E.q = q;
63
+ E.n || gc(E);
64
+ };
65
+ const C: any = {
66
+ key,
67
+ fresh,
68
+ put,
69
+ get: (k: any) => map.get(k),
70
+ // the fetches identical cacheable requests share (the driver's flight records), key → flight
71
+ f: new Map(),
72
+ // resource R now uses key k (or none): the entries' subscriber counts, gc when unused
73
+ use: (R: any, k?: string) => {
74
+ if (R.k === k) return;
75
+ const O = map.get(R.k);
76
+ if (O && !--O.n) gc(O);
77
+ if ((R.k = k) !== undefined) {
78
+ const E = entry(k as string, R.q);
79
+ E.n++;
80
+ clearTimeout(E.g);
81
+ }
82
+ },
83
+ // D80: marks the matching entries stale (`dry`: only counts them)
84
+ inval: (x: any, dry?: any) => {
85
+ let n = 0;
86
+ map.forEach(E => { if (hit(x, E.q)) { n++; dry || (E.s = 1); } });
87
+ return n;
88
+ },
89
+ // focus / reconnect call `fn` (the driver refetches its stale mounted resources); `on`: the
90
+ // test fake's signal source
91
+ on: (on: any, fn: () => void) => (on || onBrowserSignals)((sig: string) => {
92
+ o[sig == 'focus' ? 'refetchOnFocus' : 'refetchOnReconnect'] === false || fn();
93
+ }),
94
+ list: () => [...map.values()].map(E => ({key: E.k, age: E.at && now() - E.at, stale: !fresh(E.k, E.q), subscribers: E.n, data: E.v, tags: E.q?.tags})),
95
+ dehydrate: () => [...map.values()].filter(E => E.at > 0).map(E => ({key: E.k, data: E.v, updatedAt: E.at, tags: E.q?.tags})),
96
+ hydrate: (snap: any) => snap?.forEach?.((e: any) => {
97
+ if (!(map.get(e.key)?.at >= e.updatedAt)) put(e.key, {url: e.key.split(' ')[1], tags: e.tags}, e.data, e.updatedAt);
98
+ }),
99
+ set: (q: any, data: any) => {
100
+ const k = key(q, 1);
101
+ if (k !== undefined) put(k, typeof q == 'string' ? {url: q} : q, data);
102
+ },
103
+ prefetch: (q: any) => C.d?.({prefetch: q}),
104
+ };
105
+ C.hydrate(o.initial);
106
+ return C;
107
+ }
@@ -17,7 +17,7 @@
17
17
  * set((state, title) => ({ title }))
18
18
  */
19
19
  export function set<S = any>(
20
- partial: Partial<S> | ((state: S, data: any, next: Function, props: any) => Partial<S>)
20
+ partial: (Partial<S> & object) | ((state: S, data: any, next: Function, props: any) => Partial<S>)
21
21
  ): (state: S, data: any, next: Function, props: any) => S {
22
22
  if (typeof partial === 'function') {
23
23
  return (state, data, next, props) => ({
@@ -0,0 +1,92 @@
1
+ /**
2
+ * PLAN-4 3-E (GS-10 stretch): a dev-only bridge to the Redux DevTools extension
3
+ * (window.__REDUX_DEVTOOLS_EXTENSION__), in 'sygnal/devtools'. Sends each recorded action
4
+ * (./devtoolsActions; built-in ones left out) with the root's state after it, as
5
+ * `{ type: '<Component>/<ACTION>', payload, cause, instance }`. Jump to state / action in the
6
+ * extension replaces the root's state (the Sygnal DevTools time-travel path). sygnal/vite calls it
7
+ * in dev with sygnal({ devtools: { redux: true } }).
8
+ */
9
+ import {recordActions, onAction, resolveInstance, preview} from './devtoolsActions'
10
+ import type {DevtoolsAction} from './devtoolsActions'
11
+
12
+ export interface ReduxDevtoolsOptions {
13
+ /** the instance name in the extension (default 'Sygnal') */
14
+ name?: string
15
+ /** which actions to send (default: all but 'built-in' ones) */
16
+ filter?: (action: DevtoolsAction) => boolean
17
+ }
18
+
19
+ const clone = (s: any) => preview(s)
20
+
21
+ let shared: (() => void) | undefined
22
+
23
+ /**
24
+ * Connect the app's root (run()'s result, a component, an instance id; default: the newest root)
25
+ * to the Redux DevTools extension. Returns a disconnect function; does nothing without the
26
+ * extension.
27
+ */
28
+ export function connectReduxDevtools(target?: any, options?: ReduxDevtoolsOptions): () => void {
29
+ // the default connection (sygnal/vite may inject the call in several files) is made once
30
+ if (target === undefined && options === undefined) {
31
+ if (!shared) {
32
+ const off = connect(undefined, {})
33
+ shared = () => { off(); shared = undefined }
34
+ }
35
+ return shared
36
+ }
37
+ return connect(target, options || {})
38
+ }
39
+
40
+ function connect(target: any, options: ReduxDevtoolsOptions): () => void {
41
+ const ext = typeof window != 'undefined' && (window as any).__REDUX_DEVTOOLS_EXTENSION__
42
+ if (!ext || typeof ext.connect != 'function') return () => {}
43
+ recordActions()
44
+ const conn = ext.connect({name: options.name || 'Sygnal', features: {jump: true, pause: true, export: true}})
45
+ const keep = options.filter || ((a: DevtoolsAction) => a.cause != 'built-in')
46
+ let root: any, started = false, timer: any = null
47
+ const queue: DevtoolsAction[] = []
48
+
49
+ const rootOf = () => {
50
+ if (!root || root.disposed || !root.c) root = resolveInstance(target)
51
+ return root
52
+ }
53
+ const flush = () => {
54
+ timer = null
55
+ const r = start()
56
+ if (!r) return
57
+ for (const a of queue.splice(0)) {
58
+ const state = a.instance === r.id && 'after' in a ? a.after : r.c.currentState
59
+ conn.send({type: `${a.component}/${a.type}`, payload: preview(a.data), cause: a.cause, instance: a.instance}, clone(state))
60
+ }
61
+ }
62
+ const start = () => {
63
+ const r = rootOf()
64
+ if (r && !started) { started = true; conn.init(clone(r.c.currentState)) }
65
+ return r
66
+ }
67
+ start()
68
+ const off = onAction((a, kind) => {
69
+ if (kind == 'reset') { queue.length = 0; started = false; start(); return }
70
+ if (kind != 'add') return
71
+ // connected before run() (sygnal/vite): the root's first action, its state still the initial one
72
+ if (!started) start()
73
+ if (!keep(a!)) return
74
+ queue.push(a!)
75
+ if (timer === null) timer = setTimeout(flush, 0)
76
+ })
77
+ const unsubscribe = conn.subscribe((msg: any) => {
78
+ if (msg?.type !== 'DISPATCH' || !msg.state) return
79
+ const t = msg.payload?.type
80
+ if (t !== 'JUMP_TO_STATE' && t !== 'JUMP_TO_ACTION') return
81
+ const r = rootOf()
82
+ const dt = (window as any).__SYGNAL_DEVTOOLS__
83
+ if (!r || !dt || typeof dt._timeTravel != 'function') return
84
+ try { dt._timeTravel({componentId: Number(r.id), componentName: r.name, state: JSON.parse(msg.state)}) } catch (_) { /* a bad state from the extension */ }
85
+ })
86
+ return () => {
87
+ off()
88
+ if (timer !== null) clearTimeout(timer)
89
+ if (typeof unsubscribe == 'function') unsubscribe()
90
+ try { ext.disconnect?.() } catch (_) { /* ignore */ }
91
+ }
92
+ }
@@ -0,0 +1,54 @@
1
+ import xs from 'xstream';
2
+ import {error as logError} from './diagnostics/legacy';
3
+
4
+ /*
5
+ * PLAN-3 reply actions: the shared part of every driver that answers a request
6
+ * with the sender's own actions (makeFetchDriver, driverFromAsync, the socket driver).
7
+ *
8
+ * - The core stamps each object a component sends to a source marked
9
+ * `__sygnalReplies: true` with the sending instance (`__emitterId`, the same
10
+ * non-enumerable stamp EVENTS gets), and merges `source.replies(instance)` into
11
+ * that instance's actions.
12
+ * - A request that names `ok` / `error` actions from a stamped sender gets reply actions:
13
+ * the driver answers it with `reply(sender, type, data)`, which reaches exactly
14
+ * that instance (never select()/errors()).
15
+ * - When the instance stops listening (disposed), `onStop(sender)` lets the
16
+ * driver abort what it still has in flight for it.
17
+ */
18
+
19
+ const SENDER = '__emitterId';
20
+
21
+ /** the instance that sent a request (undefined when it wasn't sent by a component) */
22
+ export const senderOf = (req: any) => req[SENDER];
23
+
24
+ /** `to` (a copy of `from`) with `from`'s sender stamp (a spread drops non-enumerables) */
25
+ export const keepSender = (from: any, to: any) => {
26
+ if (from[SENDER] !== undefined) Object.defineProperty(to, SENDER, {value: from[SENDER]});
27
+ return to;
28
+ };
29
+
30
+ /**
31
+ * D57: a request object with a `then` / `catch` key is a thenable (it breaks `await`), so
32
+ * reply-action drivers refuse it (SYG610). True when the request may be sent.
33
+ */
34
+ export const allowed = (req: any, driver: string) =>
35
+ !('then' in req || 'catch' in req) ||
36
+ logError('SYG610', req.__emitterName, `${driver}: request not sent: it has a 'then'/'catch' key`, "Use ok: 'ACTION' / error: 'ACTION'", req);
37
+
38
+ /** the reply-action half of a driver's source */
39
+ export const makeReplies = (onStop?: (sender: any) => void) => {
40
+ const to = new Map<any, any>();
41
+ return {
42
+ replies: {
43
+ __sygnalReplies: true,
44
+ replies: (sender: any) => xs.create<any>({
45
+ start: l => { to.set(sender, l); },
46
+ stop: () => { to.delete(sender); onStop?.(sender); },
47
+ }),
48
+ },
49
+ /** deliver `{ type, data }` to the sender's actions (dropped when it's gone) */
50
+ reply: (sender: any, type: any, data: any) => { to.get(sender)?.next({type, data}); },
51
+ /** whether the sender listens to its replies now (a reply to it would not be dropped) */
52
+ listening: (sender: any) => to.has(sender),
53
+ };
54
+ };