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
@@ -1,10 +1,9 @@
1
1
  import {Driver} from '../run/types';
2
- import {init, Module, Options as SnabbdomOptions, VNode, toVNode} from './snabbdom';
2
+ import {init, Module, Options as SnabbdomOptions, VNode} from './snabbdom';
3
3
  import xs, {Stream, Listener} from 'xstream';
4
- import {concat, sampleCombine} from '../../extra/xstreamExtras';
5
4
  import {MainDOMSource} from './MainDOMSource';
6
5
  import {VNodeWrapper} from './VNodeWrapper';
7
- import {getValidNode, checkValidContainer} from './utils';
6
+ import {getValidNode, checkValidContainer, POKE, flat} from './utils';
8
7
  import defaultModules from './modules';
9
8
  import {IsolateModule} from './IsolateModule';
10
9
  import {EventDelegator} from './EventDelegator';
@@ -36,17 +35,11 @@ export interface DOMDriverOptions {
36
35
  snabbdomOptions?: SnabbdomOptions;
37
36
  }
38
37
 
39
- function dropCompletion<T>(input: Stream<T>): Stream<T> {
40
- return xs.merge(input, xs.never());
41
- }
42
-
43
38
  function unwrapElementFromVNode(vnode: VNode): Element {
44
39
  return vnode.elm as Element;
45
40
  }
46
41
 
47
- function defaultReportSnabbdomError(err: any): void {
48
- (console.error || console.log)(err);
49
- }
42
+ const ERR = 'data-sygnal-error';
50
43
 
