@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.
- package/LICENSE +21 -0
- package/README.md +8 -2
- package/THIRD_PARTY_NOTICES.md +145 -0
- package/dist/Appear.d.ts +24 -0
- package/dist/Appear.d.ts.map +1 -0
- package/dist/Appear.js +25 -0
- package/dist/Appear.js.map +1 -0
- package/dist/Color.d.ts +14 -0
- package/dist/Color.d.ts.map +1 -0
- package/dist/Color.js +16 -0
- package/dist/Color.js.map +1 -0
- package/dist/DomAnnouncer.d.ts +13 -0
- package/dist/DomAnnouncer.d.ts.map +1 -0
- package/dist/DomAnnouncer.js +14 -0
- package/dist/DomAnnouncer.js.map +1 -0
- package/dist/DomClock.d.ts +11 -0
- package/dist/DomClock.d.ts.map +1 -0
- package/dist/DomClock.js +12 -0
- package/dist/DomClock.js.map +1 -0
- package/dist/DomDefaultTransition.d.ts +12 -0
- package/dist/DomDefaultTransition.d.ts.map +1 -0
- package/dist/DomDefaultTransition.js +13 -0
- package/dist/DomDefaultTransition.js.map +1 -0
- package/dist/DomInput.d.ts +13 -0
- package/dist/DomInput.d.ts.map +1 -0
- package/dist/DomInput.js +15 -0
- package/dist/DomInput.js.map +1 -0
- package/dist/DomInterpolators.d.ts +13 -0
- package/dist/DomInterpolators.d.ts.map +1 -0
- package/dist/DomInterpolators.js +14 -0
- package/dist/DomInterpolators.js.map +1 -0
- package/dist/DomKeyRules.d.ts +13 -0
- package/dist/DomKeyRules.d.ts.map +1 -0
- package/dist/DomKeyRules.js +14 -0
- package/dist/DomKeyRules.js.map +1 -0
- package/dist/DomPositionalKeys.d.ts +13 -0
- package/dist/DomPositionalKeys.d.ts.map +1 -0
- package/dist/DomPositionalKeys.js +14 -0
- package/dist/DomPositionalKeys.js.map +1 -0
- package/dist/DomProbe.d.ts +13 -0
- package/dist/DomProbe.d.ts.map +1 -0
- package/dist/DomProbe.js +14 -0
- package/dist/DomProbe.js.map +1 -0
- package/dist/DomProjectionSystem.d.ts +28 -0
- package/dist/DomProjectionSystem.d.ts.map +1 -0
- package/dist/DomProjectionSystem.js +41 -0
- package/dist/DomProjectionSystem.js.map +1 -0
- package/dist/DomRenderer.d.ts +13 -0
- package/dist/DomRenderer.d.ts.map +1 -0
- package/dist/DomRenderer.js +15 -0
- package/dist/DomRenderer.js.map +1 -0
- package/dist/FlushGate.d.ts +48 -0
- package/dist/FlushGate.d.ts.map +1 -0
- package/dist/FlushGate.js +62 -0
- package/dist/FlushGate.js.map +1 -0
- package/dist/MeasuredPair.d.ts +14 -0
- package/dist/MeasuredPair.d.ts.map +1 -0
- package/dist/MeasuredPair.js +16 -0
- package/dist/MeasuredPair.js.map +1 -0
- package/dist/OwnValues.d.ts +14 -0
- package/dist/OwnValues.d.ts.map +1 -0
- package/dist/OwnValues.js +15 -0
- package/dist/OwnValues.js.map +1 -0
- package/dist/ProjectionSystem.d.ts +34 -0
- package/dist/ProjectionSystem.d.ts.map +1 -0
- package/dist/ProjectionSystem.js +29 -0
- package/dist/ProjectionSystem.js.map +1 -0
- package/dist/ReorderController.d.ts +41 -0
- package/dist/ReorderController.d.ts.map +1 -0
- package/dist/ReorderController.js +44 -0
- package/dist/ReorderController.js.map +1 -0
- package/dist/Report.d.ts +12 -0
- package/dist/Report.d.ts.map +1 -0
- package/dist/Report.js +13 -0
- package/dist/Report.js.map +1 -0
- package/dist/ScrollOffset.d.ts +34 -0
- package/dist/ScrollOffset.d.ts.map +1 -0
- package/dist/ScrollOffset.js +42 -0
- package/dist/ScrollOffset.js.map +1 -0
- package/dist/Style.d.ts +15 -0
- package/dist/Style.d.ts.map +1 -0
- package/dist/Style.js +17 -0
- package/dist/Style.js.map +1 -0
- package/dist/Svg.d.ts +13 -0
- package/dist/Svg.d.ts.map +1 -0
- package/dist/Svg.js +14 -0
- package/dist/Svg.js.map +1 -0
- package/dist/Variables.d.ts +13 -0
- package/dist/Variables.d.ts.map +1 -0
- package/dist/Variables.js +14 -0
- package/dist/Variables.js.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/announcer.d.ts +50 -0
- package/dist/internal/announcer.d.ts.map +1 -0
- package/dist/internal/announcer.js +115 -0
- package/dist/internal/announcer.js.map +1 -0
- package/dist/internal/appear.d.ts +169 -0
- package/dist/internal/appear.d.ts.map +1 -0
- package/dist/internal/appear.js +499 -0
- package/dist/internal/appear.js.map +1 -0
- package/dist/internal/auto-scroll.d.ts +24 -0
- package/dist/internal/auto-scroll.d.ts.map +1 -0
- package/dist/internal/auto-scroll.js +124 -0
- package/dist/internal/auto-scroll.js.map +1 -0
- package/dist/internal/clock.d.ts +19 -0
- package/dist/internal/clock.d.ts.map +1 -0
- package/dist/internal/clock.js +39 -0
- package/dist/internal/clock.js.map +1 -0
- package/dist/internal/colors.d.ts +45 -0
- package/dist/internal/colors.d.ts.map +1 -0
- package/dist/internal/colors.js +193 -0
- package/dist/internal/colors.js.map +1 -0
- package/dist/internal/complex.d.ts +24 -0
- package/dist/internal/complex.d.ts.map +1 -0
- package/dist/internal/complex.js +28 -0
- package/dist/internal/complex.js.map +1 -0
- package/dist/internal/content-watch.d.ts +20 -0
- package/dist/internal/content-watch.d.ts.map +1 -0
- package/dist/internal/content-watch.js +76 -0
- package/dist/internal/content-watch.js.map +1 -0
- package/dist/internal/default-transition.d.ts +25 -0
- package/dist/internal/default-transition.d.ts.map +1 -0
- package/dist/internal/default-transition.js +41 -0
- package/dist/internal/default-transition.js.map +1 -0
- package/dist/internal/drag-defaults.d.ts +18 -0
- package/dist/internal/drag-defaults.d.ts.map +1 -0
- package/dist/internal/drag-defaults.js +38 -0
- package/dist/internal/drag-defaults.js.map +1 -0
- package/dist/internal/gate.d.ts +186 -0
- package/dist/internal/gate.d.ts.map +1 -0
- package/dist/internal/gate.js +453 -0
- package/dist/internal/gate.js.map +1 -0
- package/dist/internal/input.d.ts +41 -0
- package/dist/internal/input.d.ts.map +1 -0
- package/dist/internal/input.js +476 -0
- package/dist/internal/input.js.map +1 -0
- package/dist/internal/interpolators.d.ts +19 -0
- package/dist/internal/interpolators.d.ts.map +1 -0
- package/dist/internal/interpolators.js +30 -0
- package/dist/internal/interpolators.js.map +1 -0
- package/dist/internal/key-rules.d.ts +11 -0
- package/dist/internal/key-rules.d.ts.map +1 -0
- package/dist/internal/key-rules.js +244 -0
- package/dist/internal/key-rules.js.map +1 -0
- package/dist/internal/measure.d.ts +150 -0
- package/dist/internal/measure.d.ts.map +1 -0
- package/dist/internal/measure.js +278 -0
- package/dist/internal/measure.js.map +1 -0
- package/dist/internal/measured-pairs.d.ts +38 -0
- package/dist/internal/measured-pairs.d.ts.map +1 -0
- package/dist/internal/measured-pairs.js +251 -0
- package/dist/internal/measured-pairs.js.map +1 -0
- package/dist/internal/own-values.d.ts +35 -0
- package/dist/internal/own-values.d.ts.map +1 -0
- package/dist/internal/own-values.js +117 -0
- package/dist/internal/own-values.js.map +1 -0
- package/dist/internal/pass-ends.d.ts +21 -0
- package/dist/internal/pass-ends.d.ts.map +1 -0
- package/dist/internal/pass-ends.js +14 -0
- package/dist/internal/pass-ends.js.map +1 -0
- package/dist/internal/positional-keys.d.ts +16 -0
- package/dist/internal/positional-keys.d.ts.map +1 -0
- package/dist/internal/positional-keys.js +21 -0
- package/dist/internal/positional-keys.js.map +1 -0
- package/dist/internal/probe.d.ts +10 -0
- package/dist/internal/probe.d.ts.map +1 -0
- package/dist/internal/probe.js +117 -0
- package/dist/internal/probe.js.map +1 -0
- package/dist/internal/projection-defaults.d.ts +34 -0
- package/dist/internal/projection-defaults.d.ts.map +1 -0
- package/dist/internal/projection-defaults.js +21 -0
- package/dist/internal/projection-defaults.js.map +1 -0
- package/dist/internal/projection.d.ts +529 -0
- package/dist/internal/projection.d.ts.map +1 -0
- package/dist/internal/projection.js +4990 -0
- package/dist/internal/projection.js.map +1 -0
- package/dist/internal/renderer.d.ts +86 -0
- package/dist/internal/renderer.d.ts.map +1 -0
- package/dist/internal/renderer.js +616 -0
- package/dist/internal/renderer.js.map +1 -0
- package/dist/internal/reorder.d.ts +159 -0
- package/dist/internal/reorder.d.ts.map +1 -0
- package/dist/internal/reorder.js +437 -0
- package/dist/internal/reorder.js.map +1 -0
- package/dist/internal/report.d.ts +30 -0
- package/dist/internal/report.d.ts.map +1 -0
- package/dist/internal/report.js +60 -0
- package/dist/internal/report.js.map +1 -0
- package/dist/internal/require-global.d.ts +11 -0
- package/dist/internal/require-global.d.ts.map +1 -0
- package/dist/internal/require-global.js +31 -0
- package/dist/internal/require-global.js.map +1 -0
- package/dist/internal/reveal.d.ts +16 -0
- package/dist/internal/reveal.d.ts.map +1 -0
- package/dist/internal/reveal.js +66 -0
- package/dist/internal/reveal.js.map +1 -0
- package/dist/internal/runner.d.ts +21 -0
- package/dist/internal/runner.d.ts.map +1 -0
- package/dist/internal/runner.js +48 -0
- package/dist/internal/runner.js.map +1 -0
- package/dist/internal/scale-correction.d.ts +80 -0
- package/dist/internal/scale-correction.d.ts.map +1 -0
- package/dist/internal/scale-correction.js +227 -0
- package/dist/internal/scale-correction.js.map +1 -0
- package/dist/internal/scroll.d.ts +35 -0
- package/dist/internal/scroll.d.ts.map +1 -0
- package/dist/internal/scroll.js +244 -0
- package/dist/internal/scroll.js.map +1 -0
- package/dist/internal/scrollers.d.ts +35 -0
- package/dist/internal/scrollers.d.ts.map +1 -0
- package/dist/internal/scrollers.js +38 -0
- package/dist/internal/scrollers.js.map +1 -0
- package/dist/internal/starting-style.d.ts +141 -0
- package/dist/internal/starting-style.d.ts.map +1 -0
- package/dist/internal/starting-style.js +300 -0
- package/dist/internal/starting-style.js.map +1 -0
- package/dist/internal/svg.d.ts +60 -0
- package/dist/internal/svg.d.ts.map +1 -0
- package/dist/internal/svg.js +257 -0
- package/dist/internal/svg.js.map +1 -0
- package/dist/internal/variables.d.ts +45 -0
- package/dist/internal/variables.d.ts.map +1 -0
- package/dist/internal/variables.js +92 -0
- package/dist/internal/variables.js.map +1 -0
- package/package.json +65 -4
- package/src/Appear.ts +45 -0
- package/src/Color.ts +24 -0
- package/src/DomAnnouncer.ts +22 -0
- package/src/DomClock.ts +13 -0
- package/src/DomDefaultTransition.ts +16 -0
- package/src/DomInput.ts +22 -0
- package/src/DomInterpolators.ts +16 -0
- package/src/DomKeyRules.ts +17 -0
- package/src/DomPositionalKeys.ts +16 -0
- package/src/DomProbe.ts +15 -0
- package/src/DomProjectionSystem.ts +44 -0
- package/src/DomRenderer.ts +26 -0
- package/src/FlushGate.ts +81 -0
- package/src/MeasuredPair.ts +15 -0
- package/src/OwnValues.ts +19 -0
- package/src/ProjectionSystem.ts +51 -0
- package/src/ReorderController.ts +72 -0
- package/src/Report.ts +17 -0
- package/src/ScrollOffset.ts +54 -0
- package/src/Style.ts +37 -0
- package/src/Svg.ts +21 -0
- package/src/Variables.ts +17 -0
- package/src/index.ts +106 -0
- package/src/internal/announcer.ts +132 -0
- package/src/internal/appear.ts +739 -0
- package/src/internal/auto-scroll.ts +176 -0
- package/src/internal/clock.ts +45 -0
- package/src/internal/colors.ts +258 -0
- package/src/internal/complex.ts +29 -0
- package/src/internal/content-watch.ts +84 -0
- package/src/internal/culori.d.ts +58 -0
- package/src/internal/default-transition.ts +50 -0
- package/src/internal/drag-defaults.ts +54 -0
- package/src/internal/gate.ts +593 -0
- package/src/internal/input.ts +579 -0
- package/src/internal/interpolators.ts +33 -0
- package/src/internal/key-rules.ts +357 -0
- package/src/internal/measure.ts +357 -0
- package/src/internal/measured-pairs.ts +297 -0
- package/src/internal/own-values.ts +166 -0
- package/src/internal/pass-ends.ts +23 -0
- package/src/internal/positional-keys.ts +24 -0
- package/src/internal/probe.ts +176 -0
- package/src/internal/projection-defaults.ts +45 -0
- package/src/internal/projection.ts +5999 -0
- package/src/internal/renderer.ts +646 -0
- package/src/internal/reorder.ts +604 -0
- package/src/internal/report.ts +69 -0
- package/src/internal/require-global.ts +46 -0
- package/src/internal/reveal.ts +69 -0
- package/src/internal/runner.ts +64 -0
- package/src/internal/scale-correction.ts +310 -0
- package/src/internal/scroll.ts +356 -0
- package/src/internal/scrollers.ts +51 -0
- package/src/internal/starting-style.ts +396 -0
- package/src/internal/svg.ts +300 -0
- 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
|
+
|