@wardic/dom 0.0.0-stage → 1.0.0-rc.2

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 (285) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +8 -2
  3. package/THIRD_PARTY_NOTICES.md +145 -0
  4. package/dist/Appear.d.ts +24 -0
  5. package/dist/Appear.d.ts.map +1 -0
  6. package/dist/Appear.js +25 -0
  7. package/dist/Appear.js.map +1 -0
  8. package/dist/Color.d.ts +14 -0
  9. package/dist/Color.d.ts.map +1 -0
  10. package/dist/Color.js +16 -0
  11. package/dist/Color.js.map +1 -0
  12. package/dist/DomAnnouncer.d.ts +13 -0
  13. package/dist/DomAnnouncer.d.ts.map +1 -0
  14. package/dist/DomAnnouncer.js +14 -0
  15. package/dist/DomAnnouncer.js.map +1 -0
  16. package/dist/DomClock.d.ts +11 -0
  17. package/dist/DomClock.d.ts.map +1 -0
  18. package/dist/DomClock.js +12 -0
  19. package/dist/DomClock.js.map +1 -0
  20. package/dist/DomDefaultTransition.d.ts +12 -0
  21. package/dist/DomDefaultTransition.d.ts.map +1 -0
  22. package/dist/DomDefaultTransition.js +13 -0
  23. package/dist/DomDefaultTransition.js.map +1 -0
  24. package/dist/DomInput.d.ts +13 -0
  25. package/dist/DomInput.d.ts.map +1 -0
  26. package/dist/DomInput.js +15 -0
  27. package/dist/DomInput.js.map +1 -0
  28. package/dist/DomInterpolators.d.ts +13 -0
  29. package/dist/DomInterpolators.d.ts.map +1 -0
  30. package/dist/DomInterpolators.js +14 -0
  31. package/dist/DomInterpolators.js.map +1 -0
  32. package/dist/DomKeyRules.d.ts +13 -0
  33. package/dist/DomKeyRules.d.ts.map +1 -0
  34. package/dist/DomKeyRules.js +14 -0
  35. package/dist/DomKeyRules.js.map +1 -0
  36. package/dist/DomPositionalKeys.d.ts +13 -0
  37. package/dist/DomPositionalKeys.d.ts.map +1 -0
  38. package/dist/DomPositionalKeys.js +14 -0
  39. package/dist/DomPositionalKeys.js.map +1 -0
  40. package/dist/DomProbe.d.ts +13 -0
  41. package/dist/DomProbe.d.ts.map +1 -0
  42. package/dist/DomProbe.js +14 -0
  43. package/dist/DomProbe.js.map +1 -0
  44. package/dist/DomProjectionSystem.d.ts +28 -0
  45. package/dist/DomProjectionSystem.d.ts.map +1 -0
  46. package/dist/DomProjectionSystem.js +41 -0
  47. package/dist/DomProjectionSystem.js.map +1 -0
  48. package/dist/DomRenderer.d.ts +13 -0
  49. package/dist/DomRenderer.d.ts.map +1 -0
  50. package/dist/DomRenderer.js +15 -0
  51. package/dist/DomRenderer.js.map +1 -0
  52. package/dist/FlushGate.d.ts +48 -0
  53. package/dist/FlushGate.d.ts.map +1 -0
  54. package/dist/FlushGate.js +62 -0
  55. package/dist/FlushGate.js.map +1 -0
  56. package/dist/MeasuredPair.d.ts +14 -0
  57. package/dist/MeasuredPair.d.ts.map +1 -0
  58. package/dist/MeasuredPair.js +16 -0
  59. package/dist/MeasuredPair.js.map +1 -0
  60. package/dist/OwnValues.d.ts +14 -0
  61. package/dist/OwnValues.d.ts.map +1 -0
  62. package/dist/OwnValues.js +15 -0
  63. package/dist/OwnValues.js.map +1 -0
  64. package/dist/ProjectionSystem.d.ts +34 -0
  65. package/dist/ProjectionSystem.d.ts.map +1 -0
  66. package/dist/ProjectionSystem.js +29 -0
  67. package/dist/ProjectionSystem.js.map +1 -0
  68. package/dist/ReorderController.d.ts +41 -0
  69. package/dist/ReorderController.d.ts.map +1 -0
  70. package/dist/ReorderController.js +44 -0
  71. package/dist/ReorderController.js.map +1 -0
  72. package/dist/Report.d.ts +12 -0
  73. package/dist/Report.d.ts.map +1 -0
  74. package/dist/Report.js +13 -0
  75. package/dist/Report.js.map +1 -0
  76. package/dist/ScrollOffset.d.ts +34 -0
  77. package/dist/ScrollOffset.d.ts.map +1 -0
  78. package/dist/ScrollOffset.js +42 -0
  79. package/dist/ScrollOffset.js.map +1 -0
  80. package/dist/Style.d.ts +15 -0
  81. package/dist/Style.d.ts.map +1 -0
  82. package/dist/Style.js +17 -0
  83. package/dist/Style.js.map +1 -0
  84. package/dist/Svg.d.ts +13 -0
  85. package/dist/Svg.d.ts.map +1 -0
  86. package/dist/Svg.js +14 -0
  87. package/dist/Svg.js.map +1 -0
  88. package/dist/Variables.d.ts +13 -0
  89. package/dist/Variables.d.ts.map +1 -0
  90. package/dist/Variables.js +14 -0
  91. package/dist/Variables.js.map +1 -0
  92. package/dist/index.d.ts +72 -0
  93. package/dist/index.d.ts.map +1 -0
  94. package/dist/index.js +60 -0
  95. package/dist/index.js.map +1 -0
  96. package/dist/internal/announcer.d.ts +50 -0
  97. package/dist/internal/announcer.d.ts.map +1 -0
  98. package/dist/internal/announcer.js +115 -0
  99. package/dist/internal/announcer.js.map +1 -0
  100. package/dist/internal/appear.d.ts +169 -0
  101. package/dist/internal/appear.d.ts.map +1 -0
  102. package/dist/internal/appear.js +499 -0
  103. package/dist/internal/appear.js.map +1 -0
  104. package/dist/internal/auto-scroll.d.ts +24 -0
  105. package/dist/internal/auto-scroll.d.ts.map +1 -0
  106. package/dist/internal/auto-scroll.js +124 -0
  107. package/dist/internal/auto-scroll.js.map +1 -0
  108. package/dist/internal/clock.d.ts +19 -0
  109. package/dist/internal/clock.d.ts.map +1 -0
  110. package/dist/internal/clock.js +39 -0
  111. package/dist/internal/clock.js.map +1 -0
  112. package/dist/internal/colors.d.ts +45 -0
  113. package/dist/internal/colors.d.ts.map +1 -0
  114. package/dist/internal/colors.js +193 -0
  115. package/dist/internal/colors.js.map +1 -0
  116. package/dist/internal/complex.d.ts +24 -0
  117. package/dist/internal/complex.d.ts.map +1 -0
  118. package/dist/internal/complex.js +28 -0
  119. package/dist/internal/complex.js.map +1 -0
  120. package/dist/internal/content-watch.d.ts +20 -0
  121. package/dist/internal/content-watch.d.ts.map +1 -0
  122. package/dist/internal/content-watch.js +76 -0
  123. package/dist/internal/content-watch.js.map +1 -0
  124. package/dist/internal/default-transition.d.ts +25 -0
  125. package/dist/internal/default-transition.d.ts.map +1 -0
  126. package/dist/internal/default-transition.js +41 -0
  127. package/dist/internal/default-transition.js.map +1 -0
  128. package/dist/internal/drag-defaults.d.ts +18 -0
  129. package/dist/internal/drag-defaults.d.ts.map +1 -0
  130. package/dist/internal/drag-defaults.js +38 -0
  131. package/dist/internal/drag-defaults.js.map +1 -0
  132. package/dist/internal/gate.d.ts +186 -0
  133. package/dist/internal/gate.d.ts.map +1 -0
  134. package/dist/internal/gate.js +453 -0
  135. package/dist/internal/gate.js.map +1 -0
  136. package/dist/internal/input.d.ts +41 -0
  137. package/dist/internal/input.d.ts.map +1 -0
  138. package/dist/internal/input.js +476 -0
  139. package/dist/internal/input.js.map +1 -0
  140. package/dist/internal/interpolators.d.ts +19 -0
  141. package/dist/internal/interpolators.d.ts.map +1 -0
  142. package/dist/internal/interpolators.js +30 -0
  143. package/dist/internal/interpolators.js.map +1 -0
  144. package/dist/internal/key-rules.d.ts +11 -0
  145. package/dist/internal/key-rules.d.ts.map +1 -0
  146. package/dist/internal/key-rules.js +244 -0
  147. package/dist/internal/key-rules.js.map +1 -0
  148. package/dist/internal/measure.d.ts +150 -0
  149. package/dist/internal/measure.d.ts.map +1 -0
  150. package/dist/internal/measure.js +278 -0
  151. package/dist/internal/measure.js.map +1 -0
  152. package/dist/internal/measured-pairs.d.ts +38 -0
  153. package/dist/internal/measured-pairs.d.ts.map +1 -0
  154. package/dist/internal/measured-pairs.js +251 -0
  155. package/dist/internal/measured-pairs.js.map +1 -0
  156. package/dist/internal/own-values.d.ts +35 -0
  157. package/dist/internal/own-values.d.ts.map +1 -0
  158. package/dist/internal/own-values.js +117 -0
  159. package/dist/internal/own-values.js.map +1 -0
  160. package/dist/internal/pass-ends.d.ts +21 -0
  161. package/dist/internal/pass-ends.d.ts.map +1 -0
  162. package/dist/internal/pass-ends.js +14 -0
  163. package/dist/internal/pass-ends.js.map +1 -0
  164. package/dist/internal/positional-keys.d.ts +16 -0
  165. package/dist/internal/positional-keys.d.ts.map +1 -0
  166. package/dist/internal/positional-keys.js +21 -0
  167. package/dist/internal/positional-keys.js.map +1 -0
  168. package/dist/internal/probe.d.ts +10 -0
  169. package/dist/internal/probe.d.ts.map +1 -0
  170. package/dist/internal/probe.js +117 -0
  171. package/dist/internal/probe.js.map +1 -0
  172. package/dist/internal/projection-defaults.d.ts +34 -0
  173. package/dist/internal/projection-defaults.d.ts.map +1 -0
  174. package/dist/internal/projection-defaults.js +21 -0
  175. package/dist/internal/projection-defaults.js.map +1 -0
  176. package/dist/internal/projection.d.ts +529 -0
  177. package/dist/internal/projection.d.ts.map +1 -0
  178. package/dist/internal/projection.js +4990 -0
  179. package/dist/internal/projection.js.map +1 -0
  180. package/dist/internal/renderer.d.ts +86 -0
  181. package/dist/internal/renderer.d.ts.map +1 -0
  182. package/dist/internal/renderer.js +616 -0
  183. package/dist/internal/renderer.js.map +1 -0
  184. package/dist/internal/reorder.d.ts +159 -0
  185. package/dist/internal/reorder.d.ts.map +1 -0
  186. package/dist/internal/reorder.js +437 -0
  187. package/dist/internal/reorder.js.map +1 -0
  188. package/dist/internal/report.d.ts +30 -0
  189. package/dist/internal/report.d.ts.map +1 -0
  190. package/dist/internal/report.js +60 -0
  191. package/dist/internal/report.js.map +1 -0
  192. package/dist/internal/require-global.d.ts +11 -0
  193. package/dist/internal/require-global.d.ts.map +1 -0
  194. package/dist/internal/require-global.js +31 -0
  195. package/dist/internal/require-global.js.map +1 -0
  196. package/dist/internal/reveal.d.ts +16 -0
  197. package/dist/internal/reveal.d.ts.map +1 -0
  198. package/dist/internal/reveal.js +66 -0
  199. package/dist/internal/reveal.js.map +1 -0
  200. package/dist/internal/runner.d.ts +21 -0
  201. package/dist/internal/runner.d.ts.map +1 -0
  202. package/dist/internal/runner.js +48 -0
  203. package/dist/internal/runner.js.map +1 -0
  204. package/dist/internal/scale-correction.d.ts +80 -0
  205. package/dist/internal/scale-correction.d.ts.map +1 -0
  206. package/dist/internal/scale-correction.js +227 -0
  207. package/dist/internal/scale-correction.js.map +1 -0
  208. package/dist/internal/scroll.d.ts +35 -0
  209. package/dist/internal/scroll.d.ts.map +1 -0
  210. package/dist/internal/scroll.js +244 -0
  211. package/dist/internal/scroll.js.map +1 -0
  212. package/dist/internal/scrollers.d.ts +35 -0
  213. package/dist/internal/scrollers.d.ts.map +1 -0
  214. package/dist/internal/scrollers.js +38 -0
  215. package/dist/internal/scrollers.js.map +1 -0
  216. package/dist/internal/starting-style.d.ts +141 -0
  217. package/dist/internal/starting-style.d.ts.map +1 -0
  218. package/dist/internal/starting-style.js +300 -0
  219. package/dist/internal/starting-style.js.map +1 -0
  220. package/dist/internal/svg.d.ts +60 -0
  221. package/dist/internal/svg.d.ts.map +1 -0
  222. package/dist/internal/svg.js +257 -0
  223. package/dist/internal/svg.js.map +1 -0
  224. package/dist/internal/variables.d.ts +45 -0
  225. package/dist/internal/variables.d.ts.map +1 -0
  226. package/dist/internal/variables.js +92 -0
  227. package/dist/internal/variables.js.map +1 -0
  228. package/package.json +65 -4
  229. package/src/Appear.ts +45 -0
  230. package/src/Color.ts +24 -0
  231. package/src/DomAnnouncer.ts +22 -0
  232. package/src/DomClock.ts +13 -0
  233. package/src/DomDefaultTransition.ts +16 -0
  234. package/src/DomInput.ts +22 -0
  235. package/src/DomInterpolators.ts +16 -0
  236. package/src/DomKeyRules.ts +17 -0
  237. package/src/DomPositionalKeys.ts +16 -0
  238. package/src/DomProbe.ts +15 -0
  239. package/src/DomProjectionSystem.ts +44 -0
  240. package/src/DomRenderer.ts +26 -0
  241. package/src/FlushGate.ts +81 -0
  242. package/src/MeasuredPair.ts +15 -0
  243. package/src/OwnValues.ts +19 -0
  244. package/src/ProjectionSystem.ts +51 -0
  245. package/src/ReorderController.ts +72 -0
  246. package/src/Report.ts +17 -0
  247. package/src/ScrollOffset.ts +54 -0
  248. package/src/Style.ts +37 -0
  249. package/src/Svg.ts +21 -0
  250. package/src/Variables.ts +17 -0
  251. package/src/index.ts +106 -0
  252. package/src/internal/announcer.ts +132 -0
  253. package/src/internal/appear.ts +739 -0
  254. package/src/internal/auto-scroll.ts +176 -0
  255. package/src/internal/clock.ts +45 -0
  256. package/src/internal/colors.ts +258 -0
  257. package/src/internal/complex.ts +29 -0
  258. package/src/internal/content-watch.ts +84 -0
  259. package/src/internal/culori.d.ts +58 -0
  260. package/src/internal/default-transition.ts +50 -0
  261. package/src/internal/drag-defaults.ts +54 -0
  262. package/src/internal/gate.ts +593 -0
  263. package/src/internal/input.ts +579 -0
  264. package/src/internal/interpolators.ts +33 -0
  265. package/src/internal/key-rules.ts +357 -0
  266. package/src/internal/measure.ts +357 -0
  267. package/src/internal/measured-pairs.ts +297 -0
  268. package/src/internal/own-values.ts +166 -0
  269. package/src/internal/pass-ends.ts +23 -0
  270. package/src/internal/positional-keys.ts +24 -0
  271. package/src/internal/probe.ts +176 -0
  272. package/src/internal/projection-defaults.ts +45 -0
  273. package/src/internal/projection.ts +5999 -0
  274. package/src/internal/renderer.ts +646 -0
  275. package/src/internal/reorder.ts +604 -0
  276. package/src/internal/report.ts +69 -0
  277. package/src/internal/require-global.ts +46 -0
  278. package/src/internal/reveal.ts +69 -0
  279. package/src/internal/runner.ts +64 -0
  280. package/src/internal/scale-correction.ts +310 -0
  281. package/src/internal/scroll.ts +356 -0
  282. package/src/internal/scrollers.ts +51 -0
  283. package/src/internal/starting-style.ts +396 -0
  284. package/src/internal/svg.ts +300 -0
  285. package/src/internal/variables.ts +145 -0
