@playfast/reform 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -56
- package/package.json +17 -17
- package/src/boundary/boundary.test.ts +63 -49
- package/src/boundary/boundary.ts +144 -113
- package/src/calc/asyncCalc.invalidate.test.ts +518 -32
- package/src/calc/asyncCalc.test.ts +207 -213
- package/src/calc/asyncCalc.ts +131 -154
- package/src/calc/asyncData.ts +0 -37
- package/src/calc/calc.test.ts +7 -33
- package/src/calc/calc.ts +44 -58
- package/src/calc/calcFamily.test.ts +80 -34
- package/src/calc/calcFamily.ts +54 -65
- package/src/calc/compose.test.ts +1 -12
- package/src/calc/compose.ts +1 -36
- package/src/calc/queryState.ts +0 -23
- package/src/channel/channel.ts +126 -111
- package/src/compose/composition.test.ts +19 -0
- package/src/compose/composition.ts +351 -127
- package/src/compose/host.ts +0 -6
- package/src/compose/props.ts +13 -12
- package/src/compose/provide.ts +189 -62
- package/src/compose/slot.ts +158 -49
- package/src/compose/structure.test.ts +6 -9
- package/src/compose/structure.ts +108 -79
- package/src/compose/ui.test.ts +19 -7
- package/src/compose/ui.ts +155 -156
- package/src/compose/ui.typecheck.ts +40 -18
- package/src/definition/definition.ts +22 -41
- package/src/event/event.fromSource.test.ts +172 -0
- package/src/event/event.test.ts +10 -3
- package/src/event/event.ts +102 -27
- package/src/event/eventGroup.ts +0 -1
- package/src/feature/feature.mount.test.ts +122 -43
- package/src/feature/feature.test.ts +46 -21
- package/src/feature/feature.ts +1347 -256
- package/src/feature/feature.typecheck.ts +273 -68
- package/src/graph/closure.ts +403 -0
- package/src/index.ts +68 -126
- package/src/internal/bucketCache.ts +39 -0
- package/src/internal/capture.ts +9 -24
- package/src/internal/env.ts +7 -0
- package/src/internal/errors.test.ts +0 -6
- package/src/internal/errors.ts +37 -60
- package/src/internal/inspect.test.ts +0 -6
- package/src/internal/inspect.ts +0 -12
- package/src/internal/queryDriver.ts +105 -201
- package/src/internal/queryDriverStore.ts +156 -0
- package/src/internal/queryDriverTypes.ts +59 -0
- package/src/internal/queryEvents.ts +0 -12
- package/src/internal/queryStore.ts +0 -14
- package/src/internal/reuse.test.ts +10 -14
- package/src/internal/reuse.ts +5 -30
- package/src/internal/scheduler.ts +4 -44
- package/src/internal/seeds.ts +0 -14
- package/src/internal/sources.ts +26 -49
- package/src/internal/stateRegistry.ts +0 -14
- package/src/internal/store.test.ts +1 -3
- package/src/internal/store.ts +0 -37
- package/src/internal/track.ts +3 -20
- package/src/internal/variance.ts +5 -0
- package/src/internal.ts +222 -0
- package/src/namespace/namespace.test.ts +46 -0
- package/src/namespace/namespace.ts +106 -0
- package/src/procedure/procedure.ts +40 -35
- package/src/reducer/reducer.ts +78 -52
- package/src/remote/remoteState.test.ts +590 -397
- package/src/remote/remoteState.ts +425 -347
- package/src/remote/remoteState.typecheck.ts +47 -56
- package/src/runtime/appRuntime.activation.test.ts +100 -0
- package/src/runtime/appRuntime.test.ts +249 -0
- package/src/runtime/appRuntime.ts +415 -97
- package/src/runtime/bus.ts +2 -15
- package/src/runtime/eventBudget.test.ts +152 -0
- package/src/runtime/eventBudget.ts +120 -0
- package/src/runtime/hardening.test.ts +73 -13
- package/src/runtime/instrumentation.test.ts +55 -52
- package/src/runtime/instrumentation.ts +6 -60
- package/src/runtime/loop.test.ts +36 -17
- package/src/runtime/loop.ts +87 -88
- package/src/runtime/queries.ts +0 -17
- package/src/scene/featureScene.test.ts +61 -0
- package/src/scene/scene.ts +247 -104
- package/src/scene/seedScene.test.ts +42 -79
- package/src/state/state.nominal.typecheck.ts +68 -0
- package/src/state/state.test.ts +17 -0
- package/src/state/state.ts +80 -46
- package/src/state/stateFamily.test.ts +16 -11
- package/src/state/stateFamily.ts +55 -65
- package/src/state/stateGroup.ts +53 -64
- package/src/state/token.ts +40 -23
- package/src/synced/syncedStore.ts +46 -58
- package/src/testkit/flight.testkit.ts +75 -0
- package/src/ui/node.ts +0 -6
- package/src/ui/trigger.ts +0 -5
- package/src/wire/tree.test.ts +8 -7
- package/src/wire/tree.ts +0 -39
- package/src/wire/triggers.test.ts +6 -6
- package/src/wire/triggers.ts +14 -32
- package/src/internal/ctx.ts +0 -16
package/src/compose/provide.ts
CHANGED
|
@@ -1,74 +1,201 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Context, Effect, Layer, Predicate } from 'effect'
|
|
2
2
|
import { InvalidProvideTarget } from '../internal/errors'
|
|
3
|
-
import type { CompositionClass } from './composition'
|
|
4
3
|
import {
|
|
4
|
+
type AnyFeatureBinding,
|
|
5
5
|
type EngineServices,
|
|
6
|
-
type
|
|
6
|
+
type FeatureBinding,
|
|
7
|
+
type FeatureRequirement,
|
|
7
8
|
isFeature,
|
|
8
|
-
|
|
9
|
-
type RequireCarrier,
|
|
9
|
+
tree,
|
|
10
10
|
} from '../feature/feature'
|
|
11
|
-
import
|
|
12
|
-
|
|
11
|
+
import type {
|
|
12
|
+
AnyComposition,
|
|
13
|
+
CompositionClass,
|
|
14
|
+
CompositionId,
|
|
15
|
+
RequiredSlotIdentities,
|
|
16
|
+
} from './composition'
|
|
17
|
+
import type {
|
|
18
|
+
AnyFeatureGraphSummary,
|
|
19
|
+
CompositionChildSummary,
|
|
20
|
+
ExactServiceExclude,
|
|
21
|
+
FeatureSummaryOpenServices,
|
|
22
|
+
FeatureSummarySupportOpenServices,
|
|
23
|
+
} from '../graph/closure'
|
|
24
|
+
import {
|
|
25
|
+
type AnyFeatureSlotTarget,
|
|
26
|
+
type AnySlot,
|
|
27
|
+
isSlot,
|
|
28
|
+
type SlotClass,
|
|
29
|
+
type SlotService,
|
|
30
|
+
type SlotTarget,
|
|
31
|
+
} from './slot'
|
|
32
|
+
import {
|
|
33
|
+
type CallableView,
|
|
34
|
+
type Contract,
|
|
35
|
+
type AnyUi,
|
|
36
|
+
isUi,
|
|
37
|
+
type UiName,
|
|
38
|
+
type UiContract,
|
|
39
|
+
type UiService,
|
|
40
|
+
type ViewImpl,
|
|
41
|
+
} from './ui'
|
|
42
|
+
|
|
43
|
+
type TargetOfSlotClass<Slot extends AnySlot> = InstanceType<Slot>['_slot']
|
|
44
|
+
type NameOfSlotClass<Slot extends AnySlot> = InstanceType<Slot>['_slotName']
|
|
45
|
+
type IdentityOfSlotClass<Slot extends AnySlot> = InstanceType<Slot>['_slotIdentity']
|
|
46
|
+
type FeatureTargetOfSlotClass<Slot extends AnySlot> =
|
|
47
|
+
TargetOfSlotClass<Slot> extends AnyFeatureSlotTarget ? TargetOfSlotClass<Slot> : never
|
|
48
|
+
|
|
49
|
+
type CompositionIdentityOf<Composition extends AnyComposition> =
|
|
50
|
+
InstanceType<Composition>['_compositionIdentity']
|
|
13
51
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
// Preserve exact residual inputs that Layer.provideMerge's union subtraction hides.
|
|
53
|
+
export const provideMergeExact =
|
|
54
|
+
<Provided, ProviderError, ProviderInput>(
|
|
55
|
+
provider: Layer.Layer<Provided, ProviderError, ProviderInput>,
|
|
56
|
+
) =>
|
|
57
|
+
<Output, Error, Input>(
|
|
58
|
+
consumer: Layer.Layer<Output, Error, Input>,
|
|
59
|
+
): Layer.Layer<
|
|
60
|
+
Provided | Output,
|
|
61
|
+
ProviderError | Error,
|
|
62
|
+
ProviderInput | Exclude<Input, Provided> | ExactServiceExclude<Input, Provided>
|
|
63
|
+
> =>
|
|
64
|
+
Layer.merge(
|
|
65
|
+
Layer.provideMerge(provider)(consumer),
|
|
66
|
+
Layer.effectDiscard(Effect.context<ExactServiceExclude<Input, Provided>>()),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const compositionSlotLayer = <
|
|
70
|
+
Target extends SlotTarget,
|
|
71
|
+
SlotName extends string,
|
|
72
|
+
SlotIdentity,
|
|
73
|
+
P,
|
|
74
|
+
C extends UiContract,
|
|
75
|
+
S extends ReadonlyArray<unknown>,
|
|
76
|
+
CompositionName extends string,
|
|
77
|
+
CompositionIdentity,
|
|
78
|
+
>(
|
|
79
|
+
slotDefinition: SlotClass<Target, SlotName, SlotIdentity>,
|
|
80
|
+
composition: CompositionClass<P, C, S, CompositionName, CompositionIdentity>,
|
|
81
|
+
): Layer.Layer<SlotService<SlotName, SlotIdentity, CompositionChildSummary<CompositionIdentity>>> =>
|
|
82
|
+
Layer.succeed(
|
|
83
|
+
Context.GenericTag<
|
|
84
|
+
SlotService<SlotName, SlotIdentity, CompositionChildSummary<CompositionIdentity>>,
|
|
85
|
+
AnyComposition
|
|
86
|
+
>(slotDefinition.tag.key),
|
|
87
|
+
composition,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
export const provideExisting = <
|
|
91
|
+
P,
|
|
92
|
+
C extends UiContract,
|
|
93
|
+
S extends ReadonlyArray<unknown>,
|
|
94
|
+
CompositionName extends string,
|
|
95
|
+
CompositionIdentity,
|
|
96
|
+
SlotName extends string,
|
|
97
|
+
SlotIdentity,
|
|
98
|
+
>(
|
|
99
|
+
hole: SlotClass<
|
|
100
|
+
CompositionClass<P, C, S, CompositionName, CompositionIdentity>,
|
|
101
|
+
SlotName,
|
|
102
|
+
SlotIdentity
|
|
103
|
+
>,
|
|
104
|
+
composition: NoInfer<CompositionClass<P, C, S, CompositionName, CompositionIdentity>>,
|
|
105
|
+
): Layer.Layer<
|
|
106
|
+
SlotService<SlotName, SlotIdentity, CompositionChildSummary<CompositionIdentity>>,
|
|
107
|
+
never,
|
|
108
|
+
CompositionId<CompositionName, CompositionIdentity, RequiredSlotIdentities<C>>
|
|
109
|
+
> =>
|
|
110
|
+
Layer.effect(
|
|
111
|
+
Context.GenericTag<
|
|
112
|
+
SlotService<SlotName, SlotIdentity, CompositionChildSummary<CompositionIdentity>>,
|
|
113
|
+
CompositionClass<P, C, S, CompositionName, CompositionIdentity>
|
|
114
|
+
>(hole.tag.key),
|
|
115
|
+
Effect.as(composition.tag, composition),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const featureSlotLayer = <
|
|
119
|
+
Target extends SlotTarget,
|
|
120
|
+
SlotName extends string,
|
|
121
|
+
SlotIdentity,
|
|
122
|
+
Summary extends AnyFeatureGraphSummary,
|
|
37
123
|
ROut,
|
|
38
|
-
|
|
124
|
+
DirectRequires extends ReadonlyArray<FeatureRequirement>,
|
|
39
125
|
>(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
126
|
+
slotDefinition: SlotClass<Target, SlotName, SlotIdentity>,
|
|
127
|
+
binding: FeatureBinding<Summary, ROut, DirectRequires>,
|
|
128
|
+
): Layer.Layer<SlotService<SlotName, SlotIdentity, Summary>> =>
|
|
129
|
+
Layer.succeed(
|
|
130
|
+
Context.GenericTag<SlotService<SlotName, SlotIdentity, Summary>, AnyFeatureBinding>(
|
|
131
|
+
slotDefinition.tag.key,
|
|
132
|
+
),
|
|
133
|
+
binding,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const isComposition = (candidate: unknown): candidate is AnyComposition =>
|
|
137
|
+
(typeof candidate === 'function' || typeof candidate === 'object') &&
|
|
138
|
+
candidate !== null &&
|
|
139
|
+
Predicate.hasProperty(candidate, 'manifest') &&
|
|
140
|
+
Predicate.isRecord(candidate.manifest) &&
|
|
141
|
+
candidate.manifest['kind'] === 'Composition' &&
|
|
142
|
+
Predicate.hasProperty(candidate, 'capture') &&
|
|
143
|
+
typeof candidate.capture === 'function'
|
|
144
|
+
|
|
145
|
+
const isCallableView = (candidate: unknown): candidate is CallableView =>
|
|
146
|
+
typeof candidate === 'function'
|
|
147
|
+
|
|
148
|
+
export function provide<U extends AnyUi>(
|
|
149
|
+
contract: U,
|
|
150
|
+
view: ViewImpl<Contract<U>>,
|
|
151
|
+
): Layer.Layer<UiService<UiName<U>>>
|
|
152
|
+
export function provide<Comp extends AnyComposition, N extends string, Identity>(
|
|
153
|
+
hole: SlotClass<Comp, N, Identity>,
|
|
154
|
+
composition: NoInfer<Comp>,
|
|
155
|
+
): Layer.Layer<SlotService<N, Identity, CompositionChildSummary<CompositionIdentityOf<Comp>>>>
|
|
156
|
+
export function provide<Slot extends AnySlot>(
|
|
157
|
+
hole: Slot,
|
|
158
|
+
feature: NoInfer<FeatureTargetOfSlotClass<Slot>>,
|
|
159
|
+
): Layer.Layer<
|
|
160
|
+
SlotService<
|
|
161
|
+
NameOfSlotClass<Slot>,
|
|
162
|
+
IdentityOfSlotClass<Slot>,
|
|
163
|
+
FeatureTargetOfSlotClass<Slot>['summary']
|
|
46
164
|
>,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (tag === undefined) {
|
|
60
|
-
return Layer.die(new InvalidProvideTarget())
|
|
165
|
+
never,
|
|
166
|
+
| EngineServices
|
|
167
|
+
| FeatureSummaryOpenServices<FeatureTargetOfSlotClass<Slot>['summary']>
|
|
168
|
+
| FeatureSummarySupportOpenServices<FeatureTargetOfSlotClass<Slot>['summary']>
|
|
169
|
+
>
|
|
170
|
+
export function provide(...args: readonly [target: unknown, implementation: unknown]): unknown {
|
|
171
|
+
const [target, implementation] = args
|
|
172
|
+
if (isUi(target)) {
|
|
173
|
+
if (!isCallableView(implementation)) {
|
|
174
|
+
return Layer.die(new InvalidProvideTarget())
|
|
175
|
+
}
|
|
176
|
+
return target.capture<unknown>((contract) => Layer.succeed(contract.provider, implementation))
|
|
61
177
|
}
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
// Lazy: bind the `FeatureBinding`; the host drives load → mount, painting the
|
|
65
|
-
// placeholders. Default: bind the composition (so the existing `Compose` path
|
|
66
|
-
// renders it, no host, no flash) and merge the eager `.live` layer, whose `RIn`
|
|
67
|
-
// surfaces to the scene. Either way the overload's return type carries the
|
|
68
|
-
// shared `RIn`, so the scene still type-demands `Core`.
|
|
69
|
-
return eager === undefined
|
|
70
|
-
? Layer.succeed(tag, impl.binding)
|
|
71
|
-
: Layer.merge(Layer.succeed(tag, impl.binding.composition), eager.layer)
|
|
178
|
+
if (!isSlot(target)) {
|
|
179
|
+
return Layer.die(new InvalidProvideTarget())
|
|
72
180
|
}
|
|
73
|
-
return
|
|
181
|
+
return target.capture<unknown>((slotDefinition) => {
|
|
182
|
+
if (isFeature(implementation)) {
|
|
183
|
+
const native = tree<typeof implementation>(implementation.token)
|
|
184
|
+
return native.binding.capture<unknown>((featureBinding) => {
|
|
185
|
+
// Lazy features merge only eager support; default features merge their live module.
|
|
186
|
+
const eager = native.eagerModule ?? native.supportModule
|
|
187
|
+
const binding = featureSlotLayer(slotDefinition, featureBinding)
|
|
188
|
+
if (eager === undefined) {
|
|
189
|
+
return binding
|
|
190
|
+
}
|
|
191
|
+
return eager.capture<unknown>((module) => Layer.merge(binding, module.layer))
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
if (!isComposition(implementation)) {
|
|
195
|
+
return Layer.die(new InvalidProvideTarget())
|
|
196
|
+
}
|
|
197
|
+
return implementation.capture<unknown>((composition) =>
|
|
198
|
+
compositionSlotLayer(slotDefinition, composition),
|
|
199
|
+
)
|
|
200
|
+
})
|
|
74
201
|
}
|
package/src/compose/slot.ts
CHANGED
|
@@ -1,68 +1,177 @@
|
|
|
1
|
-
import { Context } from 'effect'
|
|
2
|
-
import { definitionClass } from '../definition/definition'
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
1
|
+
import { Context, GlobalValue, MutableRef } from 'effect'
|
|
2
|
+
import { type Manifest, definitionClass } from '../definition/definition'
|
|
3
|
+
import type { AnyFeatureBinding, ProvideableFeature } from '../feature/feature'
|
|
4
|
+
import type {
|
|
5
|
+
AnyFeatureGraphSummary,
|
|
6
|
+
FeatureSummaryCompositionIdentity,
|
|
7
|
+
FeatureSummaryCompositionProps,
|
|
8
|
+
FeatureSummaryContract,
|
|
9
|
+
FeatureSummaryMountProps,
|
|
10
|
+
FeatureSummaryName,
|
|
11
|
+
FeatureSummaryStates,
|
|
12
|
+
FeatureSummaryIdentity,
|
|
13
|
+
SlotBindingSummary,
|
|
14
|
+
} from '../graph/closure'
|
|
5
15
|
import type { Node } from '../ui/node'
|
|
16
|
+
import type { AnyComposition, CompositionClass } from './composition'
|
|
6
17
|
|
|
7
18
|
export type { Node }
|
|
8
19
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export type
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
export type { AnyComposition } from './composition'
|
|
21
|
+
|
|
22
|
+
type AnyProvideableFeature = ProvideableFeature<unknown, AnyFeatureGraphSummary>
|
|
23
|
+
|
|
24
|
+
export type FeatureSlotTarget<Feature extends AnyProvideableFeature> = ProvideableFeature<
|
|
25
|
+
FeatureSummaryIdentity<Feature['summary']>,
|
|
26
|
+
Feature['summary']
|
|
27
|
+
>
|
|
28
|
+
|
|
29
|
+
export type AnyFeatureSlotTarget = FeatureSlotTarget<AnyProvideableFeature>
|
|
30
|
+
|
|
31
|
+
export type SlotTarget = AnyComposition | AnyFeatureSlotTarget
|
|
32
|
+
|
|
33
|
+
export type CompositionOfSlotTarget<Target extends SlotTarget> = Target extends AnyComposition
|
|
34
|
+
? Target
|
|
35
|
+
: Target extends AnyFeatureSlotTarget
|
|
36
|
+
? CompositionClass<
|
|
37
|
+
FeatureSummaryCompositionProps<Target['summary']>,
|
|
38
|
+
FeatureSummaryContract<Target['summary']>,
|
|
39
|
+
FeatureSummaryStates<Target['summary']>,
|
|
40
|
+
FeatureSummaryName<Target['summary']>,
|
|
41
|
+
FeatureSummaryCompositionIdentity<Target['summary']>
|
|
42
|
+
>
|
|
43
|
+
: never
|
|
44
|
+
|
|
45
|
+
export type PropsOfSlotTarget<Target extends SlotTarget> = Target extends AnyComposition
|
|
46
|
+
? Target['Props']
|
|
47
|
+
: Target extends AnyFeatureSlotTarget
|
|
48
|
+
? FeatureSummaryMountProps<Target['summary']>
|
|
49
|
+
: never
|
|
50
|
+
|
|
51
|
+
export type ContractOfSlotTarget<Target extends SlotTarget> = Target extends AnyComposition
|
|
52
|
+
? Target['Contract']
|
|
53
|
+
: Target extends AnyFeatureSlotTarget
|
|
54
|
+
? FeatureSummaryContract<Target['summary']>
|
|
55
|
+
: never
|
|
56
|
+
|
|
57
|
+
export type SlotChild<Target extends SlotTarget> = Target extends AnyComposition
|
|
58
|
+
? Target | (AnyFeatureBinding & { readonly composition: Target })
|
|
59
|
+
: Target extends AnyFeatureSlotTarget
|
|
60
|
+
? AnyFeatureBinding & {
|
|
61
|
+
readonly summary: Target['summary']
|
|
62
|
+
}
|
|
63
|
+
: never
|
|
64
|
+
|
|
65
|
+
export type AnySlotChild = AnyComposition | AnyFeatureBinding
|
|
66
|
+
|
|
67
|
+
export interface AnySlotInstance {
|
|
68
|
+
readonly _slot: SlotTarget
|
|
69
|
+
readonly _slotName: string
|
|
70
|
+
readonly _slotIdentity: unknown
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface SlotInstance<
|
|
74
|
+
Target extends SlotTarget,
|
|
75
|
+
N extends string,
|
|
76
|
+
Identity,
|
|
77
|
+
> extends AnySlotInstance {
|
|
78
|
+
readonly _slot: Target
|
|
79
|
+
readonly _slotName: N
|
|
80
|
+
readonly _slotIdentity: Identity
|
|
29
81
|
}
|
|
30
82
|
|
|
31
|
-
/**
|
|
32
|
-
* A DI hole for a child composition. The parent declares
|
|
33
|
-
* `slot('Item')<typeof TodoItem>()` and renders through it; the concrete child
|
|
34
|
-
* is wired with `provide(Item, TodoItem)`, so the parent never imports the
|
|
35
|
-
* child's *implementation* (the `typeof` is a type-only reference to the
|
|
36
|
-
* definition). `Comp` lives only in the phantom instance (so `SlotClass` is
|
|
37
|
-
* covariant and a `{ Item: ItemSlot }` map is well-typed); the match is enforced
|
|
38
|
-
* by `provide`'s signature, not the (invariant) tag.
|
|
39
|
-
*/
|
|
40
|
-
/** Nominal brand identifying a slot at runtime (Effect's `TypeId` idiom). */
|
|
41
83
|
export const SlotTypeId: unique symbol = Symbol.for('reform/Slot')
|
|
42
84
|
export type SlotTypeId = typeof SlotTypeId
|
|
43
85
|
|
|
44
|
-
export
|
|
45
|
-
|
|
46
|
-
|
|
86
|
+
export const SlotServiceTypeId: unique symbol = Symbol.for('reform/SlotService')
|
|
87
|
+
export type SlotServiceTypeId = typeof SlotServiceTypeId
|
|
88
|
+
|
|
89
|
+
export interface SlotService<
|
|
90
|
+
N extends string,
|
|
91
|
+
Identity = unknown,
|
|
92
|
+
out Binding extends SlotBindingSummary = SlotBindingSummary,
|
|
93
|
+
> {
|
|
94
|
+
readonly [SlotServiceTypeId]: {
|
|
95
|
+
readonly name: N
|
|
96
|
+
readonly identity: Identity
|
|
97
|
+
readonly binding: Binding
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface SlotManifest<N extends string = string> extends Manifest {
|
|
47
102
|
readonly kind: 'Slot'
|
|
48
|
-
|
|
49
|
-
|
|
103
|
+
readonly name: N
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type SlotCapture<Result> = <Target extends SlotTarget, N extends string, Identity>(
|
|
107
|
+
slotDefinition: SlotClass<Target, N, Identity>,
|
|
108
|
+
) => Result
|
|
109
|
+
|
|
110
|
+
export interface AnySlot {
|
|
111
|
+
new (): AnySlotInstance
|
|
112
|
+
readonly [SlotTypeId]: SlotTypeId
|
|
113
|
+
readonly manifest: SlotManifest
|
|
114
|
+
readonly identity: symbol
|
|
115
|
+
readonly capture: <Result>(visit: SlotCapture<Result>) => Result
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface SlotClass<Target extends SlotTarget, N extends string, Identity> {
|
|
119
|
+
new (): SlotInstance<Target, N, Identity>
|
|
120
|
+
readonly [SlotTypeId]: SlotTypeId
|
|
121
|
+
readonly manifest: SlotManifest<N>
|
|
122
|
+
readonly identity: symbol
|
|
123
|
+
readonly capture: <Result>(visit: SlotCapture<Result>) => Result
|
|
124
|
+
readonly tag: Context.Tag<SlotService<N, Identity>, SlotChild<Target>>
|
|
125
|
+
// Same runtime key, widened only after provide's typed overloads validate the child.
|
|
126
|
+
readonly provider: Context.Tag<SlotService<N, Identity>, AnySlotChild>
|
|
50
127
|
}
|
|
51
128
|
|
|
52
|
-
|
|
53
|
-
export const isSlot = (candidate: unknown): candidate is SlotClass =>
|
|
129
|
+
export const isSlot = (candidate: unknown): candidate is AnySlot =>
|
|
54
130
|
(typeof candidate === 'function' || typeof candidate === 'object') &&
|
|
55
131
|
candidate !== null &&
|
|
56
132
|
SlotTypeId in candidate
|
|
57
133
|
|
|
58
|
-
|
|
59
|
-
|
|
134
|
+
export type TargetOfSlot<S> =
|
|
135
|
+
S extends SlotInstance<infer Target, infer _N, infer _Identity> ? Target : never
|
|
136
|
+
|
|
137
|
+
export type SlotProps<S> =
|
|
138
|
+
S extends SlotInstance<infer Target, infer _N, infer _Identity>
|
|
139
|
+
? PropsOfSlotTarget<Target>
|
|
140
|
+
: never
|
|
141
|
+
|
|
142
|
+
export type SlotComposition<S> =
|
|
143
|
+
S extends SlotInstance<infer Target, infer _N, infer _Identity>
|
|
144
|
+
? CompositionOfSlotTarget<Target>
|
|
145
|
+
: never
|
|
146
|
+
|
|
147
|
+
export type SlotContract<S> =
|
|
148
|
+
S extends SlotInstance<infer Target, infer _N, infer _Identity>
|
|
149
|
+
? ContractOfSlotTarget<Target>
|
|
150
|
+
: never
|
|
151
|
+
|
|
152
|
+
const slotDefinitionSequence: MutableRef.MutableRef<number> = GlobalValue.globalValue(
|
|
153
|
+
Symbol.for('reform/SlotDefinitionSequence'),
|
|
154
|
+
() => MutableRef.make(0),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
const nextSlotTagKey = (name: string): string =>
|
|
158
|
+
`reform/slot/${name}/${MutableRef.incrementAndGet(slotDefinitionSequence)}`
|
|
60
159
|
|
|
61
160
|
export const slot =
|
|
62
|
-
(name:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const tag = Context.GenericTag<
|
|
67
|
-
|
|
161
|
+
<const N extends string>(name: N) =>
|
|
162
|
+
<Self, Target extends SlotTarget = AnyComposition>(): SlotClass<Target, N, Self> => {
|
|
163
|
+
const key = nextSlotTagKey(name)
|
|
164
|
+
const identity = Symbol(name)
|
|
165
|
+
const tag = Context.GenericTag<SlotService<N, Self>, SlotChild<Target>>(key)
|
|
166
|
+
const provider = Context.GenericTag<SlotService<N, Self>, AnySlotChild>(key)
|
|
167
|
+
const manifest: SlotManifest<N> = { kind: 'Slot', name }
|
|
168
|
+
const slotDefinition: SlotClass<Target, N, Self> = definitionClass<SlotClass<Target, N, Self>>({
|
|
169
|
+
[SlotTypeId]: SlotTypeId,
|
|
170
|
+
manifest,
|
|
171
|
+
identity,
|
|
172
|
+
tag,
|
|
173
|
+
provider,
|
|
174
|
+
capture: <Result>(visit: SlotCapture<Result>): Result => visit(slotDefinition),
|
|
175
|
+
})
|
|
176
|
+
return slotDefinition
|
|
68
177
|
}
|
|
@@ -7,17 +7,13 @@ import { absent, each, isStructure, mount, one, structureEquals, when } from './
|
|
|
7
7
|
|
|
8
8
|
type List = Contract<typeof ListUi>
|
|
9
9
|
|
|
10
|
-
// A minimal parent/child contract pair: a list parent with one keyed slot, so the
|
|
11
|
-
// type-level `mount` checks have a real contract to bind against. Row carries a
|
|
12
|
-
// props schema so its external props (`Comp['Props']`) are `{ label: string }`,
|
|
13
|
-
// which is what each fill is checked against.
|
|
14
10
|
class RowUi extends ui('Row')<{ props: { label: string } }>() {}
|
|
15
11
|
class Row extends Composition.make('Row', {
|
|
16
12
|
title: 'Row',
|
|
17
13
|
props: S.Struct({ label: S.String }),
|
|
18
14
|
ui: RowUi,
|
|
19
|
-
}) {}
|
|
20
|
-
class RowSlot extends slot('Row')<typeof Row>() {}
|
|
15
|
+
})<Row>() {}
|
|
16
|
+
class RowSlot extends slot('Row')<RowSlot, typeof Row>() {}
|
|
21
17
|
|
|
22
18
|
class ListUi extends ui('List')<{
|
|
23
19
|
props: { total: number }
|
|
@@ -26,7 +22,10 @@ class ListUi extends ui('List')<{
|
|
|
26
22
|
|
|
27
23
|
describe('structure combinators', () => {
|
|
28
24
|
test('each produces one keyed entry per item, props mapped', () => {
|
|
29
|
-
const fill = each([{ id: 'a' }, { id: 'b' }], {
|
|
25
|
+
const fill = each([{ id: 'a' }, { id: 'b' }], {
|
|
26
|
+
key: (t) => t.id,
|
|
27
|
+
props: (t) => ({ label: t.id }),
|
|
28
|
+
})
|
|
30
29
|
expect(fill).toEqual({
|
|
31
30
|
_tag: 'Each',
|
|
32
31
|
items: [
|
|
@@ -65,8 +64,6 @@ describe('structure combinators', () => {
|
|
|
65
64
|
})
|
|
66
65
|
})
|
|
67
66
|
|
|
68
|
-
// Type-level contract enforcement — never called; exists only so the negative
|
|
69
|
-
// cases are typechecked. Each `@ts-expect-error` must sit on the line that errors.
|
|
70
67
|
export const _typeChecks = (): void => {
|
|
71
68
|
mount<List>({
|
|
72
69
|
props: { total: 1 },
|