@playfast/reform 1.0.1 → 1.2.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.
- package/README.md +103 -56
- package/package.json +17 -17
- package/src/boundary/boundary.test.ts +63 -49
- package/src/boundary/boundary.ts +144 -113
- package/src/calc/asyncCalc.invalidate.test.ts +518 -32
- package/src/calc/asyncCalc.test.ts +207 -213
- package/src/calc/asyncCalc.ts +131 -154
- package/src/calc/asyncData.ts +0 -37
- package/src/calc/calc.test.ts +7 -33
- package/src/calc/calc.ts +44 -58
- package/src/calc/calcFamily.test.ts +80 -34
- package/src/calc/calcFamily.ts +54 -65
- package/src/calc/compose.test.ts +1 -12
- package/src/calc/compose.ts +1 -36
- package/src/calc/queryState.ts +0 -23
- package/src/channel/channel.ts +126 -111
- package/src/compose/composition.test.ts +19 -0
- package/src/compose/composition.ts +351 -127
- package/src/compose/host.ts +0 -6
- package/src/compose/props.ts +13 -12
- package/src/compose/provide.ts +189 -62
- package/src/compose/slot.ts +158 -49
- package/src/compose/structure.test.ts +6 -9
- package/src/compose/structure.ts +108 -79
- package/src/compose/ui.test.ts +19 -7
- package/src/compose/ui.ts +155 -156
- package/src/compose/ui.typecheck.ts +40 -18
- package/src/definition/definition.ts +22 -41
- package/src/event/event.fromSource.test.ts +172 -0
- package/src/event/event.test.ts +10 -3
- package/src/event/event.ts +102 -27
- package/src/event/eventGroup.ts +0 -1
- package/src/feature/feature.mount.test.ts +122 -43
- package/src/feature/feature.test.ts +46 -21
- package/src/feature/feature.ts +1347 -256
- package/src/feature/feature.typecheck.ts +273 -68
- package/src/graph/closure.ts +403 -0
- package/src/index.ts +68 -126
- package/src/internal/bucketCache.ts +39 -0
- package/src/internal/capture.ts +9 -24
- package/src/internal/env.ts +7 -0
- package/src/internal/errors.test.ts +0 -6
- package/src/internal/errors.ts +37 -60
- package/src/internal/inspect.test.ts +0 -6
- package/src/internal/inspect.ts +0 -12
- package/src/internal/queryDriver.ts +105 -201
- package/src/internal/queryDriverStore.ts +156 -0
- package/src/internal/queryDriverTypes.ts +59 -0
- package/src/internal/queryEvents.ts +0 -12
- package/src/internal/queryStore.ts +0 -14
- package/src/internal/reuse.test.ts +10 -14
- package/src/internal/reuse.ts +5 -30
- package/src/internal/scheduler.ts +4 -44
- package/src/internal/seeds.ts +0 -14
- package/src/internal/sources.ts +26 -49
- package/src/internal/stateRegistry.ts +0 -14
- package/src/internal/store.test.ts +1 -3
- package/src/internal/store.ts +0 -37
- package/src/internal/track.ts +3 -20
- package/src/internal/variance.ts +5 -0
- package/src/internal.ts +222 -0
- package/src/namespace/namespace.test.ts +46 -0
- package/src/namespace/namespace.ts +106 -0
- package/src/procedure/procedure.ts +40 -35
- package/src/reducer/reducer.ts +78 -52
- package/src/remote/remoteState.test.ts +590 -397
- package/src/remote/remoteState.ts +425 -347
- package/src/remote/remoteState.typecheck.ts +47 -56
- package/src/runtime/appRuntime.activation.test.ts +100 -0
- package/src/runtime/appRuntime.test.ts +249 -0
- package/src/runtime/appRuntime.ts +415 -97
- package/src/runtime/bus.ts +2 -15
- package/src/runtime/eventBudget.test.ts +152 -0
- package/src/runtime/eventBudget.ts +120 -0
- package/src/runtime/hardening.test.ts +73 -13
- package/src/runtime/instrumentation.test.ts +55 -52
- package/src/runtime/instrumentation.ts +6 -60
- package/src/runtime/loop.test.ts +36 -17
- package/src/runtime/loop.ts +87 -88
- package/src/runtime/queries.ts +0 -17
- package/src/scene/featureScene.test.ts +61 -0
- package/src/scene/scene.ts +247 -104
- package/src/scene/seedScene.test.ts +42 -79
- package/src/state/state.nominal.typecheck.ts +68 -0
- package/src/state/state.test.ts +17 -0
- package/src/state/state.ts +80 -46
- package/src/state/stateFamily.test.ts +16 -11
- package/src/state/stateFamily.ts +55 -65
- package/src/state/stateGroup.ts +53 -64
- package/src/state/token.ts +40 -23
- package/src/synced/syncedStore.ts +46 -58
- package/src/testkit/flight.testkit.ts +75 -0
- package/src/ui/node.ts +0 -6
- package/src/ui/trigger.ts +0 -5
- package/src/wire/tree.test.ts +8 -7
- package/src/wire/tree.ts +0 -39
- package/src/wire/triggers.test.ts +6 -6
- package/src/wire/triggers.ts +14 -32
- package/src/internal/ctx.ts +0 -16
package/src/boundary/boundary.ts
CHANGED
|
@@ -1,191 +1,222 @@
|
|
|
1
|
-
import { Context, Data, Effect, Layer, MutableRef, Predicate } from 'effect'
|
|
1
|
+
import { Context, Data, Effect, Equal, Layer, MutableRef, Option, Predicate } from 'effect'
|
|
2
|
+
import type { Effect as EffectType } from 'effect/Effect'
|
|
2
3
|
import { type Manifest, yieldableClass } from '../definition/definition'
|
|
3
4
|
import { resolveScheduler } from '../internal/scheduler'
|
|
4
|
-
import { type InputStores,
|
|
5
|
+
import { type InputStores, wireSources } from '../internal/sources'
|
|
5
6
|
import { makeDerivedStore, type Store } from '../internal/store'
|
|
6
7
|
import { readTracked } from '../internal/track'
|
|
7
|
-
import { type
|
|
8
|
-
|
|
9
|
-
// Boundary: the Suspense-boundary analog, value-level. One derived store that
|
|
10
|
-
// merges the lifecycles of several async sources into a single
|
|
11
|
-
// Pending/Errored/Ready value, so ONE surface (a splash, a skeleton) covers a
|
|
12
|
-
// whole subtree's first load instead of every query consumer painting its own.
|
|
13
|
-
//
|
|
14
|
-
// Reform can do this without React's throw-a-promise machinery because
|
|
15
|
-
// fetching is layer-driven, not render-driven: an `AsyncCalc` runs because its
|
|
16
|
-
// layer is built, never because something rendered. A boundary therefore only
|
|
17
|
-
// *aggregates* lifecycles — it cannot create render-waterfalls, and content it
|
|
18
|
-
// reveals already has its data.
|
|
19
|
-
//
|
|
20
|
-
// The arm mapping IS the semantics, and most of it falls out of `AsyncData`:
|
|
21
|
-
// Loading → Pending (the arm only exists before the first value,
|
|
22
|
-
// so a boundary is first-load-only by construction)
|
|
23
|
-
// Success → Ready (even `refetching: true` — SWR refetches never
|
|
24
|
-
// re-show the fallback; no transition machinery)
|
|
25
|
-
// Idle → Ready (a gated query that is off is DELIBERATELY off —
|
|
26
|
-
// a signed-out viewer must not wait on a query
|
|
27
|
-
// that only runs signed-in)
|
|
28
|
-
// Error → Errored (the error-boundary half, also value-level)
|
|
29
|
-
//
|
|
30
|
-
// Chained gates (B enables when A succeeds) cannot leak a Ready between hops:
|
|
31
|
-
// the boundary's layer requires every covered store, so each driver subscribes
|
|
32
|
-
// upstream BEFORE the boundary does, the scheduler flush runs listeners in
|
|
33
|
-
// subscription order to a fixpoint within one microtask, and reads are
|
|
34
|
-
// pull-fresh — by the time any subscriber reads, the next hop is already
|
|
35
|
-
// Loading. `boundary.test.ts` pins this with a recorded-notification proof.
|
|
36
|
-
|
|
37
|
-
/** Some covered source is still on its FIRST load — show the fallback. */
|
|
8
|
+
import { type AnySource, type SourceCapture, type SourceValue } from '../state/token'
|
|
9
|
+
|
|
38
10
|
export interface BoundaryPending {
|
|
39
11
|
readonly _tag: 'Pending'
|
|
40
12
|
}
|
|
41
13
|
|
|
42
|
-
/** Some covered source failed before its first value. */
|
|
43
14
|
export interface BoundaryErrored {
|
|
44
15
|
readonly _tag: 'Errored'
|
|
45
|
-
/** The failures, in `over` order (heterogeneous — display via `String`). */
|
|
46
16
|
readonly errors: ReadonlyArray<unknown>
|
|
47
17
|
}
|
|
48
18
|
|
|
49
|
-
/** Every covered source has settled (or is deliberately gated off). */
|
|
50
19
|
export interface BoundaryReady {
|
|
51
20
|
readonly _tag: 'Ready'
|
|
52
21
|
}
|
|
53
22
|
|
|
54
23
|
export type BoundaryState = BoundaryPending | BoundaryErrored | BoundaryReady
|
|
55
24
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
25
|
+
export const BoundaryStoreTypeId: unique symbol = Symbol.for('reform/BoundaryStore')
|
|
26
|
+
export type BoundaryStoreTypeId = typeof BoundaryStoreTypeId
|
|
27
|
+
|
|
28
|
+
export interface BoundaryStore<N extends string> {
|
|
29
|
+
readonly [BoundaryStoreTypeId]: N
|
|
30
|
+
}
|
|
31
|
+
|
|
60
32
|
const pendingArm: BoundaryState = { _tag: 'Pending' }
|
|
61
33
|
const readyArm: BoundaryState = { _tag: 'Ready' }
|
|
62
34
|
const erroredArm = (errors: ReadonlyArray<unknown>): BoundaryState =>
|
|
63
35
|
Data.struct({ _tag: 'Errored' as const, errors: Data.array(errors) })
|
|
64
36
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
37
|
+
export type AnyLifecycleSource = AnySource
|
|
38
|
+
|
|
39
|
+
declare const BoundaryLifecycleMismatchTypeId: unique symbol
|
|
40
|
+
|
|
41
|
+
interface BoundaryLifecycleMismatch<Value> {
|
|
42
|
+
readonly [BoundaryLifecycleMismatchTypeId]: Value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type CheckedLifecycleValue<Value> = Value extends { readonly _tag: 'Idle' | 'Loading' }
|
|
46
|
+
? Value
|
|
47
|
+
: Value extends {
|
|
48
|
+
readonly _tag: 'Success'
|
|
49
|
+
readonly value: infer _Success
|
|
50
|
+
readonly refetching: boolean
|
|
51
|
+
}
|
|
52
|
+
? Value
|
|
53
|
+
: Value extends {
|
|
54
|
+
readonly _tag: 'Error'
|
|
55
|
+
readonly error: infer _Failure
|
|
56
|
+
readonly refetching: boolean
|
|
57
|
+
}
|
|
58
|
+
? Value
|
|
59
|
+
: BoundaryLifecycleMismatch<Value>
|
|
60
|
+
|
|
61
|
+
type CheckedLifecycleSource<Source extends AnySource> = [SourceValue<Source>] extends [never]
|
|
62
|
+
? BoundaryLifecycleMismatch<Source>
|
|
63
|
+
: SourceValue<Source> extends CheckedLifecycleValue<SourceValue<Source>>
|
|
64
|
+
? Source
|
|
65
|
+
: BoundaryLifecycleMismatch<SourceValue<Source>>
|
|
66
|
+
|
|
67
|
+
type CheckedLifecycleSources<Over extends ReadonlyArray<AnyLifecycleSource>> = {
|
|
68
|
+
readonly [Index in keyof Over]: Over[Index] extends AnySource
|
|
69
|
+
? CheckedLifecycleSource<Over[Index]>
|
|
70
|
+
: BoundaryLifecycleMismatch<Over[Index]>
|
|
71
|
+
}
|
|
76
72
|
|
|
77
73
|
export interface BoundaryManifest<N extends string> extends Manifest {
|
|
78
74
|
readonly kind: 'Boundary'
|
|
79
75
|
readonly name: N
|
|
80
76
|
}
|
|
81
77
|
|
|
82
|
-
export interface BoundaryClass<
|
|
83
|
-
extends
|
|
78
|
+
export interface BoundaryClass<
|
|
79
|
+
N extends string,
|
|
80
|
+
out Over extends ReadonlyArray<AnyLifecycleSource>,
|
|
81
|
+
> extends EffectType<BoundaryState, never, BoundaryStore<N>> {
|
|
84
82
|
new (): {}
|
|
85
83
|
readonly manifest: BoundaryManifest<N>
|
|
86
|
-
readonly store: Context.Tag<
|
|
87
|
-
/** The boundary's name, so it doubles as a `Source` input to a calc. */
|
|
84
|
+
readonly store: Context.Tag<BoundaryStore<N>, Store<BoundaryState>>
|
|
88
85
|
readonly name: N
|
|
89
|
-
/** The covered lifecycle sources — read by `live` and the editor's graph. */
|
|
90
86
|
readonly over: Over
|
|
87
|
+
readonly capture: <Result>(visit: SourceCapture<Result>) => Result
|
|
91
88
|
}
|
|
92
89
|
|
|
93
90
|
export interface BoundaryConfig<Over extends ReadonlyArray<AnyLifecycleSource>> {
|
|
94
|
-
readonly over: Over
|
|
91
|
+
readonly over: Over & CheckedLifecycleSources<Over>
|
|
95
92
|
}
|
|
96
93
|
|
|
97
94
|
export interface BoundaryOptions {
|
|
98
|
-
/**
|
|
99
|
-
* Latch: once Ready, stay Ready. A BOOT boundary wants this — a covered
|
|
100
|
-
* route-gated query that first enables on a later navigation (Idle →
|
|
101
|
-
* Loading) must not re-splash the whole shell; its own screen owns that
|
|
102
|
-
* loading state. Without the latch the boundary re-pends on any covered
|
|
103
|
-
* first load, which is what a per-screen boundary wants.
|
|
104
|
-
*/
|
|
105
95
|
// oxlint-disable-next-line reform-rules/no-optional-fields -- public options-bag input; omission is the documented default (no latch) and external callers pass a plain `{ once: true }` literal
|
|
106
96
|
readonly once?: boolean
|
|
107
97
|
}
|
|
108
98
|
|
|
109
|
-
/**
|
|
110
|
-
* Define a boundary over async sources. `yield* MyBoundary` reads the merged
|
|
111
|
-
* `Pending | Errored | Ready` lifecycle; the consuming view renders ONE
|
|
112
|
-
* fallback on Pending and its content slot on Ready — content reveals
|
|
113
|
-
* together, with data already present (fetching never waited on rendering).
|
|
114
|
-
*/
|
|
115
99
|
export const make = <const N extends string, const Over extends ReadonlyArray<AnyLifecycleSource>>(
|
|
116
100
|
name: N,
|
|
117
101
|
config: BoundaryConfig<Over>,
|
|
118
102
|
): BoundaryClass<N, Over> => {
|
|
119
|
-
const store = Context.GenericTag<
|
|
103
|
+
const store = Context.GenericTag<BoundaryStore<N>, Store<BoundaryState>>(
|
|
120
104
|
`reform/boundary/${name}`,
|
|
121
105
|
)
|
|
122
106
|
const manifest: BoundaryManifest<N> = { kind: 'Boundary', name }
|
|
123
107
|
const read = Effect.flatMap(store, readTracked)
|
|
124
|
-
|
|
108
|
+
const boundary: BoundaryClass<N, Over> = yieldableClass(read, {
|
|
109
|
+
manifest,
|
|
110
|
+
store,
|
|
111
|
+
name,
|
|
112
|
+
over: config.over,
|
|
113
|
+
capture: <Result>(visit: SourceCapture<Result>): Result => visit(boundary),
|
|
114
|
+
})
|
|
115
|
+
return boundary
|
|
125
116
|
}
|
|
126
117
|
|
|
127
|
-
// The snapshot values arrive through `wireSources`' erased key projection
|
|
128
|
-
// (`ReadonlyArray<unknown>`), so read the two facts the merge needs through
|
|
129
|
-
// honest structural guards — no cast back to `AnyAsyncData`.
|
|
130
118
|
const tagOf = (arm: unknown): string =>
|
|
131
119
|
Predicate.hasProperty(arm, '_tag') && Predicate.isString(arm._tag) ? arm._tag : ''
|
|
132
|
-
const errorOf = (arm: unknown):
|
|
133
|
-
Predicate.hasProperty(arm, 'error') && tagOf(arm) === 'Error'
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
120
|
+
const errorOf = (arm: unknown): Option.Option<unknown> =>
|
|
121
|
+
Predicate.hasProperty(arm, 'error') && tagOf(arm) === 'Error'
|
|
122
|
+
? Option.some(arm.error)
|
|
123
|
+
: Option.none()
|
|
124
|
+
|
|
125
|
+
interface SlotUpdate {
|
|
126
|
+
readonly slot: number
|
|
127
|
+
readonly arm: unknown
|
|
128
|
+
}
|
|
129
|
+
|
|
141
130
|
export const live = <N extends string, Over extends ReadonlyArray<AnyLifecycleSource>>(
|
|
142
131
|
boundary: BoundaryClass<N, Over>,
|
|
143
132
|
options: BoundaryOptions = {},
|
|
144
|
-
): Layer.Layer<
|
|
145
|
-
// Scoped so the source subscriptions are released with the layer's scope.
|
|
133
|
+
): Layer.Layer<BoundaryStore<N>, never, InputStores<Over>> =>
|
|
146
134
|
Layer.scoped(
|
|
147
135
|
boundary.store,
|
|
148
136
|
Effect.gen(function* () {
|
|
149
137
|
const scheduler = yield* resolveScheduler
|
|
150
|
-
const
|
|
138
|
+
const sourceWires = yield* Effect.forEach(boundary.over, (source) => wireSources([source]))
|
|
139
|
+
const readSource = (source: (typeof sourceWires)[number]): unknown =>
|
|
140
|
+
source.keyOf(source.snapshot())[0]
|
|
141
|
+
const arms = sourceWires.map((source) => MutableRef.make(readSource(source)))
|
|
142
|
+
const loadingCount = MutableRef.make(
|
|
143
|
+
arms.reduce((count, arm) => count + (tagOf(MutableRef.get(arm)) === 'Loading' ? 1 : 0), 0),
|
|
144
|
+
)
|
|
145
|
+
const errorCount = MutableRef.make(
|
|
146
|
+
arms.reduce(
|
|
147
|
+
(count, arm) => count + (Option.isSome(errorOf(MutableRef.get(arm))) ? 1 : 0),
|
|
148
|
+
0,
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
const collectErrors = (): ReadonlyArray<unknown> =>
|
|
152
|
+
arms.flatMap((arm) => Option.toArray(errorOf(MutableRef.get(arm))))
|
|
153
|
+
const errorArm = MutableRef.make(erroredArm(collectErrors()))
|
|
154
|
+
const updateSlot = (update: SlotUpdate): void => {
|
|
155
|
+
const armRef = arms[update.slot]
|
|
156
|
+
if (armRef === undefined) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
const previous = MutableRef.get(armRef)
|
|
160
|
+
if (Equal.equals(previous, update.arm)) {
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
MutableRef.set(armRef, update.arm)
|
|
164
|
+
MutableRef.update(
|
|
165
|
+
loadingCount,
|
|
166
|
+
(count) =>
|
|
167
|
+
count +
|
|
168
|
+
(tagOf(update.arm) === 'Loading' ? 1 : 0) -
|
|
169
|
+
(tagOf(previous) === 'Loading' ? 1 : 0),
|
|
170
|
+
)
|
|
171
|
+
const previousError = errorOf(previous)
|
|
172
|
+
const nextError = errorOf(update.arm)
|
|
173
|
+
if (!Equal.equals(previousError, nextError)) {
|
|
174
|
+
MutableRef.update(
|
|
175
|
+
errorCount,
|
|
176
|
+
(count) =>
|
|
177
|
+
count + (Option.isSome(nextError) ? 1 : 0) - (Option.isSome(previousError) ? 1 : 0),
|
|
178
|
+
)
|
|
179
|
+
MutableRef.set(errorArm, erroredArm(collectErrors()))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const synchronize = (): void =>
|
|
183
|
+
sourceWires.forEach((source, slot) => updateSlot({ slot, arm: readSource(source) }))
|
|
151
184
|
|
|
152
185
|
const latched = MutableRef.make(false)
|
|
153
|
-
const
|
|
154
|
-
{ readonly key: ReadonlyArray<unknown>; readonly value: BoundaryState } | undefined
|
|
155
|
-
>(undefined)
|
|
186
|
+
const incremental = MutableRef.make(false)
|
|
156
187
|
const recompute = (): BoundaryState => {
|
|
157
188
|
if (options.once === true && MutableRef.get(latched)) {
|
|
158
189
|
return readyArm
|
|
159
190
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (prev !== undefined && sameKey(arms, prev.key)) {
|
|
163
|
-
return prev.value
|
|
191
|
+
if (!MutableRef.get(incremental)) {
|
|
192
|
+
synchronize()
|
|
164
193
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
?
|
|
169
|
-
:
|
|
170
|
-
? pendingArm
|
|
171
|
-
: readyArm
|
|
172
|
-
MutableRef.set(memo, { key: arms, value: mergedArm })
|
|
173
|
-
return mergedArm
|
|
194
|
+
return MutableRef.get(errorCount) > 0
|
|
195
|
+
? MutableRef.get(errorArm)
|
|
196
|
+
: MutableRef.get(loadingCount) > 0
|
|
197
|
+
? pendingArm
|
|
198
|
+
: readyArm
|
|
174
199
|
}
|
|
175
200
|
|
|
176
|
-
const
|
|
201
|
+
const subscribe = (onChange: () => void): (() => void) => {
|
|
202
|
+
const publish = (): void => {
|
|
203
|
+
MutableRef.set(incremental, true)
|
|
204
|
+
onChange()
|
|
205
|
+
MutableRef.set(incremental, false)
|
|
206
|
+
}
|
|
207
|
+
const settle = (): void => scheduler.schedule([publish])
|
|
208
|
+
const unsubscribes = sourceWires.map((source, sourceIndex) =>
|
|
209
|
+
source.subscribe(() => {
|
|
210
|
+
updateSlot({ slot: sourceIndex, arm: readSource(source) })
|
|
211
|
+
scheduler.schedule([settle])
|
|
212
|
+
}),
|
|
213
|
+
)
|
|
214
|
+
return () => unsubscribes.forEach((unsubscribe) => unsubscribe())
|
|
215
|
+
}
|
|
216
|
+
const derived = makeDerivedStore(recompute, subscribe, scheduler)
|
|
177
217
|
yield* Effect.addFinalizer(() => Effect.sync(derived.unsubscribe))
|
|
178
218
|
|
|
179
219
|
if (options.once === true) {
|
|
180
|
-
// Latch on CONVERGED values only — at build and on (coalesced,
|
|
181
|
-
// post-fixpoint) notifications — never inside `recompute`. A chained
|
|
182
|
-
// gate with a plain Calc hop between the async stores (bootstrap →
|
|
183
|
-
// current-workspace → boards) propagates over TWO flush rounds, so a
|
|
184
|
-
// mid-flush recompute can observe the frame where the downstream
|
|
185
|
-
// driver has not yet flipped its query to Loading. That intermediate
|
|
186
|
-
// Ready self-corrects within the flush for readers, but a latch taken
|
|
187
|
-
// there would freeze it; a subscriber runs only after the fixpoint,
|
|
188
|
-
// where the value is converged.
|
|
189
220
|
if (recompute()._tag === 'Ready') {
|
|
190
221
|
MutableRef.set(latched, true)
|
|
191
222
|
}
|