@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.
Files changed (99) hide show
  1. package/README.md +103 -56
  2. package/package.json +17 -17
  3. package/src/boundary/boundary.test.ts +63 -49
  4. package/src/boundary/boundary.ts +144 -113
  5. package/src/calc/asyncCalc.invalidate.test.ts +518 -32
  6. package/src/calc/asyncCalc.test.ts +207 -213
  7. package/src/calc/asyncCalc.ts +131 -154
  8. package/src/calc/asyncData.ts +0 -37
  9. package/src/calc/calc.test.ts +7 -33
  10. package/src/calc/calc.ts +44 -58
  11. package/src/calc/calcFamily.test.ts +80 -34
  12. package/src/calc/calcFamily.ts +54 -65
  13. package/src/calc/compose.test.ts +1 -12
  14. package/src/calc/compose.ts +1 -36
  15. package/src/calc/queryState.ts +0 -23
  16. package/src/channel/channel.ts +126 -111
  17. package/src/compose/composition.test.ts +19 -0
  18. package/src/compose/composition.ts +351 -127
  19. package/src/compose/host.ts +0 -6
  20. package/src/compose/props.ts +13 -12
  21. package/src/compose/provide.ts +189 -62
  22. package/src/compose/slot.ts +158 -49
  23. package/src/compose/structure.test.ts +6 -9
  24. package/src/compose/structure.ts +108 -79
  25. package/src/compose/ui.test.ts +19 -7
  26. package/src/compose/ui.ts +155 -156
  27. package/src/compose/ui.typecheck.ts +40 -18
  28. package/src/definition/definition.ts +22 -41
  29. package/src/event/event.fromSource.test.ts +172 -0
  30. package/src/event/event.test.ts +10 -3
  31. package/src/event/event.ts +102 -27
  32. package/src/event/eventGroup.ts +0 -1
  33. package/src/feature/feature.mount.test.ts +122 -43
  34. package/src/feature/feature.test.ts +46 -21
  35. package/src/feature/feature.ts +1347 -256
  36. package/src/feature/feature.typecheck.ts +273 -68
  37. package/src/graph/closure.ts +403 -0
  38. package/src/index.ts +68 -126
  39. package/src/internal/bucketCache.ts +39 -0
  40. package/src/internal/capture.ts +9 -24
  41. package/src/internal/env.ts +7 -0
  42. package/src/internal/errors.test.ts +0 -6
  43. package/src/internal/errors.ts +37 -60
  44. package/src/internal/inspect.test.ts +0 -6
  45. package/src/internal/inspect.ts +0 -12
  46. package/src/internal/queryDriver.ts +105 -201
  47. package/src/internal/queryDriverStore.ts +156 -0
  48. package/src/internal/queryDriverTypes.ts +59 -0
  49. package/src/internal/queryEvents.ts +0 -12
  50. package/src/internal/queryStore.ts +0 -14
  51. package/src/internal/reuse.test.ts +10 -14
  52. package/src/internal/reuse.ts +5 -30
  53. package/src/internal/scheduler.ts +4 -44
  54. package/src/internal/seeds.ts +0 -14
  55. package/src/internal/sources.ts +26 -49
  56. package/src/internal/stateRegistry.ts +0 -14
  57. package/src/internal/store.test.ts +1 -3
  58. package/src/internal/store.ts +0 -37
  59. package/src/internal/track.ts +3 -20
  60. package/src/internal/variance.ts +5 -0
  61. package/src/internal.ts +222 -0
  62. package/src/namespace/namespace.test.ts +46 -0
  63. package/src/namespace/namespace.ts +106 -0
  64. package/src/procedure/procedure.ts +40 -35
  65. package/src/reducer/reducer.ts +78 -52
  66. package/src/remote/remoteState.test.ts +590 -397
  67. package/src/remote/remoteState.ts +425 -347
  68. package/src/remote/remoteState.typecheck.ts +47 -56
  69. package/src/runtime/appRuntime.activation.test.ts +100 -0
  70. package/src/runtime/appRuntime.test.ts +249 -0
  71. package/src/runtime/appRuntime.ts +415 -97
  72. package/src/runtime/bus.ts +2 -15
  73. package/src/runtime/eventBudget.test.ts +152 -0
  74. package/src/runtime/eventBudget.ts +120 -0
  75. package/src/runtime/hardening.test.ts +73 -13
  76. package/src/runtime/instrumentation.test.ts +55 -52
  77. package/src/runtime/instrumentation.ts +6 -60
  78. package/src/runtime/loop.test.ts +36 -17
  79. package/src/runtime/loop.ts +87 -88
  80. package/src/runtime/queries.ts +0 -17
  81. package/src/scene/featureScene.test.ts +61 -0
  82. package/src/scene/scene.ts +247 -104
  83. package/src/scene/seedScene.test.ts +42 -79
  84. package/src/state/state.nominal.typecheck.ts +68 -0
  85. package/src/state/state.test.ts +17 -0
  86. package/src/state/state.ts +80 -46
  87. package/src/state/stateFamily.test.ts +16 -11
  88. package/src/state/stateFamily.ts +55 -65
  89. package/src/state/stateGroup.ts +53 -64
  90. package/src/state/token.ts +40 -23
  91. package/src/synced/syncedStore.ts +46 -58
  92. package/src/testkit/flight.testkit.ts +75 -0
  93. package/src/ui/node.ts +0 -6
  94. package/src/ui/trigger.ts +0 -5
  95. package/src/wire/tree.test.ts +8 -7
  96. package/src/wire/tree.ts +0 -39
  97. package/src/wire/triggers.test.ts +6 -6
  98. package/src/wire/triggers.ts +14 -32
  99. package/src/internal/ctx.ts +0 -16