51
44
  function makeDOMReady$(): Stream<null> {
52
45
  return xs.create<null>({
@@ -68,11 +61,64 @@ function makeDOMReady$(): Stream<null> {
68
61
  });
69
62
  }
70
63
 
71
- function addRootScope(vnode: VNode): VNode {
72
- vnode.data = vnode.data || {};
73
- (vnode.data as any).isolate = [];
74
- return vnode;
75
- }
64
+ // G-456 (D217): the app's first patch adopts markup already in the mount point (server HTML, an
65
+ // Astro island, a Vike page). Its old vnode is built from the DOM with the client's vnode as the
66
+ // template, by position (no toVNode): an element with the client's tag takes the client's
67
+ // selector and key (so component roots and Collection items match), and its data is what the
68
+ // modules need to reach the client's from what the server wrote: the dataset (stale keys go:
69
+ // data-sygnal-ssr), the class as the className prop (the className module drops stale classes),
70
+ // and each attribute the client sets as an attribute or a prop, with the server's value (nothing
71
+ // is written again: an iframe's src isn't reloaded). Any other attribute is removed now; so is a
72
+ // style (G-483: the style module writes the client's declarations, so none of the server's
73
+ // stays). A single text child keeps its text node. Whitespace and comments where the client has
74
+ // no text go; any other node that doesn't match stays as a placeholder that snabbdom replaces
75
+ // with the client's node, in place (the lists stay aligned, so the nodes after it are still
76
+ // adopted). Made again that way, as a fresh render makes them: an element whose selector has a
77
+ // class or id (G-486: `h('p.card')`, the Portal placeholder; patching never corrects them), and
78
+ // one whose hook expects a new element (G-485): a create or init hook (a thunk), an insert hook
79
+ // without a postpatch (a Transition's enter, a measured row, the toaster region, a lazy()
80
+ // placeholder), and any insert hook of the user's own (G-521: with a postpatch too; `u` when a
81
+ // ref, autoFocus or widget chained theirs after it; `s` marks a function chainHooks made)
82
+ const adopt = (e: any, v: any): any => {
83
+ // (a hole, `{cond && <X/>}`, has no node: snabbdom skips it. G-481: no fragment either, they
84
+ // are flat by now: their nodes pair with their children in their parent's run)
85
+ const c = (v.children || []).filter((z: any) => z), out: any[] = [];
86
+ for (let j = 0, x = e.firstChild, y: any; x; x = y) {
87
+ const w = c[j], t = x.nodeType, d = w?.data || {}, k = d.hook;
88
+ // a node that doesn't match stays in its place under a selector no vnode has: snabbdom makes
89
+ // the client's node before it and removes it (the lists stay aligned: no other node is paired)
90
+ let n: any = {sel: '', data: {}, elm: x};
91
+ // G-520: client text vnodes next to each other (`Hello, {name}!`) are one server text node:
92
+ // it is split by the client's text when that is its start, so the nodes after stay paired
93
+ t == 3 && c[j + 1]?.text != null && !c[j + 1].sel && w?.text && x.data.startsWith(w.text) && x.splitText(w.text.length);
94
+ y = x.nextSibling;
95
+ if (t == 3 && w && !w.sel && w.text != null) n = {text: x.data, elm: x};
96
+ else if (t == 1 && x.localName == w?.sel && !(k && (k.create || k.init || k.u || k.insert && !(k.postpatch && k.insert.s)))) {
97
+ const p = d.props || {}, a = d.attrs || {}, o: any = {}, at: any = {}, pr: any = {}, f = x.firstChild;
98
+ o.class = 'className';
99
+ for (const m in p) o[m == 'htmlFor' ? 'for' : m.toLowerCase()] = m;
100
+ // G-482: a data-* attribute the client sets as an attribute isn't in the dataset (the
101
+ // dataset module would remove it): it goes, and the attributes module writes it again.
102
+ // G-487: `open` (a <details> the user opened) stays when the client doesn't render it
103
+ for (const {name: m, value: y} of [...x.attributes])
104
+ /^data-/.test(m) ? m in a && x.removeAttribute(m)
105
+ : m in a ? at[m] = y
106
+ : m == 'open' ? 0
107
+ : m in o && !d.ns ? pr[o[m]] = y
108
+ : x.removeAttribute(m);
109
+ // G-484: a textarea's value no longer follows its text, which goes
110
+ x.localName == 'textarea' && (x.value = x.value);
111
+ const s = w.text != null && f?.nodeType == 3 && !f.nextSibling;
112
+ n = {sel: w.sel, key: w.key, data: {dataset: {...x.dataset}, attrs: at, props: pr}, children: s ? undefined : adopt(x, w), text: s ? f.data : undefined, elm: x};
113
+ } else if (t != 1 && (t != 3 || !/\S/.test(x.data))) {
114
+ x.remove();
115
+ continue;
116
+ }
117
+ out.push(n);
118
+ j++;
119
+ }
120
+ return out;
121
+ };
76
122
 
77
123
  function makeDOMDriver(
78
124
  container: string | Element | DocumentFragment,
@@ -86,66 +132,79 @@ function makeDOMDriver(
86
132
  const patch = init([isolateModule.createModule() as Partial<Module>].concat(modules), undefined, snabbdomOptions);
87
133
  const domReady$ = makeDOMReady$();
88
134
  let vnodeWrapper: VNodeWrapper;
89
- let mutationObserver: MutationObserver;
90
- const mutationConfirmed$ = xs.create<null>({
91
- start(listener) {
92
- mutationObserver = new MutationObserver(() => listener.next(null));
93
- },
94
- stop() {
95
- mutationObserver.disconnect();
96
- },
97
- });
98
135
 
99
136
  function DOMDriver(vnode$: Stream<VNode>, name = 'DOM'): MainDOMSource {
100
137
  domDriverInputGuard(vnode$);
101
138
  const sanitation$ = xs.create<null>();
102
139
 
140
+ let r: any;
103
141
  const firstRoot$ = domReady$.map(() => {
104
142
  const firstRoot = getValidNode(container) || document.body;
105
143
  vnodeWrapper = new VNodeWrapper(firstRoot);
144
+ r = {sel: '', data: {isolate: []}, elm: firstRoot};
106
145
  return firstRoot;
107
146
  });
108
147
 
109
148
  const rememberedVNode$ = vnode$.remember();
110
149
  rememberedVNode$.addListener({});
111
150
 
112
- mutationConfirmed$.addListener({});
113
-
114
- const elementAfterPatch$ = firstRoot$
151
+ // P45-C (rec 9, D146): the root element, when the DOM is ready and then after each patch
152
+ // (it was a MutationObserver on the root's subtree: DOM changes made outside a patch no
153
+ // longer emit)
154
+ // G-261: and when Sygnal changes the DOM outside a patch (a Transition's leave, a Portal
155
+ // mounted late): an event that bubbles up to the root. G-276: on the current root (a patch
156
+ // can replace the first one). G-277: the innermost app's root takes it (a nested app or
157
+ // custom element doesn't re-emit the outer app's DOM source)
158
+ let pl: any, cur: any;
159
+ const poke = (e: Event) => pl && (e.stopPropagation(), pl.next(cur));
160
+ const off = () => cur?.removeEventListener(POKE, poke);
161
+ // G-545: run() gives any DOM driver its reporter (onError, phase 'patch') on the isolate
162
+ // module; an explicit reportSnabbdomError comes first
163
+ const rep = (e: any) => (options.reportSnabbdomError || (isolateModule as any).rep || console.error)(e);
164
+ let dead = 0, end = 0;
165
+ const rootElement$ = firstRoot$
115
166
  .map(
116
- firstRoot =>
117
- xs
118
- .merge(rememberedVNode$.endWhen(sanitation$), sanitation$)
119
- .map(vnode => vnodeWrapper.call(vnode))
120
- .startWith(addRootScope(toVNode(firstRoot)))
121
- .fold(patch, toVNode(firstRoot))
122
- .drop(1)
123
- .map(unwrapElementFromVNode)
124
- .startWith(firstRoot as any)
125
- .map(el => {
126
- mutationObserver.observe(el, {
127
- childList: true,
128
- attributes: true,
129
- characterData: true,
130
- subtree: true,
131
- attributeOldValue: true,
132
- characterDataOldValue: true,
133
- });
134
- return el;
135
- })
136
- .compose(dropCompletion)
167
+ (firstRoot, P = (o: any, v: any) => (v = flat(v), patch(o == r ? (cur.removeAttribute?.(ERR), {...r, sel: v.sel, key: v.key, children: adopt(firstRoot, v)}) : o, v))) =>
168
+ xs.merge(
169
+ xs
170
+ .merge(rememberedVNode$.endWhen(sanitation$), sanitation$.map(() => (end = 1, null)))
171
+ .map(vnode => vnodeWrapper.call(vnode))
172
+ // the first step gives the root its scope; the second, the app's first patch,
173
+ // adopts the markup in it (G-456). The root keeps its own attributes (G-466)
174
+ .startWith(r)
175
+ // G-540: a patch that throws (a hook, a module) is reported once, and the app's DOM
176
+ // stops updating: snabbdom stopped half way (some elements patched, created or
177
+ // removed, the rest not; insert hooks not run), and no vnode tree matches that DOM
178
+ // (patching on from either tree, or adopting the DOM again, left wrong or duplicated
179
+ // DOM). The stream doesn't end (its events, the root element), and a patch to no
180
+ // children (dispose) still runs, so what the last good tree mounted (a Portal's
181
+ // content) is removed; its errors aren't reported again. G-549: only dispose (`end`:
182
+ // the sanitation signal), not a live render that has no children. G-543 (D224): the
183
+ // root element is marked data-sygnal-error="patch" while its DOM is stopped (CSS can
184
+ // show it; dispose removes it). G-550: marked before the error is reported (onError
185
+ // can read it); the first patch clears a mark a failed, undisposed app left in the
186
+ // container, and dispose removes it only when this app set it; a throw while
187
+ // flattening the tree (a key JSON.stringify can't take) is a patch error too (P
188
+ // flattens). A DocumentFragment mount point has no attributes: no mark
189
+ .fold((o: any, v: any) => {
190
+ if (!dead || end) try { o = P(o, v) } catch (e) { dead++ || (cur.setAttribute?.(ERR, 'patch'), rep(e)) }
191
+ end && dead && cur.removeAttribute?.(ERR)
192
+ return o
193
+ }, {...r, data: {}})
194
+ .drop(1)
195
+ .map(unwrapElementFromVNode)
196
+ .startWith(firstRoot as any)
197
+ .map((el: any) => (off(), el.addEventListener(POKE, poke), cur = el)),
198
+ // never completes (nor does the root element). G-285: the listener moves with the
199
+ // root and goes when the app stops (a disposed app's driver isn't kept by its root)
200
+ xs.create<any>({start: l => pl = l, stop: () => (pl = 0, off())})
201
+ )
137
202
  )
138
- .flatten();
139
-
140
- const rootElement$ = concat(domReady$, mutationConfirmed$)
203
+ .flatten()
141
204
  .endWhen(sanitation$)
142
- .compose(sampleCombine(elementAfterPatch$))
143
- .map(arr => arr[1])
144
205
  .remember();
145
206
 
146
- rootElement$.addListener({
147
- error: options.reportSnabbdomError || defaultReportSnabbdomError,
148
- });
207
+ rootElement$.addListener({error: rep});
149
208
 
150
209
  const delegator = new EventDelegator(rootElement$, isolateModule);
151
210
 
@@ -95,7 +95,15 @@ export class MockedDOMSource {
95
95
  return out;
96
96
  }
97
97
 
98
- public select(selector: string): MockedDOMSource {
98
+ public select(selector: any): MockedDOMSource {
99
+ // CT-1: a control selects by its marker; a component (any other function) is rejected
100
+ // like the real DOM source does (SYG124 in the dev entry names the fixes)
101
+ if (typeof selector == 'function') {
102
+ if (!selector.__sygnalControl) {
103
+ throw new Error(`DOM driver's select() expects the argument to be a string as a CSS selector`);
104
+ }
105
+ selector = '' + selector;
106
+ }
99
107
  const mockConfigForSelector = this._mockConfig[selector] || {};
100
108
 
101
109
  return new MockedDOMSource(
@@ -113,6 +121,18 @@ export class MockedDOMSource {
113
121
  return source.select('.' + SCOPE_PREFIX + scope);
114
122
  }
115
123
 
124
+ /**
125
+ * PLAN-4.6: isolateSink for one vnode (a copy, with the scope class). 4-I G-558: a fragment (a
126
+ * Collection's, `<>…</>`) has no element: each of its top-level elements gets the class (as the
127
+ * real driver's scoped()); a text vnode is left as it is
128
+ */
129
+ public isolateValue(vnode: any, scope: string): any {
130
+ // G-305: the scope's class token exactly (scope s1 is not s14)
131
+ return !vnode || typeof vnode != 'object' ? vnode
132
+ : !vnode.sel ? (vnode.children ? {...vnode, children: vnode.children.map((c: any) => this.isolateValue(c, scope))} : vnode)
133
+ : vnode.sel.split('.').indexOf(SCOPE_PREFIX + scope) > 0 ? vnode : {...vnode, sel: vnode.sel + `.${SCOPE_PREFIX}${scope}`};
134
+ }
135
+
116
136
  public isolateSink(sink: any, scope: string): any {
117
137
  return adapt(
118
138
  xs.fromObservable<any>(sink).map((vnode: VNode) => {
@@ -1,19 +1,32 @@
1
- import {Module, classModule, propsModule, attributesModule, datasetModule} from './snabbdom';
1
+ import {Module, classModule, attributesModule, datasetModule} from './snabbdom';
2
2
  import {styleModule} from './styleModule';
3
3
  import {selectModule} from './selectModule';
4
4
  import {controlledInputModule} from './controlledInputModule';
5
5
  import {classNameModule} from './classNameModule';
6
+ import {propsModule} from './propsModule';
7
+ import {isField} from './controlledInputModule';
8
+ import {sameData} from './utils';
6
9
 
7
- const modules: Array<Module> = [
10
+ const list: Array<Module> = [
8
11
  styleModule,
9
12
  classModule,
10
- propsModule,
13
+ propsModule, // Sygnal's: also clears removed and nullish props (B-015, G-109)
11
14
  classNameModule, // after propsModule (B-012)
12
15
  attributesModule,
13
16
  datasetModule,
14
17
  selectModule,
15
18
  controlledInputModule,
16
19
  ];
20
+ // P46-P: one update hook for these modules, in their order. A vnode whose data is the same as
21
+ // last time (each bucket shallow-equal) needs none of them, unless it is a form field (the
22
+ // controlled-input and select modules re-sync it every patch)
23
+ const ups = list.map(m => m.update!);
24
+ const modules: Array<Module> = list.map(({update, ...m}) => m as Module).concat({
25
+ update(o: any, v: any) {
26
+ if (!isField(v) && sameData(o.data, v.data)) return;
27
+ for (let i = 0; i < ups.length; i++) ups[i](o, v);
28
+ },
29
+ } as Module);
17
30
 
18
31
  export {styleModule, classModule, propsModule, attributesModule, datasetModule, selectModule, controlledInputModule, classNameModule};
19
32
 
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Sygnal's props module: snabbdom's propsModule, plus clearing removed and
3
+ * nullish props (B-015, G-109).
4
+ *
5
+ * Problem: snabbdom's propsModule writes new and changed props but never
6
+ * unsets a prop that disappears, so a reused element keeps `title`, `disabled`,
7
+ * `href`, ... from the previous render. A nullish prop is written as the
8
+ * string "undefined"/"null" (`title={cond ? 'x' : undefined}`, `src={null}`).
9
+ *
10
+ * Fix: write new and changed props except nullish ones (G-109: nothing is ever
11
+ * written as "null"). For each prop that is gone (or became nullish), remove
12
+ * the reflected attribute, which resets the property (numbers such as
13
+ * maxLength fall back to their default too). Without such an attribute, reset
14
+ * the property by its current type (boolean -> false, string -> '', other
15
+ * non-number -> undefined/null) if it differs (G-095). `className` is left
16
+ * to classNameModule (B-012). On form fields a nullish `value`/`checked` clears
17
+ * the field ('' / false, D49); a removed one is left alone (the controlled-input
18
+ * module owns them; removing them hands control back to the user, as in React).
19
+ */
20
+
21
+ import type {VNode} from 'snabbdom/build/vnode.js';
22
+
23
+ function clearProp(elm: any, key: string, nullish?: boolean): void {
24
+ if (key === 'className') return;
25
+ if (/^(value|checked)$/.test(key) && /^(INPUT|TEXTAREA|SELECT)$/.test(elm.tagName)) {
26
+ const empty = key === 'value' ? '' : false;
27
+ if (nullish && elm[key] !== empty) elm[key] = empty;
28
+ return;
29
+ }
30
+ const attr = key === 'htmlFor' ? 'for' : key.replace(/^aria(?=[A-Z])/, 'aria-').toLowerCase();
31
+ // G-095: a reflected attribute is just removed, which resets the property. Writing it first
32
+ // (src = '') would queue an img/video error event or navigate an iframe to about:blank.
33
+ if (elm.hasAttribute?.(attr)) return elm.removeAttribute(attr);
34
+ const cur = elm[key];
35
+ const t = typeof cur;
36
+ const reset = t === 'boolean' ? false : t === 'string' ? '' : cur === null || t === 'function' ? null : undefined;
37
+ try {
38
+ // numbers (maxLength, ...) are left to their default; unchanged values aren't rewritten
39
+ if (t !== 'number' && cur !== reset) elm[key] = reset;
40
+ } catch (_) {
41
+ // e.g. contentEditable rejects ''
42
+ }
43
+ }
44
+
45
+ function updateProps(oldVnode: VNode, vnode: VNode): void {
46
+ const elm: any = vnode.elm;
47
+ const props: any = vnode.data?.props || {};
48
+ const oldProps: any = oldVnode.data?.props || {};
49
+ if (!elm || oldProps === props) return;
50
+ for (const key in oldProps) {
51
+ if (!(key in props)) clearProp(elm, key);
52
+ }
53
+ for (const key in props) {
54
+ const cur = props[key];
55
+ if (cur === oldProps[key]) continue;
56
+ // a nullish className is left to classNameModule, which rebuilds the attribute (B-012)
57
+ if (cur == null) clearProp(elm, key, true);
58
+ // G-553: on create (no old sel) `value` is always written: the create hooks run before the
59
+ // children are appended, so an <option>'s value reads '' (no text yet) and value="" would be
60
+ // skipped, leaving no attribute (the option then submits its label)
61
+ else if (key !== 'value' || elm[key] !== cur || !oldVnode.sel) elm[key] = cur;
62
+ }
63
+ }
64
+
65
+ export const propsModule = {create: updateProps, update: updateProps};
@@ -1,33 +1,38 @@
1
1
  /**
2
- * Snabbdom module that fixes <select> value assignment.
2
+ * Snabbdom module that applies a <select>'s `value` after its <option>s exist.
3
3
  *
4
- * Problem: snabbdom's createElm() fires the propsModule create hook
5
- * (which sets elm.value) BEFORE appending child <option> elements.
6
- * The browser silently ignores the value assignment because no matching
7
- * option exists yet, so <select> always shows the first option.
4
+ * Problem: snabbdom runs the module create/update hooks (propsModule sets
5
+ * elm.value) BEFORE it creates or patches the element's children. When the
6
+ * matching <option> is new in this patch, the browser ignores the value and
7
+ * the <select> shows the first option. On create that is every <select>; on
8
+ * update it is a value change together with an options change (B-017).
8
9
  *
9
- * Fix: collect <select> elements that have a value prop during create,
10
- * then re-apply the value in the post hook (after all elements and
11
- * children have been inserted).
10
+ * Fix: collect <select> elements with a value prop during create and update,
11
+ * then re-apply the value in the post hook, after all children are patched.
12
12
  */
13
13
 
14
14
  import type {VNode} from 'snabbdom/build/vnode.js';
15
+ import {isField} from './controlledInputModule';
15
16
 
16
17
  let pendingSelects: Array<{elm: HTMLSelectElement; value: any}> = [];
17
18
 
18
- function createSelect(_oldVnode: VNode, vnode: VNode): void {
19
- const elm = vnode.elm as Element;
20
- if (elm && elm.tagName === 'SELECT' && vnode.data?.props?.value !== undefined) {
21
- pendingSelects.push({elm: elm as HTMLSelectElement, value: vnode.data.props.value});
19
+ function queueSelect(_oldVnode: VNode, vnode: VNode): void {
20
+ // P45-A: a form field by the vnode's tag first; most vnodes aren't
21
+ if (!isField(vnode)) return;
22
+ const elm = vnode.elm as HTMLSelectElement;
23
+ const value = vnode.data?.props?.value;
24
+ if (elm && elm.tagName === 'SELECT' && value != null) {
25
+ pendingSelects.push({elm, value});
22
26
  }
23
27
  }
24
28
 
25
29
  function postPatch(): void {
26
- for (let i = 0; i < pendingSelects.length; i++) {
27
- const {elm, value} = pendingSelects[i];
28
- elm.value = value;
29
- }
30
+ const list = pendingSelects;
30
31
  pendingSelects = [];
32
+ for (let i = 0; i < list.length; i++) {
33
+ const {elm, value} = list[i];
34
+ if (elm.value !== String(value)) elm.value = value;
35
+ }
31
36
  }
32
37
 
33
- export const selectModule = {create: createSelect, post: postPatch};
38
+ export const selectModule = {create: queueSelect, update: queueSelect, post: postPatch};
@@ -13,14 +13,9 @@
13
13
  export {h, fragment as HFragment} from 'snabbdom/build/h.js';
14
14
  export {init} from 'snabbdom/build/init.js';
15
15
  export type {Options} from 'snabbdom/build/init.js';
16
- export {toVNode} from 'snabbdom/build/tovnode.js';
17
16
  export {vnode} from 'snabbdom/build/vnode.js';
18
17
  export type {VNode, VNodeData} from 'snabbdom/build/vnode.js';
19
- import {Fragment as _Fragment} from 'snabbdom/build/jsx.js';
20
-
21
- // Tag Fragment so we can identify it even after minification mangles Function.name
22
- (_Fragment as any).__sygnalFragment = true;
23
- export const Fragment = _Fragment;
18
+ export {Fragment} from './fragment';
24
19
 
25
20
  // Module type
26
21
  export type {Module} from 'snabbdom/build/modules/module.js';
@@ -1,4 +1,5 @@
1
1
  import {h, VNode, VNodeData} from './snabbdom';
2
+ import {flat} from './utils';
2
3
 
3
4
  export interface ThunkData extends VNodeData {
4
5
  fn(): VNode;
@@ -10,6 +11,8 @@ export interface Thunk extends VNode {
10
11
  }
11
12
 
12
13
  function copyToThunk(vnode: VNode, thunkVNode: Thunk): void {
14
+ // (rendered during the patch: its fragments are flattened here, as the DOM driver does)
15
+ vnode = flat(vnode);
13
16
  thunkVNode.elm = vnode.elm;
14
17
  (vnode.data as ThunkData).fn = thunkVNode.data.fn;
15
18
  (vnode.data as ThunkData).args = thunkVNode.data.args;
@@ -86,3 +86,101 @@ export function makeInsert(
86
86
  }
87
87
  };
88
88
  }
89
+
90
+ // G-261: a DOM change Sygnal makes outside a patch (a Transition's leave, a Portal mounted late)
91
+ // re-emits the DOM source of the app whose root contains `el` (makeDOMDriver listens on its root)
92
+ export const POKE = 'sygnal-dom';
93
+ export const pokeDOM = (el: any): any => el?.dispatchEvent(new Event(POKE, {bubbles: true}));
94
+
95
+ const obj = (o: any) => o !== null && typeof o == 'object' && !Array.isArray(o);
96
+ /**
97
+ * P46-P: two vnodes' data are the same for the DOM modules: the same keys, each value identical
98
+ * or a shallow-equal object (a module bucket: props, attrs, class, dataset, style, ...)
99
+ */
100
+ export function sameData(a: any, b: any): boolean {
101
+ if (a === b) return true;
102
+ if (!a || !b) return false;
103
+ let n = 0;
104
+ for (const k in a) {
105
+ n++;
106
+ const x = a[k], y = b[k];
107
+ if (x !== y) {
108
+ if (!obj(x) || !obj(y)) return false;
109
+ let m = 0;
110
+ for (const j in x) { m++; if (x[j] !== y[j]) return false; }
111
+ for (const _ in y) m--;
112
+ if (m) return false;
113
+ }
114
+ }
115
+ for (const _ in b) n--;
116
+ return !n;
117
+ }
118
+
119
+ // G-517 / G-518 / G-522: snabbdom never sees a fragment. Before each patch, a fragment's children
120
+ // (through nested ones) are spliced into its parent's children, so they are diffed, moved and
121
+ // adopted at hydration as the parent's own (snabbdom's fragment bounds went stale: an insert into
122
+ // one landed at the parent's end, a keyed one never moved, a later patch threw NotFoundError).
123
+ // The children of a keyed fragment (a Collection item, a component's root) take its key and their
124
+ // own key, else their tag and its count among them: they move with it and stay apart from another
125
+ // item's. G-541: no two paths give the same key (a '/' separator did: fragment "src" + "a/b.ts" and
126
+ // fragment "src/a" + "b.ts"). 3-W G-544: each segment is a JSON string (the key as a string) or
127
+ // array ([tag, count], always last), which ends where it ends, appended to the prefix
128
+ // (`"src""a/b.ts"`): a key grows linearly with the nesting (JSON in JSON doubled at each level),
129
+ // and any key String() takes is fine (G-548: a BigInt; two Symbols with the same description share
130
+ // a key; a number key and its string are one key, as in snabbdom). A vnode with no fragment below is kept by identity, as is one already
131
+ // flattened (a cached subtree), so snabbdom still skips unchanged subtrees; so are a keyed
132
+ // fragment's copies while the fragment and its prefix are the same (G-539; a hit compares the
133
+ // parent's prefix, the fragment's own is made on a miss). A hoisted keyed fragment rendered in two
134
+ // places shares its copies, as any hoisted vnode shares its object (render it once). A copy (a
135
+ // parent with new children, a child with a new key) has its vnode's fields and writes its
136
+ // element there too, so the app's vnodes get their `elm` as before (testing and diagnostics read
137
+ // it); it keeps its own (a vnode object reused under another key: the old copy removes its own).
138
+ const flatOf = new WeakMap<any, any>();
139
+ const cpOf = new WeakMap<any, any>();
140
+ // 4-I G-561: a copy is an instance of one class (the vnode's own fields, `elm` an accessor on its
141
+ // prototype), not Object.create(y) with an own accessor: snabbdom's patch loop read each field
142
+ // through the prototype chain and called a per-object accessor, ~3x slower over a 1k Collection.
143
+ // (y: set again after the fields, as a copy's own y is its vnode's vnode; m starts as the vnode's
144
+ // elm, which snabbdom replaces before reading it)
145
+ class Copy {
146
+ y: any; m: any
147
+ constructor(y: any, o: any) { this.y = y; Object.assign(this, y, o).y = y }
148
+ get elm() { return this.m }
149
+ set elm(e: any) { this.y.elm = this.m = e }
150
+ }
151
+ const seg = (k: any, s: any) => (k || '') + JSON.stringify(typeof s == 'object' ? s : String(s));
152
+ export const flat = (v: any): any => {
153
+ if (!v || v.$p || !v.children) return v;
154
+ let r = flatOf.get(v);
155
+ if (!r) {
156
+ const o: any[] = [];
157
+ let d = 0;
158
+ // (m, s: locals of each call)
159
+ const put = (c: any[], k?: string, n: any = {}, m?: any, s?: any) => {
160
+ for (const x of c) {
161
+ // G-539: a keyed fragment's copies are kept per (fragment, prefix), in flatOf (flat() never
162
+ // takes a fragment): an unchanged one (a Collection item that didn't render again) gives
163
+ // snabbdom the same vnodes
164
+ if (x && !x.sel && x.children) d = 1, x.key == null ? put(x.children, k, n)
165
+ : (m = flatOf.get(x)) && m[0] === k ? o.push(...m[1])
166
+ : (s = o.length, put(x.children, seg(k, x.key)), flatOf.set(x, [k, o.slice(s)]));
167
+ else {
168
+ let y = flat(x);
169
+ y !== x && (d = 1);
170
+ if (k && y) {
171
+ // 4-H: the copy of an unchanged child under the same key is kept (the fragment is new
172
+ // when one of its children is: a Collection's), so snabbdom skips the others. 4-I
173
+ // G-561: a hit compares the prefix and the child's own part (its key, else its count),
174
+ // so the key is only built on a miss
175
+ const s = y.key ?? (n[y.sel] = -~n[y.sel]), c = cpOf.get(y);
176
+ y = c && c[0] === k && c[1] === s ? c[2] : (cpOf.set(y, [k, s, y = new Copy(y, {key: seg(k, y.key ?? [y.sel, s])})]), y);
177
+ }
178
+ o.push(y);
179
+ }
180
+ }
181
+ };
182
+ put(v.children);
183
+ flatOf.set(v, r = d ? new Copy(v, {children: o}) : v);
184
+ }
185
+ return r;
186
+ };
@@ -0,0 +1,43 @@
1
+ import xs, {Stream} from 'xstream';
2
+ import type {VNode} from './snabbdom';
3
+
4
+ /**
5
+ * PLAN-4 GS-12 (D129): the patch that something asked for runs inside
6
+ * `document.startViewTransition()`. The ask is `flags().vt`, set by the core when a component's
7
+ * `viewTransitions` action changes state (on the app's IsolateModule, so it is per app).
8
+ *
9
+ * - No API, or `prefers-reduced-motion: reduce`, or the first render: patched at once.
10
+ * - P45-C: one action is one patch (the app's render scheduler flushes every component, a
11
+ * Collection move included, into one), so the update callback patches with the latest vnode
12
+ * that arrived before the browser called back, and is done once that patch is (no quiet
13
+ * window: it was 20 ms, capped at 200 ms, for the several patches of a move).
14
+ * - A new transition while one animates: the browser skips the running one (it jumps to its end).
15
+ * - G-489: a skipped transition's `ready` rejection is handled (no uncaught InvalidStateError).
16
+ */
17
+ export function viewTransition$(vnode$: Stream<VNode>, flags: () => any): Stream<VNode> {
18
+ const g: any = globalThis, out$ = xs.create<VNode>();
19
+ let held: VNode[] | 0 = 0, first = 1;
20
+ return xs.merge(vnode$.filter(v => {
21
+ const f = flags() || {}, d = g.document, want = f.vt && !first;
22
+ // the first render has no old view to transition from (a ROUTE reply at start asks too)
23
+ f.vt = first = 0;
24
+ if (held) return held[0] = v, false;
25
+ if (!want || !d?.startViewTransition || g.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return true;
26
+ held = [v];
27
+ const update = () => {
28
+ const h = held as VNode[];
29
+ held = 0;
30
+ out$.shamefullySendNext(h[0]);
31
+ };
32
+ try {
33
+ // G-489: a transition the browser skips (duplicate names, a newer one) rejects `ready`:
34
+ // handled (the update still runs; `finished` / `updateCallbackDone` reject only when it throws).
35
+ // (a stand-in without `ready` must not throw here: the update would run twice)
36
+ d.startViewTransition(update)?.ready?.catch(Object);
37
+ } catch (_) {
38
+ // the browser refused (e.g. an invalid state): patch now
39
+ update();
40
+ }
41
+ return false;
42
+ }), out$);
43
+ }
@@ -3,6 +3,7 @@ import {dropRepeats} from '../../extra/xstreamExtras';
3
3
  import {DevToolEnabledSource} from '../run/types';
4
4
  import {adapt} from '../run/adapt';
5
5
  import {Getter, Setter, Scope, Reducer} from './types';
6
+ import {objIsEqual} from './objIsEqual';
6
7
 
7
8
  function updateArrayEntry<T>(
8
9
  array: Array<T>,
@@ -87,9 +88,13 @@ export class StateSource<S> {
87
88
  public stream: MemoryStream<S>;
88
89
  private _stream: MemoryStream<S>;
89
90
  private _name: string;
91
+ private _end?: Stream<any>;
90
92
 
91
- constructor(stream: Stream<S>, name: string) {
92
- this._stream = stream
93
+ // raw (P45-D): `stream` is another StateSource's stream mapped, already without undefined and
94
+ // repeats (a component's own source), so one stream instead of four
95
+ constructor(stream: Stream<S>, name: string, end?: Stream<any>, raw?: any) {
96
+ this._end = end;
97
+ this._stream = raw ? stream.remember() : stream
93
98
  .filter(s => typeof s !== 'undefined')
94
99
  .compose(dropRepeats())
95
100
  .remember();
@@ -105,7 +110,18 @@ export class StateSource<S> {
105
110
  */
106
111
  public select<R>(scope: Scope<S, R>): StateSource<R> {
107
112
  const get = makeGetter(scope);
108
- return new StateSource<R>(this._stream.map(get) as Stream<R>, this._name);
113
+ // G-214: keeps the end stream, so a selected source's watch() also ends on dispose
114
+ return new StateSource<R>(this._stream.map(get) as Stream<R>, this._name, this._end);
115
+ }
116
+
117
+ /**
118
+ * PLAN-4 GS-6: selector(state) whenever it changes structurally (objIsEqual). The current
119
+ * value is the baseline, not a change, unless { immediate: true }. Ends with the state stream
120
+ * or, for a component's own STATE source, when the component is disposed
121
+ */
122
+ public watch<R>(selector: (state: S) => R, {immediate}: {immediate?: boolean} = {}): Stream<R> {
123
+ const s$ = this._stream.map(selector).compose(dropRepeats(objIsEqual)).endWhen(this._end || xs.never());
124
+ return immediate ? s$ : s$.drop(1);
109
125
  }
110
126
 
111
127
  public isolateSource = isolateSource;