@@ -0,0 +1,593 @@
1
+ /**
2
+ * `makeFlushGate` — the deferred **Flush gate** (ADR-0006), framework-free here in `@wardic/dom`
3
+ * so every binding shares one (ADR-0031, decision 3). The binding hands in its `batch` and its
4
+ * sinks; the gate imports nothing from a framework.
5
+ *
6
+ * Per ADR-0006, per-frame writes never reach their element directly. Each source
7
+ * `write(id, partialValues)`s into a provider-owned gate that:
8
+ *
9
+ * 1. Accumulates *value-maps* (NOT already-projected Patches) per element,
10
+ * merging partials by key — so multiple sources can feed one element
11
+ * (e.g. a drag setting `x`/`y` while a spring drives `scale`, all
12
+ * collapsing into one grouped `transform`). Projecting per source would
13
+ * clobber the grouped transform.
14
+ * 2. Resolves the Renderer's PURE `project` ONCE at creation and uses it to
15
+ * turn each dirty element's merged value-map into a `Patch` exactly once
16
+ * per element per frame.
17
+ * 3. Commits each resulting Patch onto its element with the Renderer's `commit`, a plain
18
+ * call (ADR-0034) — one DOM pass per frame, shared by every element: at the end of the
19
+ * Frame loop's tick, in the Refresh frame that computed the values (ADR-0033). No
20
+ * framework runs per frame: a binding's JSX carries an element's style only until its
21
+ * first commit (ADR-0021, decision 6), and after it only the keys the gate doesn't hold.
22
+ *
23
+ * Three locked design decisions (this session's grills):
24
+ * - FLUSHED at the end of the Frame loop's tick (ADR-0033): the gate registers a hook on
25
+ * the Environment's loop (`afterTick`), so everything a tick writes is applied at the end
26
+ * of that tick, inside the Clock's callback, and paints in that Refresh frame. A write
27
+ * made outside a tick (a drag's move, a `release` from a task, an effect a sink's hook ran,
28
+ * a Tier 2 `Stream` on `Clock.ticks` subscribed after the loop) still requests a
29
+ * Refresh frame, only if one is not already pending, and the hook withdraws that request
30
+ * when it flushes first. `flush` does NOT auto-reschedule, so an idle accumulator
31
+ * requests no frames — the next `write` does. The frames come from the Environment's
32
+ * `RefreshFrameSource` (ADR-0031): on the web, `requestAnimationFrame` itself. No binding
33
+ * calls the browser's frame functions itself. A Tier 2 `Stream` that writes here paints in
34
+ * the loop's frame or the next one, by its subscription order (ADR-0033): run it on the
35
+ * loop (`Machine.run`) to paint with it.
36
+ * - ONE flush per frame: all dirty elements flush together. The one exception
37
+ * is `flushPass` (ADR-0018): a projection Measure pass's own writes are applied
38
+ * at the end of its task, so a new layout never paints without its projection.
39
+ * - ONE sink shape (ADR-0034): a sink is the element its patch is committed onto. The gate
40
+ * also removes a key its last patch had and this one doesn't (a dragged node's `z-index`
41
+ * once its raise is dropped, ADR-0016 *Amendment (Reorder)*), restoring the element's own
42
+ * inline value, saved before the gate first wrote the key. Two hooks reach the binding,
43
+ * outside the commits, each in the binding's `batch` (ADR-0034, *Amendment (#209)*): the
44
+ * Starting-style switch before an element's first commit (`beforeFirstCommit`), and the keys
45
+ * the gate holds on the element whenever they change (`onHeld`), so the binding's `style`
46
+ * leaves them to Wardic.
47
+ *
48
+ * Observability (CONTEXT.md convention): the hot path (`write`/`flush`) is the
49
+ * per-frame work, so — exactly like core's per-frame writes and the Clock's
50
+ * `tick` — it stays quiet (no span, no per-call log, no metric). A flush that throws (a
51
+ * binding's hook, a commit) is reported (`reportFailure`) and the gate
52
+ * goes on, so it can't fail the Frame loop's tick it runs in (ADR-0033).
53
+ */
54
+ import * as FrameLoop from "@wardic/core/FrameLoop"
55
+ import * as RefreshFrameSource from "@wardic/core/RefreshFrameSource"
56
+ import * as Renderer from "@wardic/core/Renderer"
57
+ import * as Cause from "effect/Cause"
58
+ import { Atom, AtomRegistry } from "effect/reactivity"
59
+ import * as Effect from "effect/Effect"
60
+ import * as Exit from "effect/Exit"
61
+ import type * as Fiber from "effect/Fiber"
62
+ import * as Predicate from "effect/Predicate"
63
+ import * as UndefinedOr from "effect/UndefinedOr"
64
+ import * as Scope from "effect/Scope"
65
+ import { reportFailure } from "./report.ts"
66
+ import { TEMPLATE_KEY } from "./renderer.ts"
67
+ import { dashCase } from "./svg.ts"
68
+
69
+ /**
70
+ * Where a flushed `Patch` is delivered: committed onto `target` with the Renderer's `commit`,
71
+ * at flush time.
72
+ */
73
+ export interface FlushSink {
74
+ /** The element the patch is committed onto. */
75
+ readonly target: unknown
76
+ /**
77
+ * Called once, just before the element's first commit in a frame flush (not a Measure
78
+ * pass's `flushPass`), inside the binding's `batch` with every other element's switch that
79
+ * flush: a binding drops its Starting style from its `style` prop here,
80
+ * so its framework removes those keys and the commit writes the first frame in the same
81
+ * Refresh frame, with no paint in between.
82
+ */
83
+ readonly beforeFirstCommit?: (() => void) | undefined
84
+ /**
85
+ * Called with every key the gate now holds on the element, each time that set changes: after the
86
+ * gate restores a key it dropped, and before the commit that first
87
+ * writes a key. A binding whose `style` prop reaches the element leaves these keys out of it,
88
+ * so its framework never writes over Wardic's value (`style` is a key's fallback). Called in
89
+ * the binding's `batch`, with every other element's whose keys changed that flush, so what the
90
+ * binding changes reaches the element before the commit, a framework that applies an update later
91
+ * (Solid 2) included.
92
+ */
93
+ // ADR-0034, *Amendment (#209)*.
94
+ readonly onHeld?: ((keys: ReadonlyArray<string>) => void) | undefined
95
+ /**
96
+ * Called once per frame in which the element's values were committed, after every commit of that
97
+ * frame (Motion's `onUpdate`), with its values before
98
+ * projection (`x` as a number). The object is the element's own, reused each call: valid during
99
+ * the call, copied to keep. A Measure pass's commits are told at the frame flush after them, so
100
+ * a frame tells each element once. A write it makes is the next frame's. A throw is reported,
101
+ * and the others are still told.
102
+ */
103
+ readonly onCommit?: ((values: Readonly<Record<string, unknown>>) => void) | undefined
104
+ }
105
+
106
+ /** Accumulated pre-`project` state for one registered element. */
107
+ interface Entry {
108
+ /** Merged value-map (partials `Object.assign`ed in by key). */
109
+ readonly values: Record<string, unknown>
110
+ /** Where this element's projected Patch goes. */
111
+ readonly sink: FlushSink
112
+ /** Whether a `write` has landed since the last flush. */
113
+ dirty: boolean
114
+ /**
115
+ * The keys the gate holds on the element: its last patch's, each with the element's own
116
+ * inline value from before the gate first wrote it (`""` for none), to restore when the
117
+ * key drops out.
118
+ */
119
+ readonly committed: Map<string, string>
120
+ /** Its keys' live outputs (`valueOf`), or `undefined` while nothing asked for one. */
121
+ outputs: Map<string, Output> | undefined
122
+ /** The values its sink's `onCommit` is told, one object reused, made on the first call. */
123
+ latest: Record<string, unknown> | undefined
124
+ /**
125
+ * Whether its `onCommit` threw at its last call (#257 item 4): a run of throws is logged and
126
+ * reported at its first, so one that throws every frame logs once; a call that returns ends it.
127
+ */
128
+ failing: boolean
129
+ /** Its patch, projected this flush and committed once the binding has heard the keys held. */
130
+ patch: Renderer.Patch | undefined
131
+ }
132
+
133
+ /** One key's live output: its Atom, the value last set on it, and its mount in the registry. */
134
+ interface Output {
135
+ readonly atom: Atom.Writable<unknown>
136
+ last: unknown
137
+ readonly unmount: () => void
138
+ }
139
+
140
+ /** The element's own inline value for a patch key (`""` for none, or a target with no style). */
141
+ const inlineValueOf = (target: unknown, key: string): string => {
142
+ if (Predicate.isNullish(target)) return ""
143
+ const style = (target as { readonly style?: CSSStyleDeclaration }).style
144
+ if (Predicate.isUndefined(style)) return ""
145
+ return style.getPropertyValue(dashCase(key))
146
+ }
147
+
148
+ /** A Flush gate: what a binding registers its elements with and writes their values into, one per provider. */
149
+ export interface FlushGate {
150
+ /**
151
+ * Register an element under `id` with its `sink`. Returns an unregister fn
152
+ * that removes the element from the accumulator (a subsequent `write` is a
153
+ * no-op). `values` start its value-map, unflushed: drawn with its first `write`, so they never
154
+ * commit alone.
155
+ */
156
+ // #443: its Transform template, which mustn't take its Starting style off early.
157
+ readonly register: (id: string, sink: FlushSink, values?: Readonly<Record<string, unknown>>) => () => void
158
+ /**
159
+ * Merge `partial` into the element's value-map by key, mark it dirty, and
160
+ * schedule a flush: the end of the Frame loop's tick, for a write made in one, or else the
161
+ * next Refresh frame. A `write` for an unregistered (or unregistered-since) id
162
+ * is a no-op.
163
+ */
164
+ readonly write: (id: string, partial: Record<string, unknown>) => void
165
+ /**
166
+ * Run a projection **Measure pass** now and apply exactly what it wrote before
167
+ * returning. Only `write()`s made while
168
+ * `pass()` runs are recorded; each recorded entry is `project`ed and committed once, and
169
+ * marked clean. Everything else stays dirty for its flush. A write made by an effect a sink's
170
+ * hook runs (a Solid effect) takes the ordinary path. The Projection system calls it at the
171
+ * end of a task, so a new layout never paints without its projection.
172
+ */
173
+ readonly flushPass: (pass: () => void) => void
174
+ /**
175
+ * Hand `keys` back to the element's own style: drop each from the element's
176
+ * value-map and schedule a flush, which writes `restore[key]` (CSS text) for a key it had
177
+ * written, or else the inline value it overwrote, and tells the sink (`onHeld`). A key the
178
+ * entry doesn't hold, or an unknown id, is a no-op. A binding releases a key back at its `style`
179
+ * value, so a later `style` change shows at rest. A later `write` brings the key back.
180
+ */
181
+ readonly release: (id: string, keys: ReadonlyArray<string>, restore?: Readonly<Record<string, string>>) => void
182
+ /**
183
+ * The element's value for `id`'s `key`, live: an `Atom` set as each write that
184
+ * changes it lands, so it's the value the next commit draws, before the Renderer projects it
185
+ * (`x` as a number, not a `transform`). Set at the write, not the commit, so a value derived
186
+ * from it (another key following it through a binding) is written in the same frame and
187
+ * commits with it. `undefined` before the key's first write, and once it's released or the
188
+ * element unregisters. One Atom for an id and a key, made on first ask, so per frame only the
189
+ * keys asked for cost anything.
190
+ */
191
+ readonly valueOf: (id: string, key: string) => Atom.Atom<unknown>
192
+ /**
193
+ * Call `fn` once, after the gate's next flush: the end of the Frame loop's tick, for a call
194
+ * made in one, else the next Refresh frame's, so what was written before it is on the elements.
195
+ * It schedules that flush if none is pending. A binding tells an animation's end here, as Motion's
196
+ * `onAnimationComplete` comes with the final value set. One that throws is reported, and the rest
197
+ * still run.
198
+ */
199
+ readonly afterFlush: (fn: () => void) => void
200
+ /** Takes the gate off the Frame loop, cancels a pending frame and clears what it held. The Layer's Scope calls it. */
201
+ // Wired into the provider's `onCleanup`.
202
+ readonly dispose: () => void
203
+ }
204
+
205
+ /** What a flush gate needs from its Environment: the `Renderer`, the Frame loop it flushes at the end of each tick of, and the `RefreshFrameSource`. */
206
+ // ADR-0033; ADR-0031.
207
+ export type FlushGateServices = Renderer.Renderer | RefreshFrameSource.RefreshFrameSource | FrameLoop.FrameLoop | AtomRegistry.AtomRegistry
208
+
209
+ /**
210
+ * How the gate runs its Effects: synchronously, and forked. `FlushGate.make` builds one from its
211
+ * Layer's context, forking into its Scope (#132, ADR-0041); a test's `ManagedRuntime` is one too.
212
+ */
213
+ export interface FlushGateRuntime {
214
+ readonly runSync: <A, E>(effect: Effect.Effect<A, E, FlushGateServices>) => A
215
+ readonly runFork: <A, E>(effect: Effect.Effect<A, E, FlushGateServices>) => Fiber.Fiber<A, E>
216
+ /** The Scope it was built in: its own fibers live there, ended as it closes (#618). */
217
+ readonly scope: Scope.Scope
218
+ }
219
+
220
+ /** What a binding hands its flush gate. */
221
+ export interface FlushGateOptions {
222
+ /**
223
+ * Runs a flush's hooks together: calls `apply`, and returns once every update inside it has reached
224
+ * the elements, before the gate commits. Solid 1 passes its `batch`, Solid 2 runs `apply` then
225
+ * `flush()`, and React would pass `flushSync`, untried; a binding with nothing to batch passes
226
+ * `(apply) => apply()`. Called at most twice a flush: once for the starting-style switches
227
+ * (`beforeFirstCommit`) of the elements committing for the first time, before anything is read off the
228
+ * elements; and once for the held keys (`onHeld`) of the elements whose held keys changed.
229
+ */
230
+ // ADR-0031, decision 3, ADR-0034; *Amendment (#209)*.
231
+ readonly batch: (apply: () => void) => void
232
+ }
233
+
234
+ /**
235
+ * Build a flush gate against the provider's `runtime`, running each frame flush's Starting-style
236
+ * switches through the binding's `batch`.
237
+ *
238
+ * Resolves both ONCE here: `project` is pure and `commit` a plain call (both called directly at
239
+ * flush, ADR-0034), `requestFrame` /
240
+ * `cancelFrame` schedule and cancel the flush of a write made outside a tick, and the Frame
241
+ * loop's `afterTick` takes the gate's hook for its lifetime. All come from whichever env the
242
+ * provider was given (`DomLive` / `TestLive`), so the gate inherits that environment's
243
+ * grouping/unit logic, its Refresh frames and its loop with zero duplication.
244
+ */
245
+ export const makeFlushGate = (runtime: FlushGateRuntime, options: FlushGateOptions): FlushGate => {
246
+ const { batch } = options
247
+ // Resolve the Renderer's two halves and the Refresh frame source exactly once at creation.
248
+ // `requestFrame` and `cancelFrame` are called detached, never as `frames.requestFrame(cb)`: a
249
+ // browser throws "Illegal invocation" when `requestAnimationFrame` runs with `this` bound to
250
+ // anything but the window (ADR-0031, decision 7).
251
+ const { commit, project, requestFrame, cancelFrame, afterTick } = runtime.runSync(
252
+ Effect.gen(function* () {
253
+ const { commit, project } = yield* Renderer.Renderer
254
+ const { requestFrame, cancelFrame } = yield* RefreshFrameSource.RefreshFrameSource
255
+ const { afterTick } = yield* FrameLoop.FrameLoop
256
+ return { commit, project, requestFrame, cancelFrame, afterTick }
257
+ }),
258
+ )
259
+
260
+ const accumulator = new Map<string, Entry>()
261
+ // Every live output asked for (`valueOf`), by id: an entry registered later picks its map up.
262
+ const outputsById = new Map<string, Map<string, Output>>()
263
+ const registry = runtime.runSync(AtomRegistry.AtomRegistry)
264
+ // Sinks waiting for their first commit's `beforeFirstCommit` (usually empty).
265
+ const firstCommits = new Set<string>()
266
+ let scheduled = false
267
+ // What `afterFlush` queued since the last flush, called at its end (#257).
268
+ let afterFlushes: Array<() => void> = []
269
+ // The pending Refresh frame's handle, while `scheduled`.
270
+ let frame: unknown
271
+ // The ids written while a `flushPass` pass runs; `undefined` otherwise.
272
+ let recording: Set<string> | undefined
273
+ // The entries committed since the last frame flush whose sink has an `onCommit` (#233).
274
+ const committedSince = new Set<Entry>()
275
+ // A frame flush's entries, in order, prepared and waiting for their commit; and those whose
276
+ // keys held changed, which the binding hears first. Reused each flush: the per-frame path.
277
+ const prepared: Array<Entry> = []
278
+ const heldChanged: Array<Entry> = []
279
+
280
+ /**
281
+ * Project one dirty entry's patch and restore the keys it dropped (once per element per flush),
282
+ * marking it clean: into `into`, and into `held` when the keys it holds change and its sink
283
+ * hears them. Its commit waits for the binding's `batch` of those (ADR-0034, *Amendment
284
+ * (#209)*). Plain calls only: this is the per-frame path.
285
+ */
286
+ const prepare = (entry: Entry, into: Array<Entry>, held: Array<Entry>): void => {
287
+ const patch = project(entry.values)
288
+ entry.dirty = false
289
+ entry.patch = patch
290
+ into.push(entry)
291
+ const sink = entry.sink
292
+ const target = sink.target
293
+ const committed = entry.committed
294
+ const before = committed.size
295
+ let kept = 0
296
+ let added = false
297
+ for (const key in patch) {
298
+ if (committed.has(key)) {
299
+ kept++
300
+ continue
301
+ }
302
+ committed.set(key, inlineValueOf(target, key))
303
+ added = true
304
+ }
305
+ // A key the last patch wrote and this one doesn't: the element gets its own value
306
+ // back. Only looked for when one dropped out (the usual frame keeps every key).
307
+ let restore: Renderer.Patch | undefined
308
+ if (kept < before) {
309
+ for (const [key, own] of committed) {
310
+ if (key in patch) continue
311
+ if (Predicate.isUndefined(restore)) restore = {}
312
+ restore[key] = own
313
+ committed.delete(key)
314
+ }
315
+ }
316
+ if (Predicate.isNotUndefined(restore)) commit(target, restore)
317
+ // The keys held changed (the usual frame changes none): the binding hears before the commit.
318
+ if ((added || Predicate.isNotUndefined(restore)) && Predicate.isNotUndefined(sink.onHeld)) held.push(entry)
319
+ }
320
+
321
+ /** Commit a prepared entry's patch onto its element. */
322
+ const commitPrepared = (entry: Entry): void => {
323
+ const patch = entry.patch
324
+ if (Predicate.isUndefined(patch)) return
325
+ entry.patch = undefined
326
+ commit(entry.sink.target, patch)
327
+ if (Predicate.isNotUndefined(entry.sink.onCommit)) committedSince.add(entry)
328
+ }
329
+
330
+ /**
331
+ * The prepared entries, committed, after the binding has heard the keys held change, together, in
332
+ * its `batch` (#209); both lists emptied for the next flush.
333
+ */
334
+ const commitAll = (entries: Array<Entry>, held: Array<Entry>): void => {
335
+ if (held.length > 0) batch(() => tellHeld(held))
336
+ for (let i = 0; i < entries.length; i++) commitPrepared(entries[i])
337
+ entries.length = 0
338
+ held.length = 0
339
+ }
340
+
341
+ /** Each entry's `onHeld`, with every key the gate holds on its element now. */
342
+ const tellHeld = (held: ReadonlyArray<Entry>): void => {
343
+ for (let i = 0; i < held.length; i++) {
344
+ const entry = held[i]
345
+ const onHeld = entry.sink.onHeld
346
+ if (Predicate.isNotUndefined(onHeld)) onHeld(Array.from(entry.committed.keys()))
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Tell each entry committed since the last frame flush, once, after every commit of this one
352
+ * (#233). Its values are copied into its own reused object, so its sink never holds the gate's.
353
+ */
354
+ const tellCommitted = (): void => {
355
+ for (const entry of committedSince) {
356
+ const onCommit = entry.sink.onCommit
357
+ if (Predicate.isUndefined(onCommit)) continue
358
+ let latest = entry.latest
359
+ if (Predicate.isUndefined(latest)) {
360
+ latest = {}
361
+ entry.latest = latest
362
+ }
363
+ for (const key in latest) if (!Object.hasOwn(entry.values, key)) delete latest[key]
364
+ Object.assign(latest, entry.values)
365
+ // Its Transform template rides in its values (#443), and isn't one: Motion's `onUpdate` never sees it.
366
+ delete latest[TEMPLATE_KEY]
367
+ try {
368
+ onCommit(latest)
369
+ entry.failing = false
370
+ } catch (defect) {
371
+ if (entry.failing) continue
372
+ entry.failing = true
373
+ runtime.runFork(reportFailure("wardic.gate: an onCommit failed", Cause.die(defect)))
374
+ }
375
+ }
376
+ committedSince.clear()
377
+ }
378
+
379
+ /**
380
+ * The live outputs asked for of `entry`, each set where its value changed, as a write or a
381
+ * release lands: a value derived from one (a Live value of another key, through a binding) is
382
+ * written in the same frame, and commits with it. An unchanged one skips the registry. Plain
383
+ * calls: a write is the per-frame path.
384
+ */
385
+ const setOutputs = (entry: Entry): void => {
386
+ const outputs = entry.outputs
387
+ if (Predicate.isUndefined(outputs)) return
388
+ for (const [key, output] of outputs) {
389
+ const value = entry.values[key]
390
+ if (value === output.last) continue
391
+ output.last = value
392
+ registry.set(output.atom, value)
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Run `beforeFirstCommit` for each waiting entry about to commit (dirty), in the binding's
398
+ * `batch` before any commit, so the binding applies what it changes first. A wrap that never
399
+ * commits stays waiting, so this loops over the wraps still waiting, once per frame flush.
400
+ */
401
+ const runFirstCommits = (): void => {
402
+ for (const id of firstCommits) {
403
+ const entry = accumulator.get(id)
404
+ if (Predicate.isUndefined(entry) || !entry.dirty) continue
405
+ firstCommits.delete(id)
406
+ const hook = entry.sink.beforeFirstCommit
407
+ if (Predicate.isNotUndefined(hook)) hook()
408
+ }
409
+ }
410
+
411
+ const flush = (): void => {
412
+ scheduled = false
413
+ try {
414
+ // The switches first: an element's own inline values are read once its Starting style is gone.
415
+ if (firstCommits.size > 0) batch(runFirstCommits)
416
+ // Every dirty element, once. No auto-reschedule — the next write() does. A write()
417
+ // from an effect a sink's hook runs requests the next frame (it isn't stranded).
418
+ for (const entry of accumulator.values()) if (entry.dirty) prepare(entry, prepared, heldChanged)
419
+ commitAll(prepared, heldChanged)
420
+ if (committedSince.size > 0) tellCommitted()
421
+ } catch (defect) {
422
+ prepared.length = 0
423
+ heldChanged.length = 0
424
+ runtime.runFork(reportFailure("wardic.gate: a flush failed", Cause.die(defect)))
425
+ }
426
+ if (afterFlushes.length > 0) runAfterFlushes()
427
+ }
428
+
429
+ /** The calls `afterFlush` queued, each once, a new queue for any they make. */
430
+ const runAfterFlushes = (): void => {
431
+ const due = afterFlushes
432
+ afterFlushes = []
433
+ for (const fn of due) {
434
+ try {
435
+ fn()
436
+ } catch (defect) {
437
+ runtime.runFork(reportFailure("wardic.gate: an afterFlush call failed", Cause.die(defect)))
438
+ }
439
+ }
440
+ }
441
+
442
+ const afterFlush: FlushGate["afterFlush"] = (fn) => {
443
+ afterFlushes.push(fn)
444
+ scheduleFlush()
445
+ }
446
+
447
+ /**
448
+ * The end of the Frame loop's tick (ADR-0033): flush now, in the Refresh frame that computed
449
+ * the values, and withdraw the frame the first write since the last flush requested.
450
+ */
451
+ const flushTicked = (): undefined => {
452
+ if (!scheduled) return undefined
453
+ cancelFrame(frame)
454
+ flush()
455
+ return undefined
456
+ }
457
+
458
+ const scheduleFlush = (): void => {
459
+ if (scheduled) return
460
+ scheduled = true
461
+ frame = requestFrame(flush)
462
+ }
463
+
464
+ const register: FlushGate["register"] = (id, sink, values) => {
465
+ const entry: Entry = { values: UndefinedOr.match(values, { onUndefined: () => ({}), onDefined: (start) => ({ ...start }) }), sink, dirty: false, committed: new Map(), outputs: outputsById.get(id), latest: undefined, failing: false, patch: undefined }
466
+ accumulator.set(id, entry)
467
+ if (Predicate.isNotUndefined(sink.beforeFirstCommit)) firstCommits.add(id)
468
+ return () => {
469
+ accumulator.delete(id)
470
+ firstCommits.delete(id)
471
+ committedSince.delete(entry)
472
+ endOutputs(id)
473
+ }
474
+ }
475
+
476
+ /** `id`'s live outputs, each set to `undefined` and forgotten: its element is gone. */
477
+ const endOutputs = (id: string): void => {
478
+ const outputs = outputsById.get(id)
479
+ if (Predicate.isUndefined(outputs)) return
480
+ outputsById.delete(id)
481
+ for (const output of outputs.values()) {
482
+ registry.set(output.atom, undefined)
483
+ output.unmount()
484
+ }
485
+ }
486
+
487
+ /** `id`'s current value for `key`, or `undefined` for an unregistered id or a key it hasn't written. */
488
+ const committedValueOf = (id: string, key: string): unknown => {
489
+ const entry = accumulator.get(id)
490
+ if (Predicate.isUndefined(entry)) return undefined
491
+ return entry.values[key]
492
+ }
493
+
494
+ const valueOf: FlushGate["valueOf"] = (id, key) => {
495
+ let outputs = outputsById.get(id)
496
+ if (Predicate.isUndefined(outputs)) {
497
+ outputs = new Map()
498
+ outputsById.set(id, outputs)
499
+ const entry = accumulator.get(id)
500
+ if (Predicate.isNotUndefined(entry)) entry.outputs = outputs
501
+ }
502
+ const known = outputs.get(key)
503
+ if (Predicate.isNotUndefined(known)) return known.atom
504
+ // Made `undefined` and mounted until its element unregisters, not kept alive: a kept-alive
505
+ // Atom's node never leaves the registry, and one left behind reads `undefined` afresh.
506
+ const atom = Atom.make<unknown>(undefined)
507
+ const output: Output = { atom, last: undefined, unmount: registry.mount(atom) }
508
+ outputs.set(key, output)
509
+ const value = committedValueOf(id, key)
510
+ if (Predicate.isNotUndefined(value)) {
511
+ output.last = value
512
+ registry.set(atom, value)
513
+ }
514
+ return atom
515
+ }
516
+
517
+ const write: FlushGate["write"] = (id, partial) => {
518
+ const entry = accumulator.get(id)
519
+ if (Predicate.isUndefined(entry)) return
520
+ Object.assign(entry.values, partial)
521
+ entry.dirty = true
522
+ if (Predicate.isNotUndefined(recording)) recording.add(id)
523
+ scheduleFlush()
524
+ setOutputs(entry)
525
+ }
526
+
527
+ const flushPass: FlushGate["flushPass"] = (pass) => {
528
+ const written = new Set<string>()
529
+ recording = written
530
+ try {
531
+ pass()
532
+ } finally {
533
+ recording = undefined
534
+ }
535
+ if (written.size === 0) return
536
+ // No `beforeFirstCommit` here: a Measure pass may write only a node's projection, and
537
+ // dropping the Starting style then would leave its animated keys unset until the frame
538
+ // flush (a frame later for an element mounted inside a frame). The frame flush switches.
539
+ // A commit here is told at the frame flush (#233): the write that made it scheduled that flush.
540
+ // Its own lists: a pass may run from a hook the frame flush's `batch` runs.
541
+ const entries: Array<Entry> = []
542
+ const held: Array<Entry> = []
543
+ for (const id of written) {
544
+ const entry = accumulator.get(id)
545
+ if (Predicate.isNotUndefined(entry) && entry.dirty) prepare(entry, entries, held)
546
+ }
547
+ commitAll(entries, held)
548
+ }
549
+
550
+ const release: FlushGate["release"] = (id, keys, restore) => {
551
+ const entry = accumulator.get(id)
552
+ if (Predicate.isUndefined(entry)) return
553
+ let released = false
554
+ for (const key of keys) {
555
+ if (!Object.hasOwn(entry.values, key)) continue
556
+ delete entry.values[key]
557
+ released = true
558
+ // The drop-out path in `apply` writes the saved value back: make it the one handed in.
559
+ if (Predicate.isUndefined(restore)) continue
560
+ const value: string | undefined = restore[key]
561
+ if (Predicate.isNotUndefined(value) && entry.committed.has(key)) entry.committed.set(key, value)
562
+ }
563
+ if (!released) return
564
+ entry.dirty = true
565
+ scheduleFlush()
566
+ setOutputs(entry)
567
+ }
568
+
569
+ const dispose: FlushGate["dispose"] = () => {
570
+ // Off the loop first, so no tick flushes what follows; a failure reported, never thrown (#618).
571
+ const unhooked = Effect.runSyncExit(Scope.close(ticks, Exit.void))
572
+ if (Exit.isFailure(unhooked)) runtime.runFork(reportFailure("wardic.gate: its tick hook failed to close", unhooked.cause))
573
+ if (scheduled) {
574
+ cancelFrame(frame)
575
+ scheduled = false
576
+ }
577
+ accumulator.clear()
578
+ firstCommits.clear()
579
+ committedSince.clear()
580
+ // The calls waiting on a flush that won't come: their provider is gone (#257).
581
+ afterFlushes = []
582
+ for (const id of [...outputsById.keys()]) endOutputs(id)
583
+ }
584
+
585
+ // The gate's hook on the loop, for the gate's lifetime: a child of the Scope it's built in, so a gate
586
+ // its `dispose` never reached is unhooked as that Scope closes.
587
+ const ticks = Scope.forkUnsafe(runtime.scope, "sequential")
588
+ runtime.runSync(afterTick(flushTicked).pipe(Scope.provide(ticks)))
589
+
590
+ return { register, write, flushPass, release, valueOf, afterFlush, dispose }
591
+ }
592
+
593
+