@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,65 @@
|
|
|
1
|
+
import { Effect, PubSub, Runtime, type Schema } from 'effect'
|
|
2
|
+
import { type Manifest, definitionClass } from '../definition/definition'
|
|
3
|
+
import { Bus, publish, type Tagged } from '../runtime/bus'
|
|
4
|
+
import type { Trigger } from '../ui/trigger'
|
|
5
|
+
|
|
6
|
+
/** The dispatched value of an event: `{ _tag, ...payload }`, matched with `Match.tags`. */
|
|
7
|
+
export type EventOf<N extends string, P> = Tagged & { readonly _tag: N } & P
|
|
8
|
+
|
|
9
|
+
export interface EventManifest<N extends string, P> extends Manifest {
|
|
10
|
+
readonly kind: 'Event'
|
|
11
|
+
readonly name: N
|
|
12
|
+
readonly schema: Schema.Schema<P, any>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface EventClass<out N extends string, in out P> {
|
|
16
|
+
new (): {}
|
|
17
|
+
readonly manifest: EventManifest<N, P>
|
|
18
|
+
readonly tag: N
|
|
19
|
+
/** Carrier for `Event.construct` — builds the tagged value `{ _tag, ...payload }`. */
|
|
20
|
+
readonly build: (payload: P) => EventOf<N, P>
|
|
21
|
+
/** Carrier for `Event.trigger` — the UI callback effect (High priority). */
|
|
22
|
+
readonly trigger: Effect.Effect<Trigger<P>, never, Bus>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type AnyEvent = EventClass<string, any>
|
|
26
|
+
export type EventType<E> = E extends EventClass<infer N, infer P> ? EventOf<N, P> : never
|
|
27
|
+
|
|
28
|
+
/** A pure data fact. Events depend on nothing — only reducers tie them to state. */
|
|
29
|
+
export const make = <const N extends string, P>(
|
|
30
|
+
name: N,
|
|
31
|
+
schema: Schema.Schema<P, any>,
|
|
32
|
+
): EventClass<N, P> => {
|
|
33
|
+
const build = (payload: P): EventOf<N, P> => ({ _tag: name, ...payload })
|
|
34
|
+
const trigger = Effect.gen(function* () {
|
|
35
|
+
const bus = yield* Bus
|
|
36
|
+
const runtime = yield* Effect.runtime<never>()
|
|
37
|
+
// Dispatch synchronously: on the unbounded bus `publish` only enqueues and
|
|
38
|
+
// returns, so `runSync` never suspends and no fiber is forked per UI event.
|
|
39
|
+
// The drain loop still folds reducers on its own fiber — publish is pure offer.
|
|
40
|
+
return (payload: P) => {
|
|
41
|
+
Runtime.runSync(runtime)(PubSub.publish(bus, { priority: 'High', event: build(payload) }))
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
return definitionClass<EventClass<N, P>>({
|
|
45
|
+
manifest: { kind: 'Event' as const, name, schema },
|
|
46
|
+
tag: name,
|
|
47
|
+
build,
|
|
48
|
+
trigger,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Construct the tagged value for an event (used for `boot` events). */
|
|
53
|
+
export const construct = <N extends string, P>(event: EventClass<N, P>, payload: P): EventOf<N, P> =>
|
|
54
|
+
event.build(payload)
|
|
55
|
+
|
|
56
|
+
/** Dispatch as a follow-up (Normal priority) from a procedure/effect. */
|
|
57
|
+
export const dispatch = <N extends string, P>(
|
|
58
|
+
event: EventClass<N, P>,
|
|
59
|
+
payload: P,
|
|
60
|
+
): Effect.Effect<void, never, Bus> => publish('Normal', event.build(payload))
|
|
61
|
+
|
|
62
|
+
/** A callback for the UI that dispatches at High priority via the runtime. */
|
|
63
|
+
export const trigger = <N extends string, P>(
|
|
64
|
+
event: EventClass<N, P>,
|
|
65
|
+
): Effect.Effect<Trigger<P>, never, Bus> => event.trigger
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { definitionClass } from '../definition/definition'
|
|
2
|
+
import type { AnyEvent } from './event'
|
|
3
|
+
|
|
4
|
+
export interface EventGroupClass<Members extends ReadonlyArray<AnyEvent>> {
|
|
5
|
+
new (): {}
|
|
6
|
+
readonly kind: 'EventGroup'
|
|
7
|
+
readonly members: Members
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Compose events into a group, the same variadic shape as `StateGroup`/`RpcGroup`. */
|
|
11
|
+
export const make = <const Members extends ReadonlyArray<AnyEvent>>(
|
|
12
|
+
...members: Members
|
|
13
|
+
): EventGroupClass<Members> =>
|
|
14
|
+
definitionClass<EventGroupClass<Members>>({ kind: 'EventGroup' as const, members })
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type Cause, Duration, Effect, Exit, Layer, ManagedRuntime, Scope } from 'effect'
|
|
2
|
+
import { Schema as S } from 'effect'
|
|
3
|
+
import { expect, test } from 'vitest'
|
|
4
|
+
import * as Composition from '../compose/composition'
|
|
5
|
+
import { ui } from '../compose/ui'
|
|
6
|
+
import * as Event from '../event/event'
|
|
7
|
+
import * as Reducer from '../reducer/reducer'
|
|
8
|
+
import * as State from '../state/state'
|
|
9
|
+
import { publish } from '../runtime/bus'
|
|
10
|
+
import { Engine, Reducers } from '../runtime/loop'
|
|
11
|
+
import * as Feature from './feature'
|
|
12
|
+
import { type EngineServices, featureModule, mountFeature } from './feature'
|
|
13
|
+
|
|
14
|
+
// A self-contained feature: a Count state + a reducer that folds Tick. The base
|
|
15
|
+
// seeds Count; the logic provides the reducer over it and leaves the engine
|
|
16
|
+
// (Reducers registry) OPEN, so `mountFeature` builds it against the live engine.
|
|
17
|
+
class Tick extends Event.make('Tick', S.Struct({})) {}
|
|
18
|
+
class Count extends State.make('count', S.Number) {}
|
|
19
|
+
class CountReducer extends Reducer.make('CountReducer', { states: [Count], events: [Tick] }) {}
|
|
20
|
+
|
|
21
|
+
const mod = featureModule(
|
|
22
|
+
[],
|
|
23
|
+
Reducer.live(CountReducer, (n) => n + 1).pipe(Layer.provideMerge(State.live(Count, 0))),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
// A trivial composition to hang the feature off (definitions only).
|
|
27
|
+
class CtrUi extends ui('CtrUi')<{ props: {}; events: {} }>() {}
|
|
28
|
+
class CtrComp extends Composition.make('CtrComp', { title: 'Counter', ui: CtrUi }) {}
|
|
29
|
+
|
|
30
|
+
class CounterFeature extends Feature.make('counter', {
|
|
31
|
+
composition: CtrComp,
|
|
32
|
+
module: mod,
|
|
33
|
+
boot: [Event.construct(Tick, {})],
|
|
34
|
+
}) {}
|
|
35
|
+
|
|
36
|
+
// Poll a read until it satisfies a predicate (the loop drains on a forked fiber).
|
|
37
|
+
const waitUntil = <A, R>(
|
|
38
|
+
read: Effect.Effect<A, never, R>,
|
|
39
|
+
pred: (a: A) => boolean,
|
|
40
|
+
): Effect.Effect<A, Cause.TimeoutException, R> =>
|
|
41
|
+
Effect.flatMap(read, (a) =>
|
|
42
|
+
pred(a)
|
|
43
|
+
? Effect.succeed(a)
|
|
44
|
+
: Effect.flatMap(Effect.sleep(Duration.millis(1)), () => waitUntil(read, pred)),
|
|
45
|
+
).pipe(Effect.timeout(Duration.seconds(2)))
|
|
46
|
+
|
|
47
|
+
test('a mounted feature registers on the shared loop, boots, and folds dispatched events', async () => {
|
|
48
|
+
const runtime = ManagedRuntime.make(Engine)
|
|
49
|
+
try {
|
|
50
|
+
await runtime.runPromise(
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
const engineContext = yield* Effect.context<EngineServices>()
|
|
53
|
+
const reducers = yield* Reducers
|
|
54
|
+
const baseline = reducers.entries.length
|
|
55
|
+
|
|
56
|
+
const scope = yield* Scope.make()
|
|
57
|
+
const ctx = yield* mountFeature(CounterFeature.binding, engineContext).pipe(
|
|
58
|
+
Effect.provideService(Scope.Scope, scope),
|
|
59
|
+
)
|
|
60
|
+
// Registered into the SHARED registry.
|
|
61
|
+
expect(reducers.entries.length).toBe(baseline + 1)
|
|
62
|
+
|
|
63
|
+
// boot dispatched Tick → Count folds to 1 on the shared loop.
|
|
64
|
+
const booted = yield* waitUntil(Count.pipe(Effect.provide(ctx)), (n) => n === 1)
|
|
65
|
+
expect(booted).toBe(1)
|
|
66
|
+
|
|
67
|
+
// A further dispatch on the shared bus folds again → 2.
|
|
68
|
+
yield* publish('High', Event.construct(Tick, {}))
|
|
69
|
+
const folded = yield* waitUntil(Count.pipe(Effect.provide(ctx)), (n) => n === 2)
|
|
70
|
+
expect(folded).toBe(2)
|
|
71
|
+
|
|
72
|
+
// Unmount: closing the feature scope unregisters its reducer — the entry is
|
|
73
|
+
// reclaimed, not left folding onto an orphaned store.
|
|
74
|
+
yield* Scope.close(scope, Exit.succeed(undefined))
|
|
75
|
+
expect(reducers.entries.length).toBe(baseline)
|
|
76
|
+
expect(reducers.byTag.get('Tick')).toBeUndefined()
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
} finally {
|
|
80
|
+
await runtime.dispose()
|
|
81
|
+
}
|
|
82
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Effect, Layer } from 'effect'
|
|
2
|
+
import { expect, test } from 'vitest'
|
|
3
|
+
import * as Composition from '../compose/composition'
|
|
4
|
+
import { ui } from '../compose/ui'
|
|
5
|
+
import * as Feature from './feature'
|
|
6
|
+
import { featureModule, isFeature, isFeatureBinding, lazyImport } from './feature'
|
|
7
|
+
|
|
8
|
+
// Minimal composition surface to hang features off — definitions only, no logic.
|
|
9
|
+
class TestUi extends ui('TestUi')<{ props: {}; events: {} }>() {}
|
|
10
|
+
class TestComp extends Composition.make('TestComp', { title: 'Test', ui: TestUi }) {}
|
|
11
|
+
class FailComp extends Composition.make('FailComp', { title: 'Fail', ui: TestUi }) {}
|
|
12
|
+
|
|
13
|
+
// A trivial loaded module: empty layer, no shared requirements.
|
|
14
|
+
const mod = featureModule([], Layer.empty)
|
|
15
|
+
|
|
16
|
+
class LazyFeature extends Feature.make('lazyFeature', {
|
|
17
|
+
loadingStrategy: 'lazy',
|
|
18
|
+
composition: TestComp,
|
|
19
|
+
load: lazyImport(() => Promise.resolve({ default: mod })),
|
|
20
|
+
placeholder: { loading: TestComp, failed: FailComp },
|
|
21
|
+
}) {}
|
|
22
|
+
|
|
23
|
+
class EagerFeature extends Feature.make('eagerFeature', {
|
|
24
|
+
composition: TestComp,
|
|
25
|
+
module: mod,
|
|
26
|
+
}) {}
|
|
27
|
+
|
|
28
|
+
test('a lazy feature reflects as a Feature manifest with placeholders, no chunk loaded', () => {
|
|
29
|
+
expect(LazyFeature.manifest.kind).toBe('Feature')
|
|
30
|
+
expect(LazyFeature.manifest.name).toBe('lazyFeature')
|
|
31
|
+
expect(LazyFeature.manifest.strategy).toBe('lazy')
|
|
32
|
+
expect(LazyFeature.manifest.composition.name).toBe('TestComp')
|
|
33
|
+
expect(LazyFeature.manifest.placeholder?.loading.name).toBe('TestComp')
|
|
34
|
+
expect(LazyFeature.manifest.placeholder?.failed.name).toBe('FailComp')
|
|
35
|
+
expect(LazyFeature.eagerModule).toBeUndefined()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('an eager feature reflects as default strategy with its module available', () => {
|
|
39
|
+
expect(EagerFeature.manifest.strategy).toBe('default')
|
|
40
|
+
expect(EagerFeature.manifest.placeholder).toBeUndefined()
|
|
41
|
+
expect(EagerFeature.eagerModule).toBe(mod)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('isFeature / isFeatureBinding discriminate', () => {
|
|
45
|
+
expect(isFeature(LazyFeature)).toBe(true)
|
|
46
|
+
expect(isFeature(TestComp)).toBe(false)
|
|
47
|
+
expect(isFeature(null)).toBe(false)
|
|
48
|
+
expect(isFeatureBinding(LazyFeature.binding)).toBe(true)
|
|
49
|
+
expect(isFeatureBinding(TestComp)).toBe(false)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('load is an Effect (not a thenable) that yields the module and its requires', async () => {
|
|
53
|
+
// No `.then` on `load` — it is an Effect, run on the Effect runtime.
|
|
54
|
+
expect(typeof (LazyFeature.load as { then?: unknown }).then).toBe('undefined')
|
|
55
|
+
const loaded = await Effect.runPromise(LazyFeature.load)
|
|
56
|
+
expect(loaded.requires).toEqual([])
|
|
57
|
+
expect(loaded).toBe(mod)
|
|
58
|
+
// default strategy resolves immediately to its module.
|
|
59
|
+
expect(await Effect.runPromise(EagerFeature.load)).toBe(mod)
|
|
60
|
+
})
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import { type Context, Effect, Layer, type Scope } from 'effect'
|
|
2
|
-
import { type Engine } from '../runtime/loop'
|
|
3
|
-
import { type StoresOf } from '../internal/sources'
|
|
4
|
-
import { FeatureLoadFailed } from '../internal/errors'
|
|
5
|
-
import { type Manifest } from '../definition/definition'
|
|
6
|
-
import { type CompositionClass } from '../compose/composition'
|
|
7
|
-
import { type UiContract } from '../compose/ui'
|
|
8
|
-
import { type Tagged } from '../runtime/bus'
|
|
1
|
+
import { type Context, Effect, Layer, type Scope } from 'effect'
|
|
2
|
+
import { type Engine } from '../runtime/loop'
|
|
3
|
+
import { type StoresOf } from '../internal/sources'
|
|
4
|
+
import { FeatureLoadFailed } from '../internal/errors'
|
|
5
|
+
import { type Manifest, definitionClass } from '../definition/definition'
|
|
6
|
+
import { type CompositionClass } from '../compose/composition'
|
|
7
|
+
import { type UiContract } from '../compose/ui'
|
|
8
|
+
import { publish, type Tagged } from '../runtime/bus'
|
|
9
|
+
|
|
10
|
+
// A `Feature` is the unit of lazy code-split, ownership, reflection, and proof.
|
|
11
|
+
// It splits on the seam Reform already has: definitions (manifests, schemas, the
|
|
12
|
+
// composition class, placeholders) stay eager and reflectable; the heavy `.live`
|
|
13
|
+
// layer loads on demand. The whole-app *shape* stays inspectable with no chunk
|
|
14
|
+
// loaded. Everything here is additive — `provide(slot, Composition)` and eager
|
|
15
|
+
// scenes are unchanged. See `LAZY.md` for the design rationale and variance proof.
|
|
16
|
+
|
|
9
17
|
/**
|
|
10
18
|
* The always-on engine services a feature builds against: the shared event `Bus`
|
|
11
19
|
* plus the reducer/channel/procedure registries. Derived from the `Engine`
|
|
@@ -13,20 +21,18 @@ import { type Tagged } from '../runtime/bus';
|
|
|
13
21
|
* these only as OPEN requirements (its `RIn`) and must never *provide* them — that
|
|
14
22
|
* is what keeps the engine single and shared across every lazily-mounted feature.
|
|
15
23
|
*/
|
|
16
|
-
export type EngineServices = Layer.Layer.Success<typeof Engine
|
|
24
|
+
export type EngineServices = Layer.Layer.Success<typeof Engine>
|
|
25
|
+
|
|
17
26
|
/**
|
|
18
27
|
* A shared-state carrier a feature may declare in `requires`: a `State`,
|
|
19
28
|
* `StateFamily`, `Calc`, or `AsyncCalc` (each carrying a `store` tag), or a
|
|
20
29
|
* `StateGroup` (carrying `members`, each with its own `store`). Per the design,
|
|
21
30
|
* a feature depends on shared state/calc only — never on another feature.
|
|
22
31
|
*/
|
|
23
|
-
export type RequireCarrier =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
readonly store: Context.Tag<any, any>;
|
|
28
|
-
}>;
|
|
29
|
-
};
|
|
32
|
+
export type RequireCarrier =
|
|
33
|
+
| { readonly store: Context.Tag<any, any> }
|
|
34
|
+
| { readonly members: ReadonlyArray<{ readonly store: Context.Tag<any, any> }> }
|
|
35
|
+
|
|
30
36
|
/**
|
|
31
37
|
* The Context services a `requires` tuple makes available to a feature layer: a
|
|
32
38
|
* `StateGroup` contributes its members' stores (the shared `StoresOf`); every
|
|
@@ -37,12 +43,15 @@ export type RequireCarrier = {
|
|
|
37
43
|
* is ordinary assignability — no `as`, no `infer` gymnastics in user code.
|
|
38
44
|
*/
|
|
39
45
|
export type ProvidedBy<Requires extends ReadonlyArray<RequireCarrier>> = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
[I in keyof Requires]: Requires[I] extends {
|
|
47
|
+
readonly members: infer M extends ReadonlyArray<unknown>
|
|
48
|
+
}
|
|
49
|
+
? StoresOf<M>
|
|
50
|
+
: Requires[I] extends { readonly store: Context.Tag<infer Svc, any> }
|
|
51
|
+
? Svc
|
|
52
|
+
: never
|
|
53
|
+
}[number]
|
|
54
|
+
|
|
46
55
|
/**
|
|
47
56
|
* A loaded feature's two artifacts. `requires` is the runtime-enumerable list the
|
|
48
57
|
* editor reads (types are erased, so the carriers are kept as values); `layer` is
|
|
@@ -53,9 +62,10 @@ export type ProvidedBy<Requires extends ReadonlyArray<RequireCarrier>> = {
|
|
|
53
62
|
* a feature's lazy half.
|
|
54
63
|
*/
|
|
55
64
|
export interface FeatureModule<ROut, Requires extends ReadonlyArray<RequireCarrier>> {
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
readonly requires: Requires
|
|
66
|
+
readonly layer: Layer.Layer<ROut, never, EngineServices | ProvidedBy<Requires>>
|
|
58
67
|
}
|
|
68
|
+
|
|
59
69
|
/**
|
|
60
70
|
* Publish a feature's lazy half: pair its `requires` (a runtime tuple of shared
|
|
61
71
|
* carriers) with its `layer`. The covariant `RIn` bound does the checking — a
|
|
@@ -67,7 +77,11 @@ export interface FeatureModule<ROut, Requires extends ReadonlyArray<RequireCarri
|
|
|
67
77
|
* be open `RIn`), structurally enforcing the liveness rule: a feature can never
|
|
68
78
|
* bundle its own engine — it always shares the root's bus and registries.
|
|
69
79
|
*/
|
|
70
|
-
export
|
|
80
|
+
export const featureModule = <const Requires extends ReadonlyArray<RequireCarrier>, ROut>(
|
|
81
|
+
requires: Requires,
|
|
82
|
+
layer: Layer.Layer<ROut, never, EngineServices | ProvidedBy<Requires>>,
|
|
83
|
+
): FeatureModule<ROut, Requires> => ({ requires, layer })
|
|
84
|
+
|
|
71
85
|
/**
|
|
72
86
|
* Wrap a dynamic `import()` of a feature's `.live` module as an `Effect` — Reform
|
|
73
87
|
* never exposes a Promise/`await` on the feature surface. The unavoidable thenable
|
|
@@ -75,14 +89,20 @@ export declare const featureModule: <const Requires extends ReadonlyArray<Requir
|
|
|
75
89
|
* throw) surfaces as a typed `FeatureLoadFailed` the host hands to
|
|
76
90
|
* `placeholder.failed`. The module's default export is its `FeatureModule`.
|
|
77
91
|
*/
|
|
78
|
-
export
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
export const lazyImport = <M>(
|
|
93
|
+
thunk: () => Promise<{ readonly default: M }>,
|
|
94
|
+
): Effect.Effect<M, FeatureLoadFailed> =>
|
|
95
|
+
Effect.tryPromise({
|
|
96
|
+
try: thunk,
|
|
97
|
+
catch: (cause) => new FeatureLoadFailed({ cause }),
|
|
98
|
+
}).pipe(Effect.map((module) => module.default))
|
|
99
|
+
|
|
81
100
|
/** The props the host injects into a lazy feature's `failed` placeholder. */
|
|
82
101
|
export interface FailedProps {
|
|
83
|
-
|
|
84
|
-
|
|
102
|
+
readonly error: FeatureLoadFailed
|
|
103
|
+
readonly retry: () => void
|
|
85
104
|
}
|
|
105
|
+
|
|
86
106
|
/**
|
|
87
107
|
* The reflectable shape of a feature: its strategy, the composition it mounts,
|
|
88
108
|
* and (when lazy) its placeholder compositions — all as manifests, so the editor
|
|
@@ -90,21 +110,16 @@ export interface FailedProps {
|
|
|
90
110
|
* runtime data and lives on the `FeatureBinding`, not here.
|
|
91
111
|
*/
|
|
92
112
|
export interface FeatureManifest extends Manifest {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
readonly
|
|
100
|
-
|
|
101
|
-
readonly kind: 'Composition';
|
|
102
|
-
};
|
|
103
|
-
readonly failed: Manifest & {
|
|
104
|
-
readonly kind: 'Composition';
|
|
105
|
-
};
|
|
106
|
-
};
|
|
113
|
+
readonly kind: 'Feature'
|
|
114
|
+
readonly name: string
|
|
115
|
+
readonly strategy: 'lazy' | 'default'
|
|
116
|
+
readonly composition: Manifest & { readonly kind: 'Composition' }
|
|
117
|
+
readonly placeholder?: {
|
|
118
|
+
readonly loading: Manifest & { readonly kind: 'Composition' }
|
|
119
|
+
readonly failed: Manifest & { readonly kind: 'Composition' }
|
|
120
|
+
}
|
|
107
121
|
}
|
|
122
|
+
|
|
108
123
|
/**
|
|
109
124
|
* A feature's loaded artifacts with type params erased, for the renderer-neutral,
|
|
110
125
|
* strategy-uniform host. The erasure is cast-free *into*: because `Layer`'s `ROut`
|
|
@@ -114,12 +129,14 @@ export interface FeatureManifest extends Manifest {
|
|
|
114
129
|
* documented boundary cast there, mirroring `buildRuntime`).
|
|
115
130
|
*/
|
|
116
131
|
export interface LoadedModule {
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
readonly requires: ReadonlyArray<RequireCarrier>
|
|
133
|
+
readonly layer: Layer.Layer<never, never, unknown>
|
|
119
134
|
}
|
|
135
|
+
|
|
120
136
|
/** Nominal brand identifying the value a `provide(slot, Feature)` puts under a slot tag. */
|
|
121
|
-
export
|
|
122
|
-
export type FeatureBindingTypeId = typeof FeatureBindingTypeId
|
|
137
|
+
export const FeatureBindingTypeId: unique symbol = Symbol.for('reform/FeatureBinding')
|
|
138
|
+
export type FeatureBindingTypeId = typeof FeatureBindingTypeId
|
|
139
|
+
|
|
123
140
|
/**
|
|
124
141
|
* What `provide(slot, Feature)` binds under the slot tag — everything the host
|
|
125
142
|
* needs to mount the feature, with types erased (the host is renderer-neutral and
|
|
@@ -130,19 +147,22 @@ export type FeatureBindingTypeId = typeof FeatureBindingTypeId;
|
|
|
130
147
|
* each.
|
|
131
148
|
*/
|
|
132
149
|
export interface FeatureBinding {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
readonly [FeatureBindingTypeId]: FeatureBindingTypeId
|
|
151
|
+
readonly manifest: FeatureManifest
|
|
152
|
+
readonly composition: CompositionClass<unknown>
|
|
153
|
+
readonly strategy: 'lazy' | 'default'
|
|
154
|
+
readonly load: Effect.Effect<LoadedModule, FeatureLoadFailed>
|
|
155
|
+
readonly placeholder?: {
|
|
156
|
+
readonly loading: CompositionClass<unknown>
|
|
157
|
+
readonly failed: CompositionClass<unknown>
|
|
158
|
+
}
|
|
159
|
+
readonly boot: ReadonlyArray<Tagged>
|
|
143
160
|
}
|
|
161
|
+
|
|
144
162
|
/** Whether a slot's bound child is a lazy/eager feature (vs a plain composition). */
|
|
145
|
-
export
|
|
163
|
+
export const isFeatureBinding = (u: unknown): u is FeatureBinding =>
|
|
164
|
+
typeof u === 'object' && u !== null && FeatureBindingTypeId in u
|
|
165
|
+
|
|
146
166
|
/**
|
|
147
167
|
* A feature *definition*: a reflectable manifest (`kind: 'Feature'`) that carries
|
|
148
168
|
* the eager half (composition + placeholders) and an Effect `load` for the heavy
|
|
@@ -150,25 +170,37 @@ export declare const isFeatureBinding: (u: unknown) => u is FeatureBinding;
|
|
|
150
170
|
* shared-carrier tuple the loaded module declares. `load` is an `Effect`, never a
|
|
151
171
|
* Promise — consumers `yield* Feature.load`.
|
|
152
172
|
*/
|
|
153
|
-
export interface FeatureClass<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
export interface FeatureClass<
|
|
174
|
+
P,
|
|
175
|
+
C extends UiContract,
|
|
176
|
+
ROut,
|
|
177
|
+
Requires extends ReadonlyArray<RequireCarrier>,
|
|
178
|
+
> {
|
|
179
|
+
new (): {}
|
|
180
|
+
readonly manifest: FeatureManifest
|
|
181
|
+
/** Phantom carrying the mounted composition's props type (for `provide` checks). */
|
|
182
|
+
readonly Props: P
|
|
183
|
+
/** Phantom carrying the composition's UI contract (for typed reflection consumers). */
|
|
184
|
+
readonly Contract: C
|
|
185
|
+
/** Phantom carrying the declared shared-carrier tuple. */
|
|
186
|
+
readonly Requires: Requires
|
|
187
|
+
/** Load the feature's module — `yield* Feature.load` → its `FeatureModule`, or `FeatureLoadFailed`. */
|
|
188
|
+
readonly load: Effect.Effect<FeatureModule<ROut, Requires>, FeatureLoadFailed>
|
|
189
|
+
/** The value `provide(slot, Feature)` binds under the slot tag. */
|
|
190
|
+
readonly binding: FeatureBinding
|
|
191
|
+
/** The eagerly-available module — present only for `loadingStrategy: 'default'`. */
|
|
192
|
+
readonly eagerModule: FeatureModule<ROut, Requires> | undefined
|
|
168
193
|
}
|
|
169
|
-
|
|
194
|
+
|
|
195
|
+
export type AnyFeature = FeatureClass<any, any, any, ReadonlyArray<RequireCarrier>>
|
|
196
|
+
|
|
170
197
|
/** Whether a value is a feature definition (its target shape in `provide(slot, Feature)`). */
|
|
171
|
-
export
|
|
198
|
+
export const isFeature = (u: unknown): u is AnyFeature =>
|
|
199
|
+
(typeof u === 'function' || typeof u === 'object') &&
|
|
200
|
+
u !== null &&
|
|
201
|
+
'manifest' in u &&
|
|
202
|
+
(u as { readonly manifest: { readonly kind?: unknown } }).manifest?.kind === 'Feature'
|
|
203
|
+
|
|
172
204
|
/**
|
|
173
205
|
* The placeholders a lazy feature must ship — both eager compositions (their
|
|
174
206
|
* `.live` is in the eager bundle, so they paint the load gap). `loading` receives
|
|
@@ -177,20 +209,28 @@ export declare const isFeature: (u: unknown) => u is AnyFeature;
|
|
|
177
209
|
* are not schema-decoded inputs.
|
|
178
210
|
*/
|
|
179
211
|
export interface Placeholders {
|
|
180
|
-
|
|
181
|
-
|
|
212
|
+
readonly loading: CompositionClass<any, UiContract>
|
|
213
|
+
readonly failed: CompositionClass<any, UiContract>
|
|
182
214
|
}
|
|
215
|
+
|
|
183
216
|
type FeatureConfig<P, C extends UiContract, ROut, Requires extends ReadonlyArray<RequireCarrier>> = {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
} & (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
217
|
+
readonly composition: CompositionClass<P, C>
|
|
218
|
+
readonly boot?: ReadonlyArray<Tagged>
|
|
219
|
+
} & (
|
|
220
|
+
| {
|
|
221
|
+
// EAGER: the module is statically present and merged into the runtime at
|
|
222
|
+
// boot. No load gap, so no placeholder.
|
|
223
|
+
readonly loadingStrategy?: 'default'
|
|
224
|
+
readonly module: FeatureModule<ROut, Requires>
|
|
225
|
+
}
|
|
226
|
+
| {
|
|
227
|
+
// LAZY: the module loads on mount via an Effect. Placeholders required.
|
|
228
|
+
readonly loadingStrategy: 'lazy'
|
|
229
|
+
readonly load: Effect.Effect<FeatureModule<ROut, Requires>, FeatureLoadFailed>
|
|
230
|
+
readonly placeholder: Placeholders
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
|
|
194
234
|
/**
|
|
195
235
|
* Define a feature. The whole-app shape (this manifest, the composition, the
|
|
196
236
|
* placeholders) stays eager and reflectable; the `.live` layer in `module`/`load`
|
|
@@ -198,7 +238,41 @@ type FeatureConfig<P, C extends UiContract, ROut, Requires extends ReadonlyArray
|
|
|
198
238
|
* the discriminated config *requires* `placeholder` — the compiler walks you to a
|
|
199
239
|
* complete code-split feature.
|
|
200
240
|
*/
|
|
201
|
-
export
|
|
241
|
+
export const make = <P, C extends UiContract, ROut, Requires extends ReadonlyArray<RequireCarrier>>(
|
|
242
|
+
name: string,
|
|
243
|
+
config: FeatureConfig<P, C, ROut, Requires>,
|
|
244
|
+
): FeatureClass<P, C, ROut, Requires> => {
|
|
245
|
+
const strategy: 'lazy' | 'default' = config.loadingStrategy ?? 'default'
|
|
246
|
+
const load: Effect.Effect<FeatureModule<ROut, Requires>, FeatureLoadFailed> =
|
|
247
|
+
'load' in config ? config.load : Effect.succeed(config.module)
|
|
248
|
+
const eagerModule = 'module' in config ? config.module : undefined
|
|
249
|
+
const placeholder = 'placeholder' in config ? config.placeholder : undefined
|
|
250
|
+
|
|
251
|
+
const manifest: FeatureManifest = {
|
|
252
|
+
kind: 'Feature',
|
|
253
|
+
name,
|
|
254
|
+
strategy,
|
|
255
|
+
composition: config.composition.manifest,
|
|
256
|
+
...(placeholder
|
|
257
|
+
? { placeholder: { loading: placeholder.loading.manifest, failed: placeholder.failed.manifest } }
|
|
258
|
+
: {}),
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const binding: FeatureBinding = {
|
|
262
|
+
[FeatureBindingTypeId]: FeatureBindingTypeId,
|
|
263
|
+
manifest,
|
|
264
|
+
composition: config.composition,
|
|
265
|
+
strategy,
|
|
266
|
+
// The host is strategy-uniform: `default`'s `load` resolves immediately. The
|
|
267
|
+
// assignment to the erased `LoadedModule` needs no cast — see `LoadedModule`.
|
|
268
|
+
load,
|
|
269
|
+
...(placeholder ? { placeholder: { loading: placeholder.loading, failed: placeholder.failed } } : {}),
|
|
270
|
+
boot: config.boot ?? [],
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return definitionClass<FeatureClass<P, C, ROut, Requires>>({ manifest, load, binding, eagerModule })
|
|
274
|
+
}
|
|
275
|
+
|
|
202
276
|
/**
|
|
203
277
|
* Mount a feature against a live engine. The renderer-neutral lifecycle, run on a
|
|
204
278
|
* per-feature `Scope`: load the module (an Effect — lazy = dynamic import; default
|
|
@@ -215,6 +289,18 @@ export declare const make: <P, C extends UiContract, ROut, Requires extends Read
|
|
|
215
289
|
* we re-attach the concrete `engineContext` it builds against. `R` is the engine
|
|
216
290
|
* context's service set, supplied by the host.
|
|
217
291
|
*/
|
|
218
|
-
export
|
|
219
|
-
|
|
220
|
-
|
|
292
|
+
export const mountFeature = <R>(
|
|
293
|
+
binding: FeatureBinding,
|
|
294
|
+
engineContext: Context.Context<R>,
|
|
295
|
+
): Effect.Effect<Context.Context<unknown>, FeatureLoadFailed, Scope.Scope> =>
|
|
296
|
+
Effect.gen(function* () {
|
|
297
|
+
const { layer } = yield* binding.load
|
|
298
|
+
const featureLayer = layer as Layer.Layer<unknown, never, R>
|
|
299
|
+
const context = yield* Layer.build(
|
|
300
|
+
Layer.provideMerge(featureLayer, Layer.succeedContext(engineContext)),
|
|
301
|
+
)
|
|
302
|
+
yield* Effect.forEach(binding.boot, (event) => publish('High', event), { discard: true }).pipe(
|
|
303
|
+
Effect.provide(context),
|
|
304
|
+
)
|
|
305
|
+
return context
|
|
306
|
+
})
|