@playfast/reform 0.0.5 → 0.0.8
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 +31 -10
- package/src/boundary/boundary.test.ts +301 -0
- package/src/calc/asyncCalc.test.ts +556 -0
- package/src/calc/asyncData.ts +9 -1
- package/src/calc/calc.test.ts +287 -0
- package/src/calc/calcFamily.test.ts +206 -0
- package/src/calc/compose.test.ts +68 -0
- package/src/channel/channel.ts +9 -5
- package/src/compose/host.ts +4 -1
- package/src/compose/props.ts +5 -1
- package/src/compose/ui.test.ts +62 -0
- package/src/compose/ui.ts +173 -44
- package/src/compose/ui.typecheck.ts +56 -0
- package/src/definition/definition.ts +2 -0
- package/src/event/event.test.ts +23 -0
- package/src/feature/feature.mount.test.ts +82 -0
- package/src/feature/feature.test.ts +60 -0
- package/src/feature/feature.typecheck.ts +108 -0
- package/src/index.ts +38 -2
- package/src/internal/capture.ts +4 -4
- package/src/internal/errors.test.ts +33 -0
- package/src/internal/errors.ts +92 -16
- package/src/internal/inspect.test.ts +28 -0
- package/src/internal/queryDriver.ts +1 -1
- package/src/internal/reuse.test.ts +116 -0
- package/src/internal/scheduler.ts +3 -1
- package/src/internal/sources.ts +2 -2
- package/src/internal/stateRegistry.ts +32 -0
- package/src/internal/store.test.ts +80 -0
- package/src/internal/track.ts +3 -1
- package/src/remote/remoteState.test.ts +695 -0
- package/src/remote/remoteState.typecheck.ts +195 -0
- package/src/runtime/bus.ts +6 -2
- package/src/runtime/hardening.test.ts +69 -0
- package/src/runtime/loop.test.ts +178 -0
- package/src/runtime/loop.ts +4 -2
- package/src/scene/seedScene.test.ts +169 -0
- package/src/state/state.ts +4 -1
- package/src/state/stateFamily.test.ts +138 -0
- package/src/state/stateFamily.ts +4 -1
- package/src/state/stateGroup.ts +31 -4
- package/src/state/token.ts +5 -4
- package/src/synced/syncedStore.ts +99 -0
- package/src/wire/tree.test.ts +81 -0
- package/src/wire/tree.ts +129 -0
- package/src/wire/triggers.test.ts +76 -0
- package/src/wire/triggers.ts +98 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type Cause, Context, Data, Effect, Layer, type ParseResult, Ref, Schema } from 'effect'
|
|
2
|
+
import type { Trigger } from '../ui/trigger'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The server-side bridge for trigger handles (REMOTE_UI.md §4). A rendered
|
|
6
|
+
* `WireProp` of `_tag: 'Event'` carries a `handle`; the client invokes it by
|
|
7
|
+
* sending `(handle, encodedPayload)`. Here we hold the live `Trigger` and its
|
|
8
|
+
* payload schema, decode the wire payload, and fire — so the UI never ships a
|
|
9
|
+
* function, only a handle, and the payload is validated at the seam.
|
|
10
|
+
*
|
|
11
|
+
* Handles are caller-provided keys, by convention `${nodeId}:${eventName}`: a
|
|
12
|
+
* re-render re-`register`s the same handle with that frame's fresh trigger, so the
|
|
13
|
+
* handle is stable across frames (the client's in-flight handle stays valid) and
|
|
14
|
+
* the handle space never grows. The schema is the contract's own per-event wire
|
|
15
|
+
* schema (`WiredUiManifest.events`, Phase 1) — the single source of truth for the
|
|
16
|
+
* UI boundary, decoupled from which `Event` the composition binds behind it.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type TriggerHandle = string
|
|
20
|
+
|
|
21
|
+
// `any` in the schema/trigger slots is the documented Schema-variance escape
|
|
22
|
+
// (same reason as `CompositionConfig.props`); only the registrar's call site,
|
|
23
|
+
// where the trigger and its schema agree, sees the precise type.
|
|
24
|
+
interface TriggerEntry {
|
|
25
|
+
readonly trigger: Trigger<any>
|
|
26
|
+
readonly schema: Schema.Schema<any, any>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const UnknownTriggerBase: new (args: { readonly handle: TriggerHandle }) => Cause.YieldableError & {
|
|
30
|
+
readonly _tag: 'reform/UnknownTrigger'
|
|
31
|
+
} & Readonly<{ readonly handle: TriggerHandle }> = Data.TaggedError('reform/UnknownTrigger')<{
|
|
32
|
+
readonly handle: TriggerHandle
|
|
33
|
+
}>
|
|
34
|
+
|
|
35
|
+
/** A wire message named a trigger handle the server no longer holds (a stale client). */
|
|
36
|
+
export class UnknownTrigger extends UnknownTriggerBase {
|
|
37
|
+
override get message(): string {
|
|
38
|
+
return `reform: no trigger registered under handle '${this.handle}'`
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TriggerRegistryApi {
|
|
43
|
+
/** Register (or overwrite) a trigger + its payload schema under a handle key. */
|
|
44
|
+
readonly register: <P>(
|
|
45
|
+
handle: TriggerHandle,
|
|
46
|
+
trigger: Trigger<P>,
|
|
47
|
+
schema: Schema.Schema<P, any>,
|
|
48
|
+
) => Effect.Effect<void>
|
|
49
|
+
/** Decode an encoded payload via the handle's schema and fire the trigger. */
|
|
50
|
+
readonly invoke: (
|
|
51
|
+
handle: TriggerHandle,
|
|
52
|
+
encodedPayload: unknown,
|
|
53
|
+
) => Effect.Effect<void, UnknownTrigger | ParseResult.ParseError>
|
|
54
|
+
/** Drop a handle once its node unmounts (the Phase 2 tree diff signals it). */
|
|
55
|
+
readonly revoke: (handle: TriggerHandle) => Effect.Effect<void>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const TriggerRegistryBase: Context.TagClass<
|
|
59
|
+
TriggerRegistry,
|
|
60
|
+
'reform/wire/TriggerRegistry',
|
|
61
|
+
TriggerRegistryApi
|
|
62
|
+
> = Context.Tag('reform/wire/TriggerRegistry')<TriggerRegistry, TriggerRegistryApi>()
|
|
63
|
+
|
|
64
|
+
export class TriggerRegistry extends TriggerRegistryBase {}
|
|
65
|
+
|
|
66
|
+
export const make: Effect.Effect<TriggerRegistryApi> = Effect.gen(function* () {
|
|
67
|
+
const entries = yield* Ref.make(new Map<TriggerHandle, TriggerEntry>())
|
|
68
|
+
|
|
69
|
+
const register = <P>(
|
|
70
|
+
handle: TriggerHandle,
|
|
71
|
+
trigger: Trigger<P>,
|
|
72
|
+
schema: Schema.Schema<P, any>,
|
|
73
|
+
): Effect.Effect<void> =>
|
|
74
|
+
Ref.update(entries, (map) => new Map(map).set(handle, { trigger, schema }))
|
|
75
|
+
|
|
76
|
+
const invoke = (
|
|
77
|
+
handle: TriggerHandle,
|
|
78
|
+
encodedPayload: unknown,
|
|
79
|
+
): Effect.Effect<void, UnknownTrigger | ParseResult.ParseError> =>
|
|
80
|
+
Effect.gen(function* () {
|
|
81
|
+
const entry = (yield* Ref.get(entries)).get(handle)
|
|
82
|
+
if (entry === undefined) return yield* Effect.fail(new UnknownTrigger({ handle }))
|
|
83
|
+
const payload = yield* Schema.decodeUnknown(entry.schema)(encodedPayload)
|
|
84
|
+
entry.trigger(payload)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const revoke = (handle: TriggerHandle): Effect.Effect<void> =>
|
|
88
|
+
Ref.update(entries, (map) => {
|
|
89
|
+
const next = new Map(map)
|
|
90
|
+
next.delete(handle)
|
|
91
|
+
return next
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return { register, invoke, revoke }
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
/** Provide a fresh registry — one per remote connection (each client gets its own handle space). */
|
|
98
|
+
export const layer: Layer.Layer<TriggerRegistry> = Layer.effect(TriggerRegistry, make)
|