@playfast/reform 0.0.7 → 0.0.9
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 +2 -2
- package/src/boundary/boundary.test.ts +301 -0
- package/src/boundary/boundary.ts +192 -0
- package/src/calc/asyncCalc.test.ts +556 -0
- package/src/calc/asyncCalc.ts +267 -0
- package/src/calc/asyncData.ts +96 -0
- package/src/calc/calc.test.ts +287 -0
- package/src/calc/calc.ts +120 -0
- package/src/calc/calcFamily.test.ts +206 -0
- package/src/calc/calcFamily.ts +194 -0
- package/src/calc/compose.test.ts +68 -0
- package/src/calc/compose.ts +69 -0
- package/src/channel/channel.ts +269 -0
- package/src/compose/composition.ts +146 -0
- package/src/compose/host.ts +12 -0
- package/{dist/compose/props.js → src/compose/props.ts} +7 -5
- package/src/compose/provide.ts +70 -0
- package/{dist/compose/slot.d.ts → src/compose/slot.ts} +36 -19
- package/src/compose/structure.test.ts +90 -0
- package/src/compose/structure.ts +145 -0
- package/src/compose/ui.test.ts +34 -0
- package/src/compose/ui.ts +204 -0
- package/src/compose/ui.typecheck.ts +56 -0
- package/src/definition/definition.ts +76 -0
- package/src/event/event.test.ts +23 -0
- package/src/event/event.ts +65 -0
- package/src/event/eventGroup.ts +14 -0
- package/src/feature/feature.mount.test.ts +82 -0
- package/src/feature/feature.test.ts +60 -0
- package/{dist/feature/feature.d.ts → src/feature/feature.ts} +178 -92
- package/src/feature/feature.typecheck.ts +108 -0
- package/src/index.ts +191 -0
- package/src/internal/capture.ts +38 -0
- package/{dist/internal/ctx.d.ts → src/internal/ctx.ts} +8 -4
- package/src/internal/errors.test.ts +33 -0
- package/src/internal/errors.ts +133 -0
- package/src/internal/inspect.test.ts +28 -0
- package/src/internal/inspect.ts +34 -0
- package/src/internal/queryDriver.ts +247 -0
- package/src/internal/reuse.test.ts +116 -0
- package/src/internal/reuse.ts +73 -0
- package/src/internal/scheduler.ts +93 -0
- package/{dist/internal/seeds.d.ts → src/internal/seeds.ts} +6 -3
- package/src/internal/sources.ts +104 -0
- package/{dist/internal/stateRegistry.js → src/internal/stateRegistry.ts} +18 -14
- package/src/internal/store.test.ts +80 -0
- package/src/internal/store.ts +105 -0
- package/{dist/internal/track.d.ts → src/internal/track.ts} +17 -11
- package/src/procedure/procedure.ts +89 -0
- package/src/reducer/reducer.ts +137 -0
- package/src/remote/remoteState.test.ts +695 -0
- package/src/remote/remoteState.ts +571 -0
- package/src/remote/remoteState.typecheck.ts +195 -0
- package/src/runtime/bus.ts +40 -0
- package/src/runtime/hardening.test.ts +69 -0
- package/src/runtime/loop.test.ts +178 -0
- package/src/runtime/loop.ts +171 -0
- package/src/scene/scene.ts +88 -0
- package/src/scene/seedScene.test.ts +176 -0
- package/src/state/state.ts +94 -0
- package/src/state/stateFamily.test.ts +138 -0
- package/src/state/stateFamily.ts +174 -0
- package/src/state/stateGroup.ts +125 -0
- package/{dist/state/token.d.ts → src/state/token.ts} +22 -13
- package/src/synced/syncedStore.ts +99 -0
- package/{dist/ui/node.d.ts → src/ui/node.ts} +3 -3
- package/{dist/ui/trigger.d.ts → src/ui/trigger.ts} +1 -2
- 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
- package/dist/boundary/boundary.d.ts +0 -72
- package/dist/boundary/boundary.d.ts.map +0 -1
- package/dist/boundary/boundary.js +0 -83
- package/dist/boundary/boundary.js.map +0 -1
- package/dist/calc/asyncCalc.d.ts +0 -91
- package/dist/calc/asyncCalc.d.ts.map +0 -1
- package/dist/calc/asyncCalc.js +0 -95
- package/dist/calc/asyncCalc.js.map +0 -1
- package/dist/calc/asyncData.d.ts +0 -57
- package/dist/calc/asyncData.d.ts.map +0 -1
- package/dist/calc/asyncData.js +0 -34
- package/dist/calc/asyncData.js.map +0 -1
- package/dist/calc/calc.d.ts +0 -57
- package/dist/calc/calc.d.ts.map +0 -1
- package/dist/calc/calc.js +0 -58
- package/dist/calc/calc.js.map +0 -1
- package/dist/calc/calcFamily.d.ts +0 -57
- package/dist/calc/calcFamily.d.ts.map +0 -1
- package/dist/calc/calcFamily.js +0 -124
- package/dist/calc/calcFamily.js.map +0 -1
- package/dist/calc/compose.d.ts +0 -16
- package/dist/calc/compose.d.ts.map +0 -1
- package/dist/calc/compose.js +0 -7
- package/dist/calc/compose.js.map +0 -1
- package/dist/channel/channel.d.ts +0 -115
- package/dist/channel/channel.d.ts.map +0 -1
- package/dist/channel/channel.js +0 -138
- package/dist/channel/channel.js.map +0 -1
- package/dist/compose/composition.d.ts +0 -72
- package/dist/compose/composition.d.ts.map +0 -1
- package/dist/compose/composition.js +0 -46
- package/dist/compose/composition.js.map +0 -1
- package/dist/compose/host.d.ts +0 -17
- package/dist/compose/host.d.ts.map +0 -1
- package/dist/compose/host.js +0 -6
- package/dist/compose/host.js.map +0 -1
- package/dist/compose/props.d.ts +0 -13
- package/dist/compose/props.d.ts.map +0 -1
- package/dist/compose/props.js.map +0 -1
- package/dist/compose/provide.d.ts +0 -22
- package/dist/compose/provide.d.ts.map +0 -1
- package/dist/compose/provide.js +0 -28
- package/dist/compose/provide.js.map +0 -1
- package/dist/compose/slot.d.ts.map +0 -1
- package/dist/compose/slot.js +0 -23
- package/dist/compose/slot.js.map +0 -1
- package/dist/compose/ui.d.ts +0 -135
- package/dist/compose/ui.d.ts.map +0 -1
- package/dist/compose/ui.js +0 -63
- package/dist/compose/ui.js.map +0 -1
- package/dist/definition/definition.d.ts +0 -33
- package/dist/definition/definition.d.ts.map +0 -1
- package/dist/definition/definition.js +0 -42
- package/dist/definition/definition.js.map +0 -1
- package/dist/event/event.d.ts +0 -33
- package/dist/event/event.d.ts.map +0 -1
- package/dist/event/event.js +0 -30
- package/dist/event/event.js.map +0 -1
- package/dist/event/eventGroup.d.ts +0 -9
- package/dist/event/eventGroup.d.ts.map +0 -1
- package/dist/event/eventGroup.js +0 -4
- package/dist/event/eventGroup.js.map +0 -1
- package/dist/feature/feature.d.ts.map +0 -1
- package/dist/feature/feature.js +0 -98
- package/dist/feature/feature.js.map +0 -1
- package/dist/index.d.ts +0 -53
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -62
- package/dist/index.js.map +0 -1
- package/dist/internal/capture.d.ts +0 -28
- package/dist/internal/capture.d.ts.map +0 -1
- package/dist/internal/capture.js +0 -12
- package/dist/internal/capture.js.map +0 -1
- package/dist/internal/ctx.d.ts.map +0 -1
- package/dist/internal/ctx.js +0 -2
- package/dist/internal/ctx.js.map +0 -1
- package/dist/internal/errors.d.ts +0 -89
- package/dist/internal/errors.d.ts.map +0 -1
- package/dist/internal/errors.js +0 -102
- package/dist/internal/errors.js.map +0 -1
- package/dist/internal/inspect.d.ts +0 -17
- package/dist/internal/inspect.d.ts.map +0 -1
- package/dist/internal/inspect.js +0 -32
- package/dist/internal/inspect.js.map +0 -1
- package/dist/internal/queryDriver.d.ts +0 -65
- package/dist/internal/queryDriver.d.ts.map +0 -1
- package/dist/internal/queryDriver.js +0 -134
- package/dist/internal/queryDriver.js.map +0 -1
- package/dist/internal/reuse.d.ts +0 -10
- package/dist/internal/reuse.d.ts.map +0 -1
- package/dist/internal/reuse.js +0 -68
- package/dist/internal/reuse.js.map +0 -1
- package/dist/internal/scheduler.d.ts +0 -47
- package/dist/internal/scheduler.d.ts.map +0 -1
- package/dist/internal/scheduler.js +0 -70
- package/dist/internal/scheduler.js.map +0 -1
- package/dist/internal/seeds.d.ts.map +0 -1
- package/dist/internal/seeds.js +0 -17
- package/dist/internal/seeds.js.map +0 -1
- package/dist/internal/sources.d.ts +0 -39
- package/dist/internal/sources.d.ts.map +0 -1
- package/dist/internal/sources.js +0 -59
- package/dist/internal/sources.js.map +0 -1
- package/dist/internal/stateRegistry.d.ts +0 -2
- package/dist/internal/stateRegistry.d.ts.map +0 -1
- package/dist/internal/stateRegistry.js.map +0 -1
- package/dist/internal/store.d.ts +0 -47
- package/dist/internal/store.d.ts.map +0 -1
- package/dist/internal/store.js +0 -73
- package/dist/internal/store.js.map +0 -1
- package/dist/internal/track.d.ts.map +0 -1
- package/dist/internal/track.js +0 -19
- package/dist/internal/track.js.map +0 -1
- package/dist/procedure/procedure.d.ts +0 -40
- package/dist/procedure/procedure.d.ts.map +0 -1
- package/dist/procedure/procedure.js +0 -50
- package/dist/procedure/procedure.js.map +0 -1
- package/dist/reducer/reducer.d.ts +0 -44
- package/dist/reducer/reducer.d.ts.map +0 -1
- package/dist/reducer/reducer.js +0 -63
- package/dist/reducer/reducer.js.map +0 -1
- package/dist/remote/remoteState.d.ts +0 -119
- package/dist/remote/remoteState.d.ts.map +0 -1
- package/dist/remote/remoteState.js +0 -270
- package/dist/remote/remoteState.js.map +0 -1
- package/dist/runtime/bus.d.ts +0 -27
- package/dist/runtime/bus.d.ts.map +0 -1
- package/dist/runtime/bus.js +0 -21
- package/dist/runtime/bus.js.map +0 -1
- package/dist/runtime/loop.d.ts +0 -45
- package/dist/runtime/loop.d.ts.map +0 -1
- package/dist/runtime/loop.js +0 -117
- package/dist/runtime/loop.js.map +0 -1
- package/dist/scene/scene.d.ts +0 -44
- package/dist/scene/scene.d.ts.map +0 -1
- package/dist/scene/scene.js +0 -31
- package/dist/scene/scene.js.map +0 -1
- package/dist/state/state.d.ts +0 -37
- package/dist/state/state.d.ts.map +0 -1
- package/dist/state/state.js +0 -46
- package/dist/state/state.js.map +0 -1
- package/dist/state/stateFamily.d.ts +0 -79
- package/dist/state/stateFamily.d.ts.map +0 -1
- package/dist/state/stateFamily.js +0 -99
- package/dist/state/stateFamily.js.map +0 -1
- package/dist/state/stateGroup.d.ts +0 -50
- package/dist/state/stateGroup.d.ts.map +0 -1
- package/dist/state/stateGroup.js +0 -54
- package/dist/state/stateGroup.js.map +0 -1
- package/dist/state/token.d.ts.map +0 -1
- package/dist/state/token.js +0 -20
- package/dist/state/token.js.map +0 -1
- package/dist/synced/syncedStore.d.ts +0 -51
- package/dist/synced/syncedStore.d.ts.map +0 -1
- package/dist/synced/syncedStore.js +0 -43
- package/dist/synced/syncedStore.js.map +0 -1
- package/dist/ui/node.d.ts.map +0 -1
- package/dist/ui/node.js +0 -2
- package/dist/ui/node.js.map +0 -1
- package/dist/ui/trigger.d.ts.map +0 -1
- package/dist/ui/trigger.js +0 -2
- package/dist/ui/trigger.js.map +0 -1
- package/dist/wire/tree.d.ts +0 -66
- package/dist/wire/tree.d.ts.map +0 -1
- package/dist/wire/tree.js +0 -60
- package/dist/wire/tree.js.map +0 -1
- package/dist/wire/triggers.d.ts +0 -44
- package/dist/wire/triggers.d.ts.map +0 -1
- package/dist/wire/triggers.js +0 -31
- package/dist/wire/triggers.js.map +0 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Type-level proofs enforced by `tsc --noEmit` over `src` (not a vitest file).
|
|
2
|
+
// These guard the cast-free seam: if a guarantee regresses, a `@ts-expect-error`
|
|
3
|
+
// goes unused or an assignment fails, and `tsc` breaks.
|
|
4
|
+
|
|
5
|
+
import { Layer, Schema as S } from 'effect'
|
|
6
|
+
import type { Store } from '../internal/store'
|
|
7
|
+
import * as Composition from '../compose/composition'
|
|
8
|
+
import { provide } from '../compose/provide'
|
|
9
|
+
import { type SlotChild, type SlotClass, slot } from '../compose/slot'
|
|
10
|
+
import { type Contract, type UiClass, ui } from '../compose/ui'
|
|
11
|
+
import * as State from '../state/state'
|
|
12
|
+
import * as StateGroup from '../state/stateGroup'
|
|
13
|
+
import { type EngineServices, type FeatureModule, featureModule, lazyImport } from './feature'
|
|
14
|
+
import * as Feature from './feature'
|
|
15
|
+
|
|
16
|
+
const FeedStateBase: State.StateClass<'feed', string> = State.make('feed', S.String)
|
|
17
|
+
class FeedState extends FeedStateBase {}
|
|
18
|
+
const FilterStateBase: State.StateClass<'filter', string> = State.make('filter', S.String)
|
|
19
|
+
class FilterState extends FilterStateBase {}
|
|
20
|
+
const TodosStatesBase: StateGroup.StateGroupClass<readonly [typeof FeedState, typeof FilterState]> =
|
|
21
|
+
StateGroup.make(FeedState, FilterState)
|
|
22
|
+
class TodosStates extends TodosStatesBase {}
|
|
23
|
+
|
|
24
|
+
// --- featureModule: the covariant-RIn bound (cast-free requirement honesty) ---
|
|
25
|
+
|
|
26
|
+
// A feature layer that reads FeedState's store and the shared engine — nothing else.
|
|
27
|
+
declare const readsFeed: Layer.Layer<{ readonly _out: true }, never, Store<string> | EngineServices>
|
|
28
|
+
|
|
29
|
+
// POSITIVE: declaring the State it reads in `requires` covers the read — compiles.
|
|
30
|
+
export const okState: FeatureModule<{ readonly _out: true }, readonly [typeof FeedState]> =
|
|
31
|
+
featureModule([FeedState], readsFeed)
|
|
32
|
+
|
|
33
|
+
// POSITIVE: a StateGroup contributes its members' stores via `ProvidedBy` — compiles.
|
|
34
|
+
export const okGroup: FeatureModule<{ readonly _out: true }, readonly [typeof TodosStates]> =
|
|
35
|
+
featureModule([TodosStates], readsFeed)
|
|
36
|
+
|
|
37
|
+
// NEGATIVE: empty `requires` provides only the engine, not Store<string>; covariant
|
|
38
|
+
// `RIn` makes the layer unassignable to the declared bound.
|
|
39
|
+
export const badEmpty: FeatureModule<{ readonly _out: true }, readonly []> =
|
|
40
|
+
// @ts-expect-error feature layer reads a store it does not declare in `requires`
|
|
41
|
+
featureModule([], readsFeed)
|
|
42
|
+
|
|
43
|
+
// --- provide(slot, Feature): RIn surfacing + composition match ---
|
|
44
|
+
|
|
45
|
+
const AppUiBase: UiClass<{ props: { readonly a: string }; events: {} }> = ui('AppUi')<{
|
|
46
|
+
props: { readonly a: string }
|
|
47
|
+
events: {}
|
|
48
|
+
}>()
|
|
49
|
+
class AppUi extends AppUiBase {}
|
|
50
|
+
const AppCompBase: Composition.CompositionClass<{ readonly a: string }, Contract<typeof AppUi>> =
|
|
51
|
+
Composition.make('AppComp', {
|
|
52
|
+
title: 'App',
|
|
53
|
+
props: S.Struct({ a: S.String }),
|
|
54
|
+
ui: AppUi,
|
|
55
|
+
})
|
|
56
|
+
class AppComp extends AppCompBase {}
|
|
57
|
+
const AppSlotBase: SlotClass<typeof AppComp> = slot('App')<typeof AppComp>()
|
|
58
|
+
class AppSlot extends AppSlotBase {}
|
|
59
|
+
|
|
60
|
+
const AppFeatureBase: Feature.FeatureClass<
|
|
61
|
+
{ readonly a: string },
|
|
62
|
+
Contract<typeof AppUi>,
|
|
63
|
+
never,
|
|
64
|
+
readonly []
|
|
65
|
+
> = Feature.make('appFeature', {
|
|
66
|
+
loadingStrategy: 'lazy',
|
|
67
|
+
composition: AppComp,
|
|
68
|
+
load: lazyImport(() => Promise.resolve({ default: featureModule([], Layer.empty) })),
|
|
69
|
+
placeholder: { loading: AppComp, failed: AppComp },
|
|
70
|
+
})
|
|
71
|
+
class AppFeature extends AppFeatureBase {}
|
|
72
|
+
|
|
73
|
+
// POSITIVE: the wired layer's open `RIn` is exactly `EngineServices` (requires=[]),
|
|
74
|
+
// so a scene must provide `Core` — surfaced cast-free by the overload.
|
|
75
|
+
export const wired: Layer.Layer<SlotChild, never, EngineServices> = provide(AppSlot, AppFeature)
|
|
76
|
+
|
|
77
|
+
// A feature over a DIFFERENT contract must not fill `AppSlot`.
|
|
78
|
+
const OtherUiBase: UiClass<{ props: { readonly n: number }; events: {} }> = ui('OtherUi')<{
|
|
79
|
+
props: { readonly n: number }
|
|
80
|
+
events: {}
|
|
81
|
+
}>()
|
|
82
|
+
class OtherUi extends OtherUiBase {}
|
|
83
|
+
const OtherCompBase: Composition.CompositionClass<{ readonly n: number }, Contract<typeof OtherUi>> =
|
|
84
|
+
Composition.make('OtherComp', {
|
|
85
|
+
title: 'Other',
|
|
86
|
+
props: S.Struct({ n: S.Number }),
|
|
87
|
+
ui: OtherUi,
|
|
88
|
+
})
|
|
89
|
+
class OtherComp extends OtherCompBase {}
|
|
90
|
+
const OtherFeatureBase: Feature.FeatureClass<
|
|
91
|
+
{ readonly n: number },
|
|
92
|
+
Contract<typeof OtherUi>,
|
|
93
|
+
never,
|
|
94
|
+
readonly []
|
|
95
|
+
> = Feature.make('otherFeature', {
|
|
96
|
+
loadingStrategy: 'lazy',
|
|
97
|
+
composition: OtherComp,
|
|
98
|
+
load: lazyImport(() => Promise.resolve({ default: featureModule([], Layer.empty) })),
|
|
99
|
+
placeholder: { loading: OtherComp, failed: OtherComp },
|
|
100
|
+
})
|
|
101
|
+
class OtherFeature extends OtherFeatureBase {}
|
|
102
|
+
|
|
103
|
+
// NEGATIVE: OtherFeature's composition props don't match AppSlot's child composition.
|
|
104
|
+
// @ts-expect-error feature composition does not match the slot's child type
|
|
105
|
+
export const mismatched: Layer.Layer<SlotChild, never, EngineServices> = provide(
|
|
106
|
+
AppSlot,
|
|
107
|
+
OtherFeature,
|
|
108
|
+
)
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Reform core — the renderer-neutral primitives and runtime.
|
|
2
|
+
// Definitions (`X.make`) are separate from implementations (`.live`)
|
|
3
|
+
// throughout, and nothing self-registers on import (principle #1, #6).
|
|
4
|
+
|
|
5
|
+
export * as State from './state/state'
|
|
6
|
+
export * as StateGroup from './state/stateGroup'
|
|
7
|
+
export * as StateFamily from './state/stateFamily'
|
|
8
|
+
export { StateToken } from './state/token'
|
|
9
|
+
export type { AnySource, Source } from './state/token'
|
|
10
|
+
|
|
11
|
+
export * as Event from './event/event'
|
|
12
|
+
// Direct type re-export alongside the namespace (TS2742 nameability): inferred
|
|
13
|
+
// types of layers built over intent/event unions reference `EventOf`, and the
|
|
14
|
+
// declaration emitter can only name it through a plain export.
|
|
15
|
+
export type { EventOf } from './event/event'
|
|
16
|
+
export * as EventGroup from './event/eventGroup'
|
|
17
|
+
|
|
18
|
+
export * as Reducer from './reducer/reducer'
|
|
19
|
+
export * as Calc from './calc/calc'
|
|
20
|
+
export * as CalcFamily from './calc/calcFamily'
|
|
21
|
+
export * as AsyncCalc from './calc/asyncCalc'
|
|
22
|
+
export { composeCalcs } from './calc/compose'
|
|
23
|
+
// The inputs-object/key/store types appearing in `AsyncCalc.live` /
|
|
24
|
+
// `RemoteState.live` signatures, exported so app code can annotate
|
|
25
|
+
// query-swapping helpers and layer types without re-spelling them.
|
|
26
|
+
export type { InputsObject, InputStores, InvalidateBy } from './internal/sources'
|
|
27
|
+
export {
|
|
28
|
+
AsyncData,
|
|
29
|
+
type AsyncError,
|
|
30
|
+
type AsyncIdle,
|
|
31
|
+
type AsyncLoading,
|
|
32
|
+
type AsyncSuccess,
|
|
33
|
+
} from './calc/asyncData'
|
|
34
|
+
export * as RemoteState from './remote/remoteState'
|
|
35
|
+
// Direct type re-exports alongside the namespace (TS2742 nameability): an app
|
|
36
|
+
// layer's inferred type references these (`Store<ReadonlyArray<PendingIntent<…>>>`),
|
|
37
|
+
// and the declaration emitter can only name them through a plain export.
|
|
38
|
+
export type { FailedIntent, PendingIntent, RemoteStateClass } from './remote/remoteState'
|
|
39
|
+
export * as SyncedStore from './synced/syncedStore'
|
|
40
|
+
// Direct type re-export alongside the namespace (TS2742 nameability): adapters
|
|
41
|
+
// (e.g. `@playfast/reform-db`) annotate the source handed to `SyncedStore.live`.
|
|
42
|
+
export type { SyncedSource, SyncedStoreClass } from './synced/syncedStore'
|
|
43
|
+
export * as Boundary from './boundary/boundary'
|
|
44
|
+
// Direct type re-exports alongside the namespace (same TS2742 nameability
|
|
45
|
+
// reason as the RemoteState types above): app code annotates props with these.
|
|
46
|
+
export type {
|
|
47
|
+
BoundaryErrored,
|
|
48
|
+
BoundaryPending,
|
|
49
|
+
BoundaryReady,
|
|
50
|
+
BoundaryState,
|
|
51
|
+
} from './boundary/boundary'
|
|
52
|
+
export * as Procedure from './procedure/procedure'
|
|
53
|
+
export * as Channel from './channel/channel'
|
|
54
|
+
|
|
55
|
+
export * as Composition from './compose/composition'
|
|
56
|
+
export type {
|
|
57
|
+
CompositionClass,
|
|
58
|
+
CompositionService,
|
|
59
|
+
Frame,
|
|
60
|
+
RenderEnv,
|
|
61
|
+
} from './compose/composition'
|
|
62
|
+
export {
|
|
63
|
+
absent,
|
|
64
|
+
each,
|
|
65
|
+
type EachFill,
|
|
66
|
+
isStructure,
|
|
67
|
+
mount,
|
|
68
|
+
one,
|
|
69
|
+
type OneFill,
|
|
70
|
+
type AbsentFill,
|
|
71
|
+
type SlotFill,
|
|
72
|
+
type Structure,
|
|
73
|
+
structureEquals,
|
|
74
|
+
type StructureSlots,
|
|
75
|
+
StructureTypeId,
|
|
76
|
+
when,
|
|
77
|
+
} from './compose/structure'
|
|
78
|
+
export { Props } from './compose/props'
|
|
79
|
+
export {
|
|
80
|
+
isSlot,
|
|
81
|
+
type Node,
|
|
82
|
+
slot,
|
|
83
|
+
type SlotClass,
|
|
84
|
+
type SlotInstance,
|
|
85
|
+
type SlotProps,
|
|
86
|
+
SlotTypeId,
|
|
87
|
+
} from './compose/slot'
|
|
88
|
+
export { type SlotRenderer } from './compose/host'
|
|
89
|
+
export * as Ui from './compose/ui'
|
|
90
|
+
export {
|
|
91
|
+
type DerivedContract,
|
|
92
|
+
isUi,
|
|
93
|
+
type MadeView,
|
|
94
|
+
ui,
|
|
95
|
+
type UiClass,
|
|
96
|
+
type UiContract,
|
|
97
|
+
type UiManifest,
|
|
98
|
+
UiTypeId,
|
|
99
|
+
UiViewContract,
|
|
100
|
+
type ViewImpl,
|
|
101
|
+
type WireSchemas,
|
|
102
|
+
type WiredUi,
|
|
103
|
+
type WiredUiManifest,
|
|
104
|
+
} from './compose/ui'
|
|
105
|
+
export { provide } from './compose/provide'
|
|
106
|
+
export type { Trigger } from './ui/trigger'
|
|
107
|
+
|
|
108
|
+
// Wire: the serializable rendered-tree model + pure diff/apply the remote
|
|
109
|
+
// transport sends over the wire and the client folds back. See REMOTE_UI.md §3.
|
|
110
|
+
export * as Wire from './wire/tree'
|
|
111
|
+
export type { WireNode, WirePatch, WireProp, WireTree } from './wire/tree'
|
|
112
|
+
|
|
113
|
+
// Triggers: the server-side handle registry bridging client trigger invocations
|
|
114
|
+
// back to live triggers, decoding payloads via the event schema. See REMOTE_UI.md §4.
|
|
115
|
+
export * as Triggers from './wire/triggers'
|
|
116
|
+
export {
|
|
117
|
+
TriggerRegistry,
|
|
118
|
+
type TriggerHandle,
|
|
119
|
+
type TriggerRegistryApi,
|
|
120
|
+
UnknownTrigger,
|
|
121
|
+
} from './wire/triggers'
|
|
122
|
+
|
|
123
|
+
// Feature: the lazy/eager code-split unit. Definitions stay eager and reflectable;
|
|
124
|
+
// the heavy `.live` layer loads on demand (an Effect, never a Promise). See LAZY.md.
|
|
125
|
+
export * as Feature from './feature/feature'
|
|
126
|
+
export {
|
|
127
|
+
type EngineServices,
|
|
128
|
+
type FailedProps,
|
|
129
|
+
type FeatureBinding,
|
|
130
|
+
FeatureBindingTypeId,
|
|
131
|
+
type FeatureClass,
|
|
132
|
+
type FeatureManifest,
|
|
133
|
+
featureModule,
|
|
134
|
+
type FeatureModule,
|
|
135
|
+
isFeature,
|
|
136
|
+
isFeatureBinding,
|
|
137
|
+
lazyImport,
|
|
138
|
+
type LoadedModule,
|
|
139
|
+
mountFeature,
|
|
140
|
+
type Placeholders,
|
|
141
|
+
type ProvidedBy,
|
|
142
|
+
type RequireCarrier,
|
|
143
|
+
} from './feature/feature'
|
|
144
|
+
export { type SlotChild } from './compose/slot'
|
|
145
|
+
|
|
146
|
+
// A scene bundles a composition with the closed wiring that runs it — the single
|
|
147
|
+
// handle the react host, proofs, and the dev tool all consume.
|
|
148
|
+
export { isScene, type MountedServices, type Scene, scene, seedScene } from './scene/scene'
|
|
149
|
+
|
|
150
|
+
// Runtime surface for hosts (`@reform/react`) and headless tests.
|
|
151
|
+
export { Bus, type Envelope, type Priority, publish, type Tagged } from './runtime/bus'
|
|
152
|
+
export { Engine, type ReducerEntry, Reducers } from './runtime/loop'
|
|
153
|
+
export {
|
|
154
|
+
type ChannelClass,
|
|
155
|
+
type ChannelPolicy,
|
|
156
|
+
type ChannelRuntime,
|
|
157
|
+
Channels,
|
|
158
|
+
type ProcedureEntry,
|
|
159
|
+
Procedures,
|
|
160
|
+
} from './channel/channel'
|
|
161
|
+
export { CurrentTracker, type Subscribable, type Tracker } from './internal/track'
|
|
162
|
+
export { CaptureSink, type CaptureSinkApi, type UiCapture } from './internal/capture'
|
|
163
|
+
export {
|
|
164
|
+
AsyncReducer,
|
|
165
|
+
AsyncSceneLayer,
|
|
166
|
+
DuplicateRegistration,
|
|
167
|
+
FeatureLoadFailed,
|
|
168
|
+
forceSync,
|
|
169
|
+
InvalidProvideTarget,
|
|
170
|
+
UnknownGroupState,
|
|
171
|
+
} from './internal/errors'
|
|
172
|
+
// Notification scheduler: hosts that need per-runtime isolation (concurrent SSR,
|
|
173
|
+
// multiple mounted roots) provide `Notifications` upstream of their state layers;
|
|
174
|
+
// everything else coalesces on the process-wide default.
|
|
175
|
+
export {
|
|
176
|
+
defaultScheduler,
|
|
177
|
+
makeScheduler,
|
|
178
|
+
Notifications,
|
|
179
|
+
notificationsLayer,
|
|
180
|
+
resolveScheduler,
|
|
181
|
+
type Scheduler,
|
|
182
|
+
} from './internal/scheduler'
|
|
183
|
+
export { makeStore, type Store } from './internal/store'
|
|
184
|
+
export type { FamilyStore } from './state/stateFamily'
|
|
185
|
+
// Primitive-authoring toolkit — exported for first-party companion packages
|
|
186
|
+
// (e.g. `@playfast/reform-db`, `@playfast/reform-resource`) that build their own
|
|
187
|
+
// first-class `Source`s which must participate in dependency tracking and
|
|
188
|
+
// reflection exactly like a `Calc`.
|
|
189
|
+
export { type Kind, type Manifest, yieldableClass } from './definition/definition'
|
|
190
|
+
export { readTracked } from './internal/track'
|
|
191
|
+
export { sameKey, wireSources, type WiredSources } from './internal/sources'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Context } from 'effect'
|
|
2
|
+
import type { Trigger } from '../ui/trigger'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A single rendered UI node's observable surface: which contract rendered, the
|
|
6
|
+
* props it received, and the event triggers it was handed. This is exactly what
|
|
7
|
+
* a headless inspector (the proof facade, the dev tool) needs — no DOM, no JSX.
|
|
8
|
+
*/
|
|
9
|
+
export interface UiCapture {
|
|
10
|
+
readonly name: string
|
|
11
|
+
readonly props: unknown
|
|
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
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Where captured renders are reported, when a capturing host is present. */
|
|
24
|
+
export interface CaptureSinkApi {
|
|
25
|
+
record(capture: UiCapture): void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An OPTIONAL service. Core's `ui` read consults it via `serviceOption`: when a
|
|
30
|
+
* capturing host provides it (proofs, the dev tool), every resolved view reports
|
|
31
|
+
* its `(props, events)` before rendering, giving a faithful tree without
|
|
32
|
+
* replacing any presentation. Production never provides it, so nothing is
|
|
33
|
+
* recorded and there is no cost beyond one `serviceOption` lookup per render.
|
|
34
|
+
*/
|
|
35
|
+
const CaptureSinkBase: Context.TagClass<CaptureSink, 'reform/internal/CaptureSink', CaptureSinkApi> =
|
|
36
|
+
Context.Tag('reform/internal/CaptureSink')<CaptureSink, CaptureSinkApi>()
|
|
37
|
+
|
|
38
|
+
export class CaptureSink extends CaptureSinkBase {}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { Effect } from 'effect'
|
|
2
|
-
import type { YieldWrap } from 'effect/Utils'
|
|
1
|
+
import type { Effect } from 'effect'
|
|
2
|
+
import type { YieldWrap } from 'effect/Utils'
|
|
3
|
+
|
|
3
4
|
/**
|
|
4
5
|
* The context (`R`) a generator body requires, recovered from the effects it
|
|
5
6
|
* yields. Shared by `Composition.live` and `Procedure.live`, whose bodies are
|
|
@@ -8,5 +9,8 @@ import type { YieldWrap } from 'effect/Utils';
|
|
|
8
9
|
* A body that never yields (`Eff = never`) requires nothing — guard that case
|
|
9
10
|
* first, or `infer R` resolves to `unknown` and leaks into the layer's `RIn`.
|
|
10
11
|
*/
|
|
11
|
-
export type Ctx<Eff> = [Eff] extends [never]
|
|
12
|
-
|
|
12
|
+
export type Ctx<Eff> = [Eff] extends [never]
|
|
13
|
+
? never
|
|
14
|
+
: [Eff] extends [YieldWrap<Effect.Effect<any, any, infer R>>]
|
|
15
|
+
? R
|
|
16
|
+
: never
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { expect, it } from '@effect/vitest'
|
|
2
|
+
import { Context, Effect, Layer, ManagedRuntime } from 'effect'
|
|
3
|
+
import { AsyncSceneLayer, forceSync } from '../index'
|
|
4
|
+
|
|
5
|
+
// `forceSync` is the sync-build boundary: reform materialises a scene's layer graph
|
|
6
|
+
// with `runSync` so the first frame renders synchronously (D2). A layer whose acquire
|
|
7
|
+
// suspends makes that `runSync` throw Effect's opaque `AsyncFiberException`; the
|
|
8
|
+
// boundary translates it into `AsyncSceneLayer`, which names the cause and the fix.
|
|
9
|
+
|
|
10
|
+
class Thing extends Context.Tag('test/Thing')<Thing, number>() {}
|
|
11
|
+
|
|
12
|
+
it('translates an async layer build into AsyncSceneLayer', () => {
|
|
13
|
+
// A layer whose acquire suspends — exactly what a consumer trips on.
|
|
14
|
+
const asyncLayer = Layer.effect(Thing, Effect.as(Effect.sleep('1 millis'), 1))
|
|
15
|
+
const runtime = ManagedRuntime.make(asyncLayer)
|
|
16
|
+
expect(() => forceSync(() => runtime.runSync(Effect.context<Thing>()))).toThrow(AsyncSceneLayer)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('passes a synchronous layer build through unchanged', () => {
|
|
20
|
+
const syncLayer = Layer.succeed(Thing, 1)
|
|
21
|
+
const runtime = ManagedRuntime.make(syncLayer)
|
|
22
|
+
const context = forceSync(() => runtime.runSync(Effect.context<Thing>()))
|
|
23
|
+
expect(Context.get(context, Thing)).toBe(1)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('re-throws a non-async error untouched', () => {
|
|
27
|
+
class Boom extends Error {}
|
|
28
|
+
expect(() =>
|
|
29
|
+
forceSync(() => {
|
|
30
|
+
throw new Boom('nope')
|
|
31
|
+
}),
|
|
32
|
+
).toThrow(Boom)
|
|
33
|
+
})
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Cause, Data, Option, Runtime } from 'effect'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tagged errors for the framework's synchronous boundaries. Even when thrown
|
|
5
|
+
* (React render, layer build, reflection), an error carries a `_tag` and its
|
|
6
|
+
* structured fields — never a bare `Error` — so callers and tooling can match on
|
|
7
|
+
* it. `Data.TaggedError` subclasses are real `Error`s, so `throw` still works.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The constructor shape `Data.TaggedError(tag)<A>` produces, named so the
|
|
12
|
+
* generated `.d.ts` can describe the `extends` base under `isolatedDeclarations`
|
|
13
|
+
* (which forbids an inferred expression in an extends clause).
|
|
14
|
+
*/
|
|
15
|
+
type TaggedErrorClass<Tag extends string, A extends Record<string, unknown>> = new (
|
|
16
|
+
args: A,
|
|
17
|
+
) => Cause.YieldableError & { readonly _tag: Tag } & Readonly<A>
|
|
18
|
+
|
|
19
|
+
/** The fieldless variant — a `Data.TaggedError(tag)<{}>` constructor. */
|
|
20
|
+
type EmptyTaggedErrorClass<Tag extends string> = new () => Cause.YieldableError & { readonly _tag: Tag }
|
|
21
|
+
|
|
22
|
+
const InvalidProvideTargetBase: EmptyTaggedErrorClass<'reform/InvalidProvideTarget'> =
|
|
23
|
+
Data.TaggedError('reform/InvalidProvideTarget')<{}>
|
|
24
|
+
|
|
25
|
+
/** `provide(target, …)` was handed something that is neither a ui contract nor a slot. */
|
|
26
|
+
export class InvalidProvideTarget extends InvalidProvideTargetBase {
|
|
27
|
+
override get message(): string {
|
|
28
|
+
return 'reform: provide target is neither a ui contract nor a slot'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const UnknownGroupStateBase: TaggedErrorClass<'reform/UnknownGroupState', { readonly name: string }> =
|
|
33
|
+
Data.TaggedError('reform/UnknownGroupState')<{ readonly name: string }>
|
|
34
|
+
|
|
35
|
+
/** `StateGroup.select(group, name)` named a state the group does not contain. */
|
|
36
|
+
export class UnknownGroupState extends UnknownGroupStateBase {
|
|
37
|
+
override get message(): string {
|
|
38
|
+
return `reform: unknown state '${this.name}' in group`
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const FeatureLoadFailedBase: TaggedErrorClass<'reform/FeatureLoadFailed', { readonly cause: unknown }> =
|
|
43
|
+
Data.TaggedError('reform/FeatureLoadFailed')<{ readonly cause: unknown }>
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A lazy `Feature`'s `load` Effect failed — the dynamic `import()` rejected (a
|
|
47
|
+
* missing chunk, a network failure, a module-eval throw). Surfaced as a typed
|
|
48
|
+
* failure the host hands to the feature's `placeholder.failed`, never a bare
|
|
49
|
+
* rejection. `cause` is the original rejection value.
|
|
50
|
+
*/
|
|
51
|
+
export class FeatureLoadFailed extends FeatureLoadFailedBase {
|
|
52
|
+
override get message(): string {
|
|
53
|
+
return `reform: a feature failed to load (${String(this.cause)})`
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const AsyncReducerBase: EmptyTaggedErrorClass<'reform/AsyncReducer'> =
|
|
58
|
+
Data.TaggedError('reform/AsyncReducer')<{}>
|
|
59
|
+
|
|
60
|
+
/** A reducer fold returned a thenable — folds must be pure synchronous writes. */
|
|
61
|
+
export class AsyncReducer extends AsyncReducerBase {
|
|
62
|
+
override get message(): string {
|
|
63
|
+
return 'reform: a reducer fold must be synchronous (it returned a Promise)'
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const AsyncSceneLayerBase: EmptyTaggedErrorClass<'reform/AsyncSceneLayer'> =
|
|
68
|
+
Data.TaggedError('reform/AsyncSceneLayer')<{}>
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A scene layer performed async work during reform's synchronous layer build.
|
|
72
|
+
* Reform materialises a scene's layer graph with `runSync` so the first frame
|
|
73
|
+
* renders synchronously (D2); a `Layer.effect`/`scoped` whose acquire suspends
|
|
74
|
+
* (HTTP, opening a DB, `Effect.sleep`) makes that `runSync` throw Effect's opaque
|
|
75
|
+
* `AsyncFiberException`. `forceSync` catches it and rethrows this, which names the
|
|
76
|
+
* cause and the sanctioned fix instead of pointing at Effect internals.
|
|
77
|
+
*/
|
|
78
|
+
export class AsyncSceneLayer extends AsyncSceneLayerBase {
|
|
79
|
+
override get message(): string {
|
|
80
|
+
return (
|
|
81
|
+
'reform: a scene layer performed async work while building. Reform builds the ' +
|
|
82
|
+
'scene synchronously so the first frame can render — a layer acquire must be ' +
|
|
83
|
+
'synchronous (Layer.sync/Layer.succeed). For async setup (open a connection, ' +
|
|
84
|
+
'fetch config), use Resource.live from @playfast/reform-resource, which builds ' +
|
|
85
|
+
'synchronously and resolves in the background.'
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether a thrown value is Effect's "ran async under runSync" defect. `runSync`
|
|
92
|
+
* throws the bare `AsyncFiberException`; a `ManagedRuntime.runSync` whose layer
|
|
93
|
+
* build suspends instead throws a `FiberFailure` carrying it as a `Die`, so both
|
|
94
|
+
* shapes are checked.
|
|
95
|
+
*/
|
|
96
|
+
const isAsyncBuildDefect = (error: unknown): boolean => {
|
|
97
|
+
if (Runtime.isAsyncFiberException(error)) return true
|
|
98
|
+
if (!Runtime.isFiberFailure(error)) return false
|
|
99
|
+
const die = Cause.dieOption(error[Runtime.FiberFailureCauseId])
|
|
100
|
+
return Option.isSome(die) && Runtime.isAsyncFiberException(die.value)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Run a synchronous runtime read (the scene layer build / context capture) and
|
|
105
|
+
* translate Effect's async-fiber defect into `AsyncSceneLayer`; any other throw
|
|
106
|
+
* passes through unchanged. The single home for the sync-build boundary, shared by
|
|
107
|
+
* the React host and the remote server.
|
|
108
|
+
*/
|
|
109
|
+
export const forceSync = <A>(thunk: () => A): A => {
|
|
110
|
+
try {
|
|
111
|
+
return thunk()
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (isAsyncBuildDefect(error)) throw new AsyncSceneLayer()
|
|
114
|
+
throw error
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const DuplicateRegistrationBase: TaggedErrorClass<
|
|
119
|
+
'reform/DuplicateRegistration',
|
|
120
|
+
{ readonly kind: string; readonly name: string }
|
|
121
|
+
> = Data.TaggedError('reform/DuplicateRegistration')<{ readonly kind: string; readonly name: string }>
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Two distinct primitives were registered under one name into the same runtime —
|
|
125
|
+
* a silent footgun, since names key DI tags and channel lanes. Thrown for a
|
|
126
|
+
* channel name reused with a different policy (the second would otherwise be
|
|
127
|
+
* dropped). Reducer/procedure name clashes are warned (logged), not thrown.
|
|
128
|
+
*/
|
|
129
|
+
export class DuplicateRegistration extends DuplicateRegistrationBase {
|
|
130
|
+
override get message(): string {
|
|
131
|
+
return `reform: duplicate ${this.kind} registered under name '${this.name}'`
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from '@effect/vitest'
|
|
2
|
+
import { Schema as S } from 'effect'
|
|
3
|
+
import { State } from '../index'
|
|
4
|
+
import { makeStore } from './store'
|
|
5
|
+
|
|
6
|
+
// Every reform value should print like an Effect value (an `_id`-tagged JSON),
|
|
7
|
+
// not `[object Object]` — via `toJSON`/`toString`/the Node inspect symbol.
|
|
8
|
+
|
|
9
|
+
describe('Inspectable', () => {
|
|
10
|
+
it('a Store reports its current value as tagged JSON', () => {
|
|
11
|
+
const store = makeStore(41)
|
|
12
|
+
expect(store.toJSON()).toEqual({ _id: 'reform/Store', value: 41 })
|
|
13
|
+
store.set(42)
|
|
14
|
+
expect(store.toJSON()).toEqual({ _id: 'reform/Store', value: 42 })
|
|
15
|
+
// toString and JSON.stringify route through the same toJSON.
|
|
16
|
+
expect(JSON.parse(JSON.stringify(store))).toEqual({ _id: 'reform/Store', value: 42 })
|
|
17
|
+
expect(store.toString()).toContain('reform/Store')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('a definition reports its manifest', () => {
|
|
21
|
+
class Count extends State.make('count', S.Number) {}
|
|
22
|
+
// Inspectable is attached at runtime (definitions are reflected via their
|
|
23
|
+
// manifest); reach the method through a typed view.
|
|
24
|
+
const inspectable = Count as unknown as { toJSON(): unknown }
|
|
25
|
+
expect(inspectable.toJSON()).toBe(Count.manifest)
|
|
26
|
+
expect(String(Count)).toContain('"kind": "State"')
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { format, type Inspectable, NodeInspectSymbol } from 'effect/Inspectable'
|
|
2
|
+
|
|
3
|
+
export { type Inspectable, NodeInspectSymbol }
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `Inspectable` trio built around a `toJSON`, so every reform value prints
|
|
7
|
+
* like an Effect value — `_id`-tagged JSON under `console.log`, Node's
|
|
8
|
+
* inspector, `JSON.stringify`, and `String(...)` — instead of `[object Object]`.
|
|
9
|
+
* Mirrors Effect's `BaseProto` / `Inspectable.Class` (`Inspectable.ts:170`).
|
|
10
|
+
*/
|
|
11
|
+
export const inspectable = (toJSON: () => unknown): Inspectable => ({
|
|
12
|
+
toJSON,
|
|
13
|
+
toString: () => format(toJSON()),
|
|
14
|
+
[NodeInspectSymbol]: () => toJSON(),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Attach the `Inspectable` trio to an existing object as *non-enumerable* own
|
|
19
|
+
* properties (so they don't show up in `Object.keys`/`for…in` over a definition
|
|
20
|
+
* class). Used where the value is built by other means — the definition classes,
|
|
21
|
+
* whose statics are merged in separately.
|
|
22
|
+
*/
|
|
23
|
+
export const attachInspectable = <T extends object>(target: T, toJSON: () => unknown): T => {
|
|
24
|
+
const trio = inspectable(toJSON) as unknown as Record<PropertyKey, unknown>
|
|
25
|
+
for (const key of Reflect.ownKeys(trio)) {
|
|
26
|
+
Object.defineProperty(target, key, {
|
|
27
|
+
value: trio[key],
|
|
28
|
+
enumerable: false,
|
|
29
|
+
writable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
return target
|
|
34
|
+
}
|