@playfast/reform 0.0.4 → 0.0.7
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/dist/boundary/boundary.d.ts +72 -0
- package/dist/boundary/boundary.d.ts.map +1 -0
- package/dist/boundary/boundary.js +83 -0
- package/dist/boundary/boundary.js.map +1 -0
- package/dist/calc/asyncCalc.d.ts +91 -0
- package/dist/calc/asyncCalc.d.ts.map +1 -0
- package/dist/calc/asyncCalc.js +95 -0
- package/dist/calc/asyncCalc.js.map +1 -0
- package/{src/calc/asyncData.ts → dist/calc/asyncData.d.ts} +22 -53
- package/dist/calc/asyncData.d.ts.map +1 -0
- package/dist/calc/asyncData.js +34 -0
- package/dist/calc/asyncData.js.map +1 -0
- package/dist/calc/calc.d.ts +57 -0
- package/dist/calc/calc.d.ts.map +1 -0
- package/dist/calc/calc.js +58 -0
- package/dist/calc/calc.js.map +1 -0
- package/dist/calc/calcFamily.d.ts +57 -0
- package/dist/calc/calcFamily.d.ts.map +1 -0
- package/dist/calc/calcFamily.js +124 -0
- package/dist/calc/calcFamily.js.map +1 -0
- package/dist/calc/compose.d.ts +16 -0
- package/dist/calc/compose.d.ts.map +1 -0
- package/dist/calc/compose.js +7 -0
- package/dist/calc/compose.js.map +1 -0
- package/dist/channel/channel.d.ts +115 -0
- package/dist/channel/channel.d.ts.map +1 -0
- package/dist/channel/channel.js +138 -0
- package/dist/channel/channel.js.map +1 -0
- package/dist/compose/composition.d.ts +72 -0
- package/dist/compose/composition.d.ts.map +1 -0
- package/dist/compose/composition.js +46 -0
- package/dist/compose/composition.js.map +1 -0
- package/{src/compose/host.ts → dist/compose/host.d.ts} +8 -6
- package/dist/compose/host.d.ts.map +1 -0
- package/dist/compose/host.js +6 -0
- package/dist/compose/host.js.map +1 -0
- package/dist/compose/props.d.ts +13 -0
- package/dist/compose/props.d.ts.map +1 -0
- package/{src/compose/props.ts → dist/compose/props.js} +5 -3
- package/dist/compose/props.js.map +1 -0
- package/dist/compose/provide.d.ts +22 -0
- package/dist/compose/provide.d.ts.map +1 -0
- package/dist/compose/provide.js +28 -0
- package/dist/compose/provide.js.map +1 -0
- package/{src/compose/slot.ts → dist/compose/slot.d.ts} +19 -36
- package/dist/compose/slot.d.ts.map +1 -0
- package/dist/compose/slot.js +23 -0
- package/dist/compose/slot.js.map +1 -0
- package/dist/compose/ui.d.ts +135 -0
- package/dist/compose/ui.d.ts.map +1 -0
- package/dist/compose/ui.js +63 -0
- package/dist/compose/ui.js.map +1 -0
- package/dist/definition/definition.d.ts +33 -0
- package/dist/definition/definition.d.ts.map +1 -0
- package/dist/definition/definition.js +42 -0
- package/dist/definition/definition.js.map +1 -0
- package/dist/event/event.d.ts +33 -0
- package/dist/event/event.d.ts.map +1 -0
- package/dist/event/event.js +30 -0
- package/dist/event/event.js.map +1 -0
- package/dist/event/eventGroup.d.ts +9 -0
- package/dist/event/eventGroup.d.ts.map +1 -0
- package/dist/event/eventGroup.js +4 -0
- package/dist/event/eventGroup.js.map +1 -0
- package/{src/feature/feature.ts → dist/feature/feature.d.ts} +92 -178
- package/dist/feature/feature.d.ts.map +1 -0
- package/dist/feature/feature.js +98 -0
- package/dist/feature/feature.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/{src/internal/capture.ts → dist/internal/capture.d.ts} +11 -13
- package/dist/internal/capture.d.ts.map +1 -0
- package/dist/internal/capture.js +12 -0
- package/dist/internal/capture.js.map +1 -0
- package/{src/internal/ctx.ts → dist/internal/ctx.d.ts} +4 -8
- package/dist/internal/ctx.d.ts.map +1 -0
- package/dist/internal/ctx.js +2 -0
- package/dist/internal/ctx.js.map +1 -0
- package/dist/internal/errors.d.ts +89 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/errors.js +102 -0
- package/dist/internal/errors.js.map +1 -0
- package/dist/internal/inspect.d.ts +17 -0
- package/dist/internal/inspect.d.ts.map +1 -0
- package/dist/internal/inspect.js +32 -0
- package/dist/internal/inspect.js.map +1 -0
- package/dist/internal/queryDriver.d.ts +65 -0
- package/dist/internal/queryDriver.d.ts.map +1 -0
- package/dist/internal/queryDriver.js +134 -0
- package/dist/internal/queryDriver.js.map +1 -0
- package/dist/internal/reuse.d.ts +10 -0
- package/dist/internal/reuse.d.ts.map +1 -0
- package/dist/internal/reuse.js +68 -0
- package/dist/internal/reuse.js.map +1 -0
- package/dist/internal/scheduler.d.ts +47 -0
- package/dist/internal/scheduler.d.ts.map +1 -0
- package/dist/internal/scheduler.js +70 -0
- package/dist/internal/scheduler.js.map +1 -0
- package/{src/internal/seeds.ts → dist/internal/seeds.d.ts} +3 -6
- package/dist/internal/seeds.d.ts.map +1 -0
- package/dist/internal/seeds.js +17 -0
- package/dist/internal/seeds.js.map +1 -0
- package/dist/internal/sources.d.ts +39 -0
- package/dist/internal/sources.d.ts.map +1 -0
- package/dist/internal/sources.js +59 -0
- package/dist/internal/sources.js.map +1 -0
- package/dist/internal/stateRegistry.d.ts +2 -0
- package/dist/internal/stateRegistry.d.ts.map +1 -0
- package/dist/internal/stateRegistry.js +28 -0
- package/dist/internal/stateRegistry.js.map +1 -0
- package/dist/internal/store.d.ts +47 -0
- package/dist/internal/store.d.ts.map +1 -0
- package/dist/internal/store.js +73 -0
- package/dist/internal/store.js.map +1 -0
- package/{src/internal/track.ts → dist/internal/track.d.ts} +11 -15
- package/dist/internal/track.d.ts.map +1 -0
- package/dist/internal/track.js +19 -0
- package/dist/internal/track.js.map +1 -0
- package/dist/procedure/procedure.d.ts +40 -0
- package/dist/procedure/procedure.d.ts.map +1 -0
- package/dist/procedure/procedure.js +50 -0
- package/dist/procedure/procedure.js.map +1 -0
- package/dist/reducer/reducer.d.ts +44 -0
- package/dist/reducer/reducer.d.ts.map +1 -0
- package/dist/reducer/reducer.js +63 -0
- package/dist/reducer/reducer.js.map +1 -0
- package/dist/remote/remoteState.d.ts +119 -0
- package/dist/remote/remoteState.d.ts.map +1 -0
- package/dist/remote/remoteState.js +270 -0
- package/dist/remote/remoteState.js.map +1 -0
- package/dist/runtime/bus.d.ts +27 -0
- package/dist/runtime/bus.d.ts.map +1 -0
- package/{src/runtime/bus.ts → dist/runtime/bus.js} +8 -23
- package/dist/runtime/bus.js.map +1 -0
- package/dist/runtime/loop.d.ts +45 -0
- package/dist/runtime/loop.d.ts.map +1 -0
- package/dist/runtime/loop.js +117 -0
- package/dist/runtime/loop.js.map +1 -0
- package/dist/scene/scene.d.ts +44 -0
- package/dist/scene/scene.d.ts.map +1 -0
- package/dist/scene/scene.js +31 -0
- package/dist/scene/scene.js.map +1 -0
- package/dist/state/state.d.ts +37 -0
- package/dist/state/state.d.ts.map +1 -0
- package/dist/state/state.js +46 -0
- package/dist/state/state.js.map +1 -0
- package/dist/state/stateFamily.d.ts +79 -0
- package/dist/state/stateFamily.d.ts.map +1 -0
- package/dist/state/stateFamily.js +99 -0
- package/dist/state/stateFamily.js.map +1 -0
- package/dist/state/stateGroup.d.ts +50 -0
- package/dist/state/stateGroup.d.ts.map +1 -0
- package/dist/state/stateGroup.js +54 -0
- package/dist/state/stateGroup.js.map +1 -0
- package/{src/state/token.ts → dist/state/token.d.ts} +13 -21
- package/dist/state/token.d.ts.map +1 -0
- package/dist/state/token.js +20 -0
- package/dist/state/token.js.map +1 -0
- package/dist/synced/syncedStore.d.ts +51 -0
- package/dist/synced/syncedStore.d.ts.map +1 -0
- package/dist/synced/syncedStore.js +43 -0
- package/dist/synced/syncedStore.js.map +1 -0
- package/{src/ui/node.ts → dist/ui/node.d.ts} +3 -3
- package/dist/ui/node.d.ts.map +1 -0
- package/dist/ui/node.js +2 -0
- package/dist/ui/node.js.map +1 -0
- package/{src/ui/trigger.ts → dist/ui/trigger.d.ts} +2 -1
- package/dist/ui/trigger.d.ts.map +1 -0
- package/dist/ui/trigger.js +2 -0
- package/dist/ui/trigger.js.map +1 -0
- package/dist/wire/tree.d.ts +66 -0
- package/dist/wire/tree.d.ts.map +1 -0
- package/dist/wire/tree.js +60 -0
- package/dist/wire/tree.js.map +1 -0
- package/dist/wire/triggers.d.ts +44 -0
- package/dist/wire/triggers.d.ts.map +1 -0
- package/dist/wire/triggers.js +31 -0
- package/dist/wire/triggers.js.map +1 -0
- package/package.json +32 -11
- package/src/boundary/boundary.ts +0 -192
- package/src/calc/asyncCalc.ts +0 -267
- package/src/calc/calc.ts +0 -120
- package/src/calc/calcFamily.ts +0 -194
- package/src/calc/compose.ts +0 -69
- package/src/channel/channel.ts +0 -265
- package/src/compose/composition.ts +0 -112
- package/src/compose/provide.ts +0 -70
- package/src/compose/ui.ts +0 -97
- package/src/definition/definition.ts +0 -74
- package/src/event/event.ts +0 -65
- package/src/event/eventGroup.ts +0 -14
- package/src/index.ts +0 -140
- package/src/internal/errors.ts +0 -69
- package/src/internal/inspect.ts +0 -34
- package/src/internal/queryDriver.ts +0 -247
- package/src/internal/reuse.ts +0 -73
- package/src/internal/scheduler.ts +0 -91
- package/src/internal/sources.ts +0 -104
- package/src/internal/store.ts +0 -105
- package/src/procedure/procedure.ts +0 -89
- package/src/reducer/reducer.ts +0 -137
- package/src/remote/remoteState.ts +0 -571
- package/src/runtime/loop.ts +0 -169
- package/src/scene/scene.ts +0 -76
- package/src/state/state.ts +0 -91
- package/src/state/stateFamily.ts +0 -171
- package/src/state/stateGroup.ts +0 -78
package/src/event/event.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { Effect, PubSub, Runtime, type Schema } from 'effect'
|
|
2
|
-
import { type Manifest, definitionClass } from '../definition/definition'
|
|
3
|
-
import { Bus, publish, type Tagged } from '../runtime/bus'
|
|
4
|
-
import type { Trigger } from '../ui/trigger'
|
|
5
|
-
|
|
6
|
-
/** The dispatched value of an event: `{ _tag, ...payload }`, matched with `Match.tags`. */
|
|
7
|
-
export type EventOf<N extends string, P> = Tagged & { readonly _tag: N } & P
|
|
8
|
-
|
|
9
|
-
export interface EventManifest<N extends string, P> extends Manifest {
|
|
10
|
-
readonly kind: 'Event'
|
|
11
|
-
readonly name: N
|
|
12
|
-
readonly schema: Schema.Schema<P, any>
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface EventClass<out N extends string, in out P> {
|
|
16
|
-
new (): {}
|
|
17
|
-
readonly manifest: EventManifest<N, P>
|
|
18
|
-
readonly tag: N
|
|
19
|
-
/** Carrier for `Event.construct` — builds the tagged value `{ _tag, ...payload }`. */
|
|
20
|
-
readonly build: (payload: P) => EventOf<N, P>
|
|
21
|
-
/** Carrier for `Event.trigger` — the UI callback effect (High priority). */
|
|
22
|
-
readonly trigger: Effect.Effect<Trigger<P>, never, Bus>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export type AnyEvent = EventClass<string, any>
|
|
26
|
-
export type EventType<E> = E extends EventClass<infer N, infer P> ? EventOf<N, P> : never
|
|
27
|
-
|
|
28
|
-
/** A pure data fact. Events depend on nothing — only reducers tie them to state. */
|
|
29
|
-
export const make = <const N extends string, P>(
|
|
30
|
-
name: N,
|
|
31
|
-
schema: Schema.Schema<P, any>,
|
|
32
|
-
): EventClass<N, P> => {
|
|
33
|
-
const build = (payload: P): EventOf<N, P> => ({ _tag: name, ...payload })
|
|
34
|
-
const trigger = Effect.gen(function* () {
|
|
35
|
-
const bus = yield* Bus
|
|
36
|
-
const runtime = yield* Effect.runtime<never>()
|
|
37
|
-
// Dispatch synchronously: on the unbounded bus `publish` only enqueues and
|
|
38
|
-
// returns, so `runSync` never suspends and no fiber is forked per UI event.
|
|
39
|
-
// The drain loop still folds reducers on its own fiber — publish is pure offer.
|
|
40
|
-
return (payload: P) => {
|
|
41
|
-
Runtime.runSync(runtime)(PubSub.publish(bus, { priority: 'High', event: build(payload) }))
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
return definitionClass<EventClass<N, P>>({
|
|
45
|
-
manifest: { kind: 'Event' as const, name, schema },
|
|
46
|
-
tag: name,
|
|
47
|
-
build,
|
|
48
|
-
trigger,
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Construct the tagged value for an event (used for `boot` events). */
|
|
53
|
-
export const construct = <N extends string, P>(event: EventClass<N, P>, payload: P): EventOf<N, P> =>
|
|
54
|
-
event.build(payload)
|
|
55
|
-
|
|
56
|
-
/** Dispatch as a follow-up (Normal priority) from a procedure/effect. */
|
|
57
|
-
export const dispatch = <N extends string, P>(
|
|
58
|
-
event: EventClass<N, P>,
|
|
59
|
-
payload: P,
|
|
60
|
-
): Effect.Effect<void, never, Bus> => publish('Normal', event.build(payload))
|
|
61
|
-
|
|
62
|
-
/** A callback for the UI that dispatches at High priority via the runtime. */
|
|
63
|
-
export const trigger = <N extends string, P>(
|
|
64
|
-
event: EventClass<N, P>,
|
|
65
|
-
): Effect.Effect<Trigger<P>, never, Bus> => event.trigger
|
package/src/event/eventGroup.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { definitionClass } from '../definition/definition'
|
|
2
|
-
import type { AnyEvent } from './event'
|
|
3
|
-
|
|
4
|
-
export interface EventGroupClass<Members extends ReadonlyArray<AnyEvent>> {
|
|
5
|
-
new (): {}
|
|
6
|
-
readonly kind: 'EventGroup'
|
|
7
|
-
readonly members: Members
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Compose events into a group, the same variadic shape as `StateGroup`/`RpcGroup`. */
|
|
11
|
-
export const make = <const Members extends ReadonlyArray<AnyEvent>>(
|
|
12
|
-
...members: Members
|
|
13
|
-
): EventGroupClass<Members> =>
|
|
14
|
-
definitionClass<EventGroupClass<Members>>({ kind: 'EventGroup' as const, members })
|
package/src/index.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
// Reform core — the renderer-neutral primitives and runtime.
|
|
2
|
-
// Definitions (`X.make`) are separate from implementations (`.live`)
|
|
3
|
-
// throughout, and nothing self-registers on import (principle #1, #6).
|
|
4
|
-
|
|
5
|
-
export * as State from './state/state'
|
|
6
|
-
export * as StateGroup from './state/stateGroup'
|
|
7
|
-
export * as StateFamily from './state/stateFamily'
|
|
8
|
-
export { StateToken } from './state/token'
|
|
9
|
-
export type { AnySource, Source } from './state/token'
|
|
10
|
-
|
|
11
|
-
export * as Event from './event/event'
|
|
12
|
-
// Direct type re-export alongside the namespace (TS2742 nameability): inferred
|
|
13
|
-
// types of layers built over intent/event unions reference `EventOf`, and the
|
|
14
|
-
// declaration emitter can only name it through a plain export.
|
|
15
|
-
export type { EventOf } from './event/event'
|
|
16
|
-
export * as EventGroup from './event/eventGroup'
|
|
17
|
-
|
|
18
|
-
export * as Reducer from './reducer/reducer'
|
|
19
|
-
export * as Calc from './calc/calc'
|
|
20
|
-
export * as CalcFamily from './calc/calcFamily'
|
|
21
|
-
export * as AsyncCalc from './calc/asyncCalc'
|
|
22
|
-
export { composeCalcs } from './calc/compose'
|
|
23
|
-
// The inputs-object/key/store types appearing in `AsyncCalc.live` /
|
|
24
|
-
// `RemoteState.live` signatures, exported so app code can annotate
|
|
25
|
-
// query-swapping helpers and layer types without re-spelling them.
|
|
26
|
-
export type { InputsObject, InputStores, InvalidateBy } from './internal/sources'
|
|
27
|
-
export {
|
|
28
|
-
type AsyncData,
|
|
29
|
-
type AsyncError,
|
|
30
|
-
type AsyncIdle,
|
|
31
|
-
type AsyncLoading,
|
|
32
|
-
type AsyncSuccess,
|
|
33
|
-
} from './calc/asyncData'
|
|
34
|
-
export * as RemoteState from './remote/remoteState'
|
|
35
|
-
// Direct type re-exports alongside the namespace (TS2742 nameability): an app
|
|
36
|
-
// layer's inferred type references these (`Store<ReadonlyArray<PendingIntent<…>>>`),
|
|
37
|
-
// and the declaration emitter can only name them through a plain export.
|
|
38
|
-
export type { FailedIntent, PendingIntent, RemoteStateClass } from './remote/remoteState'
|
|
39
|
-
export * as Boundary from './boundary/boundary'
|
|
40
|
-
// Direct type re-exports alongside the namespace (same TS2742 nameability
|
|
41
|
-
// reason as the RemoteState types above): app code annotates props with these.
|
|
42
|
-
export type {
|
|
43
|
-
BoundaryErrored,
|
|
44
|
-
BoundaryPending,
|
|
45
|
-
BoundaryReady,
|
|
46
|
-
BoundaryState,
|
|
47
|
-
} from './boundary/boundary'
|
|
48
|
-
export * as Procedure from './procedure/procedure'
|
|
49
|
-
export * as Channel from './channel/channel'
|
|
50
|
-
|
|
51
|
-
export * as Composition from './compose/composition'
|
|
52
|
-
export type {
|
|
53
|
-
CompositionClass,
|
|
54
|
-
CompositionService,
|
|
55
|
-
RenderEnv,
|
|
56
|
-
} from './compose/composition'
|
|
57
|
-
export { Props } from './compose/props'
|
|
58
|
-
export {
|
|
59
|
-
isSlot,
|
|
60
|
-
type Node,
|
|
61
|
-
slot,
|
|
62
|
-
type SlotClass,
|
|
63
|
-
type SlotInstance,
|
|
64
|
-
type SlotProps,
|
|
65
|
-
SlotTypeId,
|
|
66
|
-
} from './compose/slot'
|
|
67
|
-
export { CurrentSlots, type SlotHost } from './compose/host'
|
|
68
|
-
export * as Ui from './compose/ui'
|
|
69
|
-
export {
|
|
70
|
-
isUi,
|
|
71
|
-
type LogicView,
|
|
72
|
-
ui,
|
|
73
|
-
type UiClass,
|
|
74
|
-
type UiContract,
|
|
75
|
-
UiTypeId,
|
|
76
|
-
type ViewImpl,
|
|
77
|
-
} from './compose/ui'
|
|
78
|
-
export { provide } from './compose/provide'
|
|
79
|
-
export type { Trigger } from './ui/trigger'
|
|
80
|
-
|
|
81
|
-
// Feature: the lazy/eager code-split unit. Definitions stay eager and reflectable;
|
|
82
|
-
// the heavy `.live` layer loads on demand (an Effect, never a Promise). See LAZY.md.
|
|
83
|
-
export * as Feature from './feature/feature'
|
|
84
|
-
export {
|
|
85
|
-
type EngineServices,
|
|
86
|
-
type FailedProps,
|
|
87
|
-
type FeatureBinding,
|
|
88
|
-
FeatureBindingTypeId,
|
|
89
|
-
type FeatureClass,
|
|
90
|
-
type FeatureManifest,
|
|
91
|
-
featureModule,
|
|
92
|
-
type FeatureModule,
|
|
93
|
-
isFeature,
|
|
94
|
-
isFeatureBinding,
|
|
95
|
-
lazyImport,
|
|
96
|
-
type LoadedModule,
|
|
97
|
-
mountFeature,
|
|
98
|
-
type Placeholders,
|
|
99
|
-
type ProvidedBy,
|
|
100
|
-
type RequireCarrier,
|
|
101
|
-
} from './feature/feature'
|
|
102
|
-
export { type SlotChild } from './compose/slot'
|
|
103
|
-
|
|
104
|
-
// A scene bundles a composition with the closed wiring that runs it — the single
|
|
105
|
-
// handle the react host, proofs, and the dev tool all consume.
|
|
106
|
-
export { isScene, type MountedServices, type Scene, scene, seedScene } from './scene/scene'
|
|
107
|
-
|
|
108
|
-
// Runtime surface for hosts (`@reform/react`) and headless tests.
|
|
109
|
-
export { Bus, type Envelope, type Priority, publish, type Tagged } from './runtime/bus'
|
|
110
|
-
export { Engine, type ReducerEntry, Reducers } from './runtime/loop'
|
|
111
|
-
export {
|
|
112
|
-
type ChannelClass,
|
|
113
|
-
type ChannelPolicy,
|
|
114
|
-
type ChannelRuntime,
|
|
115
|
-
Channels,
|
|
116
|
-
type ProcedureEntry,
|
|
117
|
-
Procedures,
|
|
118
|
-
} from './channel/channel'
|
|
119
|
-
export { CurrentTracker, type Subscribable, type Tracker } from './internal/track'
|
|
120
|
-
export { CaptureSink, type CaptureSinkApi, type UiCapture } from './internal/capture'
|
|
121
|
-
export {
|
|
122
|
-
AsyncReducer,
|
|
123
|
-
DuplicateRegistration,
|
|
124
|
-
FeatureLoadFailed,
|
|
125
|
-
InvalidProvideTarget,
|
|
126
|
-
SlotRenderingUnavailable,
|
|
127
|
-
UnknownGroupState,
|
|
128
|
-
} from './internal/errors'
|
|
129
|
-
// Notification scheduler: hosts that need per-runtime isolation (concurrent SSR,
|
|
130
|
-
// multiple mounted roots) provide `Notifications` upstream of their state layers;
|
|
131
|
-
// everything else coalesces on the process-wide default.
|
|
132
|
-
export {
|
|
133
|
-
defaultScheduler,
|
|
134
|
-
makeScheduler,
|
|
135
|
-
Notifications,
|
|
136
|
-
notificationsLayer,
|
|
137
|
-
type Scheduler,
|
|
138
|
-
} from './internal/scheduler'
|
|
139
|
-
export type { Store } from './internal/store'
|
|
140
|
-
export type { FamilyStore } from './state/stateFamily'
|
package/src/internal/errors.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { Data } from 'effect'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Tagged errors for the framework's synchronous boundaries. Even when thrown
|
|
5
|
-
* (React render, layer build, reflection), an error carries a `_tag` and its
|
|
6
|
-
* structured fields — never a bare `Error` — so callers and tooling can match on
|
|
7
|
-
* it. `Data.TaggedError` subclasses are real `Error`s, so `throw` still works.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/** A `<slots.X/>` was rendered without a render-target host to realize it. */
|
|
11
|
-
export class SlotRenderingUnavailable extends Data.TaggedError('reform/SlotRenderingUnavailable')<{
|
|
12
|
-
readonly slot: string
|
|
13
|
-
}> {
|
|
14
|
-
override get message(): string {
|
|
15
|
-
return `reform: rendering slot '${this.slot}' requires @reform/react`
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** `provide(target, …)` was handed something that is neither a ui contract nor a slot. */
|
|
20
|
-
export class InvalidProvideTarget extends Data.TaggedError('reform/InvalidProvideTarget')<{}> {
|
|
21
|
-
override get message(): string {
|
|
22
|
-
return 'reform: provide target is neither a ui contract nor a slot'
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** `StateGroup.select(group, name)` named a state the group does not contain. */
|
|
27
|
-
export class UnknownGroupState extends Data.TaggedError('reform/UnknownGroupState')<{
|
|
28
|
-
readonly name: string
|
|
29
|
-
}> {
|
|
30
|
-
override get message(): string {
|
|
31
|
-
return `reform: unknown state '${this.name}' in group`
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* A lazy `Feature`'s `load` Effect failed — the dynamic `import()` rejected (a
|
|
37
|
-
* missing chunk, a network failure, a module-eval throw). Surfaced as a typed
|
|
38
|
-
* failure the host hands to the feature's `placeholder.failed`, never a bare
|
|
39
|
-
* rejection. `cause` is the original rejection value.
|
|
40
|
-
*/
|
|
41
|
-
export class FeatureLoadFailed extends Data.TaggedError('reform/FeatureLoadFailed')<{
|
|
42
|
-
readonly cause: unknown
|
|
43
|
-
}> {
|
|
44
|
-
override get message(): string {
|
|
45
|
-
return `reform: a feature failed to load (${String(this.cause)})`
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** A reducer fold returned a thenable — folds must be pure synchronous writes. */
|
|
50
|
-
export class AsyncReducer extends Data.TaggedError('reform/AsyncReducer')<{}> {
|
|
51
|
-
override get message(): string {
|
|
52
|
-
return 'reform: a reducer fold must be synchronous (it returned a Promise)'
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Two distinct primitives were registered under one name into the same runtime —
|
|
58
|
-
* a silent footgun, since names key DI tags and channel lanes. Thrown for a
|
|
59
|
-
* channel name reused with a different policy (the second would otherwise be
|
|
60
|
-
* dropped). Reducer/procedure name clashes are warned (logged), not thrown.
|
|
61
|
-
*/
|
|
62
|
-
export class DuplicateRegistration extends Data.TaggedError('reform/DuplicateRegistration')<{
|
|
63
|
-
readonly kind: string
|
|
64
|
-
readonly name: string
|
|
65
|
-
}> {
|
|
66
|
-
override get message(): string {
|
|
67
|
-
return `reform: duplicate ${this.kind} registered under name '${this.name}'`
|
|
68
|
-
}
|
|
69
|
-
}
|
package/src/internal/inspect.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { format, type Inspectable, NodeInspectSymbol } from 'effect/Inspectable'
|
|
2
|
-
|
|
3
|
-
export { type Inspectable, NodeInspectSymbol }
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The `Inspectable` trio built around a `toJSON`, so every reform value prints
|
|
7
|
-
* like an Effect value — `_id`-tagged JSON under `console.log`, Node's
|
|
8
|
-
* inspector, `JSON.stringify`, and `String(...)` — instead of `[object Object]`.
|
|
9
|
-
* Mirrors Effect's `BaseProto` / `Inspectable.Class` (`Inspectable.ts:170`).
|
|
10
|
-
*/
|
|
11
|
-
export const inspectable = (toJSON: () => unknown): Inspectable => ({
|
|
12
|
-
toJSON,
|
|
13
|
-
toString: () => format(toJSON()),
|
|
14
|
-
[NodeInspectSymbol]: () => toJSON(),
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Attach the `Inspectable` trio to an existing object as *non-enumerable* own
|
|
19
|
-
* properties (so they don't show up in `Object.keys`/`for…in` over a definition
|
|
20
|
-
* class). Used where the value is built by other means — the definition classes,
|
|
21
|
-
* whose statics are merged in separately.
|
|
22
|
-
*/
|
|
23
|
-
export const attachInspectable = <T extends object>(target: T, toJSON: () => unknown): T => {
|
|
24
|
-
const trio = inspectable(toJSON) as unknown as Record<PropertyKey, unknown>
|
|
25
|
-
for (const key of Reflect.ownKeys(trio)) {
|
|
26
|
-
Object.defineProperty(target, key, {
|
|
27
|
-
value: trio[key],
|
|
28
|
-
enumerable: false,
|
|
29
|
-
writable: true,
|
|
30
|
-
configurable: true,
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
return target
|
|
34
|
-
}
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Brand,
|
|
3
|
-
Cause,
|
|
4
|
-
Chunk,
|
|
5
|
-
Effect,
|
|
6
|
-
MutableRef,
|
|
7
|
-
Option,
|
|
8
|
-
Queue,
|
|
9
|
-
Schema,
|
|
10
|
-
type Scope,
|
|
11
|
-
Stream,
|
|
12
|
-
} from 'effect'
|
|
13
|
-
import { type AnyAsyncData, AsyncData } from '../calc/asyncData'
|
|
14
|
-
import { reuse } from './reuse'
|
|
15
|
-
import { resolveScheduler } from './scheduler'
|
|
16
|
-
import {
|
|
17
|
-
type InputsObject,
|
|
18
|
-
type InputStores,
|
|
19
|
-
type InvalidateBy,
|
|
20
|
-
sameKey,
|
|
21
|
-
wireSources,
|
|
22
|
-
} from './sources'
|
|
23
|
-
import { makeStore, type Store } from './store'
|
|
24
|
-
import { type AnySource } from '../state/token'
|
|
25
|
-
|
|
26
|
-
// The query-lifecycle driver shared by `AsyncCalc.live` and `RemoteState.live`:
|
|
27
|
-
// sense the inputs, run the query latest-wins (or trailing-conflated), fold the
|
|
28
|
-
// result into an `AnyAsyncData` store with stale-while-revalidate semantics.
|
|
29
|
-
// Extracted so both primitives run the exact same machinery — the asyncCalc
|
|
30
|
-
// test suite is the driver's regression suite.
|
|
31
|
-
|
|
32
|
-
/** `Gated` (whether the `Idle` arm exists) is the inverse of `alwaysOn`. */
|
|
33
|
-
export type GatedOf<AlwaysOn extends boolean> = AlwaysOn extends true ? false : true
|
|
34
|
-
|
|
35
|
-
/** The runtime gated flag for an `alwaysOn` config, typed as its `Gated` literal. */
|
|
36
|
-
export const gatedFlag = <AlwaysOn extends boolean>(alwaysOn: AlwaysOn | undefined): GatedOf<AlwaysOn> =>
|
|
37
|
-
(alwaysOn !== true) as GatedOf<AlwaysOn>
|
|
38
|
-
|
|
39
|
-
// The hidden revision behind `invalidateOn` — a module-local *branded* number.
|
|
40
|
-
// Reform store services are type-identified (`Context.GenericTag<Store<A>>`), so
|
|
41
|
-
// the hidden state's value type must be un-collidable: `Layer.provide` erases
|
|
42
|
-
// exactly `Store<Revision>` from the returned layer and can never type-level
|
|
43
|
-
// erase a user input that happens to be a number state. Never exported from the
|
|
44
|
-
// package index, so the editor catalog (which only indexes module exports)
|
|
45
|
-
// never sees it.
|
|
46
|
-
export type Revision = number & Brand.Brand<'reform/Revision'>
|
|
47
|
-
export const Revision = Brand.nominal<Revision>()
|
|
48
|
-
export const RevisionSchema: Schema.Schema<Revision, number> = Schema.Number.pipe(
|
|
49
|
-
Schema.brand('reform/Revision'),
|
|
50
|
-
)
|
|
51
|
-
export const revisionZero: Revision = Revision(0)
|
|
52
|
-
export const bumpRevision = (r: Revision): Revision => Revision(r + 1)
|
|
53
|
-
|
|
54
|
-
export interface QueryDriverOptions<Inputs extends ReadonlyArray<AnySource>, A, E, R> {
|
|
55
|
-
/** The owning definition's name — only for the defect log line. */
|
|
56
|
-
readonly name: string
|
|
57
|
-
/** The owning primitive's kind label — only for the defect log line. */
|
|
58
|
-
readonly label: string
|
|
59
|
-
/** Whether `disabled` is honored / the `Idle` arm can occur. */
|
|
60
|
-
readonly gated: boolean
|
|
61
|
-
readonly inputs: Inputs
|
|
62
|
-
readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
|
|
63
|
-
readonly invalidateBy?: InvalidateBy<Inputs> | undefined
|
|
64
|
-
readonly disabled?: ((inputs: InputsObject<Inputs>) => boolean) | undefined
|
|
65
|
-
readonly coalesce?: 'switch' | 'trailing' | undefined
|
|
66
|
-
readonly reuse?: boolean | undefined
|
|
67
|
-
/**
|
|
68
|
-
* An extra key segment + wake source beyond the declared inputs — the hidden
|
|
69
|
-
* `invalidateOn` revision. Kept out of `wireSources` so the user's `query`
|
|
70
|
-
* and `invalidateBy` see exactly the declared inputs.
|
|
71
|
-
*/
|
|
72
|
-
readonly extraKey?:
|
|
73
|
-
| {
|
|
74
|
-
readonly read: () => unknown
|
|
75
|
-
readonly subscribe: (listener: () => void) => () => void
|
|
76
|
-
}
|
|
77
|
-
| undefined
|
|
78
|
-
/**
|
|
79
|
-
* Fired when a run's `Success` value has landed in the store (same scheduler
|
|
80
|
-
* flush — never before the converged value is readable), with the run's
|
|
81
|
-
* generation. Not fired on `Error`, interrupt, or while disabled. The seam
|
|
82
|
-
* `RemoteState` settles pending intents through.
|
|
83
|
-
*/
|
|
84
|
-
readonly onSettled?: ((generation: number) => void) | undefined
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface QueryDriver<A, E> {
|
|
88
|
-
/** The lifecycle store (the full union — callers narrow to their arms). */
|
|
89
|
-
readonly store: Store<AnyAsyncData<A, E>>
|
|
90
|
-
/**
|
|
91
|
-
* The highest generation requested so far (0 before the first kick).
|
|
92
|
-
* Generations are assigned when a run is REQUESTED (enqueued), so a waiter
|
|
93
|
-
* registered as `requested() + 1` is satisfied only by a run that began
|
|
94
|
-
* after registration — the settle-ordering rule `RemoteState` builds on.
|
|
95
|
-
*/
|
|
96
|
-
readonly requested: () => number
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Build the driver: store + subscription + request queue + run fiber, all owned
|
|
101
|
-
* by the ambient scope (callers run this under `Layer.scoped`). A change to an
|
|
102
|
-
* input re-runs the query latest-wins (a new run cancels the in-flight one; or,
|
|
103
|
-
* with `coalesce: 'trailing'`, lets it finish and runs one trailing refetch);
|
|
104
|
-
* while a re-run is in flight the last `Success`/`Error` is kept with
|
|
105
|
-
* `refetching: true`.
|
|
106
|
-
*/
|
|
107
|
-
export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R>(
|
|
108
|
-
options: QueryDriverOptions<Inputs, A, E, R>,
|
|
109
|
-
): Effect.Effect<QueryDriver<A, E>, never, InputStores<Inputs> | R | Scope.Scope> =>
|
|
110
|
-
Effect.gen(function* () {
|
|
111
|
-
const scheduler = yield* resolveScheduler
|
|
112
|
-
const sources = yield* wireSources(options.inputs, options.invalidateBy)
|
|
113
|
-
const extraKey = options.extraKey
|
|
114
|
-
|
|
115
|
-
const keyOf = (args: InputsObject<Inputs>): ReadonlyArray<unknown> =>
|
|
116
|
-
extraKey === undefined ? sources.keyOf(args) : [...sources.keyOf(args), extraKey.read()]
|
|
117
|
-
const subscribeAll = (listener: () => void): (() => void) => {
|
|
118
|
-
const offSources = sources.subscribe(listener)
|
|
119
|
-
const offExtra = extraKey?.subscribe(listener)
|
|
120
|
-
return () => {
|
|
121
|
-
offSources()
|
|
122
|
-
offExtra?.()
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
const disabledNow = (args: InputsObject<Inputs> = sources.snapshot()): boolean =>
|
|
126
|
-
options.gated && options.disabled !== undefined ? options.disabled(args) : false
|
|
127
|
-
|
|
128
|
-
const store = makeStore<AnyAsyncData<A, E>>(
|
|
129
|
-
disabledNow() ? AsyncData.idle : AsyncData.loading,
|
|
130
|
-
scheduler,
|
|
131
|
-
)
|
|
132
|
-
// The last requested key, so a change that doesn't move it (or only churns
|
|
133
|
-
// an input `invalidateBy` ignores) doesn't re-fetch.
|
|
134
|
-
const lastKey = MutableRef.make<ReadonlyArray<unknown> | undefined>(undefined)
|
|
135
|
-
// The run-generation counter: bumped when a run is REQUESTED (enqueued), so
|
|
136
|
-
// `requested()` names the newest run that could possibly be in flight.
|
|
137
|
-
const generation = MutableRef.make(0)
|
|
138
|
-
const nextGeneration = (): number => {
|
|
139
|
-
MutableRef.update(generation, (n) => n + 1)
|
|
140
|
-
return MutableRef.get(generation)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Mark a re-fetch in flight without dropping the visible value (SWR).
|
|
144
|
-
const markRefetching = () => {
|
|
145
|
-
const prev = store.get()
|
|
146
|
-
if (prev._tag === 'Success') store.set(AsyncData.success(prev.value, true))
|
|
147
|
-
else if (prev._tag === 'Error') store.set(AsyncData.error(prev.error, true))
|
|
148
|
-
else store.set(AsyncData.loading)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// One run of the query, folded into the store. A failure becomes `Error`; an
|
|
152
|
-
// interrupt (latest-wins cancel) leaves the state untouched; a defect (a bug
|
|
153
|
-
// in the body) is logged and isolated — the driver keeps running. A `Success`
|
|
154
|
-
// additionally reports its generation through `onSettled`, after the write,
|
|
155
|
-
// so settle-driven consequences observe the converged value.
|
|
156
|
-
const runQuery = (request: Request): Effect.Effect<void, never, R> =>
|
|
157
|
-
options.query(request.args).pipe(
|
|
158
|
-
Effect.tapDefect((defect) =>
|
|
159
|
-
Effect.logError(`reform: ${options.label} '${options.name}' query defect`, defect),
|
|
160
|
-
),
|
|
161
|
-
Effect.matchCause({
|
|
162
|
-
onSuccess: (value) => {
|
|
163
|
-
if (!disabledNow()) {
|
|
164
|
-
// `reuse`: share unchanged subtrees with the previous Success
|
|
165
|
-
// value, so a refetch that barely moved keeps identities stable.
|
|
166
|
-
const prev = store.get()
|
|
167
|
-
const shared =
|
|
168
|
-
options.reuse === true && prev._tag === 'Success'
|
|
169
|
-
? reuse(prev.value, value)
|
|
170
|
-
: value
|
|
171
|
-
store.set(AsyncData.success(shared, false))
|
|
172
|
-
options.onSettled?.(request.generation)
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
onFailure: (cause) => {
|
|
176
|
-
const failure = Cause.failureOption(cause)
|
|
177
|
-
if (Option.isSome(failure) && !disabledNow()) {
|
|
178
|
-
store.set(AsyncData.error(failure.value, false))
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
}),
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
interface Request {
|
|
185
|
-
readonly args: InputsObject<Inputs>
|
|
186
|
-
readonly generation: number
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// The driver. `'switch'` (default): each new request cancels the in-flight
|
|
190
|
-
// run — the same latest-wins semantics a `latest` channel gives procedures.
|
|
191
|
-
// `'trailing'`: a strictly sequential consumer that, on wake, drains every
|
|
192
|
-
// request that piled up during the flight and runs the LATEST one —
|
|
193
|
-
// "exactly one trailing run after settle" by construction. The trailing
|
|
194
|
-
// run's generation is the max drained (the latest request's), so it
|
|
195
|
-
// vouches for every request it conflated.
|
|
196
|
-
const trailing = options.coalesce === 'trailing'
|
|
197
|
-
const requests = yield* Queue.unbounded<Request>()
|
|
198
|
-
yield* Effect.forkScoped(
|
|
199
|
-
trailing
|
|
200
|
-
? Effect.forever(
|
|
201
|
-
Effect.gen(function* () {
|
|
202
|
-
const first = yield* Queue.take(requests)
|
|
203
|
-
const queued = yield* Queue.takeAll(requests)
|
|
204
|
-
const request = Option.getOrElse(Chunk.last(queued), () => first)
|
|
205
|
-
// A disable that landed while the request waited: skip the run.
|
|
206
|
-
if (!disabledNow()) yield* runQuery(request)
|
|
207
|
-
// The settle may have written a stale-key result with
|
|
208
|
-
// `refetching: false`; if a newer request is already waiting,
|
|
209
|
-
// restore the syncing flag before the next take — both writes
|
|
210
|
-
// coalesce into one scheduler flush for subscribers.
|
|
211
|
-
const pending = yield* Queue.size(requests)
|
|
212
|
-
if (pending > 0 && !disabledNow()) yield* Effect.sync(markRefetching)
|
|
213
|
-
}),
|
|
214
|
-
)
|
|
215
|
-
: Stream.fromQueue(requests).pipe(
|
|
216
|
-
Stream.flatMap((request) => Stream.fromEffect(runQuery(request)), { switch: true }),
|
|
217
|
-
Stream.runDrain,
|
|
218
|
-
),
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
const trigger = () => {
|
|
222
|
-
const args = sources.snapshot()
|
|
223
|
-
if (disabledNow(args)) {
|
|
224
|
-
// Switched off: show Idle and forget the key so re-enabling always re-runs.
|
|
225
|
-
MutableRef.set(lastKey, undefined)
|
|
226
|
-
store.set(AsyncData.idle)
|
|
227
|
-
return
|
|
228
|
-
}
|
|
229
|
-
const key = keyOf(args)
|
|
230
|
-
const previous = MutableRef.get(lastKey)
|
|
231
|
-
if (previous !== undefined && sameKey(key, previous)) return
|
|
232
|
-
MutableRef.set(lastKey, key)
|
|
233
|
-
markRefetching()
|
|
234
|
-
Queue.unsafeOffer(requests, { args, generation: nextGeneration() })
|
|
235
|
-
}
|
|
236
|
-
const unsubscribe = subscribeAll(trigger)
|
|
237
|
-
yield* Effect.addFinalizer(() => Effect.sync(unsubscribe))
|
|
238
|
-
|
|
239
|
-
// Kick off the first fetch unless the query starts disabled.
|
|
240
|
-
const initial = sources.snapshot()
|
|
241
|
-
if (!disabledNow(initial)) {
|
|
242
|
-
MutableRef.set(lastKey, keyOf(initial))
|
|
243
|
-
Queue.unsafeOffer(requests, { args: initial, generation: nextGeneration() })
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return { store, requested: () => MutableRef.get(generation) }
|
|
247
|
-
})
|
package/src/internal/reuse.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { Equal } from 'effect'
|
|
2
|
-
|
|
3
|
-
// Structural sharing for recomputed calc outputs: reconcile a fresh output
|
|
4
|
-
// against the previous one, substituting previous nodes wherever value equality
|
|
5
|
-
// holds, so identities only move where values moved. Downstream memo boundaries
|
|
6
|
-
// (React subtrees keyed on object identity) then skip everything that didn't
|
|
7
|
-
// change. Referentially transparent: the result is value-equal to `next` — only
|
|
8
|
-
// identities shift toward `previous` — and neither argument is mutated, so the
|
|
9
|
-
// pass composes with the framework's Equal-based invalidation untouched.
|
|
10
|
-
|
|
11
|
-
// Bounds the walk; calc outputs are shallow plain data by framework convention.
|
|
12
|
-
const maxDepth = 16
|
|
13
|
-
|
|
14
|
-
const isPlainRecord = (v: unknown): v is Record<string, unknown> =>
|
|
15
|
-
typeof v === 'object' &&
|
|
16
|
-
v !== null &&
|
|
17
|
-
(Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null)
|
|
18
|
-
|
|
19
|
-
const go = (prev: unknown, next: unknown, depth: number): unknown => {
|
|
20
|
-
if (Object.is(prev, next)) return prev
|
|
21
|
-
// Data/Schema classes implement Equal+Hash: substitute wholesale on value
|
|
22
|
-
// equality. Plain objects/arrays fall through (their Equal is referential).
|
|
23
|
-
if (Equal.equals(prev, next)) return prev
|
|
24
|
-
if (depth <= 0) return next
|
|
25
|
-
if (Array.isArray(prev) && Array.isArray(next)) {
|
|
26
|
-
const out = next.map((item, i) => (i < prev.length ? go(prev[i], item, depth - 1) : item))
|
|
27
|
-
return prev.length === next.length && out.every((v, i) => Object.is(v, prev[i]))
|
|
28
|
-
? prev
|
|
29
|
-
: out
|
|
30
|
-
}
|
|
31
|
-
if (isPlainRecord(prev) && isPlainRecord(next)) {
|
|
32
|
-
const keys = Object.keys(next)
|
|
33
|
-
const out: Record<string, unknown> = {}
|
|
34
|
-
for (const key of keys) out[key] = key in prev ? go(prev[key], next[key], depth - 1) : next[key]
|
|
35
|
-
const allPrev =
|
|
36
|
-
keys.length === Object.keys(prev).length &&
|
|
37
|
-
keys.every((key) => key in prev && Object.is(out[key], prev[key]))
|
|
38
|
-
return allPrev ? prev : out
|
|
39
|
-
}
|
|
40
|
-
// Data/Schema class instances that DIFFER still get walked: their Equal is
|
|
41
|
-
// fieldwise over own enumerable fields, so a reconstruction over reconciled
|
|
42
|
-
// fields (same prototype; the constructor is bypassed, but every leaf is a
|
|
43
|
-
// validated value out of `prev` or `next`) stays value-equal to `next` while
|
|
44
|
-
// unchanged children — e.g. one untouched element inside a class-typed
|
|
45
|
-
// container — keep their `prev` identity.
|
|
46
|
-
if (
|
|
47
|
-
Equal.isEqual(prev) &&
|
|
48
|
-
Equal.isEqual(next) &&
|
|
49
|
-
Object.getPrototypeOf(prev) === Object.getPrototypeOf(next)
|
|
50
|
-
) {
|
|
51
|
-
const prevFields = Object.fromEntries(Object.entries(prev))
|
|
52
|
-
const nextFields = Object.entries(next)
|
|
53
|
-
const out: Record<string, unknown> = {}
|
|
54
|
-
for (const [key, value] of nextFields)
|
|
55
|
-
out[key] = key in prevFields ? go(prevFields[key], value, depth - 1) : value
|
|
56
|
-
const allPrev =
|
|
57
|
-
nextFields.length === Object.keys(prevFields).length &&
|
|
58
|
-
nextFields.every(([key]) => key in prevFields && Object.is(out[key], prevFields[key]))
|
|
59
|
-
return allPrev ? prev : Object.assign(Object.create(Object.getPrototypeOf(next)), out)
|
|
60
|
-
}
|
|
61
|
-
// Class instances without Equal (Date, Map, …) are opaque leaves.
|
|
62
|
-
return next
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Substitute `previous` nodes into `next` wherever they are value-equal. The
|
|
67
|
-
* walker only ever returns `previous`, `next`, or a key/index-wise
|
|
68
|
-
* reconstruction of `next` whose every leaf came from one of them, so the
|
|
69
|
-
* result is value-equal to `next` and the type is preserved by construction —
|
|
70
|
-
* the single cast below is that argument, in the style of `wireSources` /
|
|
71
|
-
* `narrowStore`.
|
|
72
|
-
*/
|
|
73
|
-
export const reuse = <A>(previous: A, next: A): A => go(previous, next, maxDepth) as A
|