@playfast/reform 0.0.8 → 0.0.10
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/package.json +1 -1
- package/src/calc/asyncCalc.invalidate.test.ts +166 -0
- package/src/calc/asyncCalc.ts +44 -0
- package/src/calc/queryState.ts +52 -0
- package/src/compose/composition.ts +57 -23
- package/src/compose/host.ts +7 -13
- package/src/compose/structure.test.ts +90 -0
- package/src/compose/structure.ts +145 -0
- package/src/compose/ui.test.ts +2 -30
- package/src/compose/ui.ts +21 -43
- package/src/index.ts +39 -3
- package/src/internal/capture.ts +8 -0
- package/src/internal/errors.ts +0 -12
- package/src/internal/queryDriver.ts +209 -71
- package/src/internal/queryEvents.ts +34 -0
- package/src/internal/queryStore.ts +36 -0
- package/src/runtime/loop.ts +14 -7
- package/src/runtime/queries.ts +53 -0
- package/src/scene/scene.ts +26 -14
- package/src/scene/seedScene.test.ts +37 -30
- package/src/state/stateGroup.ts +20 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { SlotProps } from './slot'
|
|
2
|
+
import type { EventsOf, UiContract } from './ui'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Structure-as-data (the headless-structure keystone). A composition's logic
|
|
6
|
+
* body returns this *value* instead of a React node: pure, serializable data that
|
|
7
|
+
* describes — for this frame — the composition's own props and, per declared slot,
|
|
8
|
+
* how that slot is filled (a list / a singleton / absent). The engine, the wire
|
|
9
|
+
* server, and the proof harness consume this directly, so none of them needs to
|
|
10
|
+
* evaluate a view or import React to discover structure. Presentation (React)
|
|
11
|
+
* resolves a contract's markup separately, by name, on a host.
|
|
12
|
+
*
|
|
13
|
+
* `live` accepts BOTH a `Node` (legacy view body) and a `Structure` during the
|
|
14
|
+
* migration; hosts discriminate at runtime via {@link isStructure}.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** A list slot fill: one keyed entry per item. `key` is the SINGLE source of both
|
|
18
|
+
* the wire `key` and the per-item `StateFamily` key — they can no longer drift. */
|
|
19
|
+
export interface EachFill<P> {
|
|
20
|
+
readonly _tag: 'Each'
|
|
21
|
+
readonly items: ReadonlyArray<{ readonly key: string; readonly props: P }>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A singleton slot fill — the slot renders exactly one child with these props. */
|
|
25
|
+
export interface OneFill<P> {
|
|
26
|
+
readonly _tag: 'One'
|
|
27
|
+
readonly props: P
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A slot that renders nothing this frame (the data-driven analog of `cond && …`). */
|
|
31
|
+
export interface AbsentFill {
|
|
32
|
+
readonly _tag: 'Absent'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** How one declared slot is filled this frame, typed by the child's external props. */
|
|
36
|
+
export type SlotFill<P> = EachFill<P> | OneFill<P> | AbsentFill
|
|
37
|
+
|
|
38
|
+
/** The per-slot fill map a contract's structure carries — one fill per declared slot,
|
|
39
|
+
* each typed by that child composition's external props (`SlotProps`). Empty when the
|
|
40
|
+
* contract declares no slots. */
|
|
41
|
+
export type StructureSlots<C extends UiContract> = C extends {
|
|
42
|
+
slots: infer S
|
|
43
|
+
}
|
|
44
|
+
? { readonly [K in keyof S]: SlotFill<SlotProps<S[K]>> }
|
|
45
|
+
: Record<never, never>
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The data a composition's logic returns for one frame: its computed props plus,
|
|
49
|
+
* for every declared slot, a fill description. Strict against the contract — props
|
|
50
|
+
* must match, and every declared slot must be filled (use {@link when} for
|
|
51
|
+
* conditional presence). Carries the child's contract per slot (via `SlotProps`),
|
|
52
|
+
* so consumers can select and descend into sub-compositions type-safely.
|
|
53
|
+
*
|
|
54
|
+
* `events` rides ON the structure: because a `mount(...)`-returning body never
|
|
55
|
+
* calls its view, the event triggers it acquired (`yield* Event.trigger(X)`) are
|
|
56
|
+
* no longer captured by a view's `CaptureSink` record — they must travel with the
|
|
57
|
+
* frame so hosts can register them (proof `actions`, the wire `TriggerRegistry`,
|
|
58
|
+
* the react presentation `events` arg). Triggers are functions, so `events` is NOT
|
|
59
|
+
* serializable and is intentionally excluded from {@link structureEquals}. Typed to
|
|
60
|
+
* the contract's event map; omit it (defaults to `{}`) when the contract has none.
|
|
61
|
+
*/
|
|
62
|
+
export interface Structure<C extends UiContract> {
|
|
63
|
+
readonly props: C['props']
|
|
64
|
+
readonly slots: StructureSlots<C>
|
|
65
|
+
readonly events?: EventsOf<C>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Runtime brand distinguishing a `Structure` value from a React `Node` (added by
|
|
69
|
+
* {@link mount}; the authoring TYPE intentionally omits it so structure literals stay
|
|
70
|
+
* plain). */
|
|
71
|
+
export const StructureTypeId: unique symbol = Symbol.for('reform/Structure')
|
|
72
|
+
export type StructureTypeId = typeof StructureTypeId
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Assemble a frame's structure, checked against the contract `C`. Identity at
|
|
76
|
+
* runtime apart from stamping the {@link StructureTypeId} brand; its real job is
|
|
77
|
+
* the compile-time contract check. `C` is recovered from the surrounding `live`
|
|
78
|
+
* body's return type (contextual typing), so call sites read `mount({ props, slots })`.
|
|
79
|
+
* A leaf composition (no declared slots) passes `slots: {}`. `events` defaults to an
|
|
80
|
+
* empty map when omitted, so a frame always carries an `events` record for hosts.
|
|
81
|
+
*/
|
|
82
|
+
export const mount = <C extends UiContract>(structure: Structure<C>): Structure<C> =>
|
|
83
|
+
Object.assign({ [StructureTypeId]: StructureTypeId, events: {} }, structure)
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A list slot fill. `key` derives the stable identity for BOTH the wire node and
|
|
87
|
+
* the per-item state family — passing the same id source to both is no longer
|
|
88
|
+
* possible to get wrong. `props` maps each item to the child's external props.
|
|
89
|
+
*/
|
|
90
|
+
export const each = <T, P>(
|
|
91
|
+
collection: Iterable<T>,
|
|
92
|
+
options: { readonly key: (item: T) => string; readonly props: (item: T) => P },
|
|
93
|
+
): EachFill<P> => ({
|
|
94
|
+
_tag: 'Each',
|
|
95
|
+
items: Array.from(collection, (item) => ({ key: options.key(item), props: options.props(item) })),
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
/** A singleton slot fill — render one child with these external props. */
|
|
99
|
+
export const one = <P>(props: P): OneFill<P> => ({ _tag: 'One', props })
|
|
100
|
+
|
|
101
|
+
/** The shared empty fill — a slot that renders nothing this frame. */
|
|
102
|
+
export const absent: AbsentFill = { _tag: 'Absent' }
|
|
103
|
+
|
|
104
|
+
/** Conditional presence: the fill when `cond`, otherwise {@link absent}. */
|
|
105
|
+
export const when = <F extends SlotFill<unknown>>(cond: boolean, fill: F): F | AbsentFill =>
|
|
106
|
+
cond ? fill : absent
|
|
107
|
+
|
|
108
|
+
/** Whether a frame value is a `Structure` (vs a legacy React `Node`). The runtime
|
|
109
|
+
* discriminator hosts use during the migration, replacing React's `isValidElement`. */
|
|
110
|
+
export const isStructure = (frame: unknown): frame is Structure<UiContract> =>
|
|
111
|
+
typeof frame === 'object' && frame !== null && StructureTypeId in frame
|
|
112
|
+
|
|
113
|
+
/** Whether a value is a plain record (not an array) — narrows for the deep compare
|
|
114
|
+
* below without a cast. */
|
|
115
|
+
const isRecord = (u: unknown): u is Record<string, unknown> =>
|
|
116
|
+
typeof u === 'object' && u !== null && !Array.isArray(u)
|
|
117
|
+
|
|
118
|
+
/** Minimal structural deep-equality for serializable shapes (plain JSON — the
|
|
119
|
+
* UI-contract guarantee). Avoids a heavyweight dep for the settle comparison. */
|
|
120
|
+
const deepEquals = (a: unknown, b: unknown): boolean => {
|
|
121
|
+
if (Object.is(a, b)) return true
|
|
122
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
123
|
+
return a.length === b.length && a.every((item, index) => deepEquals(item, b[index]))
|
|
124
|
+
}
|
|
125
|
+
if (isRecord(a) && isRecord(b)) {
|
|
126
|
+
const aKeys = Object.keys(a)
|
|
127
|
+
const bKeys = Object.keys(b)
|
|
128
|
+
return (
|
|
129
|
+
aKeys.length === bKeys.length &&
|
|
130
|
+
aKeys.every(
|
|
131
|
+
(key) => Object.prototype.hasOwnProperty.call(b, key) && deepEquals(a[key], b[key]),
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Structural equality over two structures — props and every slot fill compared by
|
|
139
|
+
* value. `events` is intentionally excluded: triggers are functions, freshly
|
|
140
|
+
* acquired each render, so comparing them by identity would never match and would
|
|
141
|
+
* break the settle fixpoint. The `StructureTypeId` brand is a symbol key, so
|
|
142
|
+
* `Object.keys` skips it. Used by the proof/server settle loop to detect a
|
|
143
|
+
* two-frame fixpoint. */
|
|
144
|
+
export const structureEquals = (a: Structure<UiContract>, b: Structure<UiContract>): boolean =>
|
|
145
|
+
deepEquals(a.props, b.props) && deepEquals(a.slots, b.slots)
|
package/src/compose/ui.test.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { expect, test } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { CaptureSink, type UiCapture } from '../internal/capture'
|
|
5
|
-
import type { Trigger } from '../ui/trigger'
|
|
2
|
+
import { Schema as S } from 'effect'
|
|
3
|
+
import { isUi, ui } from '../index'
|
|
6
4
|
|
|
7
5
|
// `ui` has two authoring forms with one source of truth each: the type-only form
|
|
8
6
|
// (local, no schema) and the schema form (wired, the type is *derived*). Both are
|
|
@@ -34,29 +32,3 @@ test('schema `ui` with no events omits the events schema', () => {
|
|
|
34
32
|
expect(S.isSchema(TitleUi.manifest.props)).toBe(true)
|
|
35
33
|
expect(TitleUi.manifest.events).toBeUndefined()
|
|
36
34
|
})
|
|
37
|
-
|
|
38
|
-
test('a contract resolves its presentation and reports to a capturing sink', () => {
|
|
39
|
-
class CounterUi extends ui('Counter', {
|
|
40
|
-
props: S.Struct({ count: S.Number }),
|
|
41
|
-
events: { bump: S.Struct({ by: S.Number }) },
|
|
42
|
-
}) {}
|
|
43
|
-
|
|
44
|
-
const view = Ui.make(CounterUi, ({ count }) => `count:${count}`)
|
|
45
|
-
const records: UiCapture[] = []
|
|
46
|
-
const sink = { record: (capture: UiCapture): void => void records.push(capture) }
|
|
47
|
-
|
|
48
|
-
const logicView = Effect.runSync(
|
|
49
|
-
CounterUi.pipe(
|
|
50
|
-
Effect.provideService(CounterUi.impl, view),
|
|
51
|
-
Effect.provideService(CaptureSink, sink),
|
|
52
|
-
),
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
const bump: Trigger<{ by: number }> = () => {}
|
|
56
|
-
const node = logicView({ count: 3 }, { bump })
|
|
57
|
-
|
|
58
|
-
expect(node).toBe('count:3')
|
|
59
|
-
expect(records).toHaveLength(1)
|
|
60
|
-
expect(records[0]?.name).toBe('Counter')
|
|
61
|
-
expect(records[0]?.props).toEqual({ count: 3 })
|
|
62
|
-
})
|
package/src/compose/ui.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import type { FunctionComponent } from 'react'
|
|
2
|
-
import { Context,
|
|
3
|
-
import { type Manifest,
|
|
4
|
-
import { CaptureSink } from '../internal/capture'
|
|
5
|
-
import { SlotRenderingUnavailable } from '../internal/errors'
|
|
2
|
+
import { Context, type Schema } from 'effect'
|
|
3
|
+
import { type Manifest, definitionClass } from '../definition/definition'
|
|
6
4
|
import type { Trigger } from '../ui/trigger'
|
|
7
|
-
import { CurrentSlots } from './host'
|
|
8
5
|
import { type Node, type SlotClass, type SlotInstance, type SlotProps } from './slot'
|
|
9
6
|
|
|
10
7
|
export interface UiContract {
|
|
@@ -14,7 +11,10 @@ export interface UiContract {
|
|
|
14
11
|
}
|
|
15
12
|
|
|
16
13
|
type PropsOf<C extends UiContract> = C['props']
|
|
17
|
-
|
|
14
|
+
/** The contract's event map (`{ [name]: Trigger<P> }`), or empty when it declares
|
|
15
|
+
* none. Exported so a `Structure<C>` can type its optional `events` field to the
|
|
16
|
+
* same map a composition body acquires. */
|
|
17
|
+
export type EventsOf<C extends UiContract> = C extends { events: infer E } ? E : Record<never, never>
|
|
18
18
|
// A slot is a React COMPONENT whose props are OPTIONAL (a `Partial`): a parent that
|
|
19
19
|
// drives a child supplies them (`<slots.Item id=… />` / `createElement(slots.Item, {…})`),
|
|
20
20
|
// but a placeholder render (the remote client, where the per-child props were captured
|
|
@@ -35,15 +35,18 @@ type SlotsOf<C extends UiContract> = C extends { slots: infer S }
|
|
|
35
35
|
// child props are `any` the whole thing still collapses to `any` (`any & X = any`), keeping
|
|
36
36
|
// the component assignable to a bare `(props: unknown) => Node` slot-renderer shape. An
|
|
37
37
|
// outer intersection would leave a required-shape object that `unknown` can't satisfy.
|
|
38
|
+
//
|
|
39
|
+
// A slot handle is BOTH renderable (the `FunctionComponent` — places the keyed children)
|
|
40
|
+
// AND inspectable (`.props` — the array of per-child fill props the structure carried, one
|
|
41
|
+
// entry per mounted child, typed by the child contract). A presentation can read the fills
|
|
42
|
+
// (`slots.Item.props.length`, a header, layout decisions) and still render them
|
|
43
|
+
// (`<slots.Item/>`); it never DERIVES multiplicity — the engine already fixed it. Read-only.
|
|
38
44
|
readonly [K in keyof S]: FunctionComponent<
|
|
39
45
|
Partial<SlotProps<S[K]> & { readonly slotKey?: string }>
|
|
40
|
-
>
|
|
46
|
+
> & { readonly props: ReadonlyArray<SlotProps<S[K]>> }
|
|
41
47
|
}
|
|
42
48
|
: Record<never, never>
|
|
43
49
|
|
|
44
|
-
/** What the composition logic calls: props + handlers in, a node out. Slots are bound by the renderer. */
|
|
45
|
-
export type LogicView<C extends UiContract> = (props: PropsOf<C>, events?: EventsOf<C>) => Node
|
|
46
|
-
|
|
47
50
|
/** What `.make` authors: the pure presentation. Slots are injected by the renderer. */
|
|
48
51
|
export type ViewImpl<C extends UiContract> = (
|
|
49
52
|
props: PropsOf<C>,
|
|
@@ -73,8 +76,9 @@ export interface WiredUiManifest extends UiManifest {
|
|
|
73
76
|
readonly props: Schema.Schema<any, any>
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
// A branded, extendable class carrying the wire `manifest` + the DI `impl` tag. Not
|
|
80
|
+
// yieldable: bodies return a `Structure`, and the host reads `impl` to resolve the view.
|
|
81
|
+
export interface UiClass<C extends UiContract> {
|
|
78
82
|
new (): {}
|
|
79
83
|
readonly [UiTypeId]: UiTypeId
|
|
80
84
|
readonly manifest: UiManifest
|
|
@@ -144,34 +148,9 @@ const buildUi = <C extends UiContract, M extends UiManifest>(
|
|
|
144
148
|
manifest: M,
|
|
145
149
|
): UiClass<C> & { readonly manifest: M } => {
|
|
146
150
|
const impl = Context.GenericTag<ViewImpl<C>, ViewImpl<C>>(`reform/ui/${name}`)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// Core has no renderer, so without a host every slot access throws.
|
|
151
|
-
const host = yield* Effect.serviceOption(CurrentSlots)
|
|
152
|
-
const slots = new Proxy({} as SlotsOf<C>, {
|
|
153
|
-
get(_t, key) {
|
|
154
|
-
if (Option.isNone(host)) {
|
|
155
|
-
throw new SlotRenderingUnavailable({ slot: String(key) })
|
|
156
|
-
}
|
|
157
|
-
return host.value.slot(String(key))
|
|
158
|
-
},
|
|
159
|
-
})
|
|
160
|
-
// A capturing host (proofs, the dev tool) reports each render's observable
|
|
161
|
-
// surface without replacing the presentation. Absent in production.
|
|
162
|
-
const sink = yield* Effect.serviceOption(CaptureSink)
|
|
163
|
-
return (props: PropsOf<C>, events?: EventsOf<C>) => {
|
|
164
|
-
if (Option.isSome(sink)) {
|
|
165
|
-
sink.value.record({
|
|
166
|
-
name,
|
|
167
|
-
props,
|
|
168
|
-
events: (events ?? {}) as Record<string, Trigger<unknown>>,
|
|
169
|
-
})
|
|
170
|
-
}
|
|
171
|
-
return render(props, slots, (events ?? {}) as EventsOf<C>)
|
|
172
|
-
}
|
|
173
|
-
})
|
|
174
|
-
return yieldableClass(read, {
|
|
151
|
+
// Extendable definition class (`class Counter extends ui('Counter', …) {}`); resolved
|
|
152
|
+
// by name on a host via `impl`, never yielded.
|
|
153
|
+
return definitionClass<UiClass<C> & { readonly manifest: M }>({
|
|
175
154
|
[UiTypeId]: UiTypeId,
|
|
176
155
|
manifest,
|
|
177
156
|
impl,
|
|
@@ -179,9 +158,8 @@ const buildUi = <C extends UiContract, M extends UiManifest>(
|
|
|
179
158
|
}
|
|
180
159
|
|
|
181
160
|
/**
|
|
182
|
-
* A renderer-neutral UI contract
|
|
183
|
-
*
|
|
184
|
-
* presentation. Core defines the contract; `@reform/react` does the rendering.
|
|
161
|
+
* A renderer-neutral UI contract: `.make` authors the presentation, a composition
|
|
162
|
+
* names it, and `@reform/react` resolves it to a view. Core defines the contract.
|
|
185
163
|
*
|
|
186
164
|
* Two authoring forms, one source of truth each:
|
|
187
165
|
*
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,23 @@ export {
|
|
|
31
31
|
type AsyncLoading,
|
|
32
32
|
type AsyncSuccess,
|
|
33
33
|
} from './calc/asyncData'
|
|
34
|
+
// The flat, data-oriented query value (`data`/`error`/`isFetching`/`isStale`) an
|
|
35
|
+
// `AsyncCalc` is driven by; `toAsyncData` projects it onto the matchable arms.
|
|
36
|
+
export { isLoading, type QueryState, toAsyncData } from './calc/queryState'
|
|
37
|
+
// Optional host seams an `AsyncCalc` consults — persistence and lifecycle signals.
|
|
38
|
+
// Concrete implementations live in edge packages (e.g. `@playfast/reform-query-browser`).
|
|
39
|
+
export {
|
|
40
|
+
noopQueryStore,
|
|
41
|
+
QueryStore,
|
|
42
|
+
type QueryStoreApi,
|
|
43
|
+
resolveQueryStore,
|
|
44
|
+
} from './internal/queryStore'
|
|
45
|
+
export {
|
|
46
|
+
noopQueryEvents,
|
|
47
|
+
QueryEvents,
|
|
48
|
+
type QueryEventsApi,
|
|
49
|
+
resolveQueryEvents,
|
|
50
|
+
} from './internal/queryEvents'
|
|
34
51
|
export * as RemoteState from './remote/remoteState'
|
|
35
52
|
// Direct type re-exports alongside the namespace (TS2742 nameability): an app
|
|
36
53
|
// layer's inferred type references these (`Store<ReadonlyArray<PendingIntent<…>>>`),
|
|
@@ -56,8 +73,25 @@ export * as Composition from './compose/composition'
|
|
|
56
73
|
export type {
|
|
57
74
|
CompositionClass,
|
|
58
75
|
CompositionService,
|
|
76
|
+
Frame,
|
|
59
77
|
RenderEnv,
|
|
60
78
|
} from './compose/composition'
|
|
79
|
+
export {
|
|
80
|
+
absent,
|
|
81
|
+
each,
|
|
82
|
+
type EachFill,
|
|
83
|
+
isStructure,
|
|
84
|
+
mount,
|
|
85
|
+
one,
|
|
86
|
+
type OneFill,
|
|
87
|
+
type AbsentFill,
|
|
88
|
+
type SlotFill,
|
|
89
|
+
type Structure,
|
|
90
|
+
structureEquals,
|
|
91
|
+
type StructureSlots,
|
|
92
|
+
StructureTypeId,
|
|
93
|
+
when,
|
|
94
|
+
} from './compose/structure'
|
|
61
95
|
export { Props } from './compose/props'
|
|
62
96
|
export {
|
|
63
97
|
isSlot,
|
|
@@ -68,12 +102,11 @@ export {
|
|
|
68
102
|
type SlotProps,
|
|
69
103
|
SlotTypeId,
|
|
70
104
|
} from './compose/slot'
|
|
71
|
-
export {
|
|
105
|
+
export { type SlotRenderer } from './compose/host'
|
|
72
106
|
export * as Ui from './compose/ui'
|
|
73
107
|
export {
|
|
74
108
|
type DerivedContract,
|
|
75
109
|
isUi,
|
|
76
|
-
type LogicView,
|
|
77
110
|
type MadeView,
|
|
78
111
|
ui,
|
|
79
112
|
type UiClass,
|
|
@@ -130,10 +163,14 @@ export { type SlotChild } from './compose/slot'
|
|
|
130
163
|
// A scene bundles a composition with the closed wiring that runs it — the single
|
|
131
164
|
// handle the react host, proofs, and the dev tool all consume.
|
|
132
165
|
export { isScene, type MountedServices, type Scene, scene, seedScene } from './scene/scene'
|
|
166
|
+
export type { SeedsOf } from './state/stateGroup'
|
|
133
167
|
|
|
134
168
|
// Runtime surface for hosts (`@reform/react`) and headless tests.
|
|
135
169
|
export { Bus, type Envelope, type Priority, publish, type Tagged } from './runtime/bus'
|
|
136
170
|
export { Engine, type ReducerEntry, Reducers } from './runtime/loop'
|
|
171
|
+
// The runtime-wide registry of live query handles. `AsyncCalc.invalidate` /
|
|
172
|
+
// `AsyncCalc.refetch` and provider layers act on a query by name through it.
|
|
173
|
+
export { Queries, type QueryHandle, type QueryRegistry } from './runtime/queries'
|
|
137
174
|
export {
|
|
138
175
|
type ChannelClass,
|
|
139
176
|
type ChannelPolicy,
|
|
@@ -151,7 +188,6 @@ export {
|
|
|
151
188
|
FeatureLoadFailed,
|
|
152
189
|
forceSync,
|
|
153
190
|
InvalidProvideTarget,
|
|
154
|
-
SlotRenderingUnavailable,
|
|
155
191
|
UnknownGroupState,
|
|
156
192
|
} from './internal/errors'
|
|
157
193
|
// Notification scheduler: hosts that need per-runtime isolation (concurrent SSR,
|
package/src/internal/capture.ts
CHANGED
|
@@ -10,6 +10,14 @@ export interface UiCapture {
|
|
|
10
10
|
readonly name: string
|
|
11
11
|
readonly props: unknown
|
|
12
12
|
readonly events: Record<string, Trigger<unknown>>
|
|
13
|
+
/**
|
|
14
|
+
* The stable slot-fill key this render was mounted under (an `each` item's
|
|
15
|
+
* `key`, or a synthesized singleton key for a `one`). Present only on the
|
|
16
|
+
* structure path — the legacy Node path defers slot expansion to host
|
|
17
|
+
* components and has no fill key, so it is omitted there. Lets the proof
|
|
18
|
+
* facade select a child by key (`byKey`) instead of by render index.
|
|
19
|
+
*/
|
|
20
|
+
readonly key?: string
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
/** Where captured renders are reported, when a capturing host is present. */
|
package/src/internal/errors.ts
CHANGED
|
@@ -19,18 +19,6 @@ type TaggedErrorClass<Tag extends string, A extends Record<string, unknown>> = n
|
|
|
19
19
|
/** The fieldless variant — a `Data.TaggedError(tag)<{}>` constructor. */
|
|
20
20
|
type EmptyTaggedErrorClass<Tag extends string> = new () => Cause.YieldableError & { readonly _tag: Tag }
|
|
21
21
|
|
|
22
|
-
const SlotRenderingUnavailableBase: TaggedErrorClass<
|
|
23
|
-
'reform/SlotRenderingUnavailable',
|
|
24
|
-
{ readonly slot: string }
|
|
25
|
-
> = Data.TaggedError('reform/SlotRenderingUnavailable')<{ readonly slot: string }>
|
|
26
|
-
|
|
27
|
-
/** A `<slots.X/>` was rendered without a render-target host to realize it. */
|
|
28
|
-
export class SlotRenderingUnavailable extends SlotRenderingUnavailableBase {
|
|
29
|
-
override get message(): string {
|
|
30
|
-
return `reform: rendering slot '${this.slot}' requires @reform/react`
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
22
|
const InvalidProvideTargetBase: EmptyTaggedErrorClass<'reform/InvalidProvideTarget'> =
|
|
35
23
|
Data.TaggedError('reform/InvalidProvideTarget')<{}>
|
|
36
24
|
|