@playfast/reform 1.1.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 +85 -55
- package/package.json +17 -17
- package/src/boundary/boundary.test.ts +62 -16
- package/src/boundary/boundary.ts +141 -32
- package/src/calc/asyncCalc.invalidate.test.ts +516 -18
- package/src/calc/asyncCalc.test.ts +207 -175
- package/src/calc/asyncCalc.ts +168 -39
- package/src/calc/calc.test.ts +3 -5
- package/src/calc/calc.ts +44 -18
- package/src/calc/calcFamily.test.ts +80 -22
- package/src/calc/calcFamily.ts +56 -25
- package/src/calc/compose.ts +1 -14
- package/src/channel/channel.ts +128 -11
- package/src/compose/composition.test.ts +19 -0
- package/src/compose/composition.ts +346 -62
- package/src/compose/props.ts +13 -6
- package/src/compose/provide.ts +187 -46
- package/src/compose/slot.ts +156 -19
- package/src/compose/structure.test.ts +6 -3
- package/src/compose/structure.ts +106 -13
- package/src/compose/ui.test.ts +19 -3
- package/src/compose/ui.ts +147 -54
- package/src/compose/ui.typecheck.ts +40 -4
- package/src/definition/definition.ts +22 -9
- package/src/event/event.fromSource.test.ts +172 -0
- package/src/event/event.test.ts +10 -0
- package/src/event/event.ts +102 -18
- package/src/feature/feature.mount.test.ts +123 -56
- package/src/feature/feature.test.ts +67 -63
- package/src/feature/feature.ts +1317 -197
- package/src/feature/feature.typecheck.ts +253 -61
- package/src/graph/closure.ts +403 -0
- package/src/index.ts +171 -245
- package/src/internal/bucketCache.ts +39 -0
- package/src/internal/capture.ts +9 -4
- package/src/internal/env.ts +1 -3
- package/src/internal/errors.ts +21 -10
- package/src/internal/queryDriver.ts +120 -15
- package/src/internal/queryDriverStore.ts +8 -5
- package/src/internal/queryDriverTypes.ts +3 -1
- package/src/internal/reuse.test.ts +10 -3
- package/src/internal/reuse.ts +5 -2
- package/src/internal/scheduler.ts +4 -1
- package/src/internal/sources.ts +25 -11
- package/src/internal/track.ts +3 -2
- package/src/internal.ts +222 -0
- package/src/namespace/namespace.ts +46 -25
- package/src/procedure/procedure.ts +38 -11
- package/src/reducer/reducer.ts +78 -34
- package/src/remote/remoteState.test.ts +515 -339
- package/src/remote/remoteState.ts +572 -107
- package/src/remote/remoteState.typecheck.ts +47 -23
- package/src/runtime/appRuntime.activation.test.ts +100 -0
- package/src/runtime/appRuntime.test.ts +157 -105
- package/src/runtime/appRuntime.ts +394 -33
- package/src/runtime/eventBudget.test.ts +1 -4
- package/src/runtime/eventBudget.ts +9 -8
- package/src/runtime/hardening.test.ts +4 -9
- package/src/runtime/instrumentation.test.ts +174 -192
- package/src/runtime/instrumentation.ts +6 -11
- package/src/runtime/loop.test.ts +36 -0
- package/src/runtime/loop.ts +69 -40
- package/src/scene/featureScene.test.ts +4 -2
- package/src/scene/scene.ts +234 -64
- package/src/scene/seedScene.test.ts +69 -86
- package/src/state/state.nominal.typecheck.ts +68 -0
- package/src/state/state.test.ts +17 -0
- package/src/state/state.ts +79 -26
- package/src/state/stateFamily.test.ts +14 -0
- package/src/state/stateFamily.ts +55 -22
- package/src/state/stateGroup.ts +53 -17
- package/src/state/token.ts +40 -10
- package/src/synced/syncedStore.ts +46 -23
- package/src/testkit/flight.testkit.ts +75 -0
- package/src/wire/tree.test.ts +8 -4
- package/src/wire/triggers.test.ts +6 -2
- package/src/wire/triggers.ts +14 -10
- package/src/calc/asyncCalcDefinitions.ts +0 -48
- package/src/channel/procedureRegistry.ts +0 -90
- package/src/feature/featureBinding.ts +0 -48
- package/src/internal/ctx.ts +0 -9
- package/src/remote/remoteStateDefinition.ts +0 -135
- package/src/remote/remoteStateLayers.ts +0 -118
- package/src/remote/remoteStateLiveTypes.ts +0 -59
- package/src/remote/remoteStateSend.ts +0 -64
|
@@ -4,20 +4,20 @@ import {
|
|
|
4
4
|
Effect,
|
|
5
5
|
MutableRef,
|
|
6
6
|
Option,
|
|
7
|
+
type ParseResult,
|
|
7
8
|
Queue,
|
|
9
|
+
Runtime,
|
|
8
10
|
type Scope,
|
|
9
11
|
Stream,
|
|
10
12
|
} from 'effect'
|
|
13
|
+
import { type AnyAsyncData } from '../calc/asyncData'
|
|
11
14
|
import { empty, type QueryState } from '../calc/queryState'
|
|
12
15
|
import { Queries, type QueryHandle } from '../runtime/queries'
|
|
13
16
|
import { reuse } from './reuse'
|
|
14
17
|
import { resolveInstrumentation } from '../runtime/instrumentation'
|
|
15
18
|
import { resolveQueryStore } from './queryStore'
|
|
16
|
-
import type { QueryDriver, QueryDriverOptions } from './queryDriverTypes'
|
|
17
19
|
import {
|
|
18
|
-
hydrateQuery,
|
|
19
20
|
incrementGeneration,
|
|
20
|
-
makePersistWriter,
|
|
21
21
|
makeQueryProjection,
|
|
22
22
|
subscribeQuerySources,
|
|
23
23
|
settleQueryError,
|
|
@@ -27,23 +27,58 @@ import { resolveScheduler } from './scheduler'
|
|
|
27
27
|
import {
|
|
28
28
|
type InputsObject,
|
|
29
29
|
type InputStores,
|
|
30
|
+
type InvalidateBy,
|
|
30
31
|
sameKey,
|
|
31
32
|
wireSources,
|
|
32
33
|
} from './sources'
|
|
33
|
-
import { makeStore } from './store'
|
|
34
|
+
import { makeStore, type Store } from './store'
|
|
34
35
|
import { type AnySource } from '../state/token'
|
|
35
36
|
|
|
36
37
|
export {
|
|
37
38
|
bumpRevision,
|
|
38
39
|
gatedFlag,
|
|
39
40
|
type GatedOf,
|
|
40
|
-
type QueryDriver,
|
|
41
|
-
type QueryDriverOptions,
|
|
42
41
|
Revision,
|
|
43
42
|
RevisionSchema,
|
|
44
43
|
revisionZero,
|
|
45
44
|
} from './queryDriverTypes'
|
|
46
45
|
|
|
46
|
+
// Keep schema invariance behind a covariant persistence boundary.
|
|
47
|
+
export interface QueryCodec<A> {
|
|
48
|
+
readonly encode: (value: A) => Effect.Effect<unknown, ParseResult.ParseError, never>
|
|
49
|
+
readonly decode: (input: unknown) => Effect.Effect<A, ParseResult.ParseError, never>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface QueryDriverOptions<Inputs extends ReadonlyArray<AnySource>, A, E, R> {
|
|
53
|
+
readonly name: string
|
|
54
|
+
readonly label: string
|
|
55
|
+
readonly gated: boolean
|
|
56
|
+
readonly inputs: Inputs
|
|
57
|
+
readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
|
|
58
|
+
readonly invalidateBy?: InvalidateBy<Inputs> | undefined
|
|
59
|
+
readonly disabled?: ((inputs: InputsObject<Inputs>) => boolean) | undefined
|
|
60
|
+
readonly coalesce?: 'switch' | 'trailing' | undefined
|
|
61
|
+
readonly reuse?: boolean | undefined
|
|
62
|
+
readonly extraKey?:
|
|
63
|
+
| {
|
|
64
|
+
readonly read: () => unknown
|
|
65
|
+
readonly subscribe: (listener: () => void) => () => void
|
|
66
|
+
}
|
|
67
|
+
| undefined
|
|
68
|
+
readonly persist?:
|
|
69
|
+
| {
|
|
70
|
+
readonly key: string | ((inputs: InputsObject<Inputs>) => string)
|
|
71
|
+
readonly codec: QueryCodec<A>
|
|
72
|
+
}
|
|
73
|
+
| undefined
|
|
74
|
+
readonly onSettled?: ((generation: number) => void) | undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface QueryDriver<A, E> {
|
|
78
|
+
readonly store: Store<AnyAsyncData<A, E>>
|
|
79
|
+
readonly requested: () => number
|
|
80
|
+
}
|
|
81
|
+
|
|
47
82
|
// oxlint-disable-next-line reform-rules/prefer-effect-fn -- generic export: Effect.fn's inferred type isn't portable under isolatedDeclarations
|
|
48
83
|
export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R>(
|
|
49
84
|
options: QueryDriverOptions<Inputs, A, E, R>,
|
|
@@ -58,6 +93,10 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
58
93
|
|
|
59
94
|
const keyOf = (args: InputsObject<Inputs>): ReadonlyArray<unknown> =>
|
|
60
95
|
extraKey === undefined ? sources.keyOf(args) : [...sources.keyOf(args), extraKey.read()]
|
|
96
|
+
const persistKey = (
|
|
97
|
+
key: string | ((inputs: InputsObject<Inputs>) => string),
|
|
98
|
+
args: InputsObject<Inputs>,
|
|
99
|
+
): string => (typeof key === 'function' ? key(args) : key)
|
|
61
100
|
const disabledNow = (args: InputsObject<Inputs> = sources.snapshot()): boolean =>
|
|
62
101
|
options.gated && options.disabled !== undefined ? options.disabled(args) : false
|
|
63
102
|
|
|
@@ -67,8 +106,17 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
67
106
|
const readers = MutableRef.make(0)
|
|
68
107
|
|
|
69
108
|
const lastKey = MutableRef.make<ReadonlyArray<unknown> | undefined>(undefined)
|
|
109
|
+
// Monotonic source epoch for the one startup hydration attempt. Comparing
|
|
110
|
+
// only the current key is insufficient: A → B → A would otherwise let the
|
|
111
|
+
// original A cache land over the later A request. Same-key refetches do not
|
|
112
|
+
// move this epoch, so cache may still paint during an in-flight revalidation.
|
|
113
|
+
const sourceEpoch = MutableRef.make(0)
|
|
70
114
|
const generation = MutableRef.make(0)
|
|
71
115
|
const nextGeneration = (): number => incrementGeneration(generation)
|
|
116
|
+
// A stale cause needs a strictly newer requested generation. Hydration
|
|
117
|
+
// captures this before the startup kick, while invalidation captures it when
|
|
118
|
+
// the value is marked stale.
|
|
119
|
+
const staleAtGeneration = MutableRef.make(0)
|
|
72
120
|
const markFetching = () => {
|
|
73
121
|
const state = stateStore.get()
|
|
74
122
|
if (!state.isFetching) {
|
|
@@ -89,7 +137,24 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
89
137
|
})
|
|
90
138
|
return shared
|
|
91
139
|
}
|
|
92
|
-
const
|
|
140
|
+
const settleError = (error: E, failedGeneration: number) => {
|
|
141
|
+
// Preserve the stale cause after failure without duplicating a newer request.
|
|
142
|
+
MutableRef.update(staleAtGeneration, (current) => Math.max(current, failedGeneration))
|
|
143
|
+
settleQueryError(stateStore, error)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const persistWriteTo = (
|
|
147
|
+
target: NonNullable<typeof persist>,
|
|
148
|
+
produced: A,
|
|
149
|
+
args: InputsObject<Inputs>,
|
|
150
|
+
): Effect.Effect<void> =>
|
|
151
|
+
target.codec.encode(produced).pipe(
|
|
152
|
+
Effect.flatMap((encoded) => queryStore.set(persistKey(target.key, args), encoded)),
|
|
153
|
+
// Persistence is best-effort and must not block the visible write.
|
|
154
|
+
Effect.ignore,
|
|
155
|
+
)
|
|
156
|
+
const persistWrite = (produced: A, args: InputsObject<Inputs>): Effect.Effect<void> =>
|
|
157
|
+
persist === undefined ? Effect.void : persistWriteTo(persist, produced, args)
|
|
93
158
|
|
|
94
159
|
const derived = makeQueryProjection({ gated: options.gated, scheduler, stateStore })
|
|
95
160
|
yield* Effect.addFinalizer(() => Effect.sync(derived.unsubscribe))
|
|
@@ -123,7 +188,7 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
123
188
|
onFailure: (cause): Option.Option<A> => {
|
|
124
189
|
const failure = Cause.failureOption(cause)
|
|
125
190
|
if (Option.isSome(failure) && !disabledNow()) {
|
|
126
|
-
|
|
191
|
+
settleError(failure.value, request.generation)
|
|
127
192
|
}
|
|
128
193
|
return Option.none()
|
|
129
194
|
},
|
|
@@ -161,6 +226,9 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
161
226
|
const enqueue = (force: boolean) => {
|
|
162
227
|
const args = sources.snapshot()
|
|
163
228
|
if (disabledNow(args)) {
|
|
229
|
+
if (MutableRef.get(lastKey) !== undefined) {
|
|
230
|
+
MutableRef.update(sourceEpoch, (current) => current + 1)
|
|
231
|
+
}
|
|
164
232
|
MutableRef.set(lastKey, undefined)
|
|
165
233
|
stateStore.set(idle)
|
|
166
234
|
return
|
|
@@ -170,6 +238,9 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
170
238
|
if (!force && previous !== undefined && sameKey(key, previous)) {
|
|
171
239
|
return
|
|
172
240
|
}
|
|
241
|
+
if (previous !== undefined && !sameKey(key, previous)) {
|
|
242
|
+
MutableRef.update(sourceEpoch, (current) => current + 1)
|
|
243
|
+
}
|
|
173
244
|
MutableRef.set(lastKey, key)
|
|
174
245
|
markFetching()
|
|
175
246
|
Queue.unsafeOffer(requests, { args, generation: nextGeneration() })
|
|
@@ -178,7 +249,8 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
178
249
|
const refetch = () => enqueue(true)
|
|
179
250
|
|
|
180
251
|
const maybeAutoRefetch = () => {
|
|
181
|
-
|
|
252
|
+
const hasCoveringRequest = MutableRef.get(generation) > MutableRef.get(staleAtGeneration)
|
|
253
|
+
if (stateStore.get().isStale && MutableRef.get(readers) > 0 && !hasCoveringRequest) {
|
|
182
254
|
refetch()
|
|
183
255
|
}
|
|
184
256
|
}
|
|
@@ -187,6 +259,7 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
187
259
|
if (state.isStale) {
|
|
188
260
|
return
|
|
189
261
|
}
|
|
262
|
+
MutableRef.set(staleAtGeneration, MutableRef.get(generation))
|
|
190
263
|
stateStore.set({ ...state, isStale: true })
|
|
191
264
|
maybeAutoRefetch()
|
|
192
265
|
}
|
|
@@ -194,14 +267,46 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
194
267
|
const unsubscribe = subscribeQuerySources(sources, extraKey, trigger)
|
|
195
268
|
yield* Effect.addFinalizer(() => Effect.sync(unsubscribe))
|
|
196
269
|
|
|
270
|
+
// Fork hydration so an asynchronous store never blocks layer acquisition.
|
|
271
|
+
// Epoch, key, and settled-state guards prevent a late cache from replacing newer truth.
|
|
197
272
|
if (persist !== undefined && !disabledNow()) {
|
|
198
273
|
const hydrateArgs = sources.snapshot()
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
274
|
+
const hydrateSourceKey = keyOf(hydrateArgs)
|
|
275
|
+
const hydrateSourceEpoch = MutableRef.get(sourceEpoch)
|
|
276
|
+
const hydrateStaleAtGeneration = MutableRef.get(generation)
|
|
277
|
+
const hydrate = queryStore.get(persistKey(persist.key, hydrateArgs)).pipe(
|
|
278
|
+
Effect.flatMap((cached) =>
|
|
279
|
+
Option.match(cached, {
|
|
280
|
+
onNone: () => Effect.void,
|
|
281
|
+
onSome: (raw) =>
|
|
282
|
+
persist.codec.decode(raw).pipe(
|
|
283
|
+
Effect.tap((decoded) =>
|
|
284
|
+
Effect.sync(() => {
|
|
285
|
+
const state = stateStore.get()
|
|
286
|
+
if (
|
|
287
|
+
disabledNow() ||
|
|
288
|
+
hydrateSourceEpoch !== MutableRef.get(sourceEpoch) ||
|
|
289
|
+
!sameKey(hydrateSourceKey, keyOf(sources.snapshot())) ||
|
|
290
|
+
Option.isSome(state.data) ||
|
|
291
|
+
Option.isSome(state.error)
|
|
292
|
+
) {
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
MutableRef.update(staleAtGeneration, (current) =>
|
|
296
|
+
Math.max(current, hydrateStaleAtGeneration),
|
|
297
|
+
)
|
|
298
|
+
stateStore.set({ ...state, data: Option.some(decoded), isStale: true })
|
|
299
|
+
}),
|
|
300
|
+
),
|
|
301
|
+
),
|
|
302
|
+
}),
|
|
303
|
+
),
|
|
304
|
+
// Corrupt/incompatible cache: ignore and fetch fresh.
|
|
305
|
+
Effect.ignore,
|
|
306
|
+
)
|
|
307
|
+
const runtime = yield* Effect.runtime<never>()
|
|
308
|
+
const scope = yield* Effect.scope
|
|
309
|
+
Runtime.runFork(runtime)(hydrate, { immediate: true, scope })
|
|
205
310
|
}
|
|
206
311
|
|
|
207
312
|
const initial = sources.snapshot()
|
|
@@ -15,9 +15,7 @@ interface HydrateQueryOptions<A, E> {
|
|
|
15
15
|
readonly stateStore: Store<QueryState<A, E>>
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export const hydrateQuery = <A, E>(
|
|
19
|
-
options: HydrateQueryOptions<A, E>,
|
|
20
|
-
): Effect.Effect<void> =>
|
|
18
|
+
export const hydrateQuery = <A, E>(options: HydrateQueryOptions<A, E>): Effect.Effect<void> =>
|
|
21
19
|
Effect.flatMap(options.queryStore.get(options.cacheKey), (cached) =>
|
|
22
20
|
Option.match(cached, {
|
|
23
21
|
onNone: () => Effect.void,
|
|
@@ -52,7 +50,10 @@ export const persistQueryValue = <A>(options: PersistQueryValueOptions<A>): Effe
|
|
|
52
50
|
|
|
53
51
|
interface PersistWriterOptions<Inputs extends ReadonlyArray<AnySource>, A> {
|
|
54
52
|
readonly persist:
|
|
55
|
-
| {
|
|
53
|
+
| {
|
|
54
|
+
readonly key: string | ((inputs: InputsObject<Inputs>) => string)
|
|
55
|
+
readonly schema: AnySchema<A>
|
|
56
|
+
}
|
|
56
57
|
| undefined
|
|
57
58
|
readonly queryStore: QueryStoreApi
|
|
58
59
|
}
|
|
@@ -136,7 +137,9 @@ interface ReaderStoreOptions<A, E> {
|
|
|
136
137
|
readonly onFirstReader: () => void
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
export const withReaderTracking = <A, E>(
|
|
140
|
+
export const withReaderTracking = <A, E>(
|
|
141
|
+
options: ReaderStoreOptions<A, E>,
|
|
142
|
+
): Store<AnyAsyncData<A, E>> => ({
|
|
140
143
|
...options.store,
|
|
141
144
|
subscribe: (listener) => {
|
|
142
145
|
const before = MutableRef.get(options.readers)
|
|
@@ -7,7 +7,9 @@ import type { AnySchema } from './variance'
|
|
|
7
7
|
|
|
8
8
|
export type GatedOf<AlwaysOn extends boolean> = AlwaysOn extends true ? false : true
|
|
9
9
|
|
|
10
|
-
export const gatedFlag = <AlwaysOn extends boolean>(
|
|
10
|
+
export const gatedFlag = <AlwaysOn extends boolean>(
|
|
11
|
+
alwaysOn: AlwaysOn | undefined,
|
|
12
|
+
): GatedOf<AlwaysOn> =>
|
|
11
13
|
// oxlint-disable-next-line reform-rules/no-type-assertion -- restate the runtime boolean as its conditional `GatedOf` literal; no guard maps a boolean to a conditional type
|
|
12
14
|
(alwaysOn !== true) as GatedOf<AlwaysOn>
|
|
13
15
|
|
|
@@ -13,7 +13,10 @@ describe('reuse', () => {
|
|
|
13
13
|
const e0 = { id: 1, name: 'a' }
|
|
14
14
|
const e1 = { id: 2, name: 'b' }
|
|
15
15
|
const prev = [e0, e1]
|
|
16
|
-
const next = [
|
|
16
|
+
const next = [
|
|
17
|
+
{ id: 1, name: 'a' },
|
|
18
|
+
{ id: 2, name: 'B' },
|
|
19
|
+
]
|
|
17
20
|
const out = reuse(prev, next)
|
|
18
21
|
expect(out).not.toBe(prev)
|
|
19
22
|
expect(out[0]).toBe(e0)
|
|
@@ -49,7 +52,10 @@ describe('reuse', () => {
|
|
|
49
52
|
class View extends S.Class<View>('View')({ title: S.String, rows: S.Array(Row) }) {}
|
|
50
53
|
const keep = new Row({ id: 1, name: 'a' })
|
|
51
54
|
const prev = new View({ title: 't', rows: [keep, new Row({ id: 2, name: 'b' })] })
|
|
52
|
-
const next = new View({
|
|
55
|
+
const next = new View({
|
|
56
|
+
title: 't',
|
|
57
|
+
rows: [new Row({ id: 1, name: 'a' }), new Row({ id: 2, name: 'B' })],
|
|
58
|
+
})
|
|
53
59
|
const out = reuse(prev, next)
|
|
54
60
|
expect(out).not.toBe(prev)
|
|
55
61
|
expect(out).toBeInstanceOf(View)
|
|
@@ -77,7 +83,8 @@ describe('reuse', () => {
|
|
|
77
83
|
|
|
78
84
|
it('returns next below the depth bound', () => {
|
|
79
85
|
type Nest = { readonly n?: Nest; readonly v: number }
|
|
80
|
-
const deep = (depth: number): Nest =>
|
|
86
|
+
const deep = (depth: number): Nest =>
|
|
87
|
+
depth === 0 ? { v: 0 } : { v: depth, n: deep(depth - 1) }
|
|
81
88
|
const prev = deep(20)
|
|
82
89
|
const next = deep(20)
|
|
83
90
|
const out = reuse(prev, next)
|
package/src/internal/reuse.ts
CHANGED
|
@@ -54,10 +54,13 @@ const reconcile = ({ prev, next, depth }: ReconcileArgs): unknown => {
|
|
|
54
54
|
}
|
|
55
55
|
if (Array.isArray(prev) && Array.isArray(next)) {
|
|
56
56
|
const reconciled = next.map((element, index) =>
|
|
57
|
-
index < prev.length
|
|
57
|
+
index < prev.length
|
|
58
|
+
? reconcile({ prev: prev[index], next: element, depth: depth - 1 })
|
|
59
|
+
: element,
|
|
58
60
|
)
|
|
59
61
|
const unchanged =
|
|
60
|
-
prev.length === next.length &&
|
|
62
|
+
prev.length === next.length &&
|
|
63
|
+
reconciled.every((entry, index) => Object.is(entry, prev[index]))
|
|
61
64
|
return unchanged ? prev : reconciled
|
|
62
65
|
}
|
|
63
66
|
if (isPlainRecord(prev) && isPlainRecord(next)) {
|
|
@@ -54,7 +54,10 @@ const NotificationsBase: Context.TagClass<Notifications, 'reform/Notifications',
|
|
|
54
54
|
Context.Tag('reform/Notifications')<Notifications, Scheduler>()
|
|
55
55
|
export class Notifications extends NotificationsBase {}
|
|
56
56
|
|
|
57
|
-
export const notificationsLayer: Layer.Layer<Notifications> = Layer.sync(
|
|
57
|
+
export const notificationsLayer: Layer.Layer<Notifications> = Layer.sync(
|
|
58
|
+
Notifications,
|
|
59
|
+
makeScheduler,
|
|
60
|
+
)
|
|
58
61
|
|
|
59
62
|
export const resolveScheduler: Effect.Effect<Scheduler> = Effect.map(
|
|
60
63
|
Effect.serviceOption(Notifications),
|
package/src/internal/sources.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { Context, Effect, Equal, Predicate } from 'effect'
|
|
2
|
+
import {
|
|
3
|
+
type AnySource,
|
|
4
|
+
type SourceIdentifier,
|
|
5
|
+
type SourceName,
|
|
6
|
+
type SourceValue,
|
|
7
|
+
} from '../state/token'
|
|
5
8
|
|
|
6
9
|
export type InputsObject<Inputs extends ReadonlyArray<AnySource>> = {
|
|
7
10
|
readonly [S in Inputs[number] as SourceName<S>]: SourceValue<S>
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
export type StoresOf<Items extends ReadonlyArray<unknown>> = {
|
|
11
|
-
[I in keyof Items]: Items[I] extends {
|
|
12
|
-
|
|
14
|
+
[I in keyof Items]: Items[I] extends {
|
|
15
|
+
readonly store: Context.Tag<infer Service, infer _Store>
|
|
16
|
+
}
|
|
17
|
+
? Service
|
|
13
18
|
: never
|
|
14
19
|
}[number]
|
|
15
20
|
|
|
@@ -26,8 +31,20 @@ const inputKey = (input: AnySource): string =>
|
|
|
26
31
|
? input.manifest['name']
|
|
27
32
|
: input.name
|
|
28
33
|
|
|
34
|
+
// Capture an invariant source tag through a covariant read-only surface.
|
|
35
|
+
interface ReadableStore<out A> {
|
|
36
|
+
readonly getSnapshot: () => A
|
|
37
|
+
readonly subscribe: (listener: () => void) => () => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const readStore = <Source extends AnySource>(
|
|
41
|
+
source: Source,
|
|
42
|
+
): Effect.Effect<ReadableStore<SourceValue<Source>>, never, SourceIdentifier<Source>> =>
|
|
43
|
+
Context.GenericTag<SourceIdentifier<Source>, ReadableStore<SourceValue<Source>>>(source.store.key)
|
|
44
|
+
|
|
29
45
|
export const sameKey = (left: ReadonlyArray<unknown>, right: ReadonlyArray<unknown>): boolean =>
|
|
30
|
-
left.length === right.length &&
|
|
46
|
+
left.length === right.length &&
|
|
47
|
+
left.every((element, index) => Equal.equals(element, right[index]))
|
|
31
48
|
|
|
32
49
|
export interface WiredSources<Inputs extends ReadonlyArray<AnySource>> {
|
|
33
50
|
readonly snapshot: () => InputsObject<Inputs>
|
|
@@ -41,10 +58,7 @@ export const wireSources = <Inputs extends ReadonlyArray<AnySource>>(
|
|
|
41
58
|
): Effect.Effect<WiredSources<Inputs>, never, InputStores<Inputs>> =>
|
|
42
59
|
// oxlint-disable-next-line reform-rules/no-type-assertion -- restate the precise InputStores<Inputs> union the signature promises; Effect.forEach over a heterogeneous tuple widens R (the single seam this cast lives at)
|
|
43
60
|
Effect.gen(function* () {
|
|
44
|
-
const sources
|
|
45
|
-
inputs,
|
|
46
|
-
(input) => input.store,
|
|
47
|
-
)
|
|
61
|
+
const sources = yield* Effect.forEach(inputs, readStore)
|
|
48
62
|
|
|
49
63
|
const snapshot = (): InputsObject<Inputs> => {
|
|
50
64
|
const args: Record<string, unknown> = {}
|
package/src/internal/track.ts
CHANGED
|
@@ -10,8 +10,9 @@ export interface Tracker {
|
|
|
10
10
|
add(source: Subscribable): void
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const CurrentTrackerBase: Context.TagClass<CurrentTracker, 'reform/Tracker', Tracker> =
|
|
14
|
-
|
|
13
|
+
const CurrentTrackerBase: Context.TagClass<CurrentTracker, 'reform/Tracker', Tracker> = Context.Tag(
|
|
14
|
+
'reform/Tracker',
|
|
15
|
+
)<CurrentTracker, Tracker>()
|
|
15
16
|
export class CurrentTracker extends CurrentTrackerBase {}
|
|
16
17
|
|
|
17
18
|
export const readTracked = <A>(store: Store<A>): Effect.Effect<A> =>
|
package/src/internal.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Quarantined surface for first-party hosts and companion packages
|
|
2
|
+
// (`@playfast/reform-*`): reflection/manifest types, TypeIds, erased `Any*`
|
|
3
|
+
// forms, capture callbacks, and runtime seams. Importable as
|
|
4
|
+
// `@playfast/reform/internal`; not part of the app-facing API and not covered
|
|
5
|
+
// by its stability expectations.
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
type AnyState,
|
|
9
|
+
type StateCapture,
|
|
10
|
+
type StateDefinition,
|
|
11
|
+
type StateManifest,
|
|
12
|
+
type StateName,
|
|
13
|
+
type StateSchemaReflection,
|
|
14
|
+
StateStoreTypeId,
|
|
15
|
+
} from './state/state'
|
|
16
|
+
export {
|
|
17
|
+
type AnyFamily,
|
|
18
|
+
type FamilyName,
|
|
19
|
+
type FamilySchemaReflection,
|
|
20
|
+
type FamilyStore,
|
|
21
|
+
type StateFamilyCapture,
|
|
22
|
+
type StateFamilyManifest,
|
|
23
|
+
StateFamilyStoreTypeId,
|
|
24
|
+
} from './state/stateFamily'
|
|
25
|
+
export { StateToken } from './state/token'
|
|
26
|
+
export type {
|
|
27
|
+
AnySource,
|
|
28
|
+
Source,
|
|
29
|
+
SourceCapture,
|
|
30
|
+
SourceIdentifier,
|
|
31
|
+
SourceName,
|
|
32
|
+
SourceValue,
|
|
33
|
+
} from './state/token'
|
|
34
|
+
export type {
|
|
35
|
+
AnyEvent,
|
|
36
|
+
EventCapture,
|
|
37
|
+
EventManifest,
|
|
38
|
+
EventName,
|
|
39
|
+
EventReflection,
|
|
40
|
+
EventSchemaReflection,
|
|
41
|
+
EventType,
|
|
42
|
+
} from './event/event'
|
|
43
|
+
export { CalcStoreTypeId } from './calc/calc'
|
|
44
|
+
export { CalcFamilyStoreTypeId } from './calc/calcFamily'
|
|
45
|
+
export { AsyncCalcStoreTypeId } from './calc/asyncCalc'
|
|
46
|
+
export { sameKey, type WiredSources, wireSources } from './internal/sources'
|
|
47
|
+
// Optional host seams an `AsyncCalc` consults — persistence and lifecycle signals.
|
|
48
|
+
// Concrete implementations live in edge packages (e.g. `@playfast/reform-query-browser`).
|
|
49
|
+
export {
|
|
50
|
+
noopQueryStore,
|
|
51
|
+
QueryStore,
|
|
52
|
+
type QueryStoreApi,
|
|
53
|
+
resolveQueryStore,
|
|
54
|
+
} from './internal/queryStore'
|
|
55
|
+
export {
|
|
56
|
+
noopQueryEvents,
|
|
57
|
+
QueryEvents,
|
|
58
|
+
type QueryEventsApi,
|
|
59
|
+
resolveQueryEvents,
|
|
60
|
+
} from './internal/queryEvents'
|
|
61
|
+
export {
|
|
62
|
+
RemoteStatePendingStoreTypeId,
|
|
63
|
+
RemoteStateTruthStoreTypeId,
|
|
64
|
+
RemoteStateVisibleStoreTypeId,
|
|
65
|
+
} from './remote/remoteState'
|
|
66
|
+
export { SyncedStoreIdentifierTypeId } from './synced/syncedStore'
|
|
67
|
+
export { BoundaryStoreTypeId } from './boundary/boundary'
|
|
68
|
+
export type {
|
|
69
|
+
AnyProcedure,
|
|
70
|
+
ProcedureCapture,
|
|
71
|
+
ProcedureManifest,
|
|
72
|
+
ProcedureReflection,
|
|
73
|
+
} from './procedure/procedure'
|
|
74
|
+
export {
|
|
75
|
+
type ChannelRuntime,
|
|
76
|
+
Channels,
|
|
77
|
+
channelsLayer,
|
|
78
|
+
type ProcedureEntry,
|
|
79
|
+
Procedures,
|
|
80
|
+
proceduresLayer,
|
|
81
|
+
} from './channel/channel'
|
|
82
|
+
export { CompositionIdTypeId } from './compose/composition'
|
|
83
|
+
export type {
|
|
84
|
+
AnyComposition,
|
|
85
|
+
CompositionCapture,
|
|
86
|
+
CompositionManifest,
|
|
87
|
+
CompositionPropsReceiver,
|
|
88
|
+
CompositionReflection,
|
|
89
|
+
CompositionTag,
|
|
90
|
+
HasExactCompositionId,
|
|
91
|
+
RequiredSlotIdentities,
|
|
92
|
+
SlotClassesOf,
|
|
93
|
+
} from './compose/composition'
|
|
94
|
+
export { StructureTypeId } from './compose/structure'
|
|
95
|
+
export { type PropsId, PropsIdTypeId, type PropsTag, propsTag } from './compose/props'
|
|
96
|
+
export {
|
|
97
|
+
type AnySlot,
|
|
98
|
+
type AnySlotChild,
|
|
99
|
+
type AnySlotInstance,
|
|
100
|
+
type SlotCapture,
|
|
101
|
+
type SlotChild,
|
|
102
|
+
type SlotManifest,
|
|
103
|
+
SlotServiceTypeId,
|
|
104
|
+
SlotTypeId,
|
|
105
|
+
} from './compose/slot'
|
|
106
|
+
export type { SlotRenderer } from './compose/host'
|
|
107
|
+
export {
|
|
108
|
+
type AnyMadeView,
|
|
109
|
+
type AnyUi,
|
|
110
|
+
type MadeViewCapture,
|
|
111
|
+
type UiCapture,
|
|
112
|
+
type UiManifest,
|
|
113
|
+
type UiName,
|
|
114
|
+
type UiReflection,
|
|
115
|
+
type UiSchemaReflection,
|
|
116
|
+
UiServiceTypeId,
|
|
117
|
+
UiTypeId,
|
|
118
|
+
type WiredUi,
|
|
119
|
+
type WiredUiManifest,
|
|
120
|
+
type WireSchemas,
|
|
121
|
+
} from './compose/ui'
|
|
122
|
+
|
|
123
|
+
// Wire: the serializable rendered-tree model + pure diff/apply the remote
|
|
124
|
+
// transport sends over the wire and the client folds back. See REMOTE_UI.md §3.
|
|
125
|
+
export * as Wire from './wire/tree'
|
|
126
|
+
export type { WireNode, WirePatch, WireProp, WireTree } from './wire/tree'
|
|
127
|
+
// Triggers: the server-side handle registry bridging client trigger invocations
|
|
128
|
+
// back to live triggers, decoding payloads via the event schema. See REMOTE_UI.md §4.
|
|
129
|
+
export * as Triggers from './wire/triggers'
|
|
130
|
+
export {
|
|
131
|
+
type TriggerHandle,
|
|
132
|
+
TriggerRegistry,
|
|
133
|
+
type TriggerRegistryApi,
|
|
134
|
+
UnknownTrigger,
|
|
135
|
+
} from './wire/triggers'
|
|
136
|
+
|
|
137
|
+
export {
|
|
138
|
+
type AnyFeatureBinding,
|
|
139
|
+
type AnyLoadedModule,
|
|
140
|
+
type CapturedFeatureBinding,
|
|
141
|
+
type FeatureBinding,
|
|
142
|
+
type FeatureBindingCapture,
|
|
143
|
+
FeatureBindingTypeId,
|
|
144
|
+
type FeatureDefinition,
|
|
145
|
+
FeatureGraphValidTypeId,
|
|
146
|
+
type FeatureIdentityToken,
|
|
147
|
+
FeatureInput,
|
|
148
|
+
type FeatureManifest,
|
|
149
|
+
featureModuleFromInput,
|
|
150
|
+
type FeatureMountProps,
|
|
151
|
+
type FeatureOpenServices,
|
|
152
|
+
type FeatureRequirement,
|
|
153
|
+
type FeatureSupportOpenServices,
|
|
154
|
+
type FeatureToken,
|
|
155
|
+
type FeatureTreeCarrier,
|
|
156
|
+
type LoadedModule,
|
|
157
|
+
type LoadedModuleCapture,
|
|
158
|
+
mountFeature,
|
|
159
|
+
type ProvideableFeature,
|
|
160
|
+
type ProvidedBy,
|
|
161
|
+
type RequireCarrier,
|
|
162
|
+
} from './feature/feature'
|
|
163
|
+
export {
|
|
164
|
+
type CompositionChildSummary,
|
|
165
|
+
CompositionChildSummaryTypeId,
|
|
166
|
+
type DescendantOpenServices,
|
|
167
|
+
type FeatureChildSummary,
|
|
168
|
+
FeatureChildSummaryTypeId,
|
|
169
|
+
type ModuleOpenServices,
|
|
170
|
+
type OpenServicesFromFeatureSlotWitnesses,
|
|
171
|
+
type SlotBindingSummary,
|
|
172
|
+
SlotBindingSummaryTypeId,
|
|
173
|
+
type ValidateFeatureGraphLayer,
|
|
174
|
+
} from './graph/closure'
|
|
175
|
+
export { type AnyScene, type AnySceneCapture, type SceneCapture, seedAnyScene } from './scene/scene'
|
|
176
|
+
export { Bus, busLayer, type Envelope, type Tagged } from './runtime/bus'
|
|
177
|
+
export {
|
|
178
|
+
type AnyFeatureMountHandlers,
|
|
179
|
+
type CapturedFeatureMountHandlers,
|
|
180
|
+
type FeatureMountHandlersExternalApi,
|
|
181
|
+
type UnspecifiedRuntimeServices,
|
|
182
|
+
UnspecifiedRuntimeServicesTypeId,
|
|
183
|
+
} from './runtime/appRuntime'
|
|
184
|
+
export {
|
|
185
|
+
EngineRequirements,
|
|
186
|
+
type EngineServices,
|
|
187
|
+
type ReducerEntry,
|
|
188
|
+
Reducers,
|
|
189
|
+
reducersLayer,
|
|
190
|
+
} from './runtime/loop'
|
|
191
|
+
export { CurrentEventBudget, defaultEventBudget, resolveEventBudget } from './runtime/eventBudget'
|
|
192
|
+
// The profiler SPI: synchronous hooks the engine calls on its hot paths, plus
|
|
193
|
+
// the FiberRef seam `@playfast/reform-profiler` writes through (`profiledScene`).
|
|
194
|
+
export {
|
|
195
|
+
CurrentInstrumentation,
|
|
196
|
+
type Instrumentation,
|
|
197
|
+
noopInstrumentation,
|
|
198
|
+
resolveInstrumentation,
|
|
199
|
+
type SpanEnd,
|
|
200
|
+
} from './runtime/instrumentation'
|
|
201
|
+
// The runtime-wide registry of live query handles. `AsyncCalc.invalidate` /
|
|
202
|
+
// `AsyncCalc.refetch` and provider layers act on a query by name through it.
|
|
203
|
+
export { Queries, queriesLayer, type QueryHandle, type QueryRegistry } from './runtime/queries'
|
|
204
|
+
export { CurrentTracker, readTracked, type Subscribable, type Tracker } from './internal/track'
|
|
205
|
+
export { type CapturedRender, CaptureSink, type CaptureSinkApi } from './internal/capture'
|
|
206
|
+
// Notification scheduler: hosts that need per-runtime isolation (concurrent SSR,
|
|
207
|
+
// multiple mounted roots) provide `Notifications` upstream of their state layers;
|
|
208
|
+
// everything else coalesces on the process-wide default.
|
|
209
|
+
export {
|
|
210
|
+
defaultScheduler,
|
|
211
|
+
makeScheduler,
|
|
212
|
+
Notifications,
|
|
213
|
+
notificationsLayer,
|
|
214
|
+
resolveScheduler,
|
|
215
|
+
type Scheduler,
|
|
216
|
+
} from './internal/scheduler'
|
|
217
|
+
export { makeStore, type Store } from './internal/store'
|
|
218
|
+
// Primitive-authoring toolkit — for first-party companion packages
|
|
219
|
+
// (e.g. `@playfast/reform-db`, `@playfast/reform-resource`) that build their own
|
|
220
|
+
// first-class `Source`s which must participate in dependency tracking and
|
|
221
|
+
// reflection exactly like a `Calc`.
|
|
222
|
+
export { type Kind, type Manifest, yieldableClass } from './definition/definition'
|