package/src/compose/ui.ts CHANGED
@@ -1,223 +1,222 @@
1
1
  import type { FunctionComponent } from 'react'
2
2
  import { Context, type Schema } from 'effect'
3
+ import type { AST } from 'effect/SchemaAST'
3
4
  import { type Manifest, definitionClass } from '../definition/definition'
4
5
  import type { Trigger } from '../ui/trigger'
5
- import { type Node, type SlotClass, type SlotInstance, type SlotProps } from './slot'
6
+ import { type AnySlot, type AnySlotInstance, type Node, type SlotProps } from './slot'
6
7
 
7
- export interface UiContract {
8
+ // Field presence drives conditional slot/event inference; Option would erase it.
9
+ interface UiContractOptionalFields {
10
+ slots: Record<string, AnySlotInstance>
11
+ events: Record<string, Trigger<never>>
12
+ }
13
+
14
+ export interface UiContract extends Partial<UiContractOptionalFields> {
8
15
  props: unknown
9
- // `slots`/`events` are optional so a local contract literal (`ui('X')<{ props }>()`)
10
- // stays terse AND so `C extends { slots: infer S }` / `{ events: infer E }`
11
- // discriminates their presence (SlotsOf/EventsOf). Option<T> would break both the
12
- // ergonomic authoring form and that conditional-type inference.
13
- // oxlint-disable-next-line reform-rules/no-optional-fields -- presence drives `extends {...}` contract inference (see note above)
14
- slots?: Record<string, SlotInstance<any>>
15
- // oxlint-disable-next-line reform-rules/no-optional-fields -- presence drives `extends {...}` contract inference (see note above)
16
- events?: Record<string, Trigger<any>>
17
16
  }
18
17
 
19
18
  type PropsOf<C extends UiContract> = C['props']
20
- /** The contract's event map (`{ [name]: Trigger<P> }`), or empty when it declares
21
- * none. Exported so a `Structure<C>` can type its optional `events` field to the
22
- * same map a composition body acquires. */
23
- export type EventsOf<C extends UiContract> = C extends { events: infer E } ? E : Record<never, never>
24
- // A slot is a React COMPONENT whose props are OPTIONAL (a `Partial`): a parent that
25
- // drives a child supplies them (`<slots.Item id=… />` / `createElement(slots.Item, {…})`),
26
- // but a placeholder render (the remote client, where the per-child props were captured
27
- // server-side and the framework expands the children) omits them (`<slots.Item />`).
28
- // One `Ui.make` view thus serves both renderers. The child still reads its own
29
- // props/state — these are only the external props the parent feeds, advisory at the
30
- // slot boundary. Typing it as `FunctionComponent` makes both JSX and `createElement` work.
31
- //
32
- // `slotKey` is the universal KEYED-SLOT selector (see `WireNode.key`): for a LIST slot
33
- // invoked once per item (`<slots.Row slotKey={id} />`), the remote client renders only the
34
- // one wire child whose React `key` matches, instead of every child of that slot at every
35
- // call site (which duplicates the whole list under each call). It is framework-handled, not
36
- // forwarded to the child, so it is allowed on EVERY slot and never collides with a child's
37
- // own props. Omit it for singleton slots (`<slots.Create />`) — they render all children.
19
+ export type EventsOf<C extends UiContract> = C extends { events: infer E }
20
+ ? E
21
+ : Readonly<Record<PropertyKey, never>>
22
+
23
+ // slotKey is framework-only selection; Partial also permits prop-less remote placeholders.
38
24
  type SlotsOf<C extends UiContract> = C extends { slots: infer S }
39
25
  ? {
40
- // `slotKey` goes INSIDE the `Partial<…>` (not intersected outside) so that when a slot's
41
- // child props are `any` the whole thing still collapses to `any` (`any & X = any`), keeping
42
- // the component assignable to a bare `(props: unknown) => Node` slot-renderer shape. An
43
- // outer intersection would leave a required-shape object that `unknown` can't satisfy.
44
- //
45
- // A slot handle is BOTH renderable (the `FunctionComponent` — places the keyed children)
46
- // AND inspectable (`.props` — the array of per-child fill props the structure carried, one
47
- // entry per mounted child, typed by the child contract). A presentation can read the fills
48
- // (`slots.Item.props.length`, a header, layout decisions) and still render them
49
- // (`<slots.Item/>`); it never DERIVES multiplicity — the engine already fixed it. Read-only.
50
26
  readonly [K in keyof S]: FunctionComponent<
51
- // oxlint-disable-next-line reform-rules/no-optional-fields -- React/JSX component-prop shape (`<slots.Row slotKey={id} />`); singleton slots omit it, framework-handled
52
- Partial<SlotProps<S[K]> & { readonly slotKey?: string }>
27
+ Partial<SlotProps<S[K]> & { readonly slotKey: string }>
53
28
  > & { readonly props: ReadonlyArray<SlotProps<S[K]>> }
54
29
  }
55
- : Record<never, never>
30
+ : Readonly<Record<PropertyKey, never>>
56
31
 
57
- /** What `.make` authors: the pure presentation. Slots are injected by the renderer. */
58
32
  export type ViewImpl<C extends UiContract> = (
59
33
  props: PropsOf<C>,
60
34
  slots: SlotsOf<C>,
61
35
  events: EventsOf<C>,
62
36
  ) => Node
63
37
 
64
- /** Nominal brand identifying a UI contract at runtime (Effect's `TypeId` idiom). */
65
38
  export const UiTypeId: unique symbol = Symbol.for('reform/Ui')
66
39
  export type UiTypeId = typeof UiTypeId
67
40
 
68
- /**
69
- * The reflectable descriptor a contract carries. The wire `props` / `events`
70
- * schemas are present *only* on the schema (wired) form — the type-only form
71
- * carries neither, so a local contract has no schema cost and no schema surface.
72
- */
73
- export interface UiManifest extends Manifest {
41
+ export const UiServiceTypeId: unique symbol = Symbol.for('reform/UiService')
42
+ export type UiServiceTypeId = typeof UiServiceTypeId
43
+
44
+ export interface UiService<N extends string> {
45
+ readonly [UiServiceTypeId]: N
46
+ }
47
+
48
+ export type CallableView = (...args: ReadonlyArray<never>) => unknown
49
+
50
+ export interface UiSchemaReflection {
51
+ readonly ast: AST
52
+ }
53
+
54
+ interface UiManifestOptionalFields {
55
+ readonly props: UiSchemaReflection
56
+ readonly events: Readonly<Record<string, UiSchemaReflection>>
57
+ }
58
+
59
+ export interface UiManifest extends Manifest, Partial<UiManifestOptionalFields> {
74
60
  readonly kind: 'Ui'
75
- // `any` in the encoded/context slots is required by Schema's variance for
76
- // branded/Class schemas; only the decoded type is ever read off these.
77
- // Present only on the wired form, so `extends { props: … }` discriminates the two
78
- // authoring forms — Option<T> would erase that wire/type-only distinction.
79
- // oxlint-disable-next-line reform-rules/no-optional-fields -- wire manifest field present only on the wired form (see note above)
80
- readonly props?: Schema.Schema<any, any>
81
- // oxlint-disable-next-line reform-rules/no-optional-fields -- wire manifest field present only on the wired form (see note above)
82
- readonly events?: Readonly<Record<string, Schema.Schema<any, any>>>
83
61
  }
84
62
 
85
- /** A `UiManifest` whose wire schema is present — the runtime brand of a remote-capable contract. */
86
63
  export interface WiredUiManifest extends UiManifest {
87
- readonly props: Schema.Schema<any, any>
64
+ readonly props: UiSchemaReflection
88
65
  }
89
66
 
90
- // A branded, extendable class carrying the wire `manifest` + the DI `impl` tag. Not
91
- // yieldable: bodies return a `Structure`, and the host reads `impl` to resolve the view.
92
- export interface UiClass<C extends UiContract> {
67
+ export type UiCapture<Result> = <C extends UiContract, N extends string>(
68
+ contract: UiClass<C, N>,
69
+ ) => Result
70
+
71
+ export interface UiReflection<
72
+ out C extends UiContract = UiContract,
73
+ out N extends string = string,
74
+ > {
75
+ new (): {}
76
+ readonly [UiTypeId]: UiTypeId
77
+ readonly manifest: UiManifest & { readonly name: N }
78
+ readonly Contract: C
79
+ }
80
+
81
+ export interface AnyUi {
93
82
  new (): {}
94
83
  readonly [UiTypeId]: UiTypeId
95
84
  readonly manifest: UiManifest
96
- /** Internal DI tag the `Ui.make` presentation is provided under. */
97
- readonly impl: Context.Tag<ViewImpl<C>, ViewImpl<C>>
98
- }
99
-
100
- /**
101
- * Recover a contract's `UiContract` type from its `Ui` class — `Ui.Contract<typeof
102
- * SomeUi>`. Lets a consumer (a proof, a reflection tool) name the contract derived
103
- * from a wired `ui('X', {…})` without re-declaring it.
104
- */
105
- export type Contract<U> = U extends UiClass<infer C> ? C : never
106
-
107
- /**
108
- * A contract authored with wire schemas. Structurally a `UiClass<C>` whose
109
- * manifest *carries* the schemas (`WiredUiManifest`), so it is the subtype the
110
- * remote seam (`provideRemote`, the wire registry) demands while plain `provide`
111
- * / `scene` / `Composition.make` keep accepting any `UiClass<C>`.
112
- */
113
- export type WiredUi<C extends UiContract> = UiClass<C> & { readonly manifest: WiredUiManifest }
114
-
115
- /** The wire schemas a contract is authored from: props, a payload schema per event, and
116
- * (type-only) the slots the view fills. Slots carry no wire schema — the children that
117
- * fill them are their own contracts — but declaring them here threads typed slots into
118
- * the derived contract, so a WIRED contract can also have slots (e.g. a parent whose
119
- * props the client reads AND that renders slot children). */
120
- export interface WireSchemas {
121
- readonly props: Schema.Schema<any, any>
122
- // `events`/`slots` optional so `Sch extends { events: … }` / `{ slots: … }`
123
- // derives the contract's facade only when declared (DerivedEvents/DerivedSlots).
124
- // oxlint-disable-next-line reform-rules/no-optional-fields -- presence drives `extends {...}` derivation (DerivedEvents)
125
- readonly events?: Readonly<Record<string, Schema.Schema<any, any>>>
126
- // Slots are declared with their slot CLASSES (`slot('X')<typeof Child>()`), the
127
- // same values `Composition.make({ slots })` takes — NOT `SlotInstance`s. The
128
- // contract's slot facade is derived from them below.
129
- // oxlint-disable-next-line reform-rules/no-optional-fields -- presence drives `extends {...}` derivation (DerivedSlots)
130
- readonly slots?: Readonly<Record<string, SlotClass>>
85
+ readonly capture: <Result>(visit: UiCapture<Result>) => Result
86
+ }
87
+
88
+ export interface UiClass<C extends UiContract, N extends string> extends UiReflection<C, N> {
89
+ readonly capture: <Result>(visit: UiCapture<Result>) => Result
90
+ readonly impl: Context.Tag<UiService<N>, ViewImpl<C>>
91
+ readonly provider: Context.Tag<UiService<N>, CallableView>
92
+ }
93
+
94
+ export type Contract<U> = U extends UiReflection<infer C, infer _N> ? C : never
95
+ export type UiName<U> = U extends UiReflection<infer _C, infer N> ? N : never
96
+
97
+ export type WiredUi<C extends UiContract, N extends string> = UiClass<C, N> & {
98
+ readonly manifest: WiredUiManifest
99
+ }
100
+
101
+ interface WireSchemasOptionalFields {
102
+ readonly events: Readonly<Record<string, Schema.Schema.AnyNoContext>>
103
+ readonly slots: Readonly<Record<string, AnySlot>>
104
+ }
105
+
106
+ export interface WireSchemas extends Partial<WireSchemasOptionalFields> {
107
+ readonly props: Schema.Schema.AnyNoContext
131
108
  }
132
109
 
133
110
  type DerivedEvents<Sch extends WireSchemas> = Sch extends {
134
- readonly events: infer E extends Record<string, Schema.Schema<any, any>>
111
+ readonly events: infer E extends Record<string, Schema.Schema.AnyNoContext>
135
112
  }
136
113
  ? { readonly [K in keyof E]: Trigger<Schema.Schema.Type<E[K]>> }
137
- : Record<never, never>
114
+ : Readonly<Record<PropertyKey, never>>
138
115
 
139
116
  type DerivedSlots<Sch extends WireSchemas> = Sch extends {
140
- readonly slots: infer S extends Record<string, SlotClass>
117
+ readonly slots: infer S extends Record<string, AnySlot>
141
118
  }
142
119
  ? { readonly [K in keyof S]: InstanceType<S[K]> }
143
- : Record<never, never>
144
-
145
- /**
146
- * The `UiContract` a set of wire schemas denotes — props and event-payload
147
- * *types* derived from the schemas via `Schema.Type`, plus any declared slots. The
148
- * schema is the single source of truth; the contract type is inferred, never written
149
- * a second time.
150
- */
120
+ : Readonly<Record<PropertyKey, never>>
121
+
151
122
  export type DerivedContract<Sch extends WireSchemas> = {
152
123
  readonly props: Schema.Schema.Type<Sch['props']>
153
124
  readonly slots: DerivedSlots<Sch>
154
125
  readonly events: DerivedEvents<Sch>
155
126
  }
156
127
 
157
- /** Whether a value is a UI contract discriminates a `provide` target by brand. */
158
- export const isUi = (candidate: unknown): candidate is UiClass<UiContract> =>
128
+ export const isUi = (candidate: unknown): candidate is AnyUi =>
159
129
  (typeof candidate === 'function' || typeof candidate === 'object') &&
160
130
  candidate !== null &&
161
131
  UiTypeId in candidate
162
132
 
163
- const buildUi = <C extends UiContract, M extends UiManifest>(
164
- name: string,
133
+ type UiDefinitionStatics<
134
+ C extends UiContract,
135
+ N extends string,
136
+ M extends UiManifest & { readonly name: N },
137
+ > = Pick<
138
+ UiClass<C, N> & { readonly manifest: M },
139
+ typeof UiTypeId | 'manifest' | 'impl' | 'provider' | 'capture'
140
+ >
141
+
142
+ const buildUi = <
143
+ N extends string,
144
+ C extends UiContract,
145
+ M extends UiManifest & { readonly name: N },
146
+ >(
147
+ name: N,
165
148
  manifest: M,
166
- ): UiClass<C> & { readonly manifest: M } => {
167
- const impl = Context.GenericTag<ViewImpl<C>, ViewImpl<C>>(`reform/ui/${name}`)
168
- // Extendable definition class (`class Counter extends ui('Counter', …) {}`); resolved
169
- // by name on a host via `impl`, never yielded.
170
- return definitionClass<UiClass<C> & { readonly manifest: M }>({
149
+ ): UiClass<C, N> & { readonly manifest: M } => {
150
+ const impl = Context.GenericTag<UiService<N>, ViewImpl<C>>(`reform/ui/${name}`)
151
+ const provider = Context.GenericTag<UiService<N>, CallableView>(`reform/ui/${name}`)
152
+ const contract = definitionClass<UiClass<C, N> & { readonly manifest: M }>({
171
153
  [UiTypeId]: UiTypeId,
172
154
  manifest,
173
155
  impl,
174
- })
156
+ provider,
157
+ capture: <Result>(visit: UiCapture<Result>): Result => visit(contract),
158
+ } satisfies UiDefinitionStatics<C, N, M>)
159
+ return contract
175
160
  }
176
161
 
177
- /**
178
- * A renderer-neutral UI contract: `.make` authors the presentation, a composition
179
- * names it, and `@reform/react` resolves it to a view. Core defines the contract.
180
- *
181
- * Two authoring forms, one source of truth each:
182
- *
183
- * - **Type-only (local):** `ui('Counter')<{ props: { count: number } }>()` — no
184
- * schema, no ceremony, exactly as before.
185
- * - **Schema (wired):** `ui('Counter', { props: S.Struct({ count: S.Number }) })`
186
- * — the schema *is* the definition; the contract type is derived via
187
- * `Schema.Type` (never re-written), and the result is a `WiredUi` the remote
188
- * seam accepts. Make a local contract remote by rewriting this one line.
189
- */
190
- export function ui(name: string): <C extends UiContract>() => UiClass<C>
191
- export function ui<const Sch extends WireSchemas>(name: string, schemas: Sch): WiredUi<DerivedContract<Sch>>
162
+ type UiDefinition<N extends string> = (<C extends UiContract>() => UiClass<C, N>) | AnyUi
163
+
164
+ export function ui<const N extends string>(name: N): <C extends UiContract>() => UiClass<C, N>
165
+ export function ui<const N extends string, const Sch extends WireSchemas>(
166
+ name: N,
167
+ schemas: Sch,
168
+ ): WiredUi<DerivedContract<Sch>, N>
192
169
  // oxlint-disable-next-line reform-rules/min-var-name -- `ui` is the public framework contract-authoring API; renaming would break every caller across the monorepo
193
- export function ui(name: string, schemas?: WireSchemas): unknown {
170
+ export function ui<const N extends string>(name: N, schemas?: WireSchemas): UiDefinition<N> {
194
171
  if (schemas === undefined) {
195
- return <C extends UiContract>(): UiClass<C> => buildUi<C, UiManifest>(name, { kind: 'Ui', name })
172
+ return <C extends UiContract>(): UiClass<C, N> =>
173
+ buildUi<N, C, UiManifest & { readonly name: N }>(name, {
174
+ kind: 'Ui',
175
+ name,
176
+ })
196
177
  }
197
178
  const eventFields = schemas.events === undefined ? {} : { events: schemas.events }
198
- const manifest: WiredUiManifest = { kind: 'Ui', name, props: schemas.props, ...eventFields }
199
- return buildUi<DerivedContract<WireSchemas>, WiredUiManifest>(name, manifest)
179
+ const manifest: WiredUiManifest & { readonly name: N } = {
180
+ kind: 'Ui',
181
+ name,
182
+ props: schemas.props,
183
+ ...eventFields,
184
+ }
185
+ return buildUi<N, DerivedContract<WireSchemas>, WiredUiManifest & { readonly name: N }>(
186
+ name,
187
+ manifest,
188
+ )
200
189
  }
201
190
 
202
- /** Brand under which a `Ui.make` view carries its own contract. */
203
191
  export const UiViewContract: unique symbol = Symbol.for('reform/ui/view-contract')
204
192
  export type UiViewContract = typeof UiViewContract
205
193
 
206
- /**
207
- * A `Ui.make` view that carries its contract, so a consumer can recover the
208
- * contract (its wire name + props schema) straight from the view — no separate
209
- * registration call. This is what makes the remote client view-set
210
- * (`remoteViews<AppContract>({ Some: SomeView, … })`) work with the very same
211
- * `Ui.make` views the local renderer uses.
212
- */
213
- export type MadeView<C extends UiContract> = ViewImpl<C> & { readonly [UiViewContract]: UiClass<C> }
214
-
215
- /**
216
- * Author the pure presentation for a contract — `Ui.make(TodoAppUi, (props,
217
- * slots, events) => node)`. The contract argument fixes the view's types AND is
218
- * carried on the returned view (`MadeView`), so the same view is reusable as a
219
- * remote client presentation. Wire the result with `provide(contract, view)`.
220
- */
221
- export const make = <C extends UiContract>(contract: UiClass<C>, view: ViewImpl<C>): MadeView<C> =>
222
- // oxlint-disable-next-line reform-rules/no-object-assign -- `view` is a callable function; spread would drop callability, so the brand is attached in place
223
- Object.assign(view, { [UiViewContract]: contract })
194
+ export type MadeViewCapture<Result> = <C extends UiContract, N extends string, U extends AnyUi>(
195
+ view: MadeView<C, N, U>,
196
+ ) => Result
197
+
198
+ export type MadeView<
199
+ C extends UiContract,
200
+ N extends string,
201
+ U extends AnyUi = UiClass<C, N>,
202
+ > = ViewImpl<C> & {
203
+ readonly [UiViewContract]: U
204
+ readonly capture: <Result>(visit: MadeViewCapture<Result>) => Result
205
+ }
206
+
207
+ export type AnyMadeView = CallableView & {
208
+ readonly [UiViewContract]: AnyUi
209
+ readonly capture: <Result>(visit: MadeViewCapture<Result>) => Result
210
+ }
211
+
212
+ export const make = <U extends AnyUi>(
213
+ contract: U,
214
+ view: ViewImpl<Contract<U>>,
215
+ ): MadeView<Contract<U>, UiName<U>, U> => {
216
+ // oxlint-disable-next-line reform-rules/no-object-assign -- `view` is callable; spread would drop callability, so its reflection statics are attached in place
217
+ const made: MadeView<Contract<U>, UiName<U>, U> = Object.assign(view, {
218
+ [UiViewContract]: contract,
219
+ capture: <Result>(visit: MadeViewCapture<Result>): Result => visit(made),
220
+ })
221
+ return made
222
+ }
@@ -1,12 +1,5 @@
1
- // Type-level proofs enforced by `tsc --noEmit` over `src` (not a vitest file).
2
- // These guard the schema-first seam: the wired form is the single source of
3
- // truth (props/event types derived, never re-written), a wired contract is a
4
- // plain `UiClass` (so `provide` / `scene` / `Composition.make` accept it), and
5
- // the remote seam (`WiredUi`) rejects a type-only contract. If a guarantee
6
- // regresses, a `@ts-expect-error` goes unused or an assignment fails, and `tsc`
7
- // breaks.
8
-
9
1
  import { Schema as S } from 'effect'
2
+ import { slot, type SlotService } from './slot'
10
3
  import {
11
4
  type Contract,
12
5
  type DerivedContract,
@@ -15,13 +8,32 @@ import {
15
8
  ui,
16
9
  type UiClass,
17
10
  type UiContract,
11
+ type UiService,
18
12
  type WiredUi,
19
13
  } from './ui'
20
14
 
21
- declare function acceptsAny<C extends UiContract>(contract: UiClass<C>): void
22
- declare function acceptsWired<C extends UiContract>(contract: WiredUi<C>): void
15
+ type ExpectFalse<Value extends false> = Value
16
+
17
+ declare function acceptsAny<C extends UiContract, N extends string>(contract: UiClass<C, N>): void
18
+ declare function acceptsWired<C extends UiContract, N extends string>(contract: WiredUi<C, N>): void
23
19
 
24
20
  class LocalUi extends ui('Local')<{ props: { n: number } }>() {}
21
+ class SameShapeUi extends ui('SameShape')<{ props: { n: number } }>() {}
22
+ class FirstSlot extends slot('FirstSlot')<FirstSlot>() {}
23
+ class SecondSlot extends slot('SecondSlot')<SecondSlot>() {}
24
+
25
+ type SameShapedUiDefinitionsRemainDistinct = ExpectFalse<
26
+ typeof LocalUi extends typeof SameShapeUi ? true : false
27
+ >
28
+ type SameShapedSlotDefinitionsRemainDistinct = ExpectFalse<
29
+ typeof FirstSlot extends typeof SecondSlot ? true : false
30
+ >
31
+ type WrongUiDoesNotCloseRequirement = ExpectFalse<
32
+ UiService<'SameShape'> extends UiService<'Local'> ? true : false
33
+ >
34
+ type WrongSlotDoesNotCloseRequirement = ExpectFalse<
35
+ SlotService<'SecondSlot', SecondSlot> extends SlotService<'FirstSlot', FirstSlot> ? true : false
36
+ >
25
37
  const wiredSchemas: {
26
38
  readonly props: S.Struct<{ n: typeof S.Number }>
27
39
  readonly events: { readonly bump: S.Struct<{ by: typeof S.Number }> }
@@ -29,24 +41,21 @@ const wiredSchemas: {
29
41
  props: S.Struct({ n: S.Number }),
30
42
  events: { bump: S.Struct({ by: S.Number }) },
31
43
  }
32
- const WiredUiClassBase: WiredUi<DerivedContract<typeof wiredSchemas>> = ui('Wired', wiredSchemas)
44
+ const WiredUiClassBase: WiredUi<DerivedContract<typeof wiredSchemas>, 'Wired'> = ui(
45
+ 'Wired',
46
+ wiredSchemas,
47
+ )
33
48
  class WiredUiClass extends WiredUiClassBase {}
34
49
 
35
- // POSITIVE: both forms are a `UiClass` — plain wiring (provide/scene/Composition) accepts either.
36
50
  acceptsAny(LocalUi)
37
51
  acceptsAny(WiredUiClass)
38
52
 
39
- // POSITIVE: the schema form is a `WiredUi` — the remote seam accepts it.
40
53
  acceptsWired(WiredUiClass)
41
54
 
42
- // NEGATIVE: a type-only contract has no wire schema, so the remote seam rejects it.
43
55
  // @ts-expect-error -- LocalUi is `UiClass`, not `WiredUi`
44
56
  acceptsWired(LocalUi)
45
57
 
46
- // POSITIVE: props/event types are DERIVED from the schemas — authoring a presentation
47
- // against the wired contract sees `n: number` and `bump: Trigger<{ by: number }>`,
48
- // none of it written a second time.
49
- export const derivedView: MadeView<Contract<typeof WiredUiClass>> = make(
58
+ const derivedView: MadeView<Contract<typeof WiredUiClass>, 'Wired'> = make(
50
59
  WiredUiClass,
51
60
  ({ n: count }, _slots, { bump }) => {
52
61
  const numeric: number = count
@@ -54,3 +63,16 @@ export const derivedView: MadeView<Contract<typeof WiredUiClass>> = make(
54
63
  return `${numeric}`
55
64
  },
56
65
  )
66
+
67
+ type UiTypeProofs = [
68
+ SameShapedUiDefinitionsRemainDistinct,
69
+ SameShapedSlotDefinitionsRemainDistinct,
70
+ WrongUiDoesNotCloseRequirement,
71
+ WrongSlotDoesNotCloseRequirement,
72
+ ]
73
+ declare const uiTypeProofs: UiTypeProofs
74
+
75
+ void uiTypeProofs
76
+ void derivedView
77
+
78
+ export {}
@@ -1,14 +1,9 @@
1
1
  import { Effect } from 'effect'
2
2
  import { attachInspectable } from '../internal/inspect'
3
3
 
4
- /** A definition's `toJSON`: its reflectable manifest, else its raw statics. */
5
4
  const describe = (statics: object): unknown =>
6
5
  ('manifest' in statics ? Reflect.get(statics, 'manifest') : undefined) ?? statics
7
6
 
8
- /**
9
- * The reflectable descriptor every primitive carries (principle #6). The editor
10
- * and proof packages read these without running any logic.
11
- */
12
7
  export type Kind =
13
8
  | 'State'
14
9
  | 'StateGroup'
@@ -35,55 +30,41 @@ export interface Manifest {
35
30
  readonly name: string
36
31
  }
37
32
 
38
- /**
39
- * Build a class whose *static* side is itself an Effect, so `yield* TheClass`
40
- * runs `read` and returns its value (the same mechanism `Context.Tag` uses).
41
- * Subclasses inherit the protocol through the prototype chain, so the user's
42
- * `class FeedState extends State.make(...) {}` is yieldable too.
43
- *
44
- * `statics` (manifest, `.live`, tag, …) are attached as own properties
45
- * and likewise inherited by the subclass.
46
- */
47
- export const yieldableClass = <A, E, R, Statics extends object>(
33
+ export function yieldableClass<A, E, R, Statics extends object>(
48
34
  read: Effect.Effect<A, E, R>,
49
35
  statics: Statics,
50
- ): { new (): {} } & Effect.Effect<A, E, R> & Statics => {
36
+ ): { new (): {} } & Effect.Effect<A, E, R> & Statics
37
+ export function yieldableClass(
38
+ read: Effect.Effect<unknown, unknown, unknown>,
39
+ statics: object,
40
+ ): unknown {
51
41
  class Base {}
52
42
  Object.setPrototypeOf(Base, read)
53
- // `defineProperty` over `Reflect.ownKeys`, not `Object.assign` over
54
- // `Object.entries`: a static can shadow a non-writable own property of the
55
- // class function (notably `name`, which a `Source`-shaped definition like a
56
- // Calc carries) and `assign` would throw on those; and `ownKeys` (unlike
57
- // `entries`) carries symbol-keyed statics — the `TypeId` brands.
58
43
  Reflect.ownKeys(statics).forEach((key) => {
59
44
  const staticValue = Reflect.get(statics, key)
60
- Object.defineProperty(Base, key, { value: staticValue, writable: true, enumerable: true, configurable: true })
45
+ Object.defineProperty(Base, key, {
46
+ value: staticValue,
47
+ writable: true,
48
+ enumerable: true,
49
+ configurable: true,
50
+ })
61
51
  })
62
- // Print like an Effect value (the manifest), not `[object Object]`.
63
52
  attachInspectable(Base, () => describe(statics))
64
- // oxlint-disable-next-line reform-rules/no-type-assertion -- phantom-shaped static side cannot structurally satisfy the yieldable Effect & Statics type
65
- return Base as never
53
+ return Base
66
54
  }
67
55
 
68
- /**
69
- * Build a *definition class*: an empty constructible carrying static marker data
70
- * (manifest, tag, policy, members, …) that the editor and proof packages reflect
71
- * on. Definition interfaces declare phantom, type-only fields — a composition's
72
- * `Props`, a family instance's `Key` — that exist purely for inference and have
73
- * no runtime value, so the assembled object cannot *structurally* satisfy the
74
- * interface. The one assertion to `Class` lives here, documented, instead of a
75
- * scattered `as never` / `as unknown as` at every `*.make`.
76
- */
77
- export const definitionClass = <Class>(statics: object): Class => {
78
- // `defineProperty` over `Reflect.ownKeys`, not `Object.assign`: a static can
79
- // shadow a non-writable own property of the class function (notably `name`)
80
- // and `assign` would throw, and `ownKeys` carries the symbol-keyed `TypeId`.
56
+ export function definitionClass<Class>(statics: object): Class
57
+ export function definitionClass(statics: object): unknown {
81
58
  const Base = class {}
82
59
  Reflect.ownKeys(statics).forEach((key) => {
83
60
  const staticValue = Reflect.get(statics, key)
84
- Object.defineProperty(Base, key, { value: staticValue, writable: true, enumerable: true, configurable: true })
61
+ Object.defineProperty(Base, key, {
62
+ value: staticValue,
63
+ writable: true,
64
+ enumerable: true,
65
+ configurable: true,
66
+ })
85
67
  })
86
68
  attachInspectable(Base, () => describe(statics))
87
- // oxlint-disable-next-line reform-rules/no-type-assertion -- the one documented seam: phantom type-only fields mean the assembled class cannot structurally satisfy Class
88
- return Base as unknown as Class
69
+ return Base
89
70
  }