@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
|
@@ -1,165 +1,389 @@
|
|
|
1
|
-
import { Context, Effect, Layer,
|
|
1
|
+
import { Context, Effect, GlobalValue, Layer, MutableRef, Option, Schema, Scope } from 'effect'
|
|
2
|
+
import type { AST } from 'effect/SchemaAST'
|
|
2
3
|
import type { YieldWrap } from 'effect/Utils'
|
|
3
4
|
import { type Manifest, definitionClass } from '../definition/definition'
|
|
4
|
-
import { type Ctx } from '../internal/ctx'
|
|
5
5
|
import type { Tracker } from '../internal/track'
|
|
6
6
|
import { CurrentTracker } from '../internal/track'
|
|
7
|
-
import type
|
|
7
|
+
import { type PropsId, type PropsTag, propsTag } from './props'
|
|
8
|
+
import type { AnySlot, AnySlotInstance, SlotClass, SlotInstance } from './slot'
|
|
8
9
|
import type { Structure } from './structure'
|
|
9
|
-
import type { UiClass, UiContract } from './ui'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* What a composition's logic returns for one frame: a serializable {@link Structure}
|
|
14
|
-
* value — its computed props, per-slot fills, and event triggers, all as DATA. A body
|
|
15
|
-
* never returns (nor evaluates) a React node, so the engine, the wire server, and the
|
|
16
|
-
* proof harness consume the frame without React; presentation resolves the contract's
|
|
17
|
-
* view separately, by name, on a host.
|
|
18
|
-
*/
|
|
10
|
+
import type { UiClass, UiContract, UiReflection } from './ui'
|
|
11
|
+
|
|
19
12
|
export type Frame<C extends UiContract = UiContract> = Structure<C>
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
readonly props: unknown
|
|
14
|
+
export interface RenderEnv<out P = unknown> {
|
|
15
|
+
readonly props: P
|
|
24
16
|
readonly tracker: Tracker
|
|
25
17
|
}
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
export interface CompositionService {
|
|
30
|
-
readonly render: (env: RenderEnv) => Effect.Effect<Frame
|
|
19
|
+
export type CompositionActivation = Layer.Layer<never, never, never>
|
|
20
|
+
|
|
21
|
+
export interface CompositionService<in out P = unknown, out C extends UiContract = UiContract> {
|
|
22
|
+
readonly render: (env: RenderEnv<P>) => Effect.Effect<Frame<C>, never, never>
|
|
23
|
+
readonly activation: (props: P) => Option.Option<CompositionActivation>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const CompositionIdTypeId: unique symbol = Symbol.for('reform/CompositionId')
|
|
27
|
+
export type CompositionIdTypeId = typeof CompositionIdTypeId
|
|
28
|
+
|
|
29
|
+
export interface CompositionId<
|
|
30
|
+
out N extends string,
|
|
31
|
+
out Identity = unknown,
|
|
32
|
+
out RequiredSlots = unknown,
|
|
33
|
+
> {
|
|
34
|
+
readonly [CompositionIdTypeId]: {
|
|
35
|
+
readonly name: N
|
|
36
|
+
readonly identity: Identity
|
|
37
|
+
readonly requiredSlots: RequiredSlots
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type IsExact<Left, Right> = [Left] extends [Right] ? ([Right] extends [Left] ? true : false) : false
|
|
42
|
+
|
|
43
|
+
type IsExactCompositionId<Service, N extends string, Identity> = Service extends {
|
|
44
|
+
readonly [CompositionIdTypeId]: {
|
|
45
|
+
readonly name: infer ServiceName extends string
|
|
46
|
+
readonly identity: infer ServiceIdentity
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
? IsExact<ServiceName, N> extends true
|
|
50
|
+
? IsExact<ServiceIdentity, Identity>
|
|
51
|
+
: false
|
|
52
|
+
: false
|
|
53
|
+
|
|
54
|
+
type IsAny<T> = 0 extends 1 & T ? true : false
|
|
55
|
+
|
|
56
|
+
export type HasExactCompositionId<Services, N extends string, Identity> =
|
|
57
|
+
IsAny<Services> extends true
|
|
58
|
+
? false
|
|
59
|
+
: true extends IsExactCompositionId<Services, N, Identity>
|
|
60
|
+
? true
|
|
61
|
+
: false
|
|
62
|
+
|
|
63
|
+
export type CompositionTag<
|
|
64
|
+
N extends string,
|
|
65
|
+
P,
|
|
66
|
+
C extends UiContract,
|
|
67
|
+
Identity = unknown,
|
|
68
|
+
> = Context.Tag<CompositionId<N, Identity, RequiredSlotIdentities<C>>, CompositionService<P, C>>
|
|
69
|
+
|
|
70
|
+
export type SlotClassesOf<C extends UiContract> = C extends {
|
|
71
|
+
readonly slots: infer Slots extends Record<string, AnySlotInstance>
|
|
72
|
+
}
|
|
73
|
+
? {
|
|
74
|
+
readonly [K in keyof Slots]: Slots[K] extends SlotInstance<
|
|
75
|
+
infer Child,
|
|
76
|
+
infer SlotName,
|
|
77
|
+
infer SlotIdentity
|
|
78
|
+
>
|
|
79
|
+
? SlotClass<Child, SlotName, SlotIdentity>
|
|
80
|
+
: never
|
|
81
|
+
}
|
|
82
|
+
: Readonly<Record<PropertyKey, never>>
|
|
83
|
+
|
|
84
|
+
type RequiredSlotIdentity<Slot> =
|
|
85
|
+
Slot extends SlotInstance<infer _Target, infer _Name, infer Identity> ? Identity : never
|
|
86
|
+
|
|
87
|
+
export type RequiredSlotIdentities<C extends UiContract> = C extends {
|
|
88
|
+
readonly slots: infer Slots extends Record<string, AnySlotInstance>
|
|
89
|
+
}
|
|
90
|
+
? RequiredSlotIdentity<Slots[keyof Slots]>
|
|
91
|
+
: never
|
|
92
|
+
|
|
93
|
+
type CompositionSlots<C extends UiContract> = C extends {
|
|
94
|
+
readonly slots: infer Slots extends Record<string, AnySlotInstance>
|
|
95
|
+
}
|
|
96
|
+
? [Slots[keyof Slots]] extends [never]
|
|
97
|
+
? {}
|
|
98
|
+
: { readonly slots: SlotClassesOf<C> }
|
|
99
|
+
: {}
|
|
100
|
+
|
|
101
|
+
export type CompositionManifest<
|
|
102
|
+
C extends UiContract = UiContract,
|
|
103
|
+
N extends string = string,
|
|
104
|
+
> = Manifest &
|
|
105
|
+
Omit<CompositionConfig, 'slots' | 'ui'> &
|
|
106
|
+
CompositionSlots<C> & {
|
|
107
|
+
readonly kind: 'Composition'
|
|
108
|
+
readonly name: N
|
|
109
|
+
readonly ui: UiReflection<C>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface CompositionInstance<out Identity = unknown> {
|
|
113
|
+
readonly _compositionIdentity: Identity
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface CompositionReflection<
|
|
117
|
+
out P = unknown,
|
|
118
|
+
out C extends UiContract = UiContract,
|
|
119
|
+
out S extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
|
|
120
|
+
out N extends string = string,
|
|
121
|
+
out Identity = unknown,
|
|
122
|
+
> {
|
|
123
|
+
new (): CompositionInstance<Identity>
|
|
124
|
+
readonly manifest: CompositionManifest<C, N>
|
|
125
|
+
readonly Props: P
|
|
126
|
+
readonly Contract: C
|
|
127
|
+
readonly States: S
|
|
128
|
+
readonly parseProps: (input: unknown) => Option.Option<P>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface CompositionPropsReceiver<in P> {
|
|
132
|
+
readonly acceptsProps: (props: P) => void
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type CompositionCapture<Result> = <
|
|
136
|
+
P,
|
|
137
|
+
C extends UiContract,
|
|
138
|
+
S extends ReadonlyArray<unknown>,
|
|
139
|
+
N extends string,
|
|
140
|
+
Identity,
|
|
141
|
+
>(
|
|
142
|
+
composition: CompositionClass<P, C, S, N, Identity>,
|
|
143
|
+
) => Result
|
|
144
|
+
|
|
145
|
+
export interface AnyComposition extends CompositionReflection {
|
|
146
|
+
readonly capture: <Result>(visit: CompositionCapture<Result>) => Result
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface CompositionSchema {
|
|
150
|
+
readonly ast: AST
|
|
31
151
|
}
|
|
32
152
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
153
|
+
type GeneratorContext<Eff> = [Eff] extends [never]
|
|
154
|
+
? never
|
|
155
|
+
: [Eff] extends [YieldWrap<Effect.Effect<infer _Success, infer _Error, infer R>>]
|
|
156
|
+
? R
|
|
157
|
+
: never
|
|
158
|
+
|
|
159
|
+
// Property presence drives Config conditional inference; Option would erase it.
|
|
160
|
+
interface CompositionConfigOptionalFields {
|
|
161
|
+
readonly description: string
|
|
162
|
+
readonly props: CompositionSchema
|
|
163
|
+
readonly states: ReadonlyArray<unknown>
|
|
164
|
+
readonly calcs: ReadonlyArray<unknown>
|
|
165
|
+
readonly events: ReadonlyArray<unknown>
|
|
166
|
+
readonly slots: Record<string, AnySlot>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface CompositionConfig extends Partial<CompositionConfigOptionalFields> {
|
|
39
170
|
readonly title: string
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
type PropsOf<Config> = Config extends { props: Schema.Schema<infer P, any> } ? P : unknown
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* The composition's state-group tuple, recovered from the `const`-inferred config
|
|
64
|
-
* so consumers can type its seeds. Erased to `readonly []` when no `states` are
|
|
65
|
-
* declared; entries that are not state groups contribute no seed keys (`SeedsOf`).
|
|
66
|
-
*/
|
|
67
|
-
type StatesOf<Config> = Config extends { states: infer S extends ReadonlyArray<unknown> }
|
|
171
|
+
readonly ui: UiReflection
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
type PropsOf<Config> = Config extends { props: infer PropsSchema }
|
|
175
|
+
? PropsSchema extends Schema.Schema<infer P, infer _Encoded, infer _Context>
|
|
176
|
+
? P
|
|
177
|
+
: unknown
|
|
178
|
+
: unknown
|
|
179
|
+
|
|
180
|
+
type EncodedPropsOf<Config> = Config extends { props: infer PropsSchema }
|
|
181
|
+
? PropsSchema extends Schema.Schema<infer _Props, infer Encoded, infer _Context>
|
|
182
|
+
? Encoded
|
|
183
|
+
: unknown
|
|
184
|
+
: unknown
|
|
185
|
+
|
|
186
|
+
type StatesOf<Config> = Config extends {
|
|
187
|
+
states: infer S extends ReadonlyArray<unknown>
|
|
188
|
+
}
|
|
68
189
|
? S
|
|
69
190
|
: readonly []
|
|
70
191
|
|
|
71
192
|
export interface CompositionClass<
|
|
72
|
-
out P,
|
|
73
|
-
out C extends UiContract = UiContract,
|
|
193
|
+
in out P,
|
|
194
|
+
in out C extends UiContract = UiContract,
|
|
74
195
|
out S extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
196
|
+
in out N extends string = string,
|
|
197
|
+
in out Identity = unknown,
|
|
198
|
+
>
|
|
199
|
+
extends CompositionReflection<P, C, S, N, Identity>, CompositionPropsReceiver<P> {
|
|
79
200
|
readonly Props: P
|
|
80
|
-
/**
|
|
81
|
-
* Phantom carrying the resolved UI contract (props + events + slots). It lets
|
|
82
|
-
* reflection consumers — proofs, the dev tool — recover the full contract from
|
|
83
|
-
* a composition value and stay fully typed. Covariant (output only); the
|
|
84
|
-
* invariant `impl` tag is never stored here, so no variance regression.
|
|
85
|
-
*/
|
|
86
201
|
readonly Contract: C
|
|
87
|
-
/**
|
|
88
|
-
* Phantom carrying the state-group tuple, so `seedScene` can type a scene's
|
|
89
|
-
* seeds against the actual state members. Same erasure trick as `Contract`.
|
|
90
|
-
*/
|
|
91
202
|
readonly States: S
|
|
92
|
-
readonly
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
203
|
+
readonly identity: symbol
|
|
204
|
+
readonly props: PropsTag<P>
|
|
205
|
+
readonly tag: CompositionTag<N, P, C, Identity>
|
|
206
|
+
readonly capture: <Result>(visit: CompositionCapture<Result>) => Result
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
type CompositionDefinitionStatics<
|
|
210
|
+
P,
|
|
211
|
+
C extends UiContract,
|
|
212
|
+
S extends ReadonlyArray<unknown>,
|
|
213
|
+
N extends string,
|
|
214
|
+
Identity,
|
|
215
|
+
> = Pick<
|
|
216
|
+
CompositionClass<P, C, S, N, Identity>,
|
|
217
|
+
'manifest' | 'identity' | 'props' | 'parseProps' | 'acceptsProps' | 'tag' | 'capture'
|
|
218
|
+
>
|
|
219
|
+
|
|
220
|
+
interface UiCarrier<C extends UiContract, UiName extends string> {
|
|
221
|
+
readonly ui: UiClass<C, UiName>
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
type ContractSlots<C extends UiContract> = CompositionSlots<C>
|
|
225
|
+
|
|
226
|
+
type ContextFreeProps<Config> = Config extends {
|
|
227
|
+
readonly props: infer PropsSchema
|
|
228
|
+
}
|
|
229
|
+
? PropsSchema extends Schema.Schema<infer _P, infer _Encoded, infer Context>
|
|
230
|
+
? [Context] extends [never]
|
|
231
|
+
? unknown
|
|
232
|
+
: { readonly propsSchemaMustNotRequireServices: never }
|
|
233
|
+
: unknown
|
|
234
|
+
: unknown
|
|
235
|
+
|
|
236
|
+
const compositionDefinitionSequence: MutableRef.MutableRef<number> = GlobalValue.globalValue(
|
|
237
|
+
Symbol.for('reform/CompositionDefinitionSequence'),
|
|
238
|
+
() => MutableRef.make(0),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
const nextCompositionTagKey = (name: string): string =>
|
|
242
|
+
`reform/composition/${name}/${MutableRef.incrementAndGet(compositionDefinitionSequence)}`
|
|
243
|
+
|
|
244
|
+
export const make =
|
|
245
|
+
<
|
|
246
|
+
const N extends string,
|
|
247
|
+
const Config extends CompositionConfig,
|
|
248
|
+
C extends UiContract,
|
|
249
|
+
const UiName extends string,
|
|
250
|
+
>(
|
|
251
|
+
name: N,
|
|
252
|
+
config: Config & UiCarrier<C, UiName> & ContractSlots<C> & ContextFreeProps<Config>,
|
|
253
|
+
) =>
|
|
254
|
+
<Self>(): CompositionClass<PropsOf<Config>, C, StatesOf<Config>, N, Self> => {
|
|
255
|
+
const identity = Symbol(name)
|
|
256
|
+
const props = propsTag<PropsOf<Config>>()
|
|
257
|
+
const propsSchema = Schema.make<PropsOf<Config>, EncodedPropsOf<Config>>(
|
|
258
|
+
config.props?.ast ?? Schema.Unknown.ast,
|
|
259
|
+
)
|
|
260
|
+
const parseProps: (input: unknown) => Option.Option<PropsOf<Config>> =
|
|
261
|
+
Schema.decodeUnknownOption(propsSchema)
|
|
262
|
+
const tag = Context.GenericTag<
|
|
263
|
+
CompositionId<N, Self, RequiredSlotIdentities<C>>,
|
|
264
|
+
CompositionService<PropsOf<Config>, C>
|
|
265
|
+
>(nextCompositionTagKey(name))
|
|
266
|
+
const manifest = {
|
|
267
|
+
kind: 'Composition',
|
|
268
|
+
name,
|
|
269
|
+
...config,
|
|
270
|
+
} satisfies CompositionManifest<C, N>
|
|
271
|
+
const composition = definitionClass<
|
|
272
|
+
CompositionClass<PropsOf<Config>, C, StatesOf<Config>, N, Self>
|
|
273
|
+
>({
|
|
274
|
+
manifest,
|
|
275
|
+
identity,
|
|
276
|
+
props,
|
|
277
|
+
parseProps,
|
|
278
|
+
acceptsProps: () => undefined,
|
|
279
|
+
tag,
|
|
280
|
+
capture: <Result>(visit: CompositionCapture<Result>): Result => visit(composition),
|
|
281
|
+
} satisfies CompositionDefinitionStatics<PropsOf<Config>, C, StatesOf<Config>, N, Self>)
|
|
282
|
+
return composition
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
type CompositionLayerInput<Eff> = Exclude<Exclude<GeneratorContext<Eff>, PropsId>, CurrentTracker>
|
|
286
|
+
|
|
287
|
+
type CompositionActivationLayerInput<Eff> = Exclude<
|
|
288
|
+
Exclude<GeneratorContext<Eff>, PropsId>,
|
|
289
|
+
Scope.Scope
|
|
290
|
+
>
|
|
291
|
+
|
|
292
|
+
export interface ScopedCompositionLiveConfig<
|
|
293
|
+
C extends UiContract,
|
|
294
|
+
RenderEff extends YieldWrap<Effect.Effect<unknown, never, unknown>>,
|
|
295
|
+
ActivationEff extends YieldWrap<Effect.Effect<unknown, never, unknown>>,
|
|
296
|
+
> {
|
|
297
|
+
readonly render: () => Generator<RenderEff, Frame<C>, never>
|
|
298
|
+
readonly activate: () => Generator<ActivationEff, void, never>
|
|
120
299
|
}
|
|
121
300
|
|
|
122
|
-
/**
|
|
123
|
-
* Wire a composition's synchronous logic (D2): read state, acquire triggers,
|
|
124
|
-
* resolve the UI, return the view. Its requirements (minus `Props`, which the
|
|
125
|
-
* host injects per render) surface as the Layer's `RIn`; the runtime runs the
|
|
126
|
-
* body per render within the captured context.
|
|
127
|
-
*/
|
|
128
301
|
export const live = <
|
|
129
302
|
P,
|
|
130
303
|
C extends UiContract,
|
|
304
|
+
S extends ReadonlyArray<unknown>,
|
|
305
|
+
N extends string,
|
|
306
|
+
Identity,
|
|
131
307
|
Eff extends YieldWrap<Effect.Effect<unknown, never, unknown>>,
|
|
132
308
|
>(
|
|
133
|
-
|
|
134
|
-
// `Structure` is checked against the composition's contract.
|
|
135
|
-
composition: CompositionClass<P, C>,
|
|
309
|
+
composition: CompositionClass<P, C, S, N, Identity>,
|
|
136
310
|
body: () => Generator<Eff, Frame<C>, never>,
|
|
137
|
-
): Layer.Layer<
|
|
138
|
-
|
|
311
|
+
): Layer.Layer<
|
|
312
|
+
CompositionId<N, Identity, RequiredSlotIdentities<C>>,
|
|
313
|
+
never,
|
|
314
|
+
CompositionLayerInput<Eff>
|
|
315
|
+
> => {
|
|
316
|
+
const logic: Effect.Effect<Frame<C>, never, GeneratorContext<Eff>> = Effect.gen(body).pipe(
|
|
317
|
+
Effect.orDie,
|
|
318
|
+
)
|
|
139
319
|
return Layer.effect(
|
|
140
320
|
composition.tag,
|
|
141
321
|
Effect.gen(function* () {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// The per-render erasure seam: the body is total once fully provided (it is
|
|
147
|
-
// synchronous, reads can't fail, every requirement is now satisfied — D2) and
|
|
148
|
-
// its `Structure<C>` erases to the service's contract-agnostic `Frame`. `gen`
|
|
149
|
-
// widens the error channel to the body's inferred `E` for a generic body, so
|
|
150
|
-
// narrowing it (and the contract) back to the erased service type needs the
|
|
151
|
-
// `unknown` bridge — the one cast this render boundary has always required.
|
|
152
|
-
// oxlint-disable-next-line reform-rules/no-type-assertion -- the one documented render-boundary erasure: a fully-provided generic body's Structure<C> narrowed back to the contract-agnostic Frame service type
|
|
153
|
-
logic.pipe(
|
|
154
|
-
Effect.provideService(Props, env.props),
|
|
322
|
+
const context = yield* Effect.context<CompositionLayerInput<Eff>>()
|
|
323
|
+
const render = (env: RenderEnv<P>): Effect.Effect<Frame<C>, never, never> => {
|
|
324
|
+
return logic.pipe(
|
|
325
|
+
Effect.provideService(composition.props, env.props),
|
|
155
326
|
Effect.provideService(CurrentTracker, env.tracker),
|
|
156
327
|
Effect.provide(context),
|
|
157
|
-
)
|
|
158
|
-
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
return { render, activation: () => Option.none() }
|
|
159
331
|
}),
|
|
160
332
|
)
|
|
161
333
|
}
|
|
162
334
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
335
|
+
export const liveScoped = <
|
|
336
|
+
P,
|
|
337
|
+
C extends UiContract,
|
|
338
|
+
S extends ReadonlyArray<unknown>,
|
|
339
|
+
N extends string,
|
|
340
|
+
Identity,
|
|
341
|
+
RenderEff extends YieldWrap<Effect.Effect<unknown, never, unknown>>,
|
|
342
|
+
ActivationEff extends YieldWrap<Effect.Effect<unknown, never, unknown>>,
|
|
343
|
+
>(
|
|
344
|
+
composition: CompositionClass<P, C, S, N, Identity>,
|
|
345
|
+
config: ScopedCompositionLiveConfig<C, RenderEff, ActivationEff>,
|
|
346
|
+
): Layer.Layer<
|
|
347
|
+
CompositionId<N, Identity, RequiredSlotIdentities<C>>,
|
|
348
|
+
never,
|
|
349
|
+
CompositionLayerInput<RenderEff> | CompositionActivationLayerInput<ActivationEff>
|
|
350
|
+
> => {
|
|
351
|
+
const renderLogic: Effect.Effect<Frame<C>, never, GeneratorContext<RenderEff>> = Effect.gen(
|
|
352
|
+
config.render,
|
|
353
|
+
).pipe(Effect.orDie)
|
|
354
|
+
const activationLogic: Effect.Effect<void, never, GeneratorContext<ActivationEff>> = Effect.gen(
|
|
355
|
+
config.activate,
|
|
356
|
+
).pipe(Effect.orDie)
|
|
357
|
+
|
|
358
|
+
return Layer.effect(
|
|
359
|
+
composition.tag,
|
|
360
|
+
Effect.gen(function* () {
|
|
361
|
+
const renderContext = yield* Effect.context<CompositionLayerInput<RenderEff>>()
|
|
362
|
+
const activationContext =
|
|
363
|
+
yield* Effect.context<CompositionActivationLayerInput<ActivationEff>>()
|
|
364
|
+
const render = (env: RenderEnv<P>): Effect.Effect<Frame<C>, never, never> =>
|
|
365
|
+
renderLogic.pipe(
|
|
366
|
+
Effect.provideService(composition.props, env.props),
|
|
367
|
+
Effect.provideService(CurrentTracker, env.tracker),
|
|
368
|
+
Effect.provide(renderContext),
|
|
369
|
+
)
|
|
370
|
+
const activation = (props: P): Option.Option<CompositionActivation> => {
|
|
371
|
+
const scoped = Layer.scopedDiscard(
|
|
372
|
+
activationLogic.pipe(Effect.provideService(composition.props, props)),
|
|
373
|
+
)
|
|
374
|
+
return Option.some(scoped.pipe(Layer.provide(Layer.succeedContext(activationContext))))
|
|
375
|
+
}
|
|
376
|
+
return { render, activation }
|
|
377
|
+
}),
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export const render = <P, C extends UiContract>(
|
|
382
|
+
service: CompositionService<P, C>,
|
|
383
|
+
env: RenderEnv<P>,
|
|
384
|
+
): Effect.Effect<Frame<C>, never, never> => service.render(env)
|
|
385
|
+
|
|
386
|
+
export const activation = <P, C extends UiContract>(
|
|
387
|
+
service: CompositionService<P, C>,
|
|
388
|
+
props: P,
|
|
389
|
+
): Option.Option<CompositionActivation> => service.activation(props)
|
package/src/compose/host.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import type { Node } from './slot'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* A slot handle the host hands a presentation: BOTH renderable (call it / use as a
|
|
5
|
-
* component to place the keyed children) AND inspectable (`.props` — the array of
|
|
6
|
-
* per-child fill props the structure carried, one entry per mounted child). The
|
|
7
|
-
* structure host (`renderStructure`) populates `.props` from the fills. Plan 06.
|
|
8
|
-
*/
|
|
9
3
|
export interface SlotRenderer {
|
|
10
4
|
(props: unknown): Node
|
|
11
5
|
readonly props: ReadonlyArray<unknown>
|
package/src/compose/props.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { Context } from 'effect'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
3
|
+
export const PropsIdTypeId: unique symbol = Symbol.for('reform/PropsId')
|
|
4
|
+
export type PropsIdTypeId = typeof PropsIdTypeId
|
|
5
|
+
|
|
6
|
+
export interface PropsId {
|
|
7
|
+
readonly [PropsIdTypeId]: PropsIdTypeId
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type PropsTag<P> = Context.Tag<PropsId, P>
|
|
11
|
+
|
|
12
|
+
export const propsTag = <P>(): PropsTag<P> => Context.GenericTag<PropsId, P>('reform/Props')
|
|
13
|
+
|
|
14
|
+
// Unknown keeps the compatibility reader from leaking untyped props.
|
|
15
|
+
export const Props: PropsTag<unknown> = propsTag<unknown>()
|