@playfast/reform 0.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +18 -1
  2. package/package.json +1 -1
  3. package/src/boundary/boundary.test.ts +1 -33
  4. package/src/boundary/boundary.ts +4 -82
  5. package/src/calc/asyncCalc.invalidate.test.ts +2 -14
  6. package/src/calc/asyncCalc.test.ts +0 -38
  7. package/src/calc/asyncCalc.ts +11 -163
  8. package/src/calc/asyncCalcDefinitions.ts +48 -0
  9. package/src/calc/asyncData.ts +0 -37
  10. package/src/calc/calc.test.ts +4 -28
  11. package/src/calc/calc.ts +16 -44
  12. package/src/calc/calcFamily.test.ts +0 -12
  13. package/src/calc/calcFamily.ts +12 -49
  14. package/src/calc/compose.test.ts +1 -12
  15. package/src/calc/compose.ts +0 -22
  16. package/src/calc/queryState.ts +0 -23
  17. package/src/channel/channel.ts +78 -173
  18. package/src/channel/procedureRegistry.ts +90 -0
  19. package/src/compose/composition.ts +7 -67
  20. package/src/compose/host.ts +0 -6
  21. package/src/compose/props.ts +3 -9
  22. package/src/compose/provide.ts +19 -33
  23. package/src/compose/slot.ts +2 -30
  24. package/src/compose/structure.test.ts +0 -6
  25. package/src/compose/structure.ts +3 -67
  26. package/src/compose/ui.test.ts +0 -4
  27. package/src/compose/ui.ts +17 -111
  28. package/src/compose/ui.typecheck.ts +0 -14
  29. package/src/definition/definition.ts +0 -32
  30. package/src/event/event.test.ts +0 -3
  31. package/src/event/event.ts +5 -14
  32. package/src/event/eventGroup.ts +0 -1
  33. package/src/feature/feature.mount.test.ts +66 -54
  34. package/src/feature/feature.test.ts +67 -46
  35. package/src/feature/feature.ts +185 -214
  36. package/src/feature/feature.typecheck.ts +29 -16
  37. package/src/feature/featureBinding.ts +48 -0
  38. package/src/index.ts +245 -202
  39. package/src/internal/capture.ts +0 -20
  40. package/src/internal/ctx.ts +2 -9
  41. package/src/internal/env.ts +9 -0
  42. package/src/internal/errors.test.ts +0 -6
  43. package/src/internal/errors.ts +14 -48
  44. package/src/internal/inspect.test.ts +0 -6
  45. package/src/internal/inspect.ts +0 -12
  46. package/src/internal/queryDriver.ts +48 -243
  47. package/src/internal/queryDriverStore.ts +153 -0
  48. package/src/internal/queryDriverTypes.ts +57 -0
  49. package/src/internal/queryEvents.ts +0 -12
  50. package/src/internal/queryStore.ts +0 -14
  51. package/src/internal/reuse.test.ts +0 -11
  52. package/src/internal/reuse.ts +0 -28
  53. package/src/internal/scheduler.ts +0 -43
  54. package/src/internal/seeds.ts +0 -14
  55. package/src/internal/sources.ts +4 -41
  56. package/src/internal/stateRegistry.ts +0 -14
  57. package/src/internal/store.test.ts +1 -3
  58. package/src/internal/store.ts +10 -37
  59. package/src/internal/track.ts +0 -18
  60. package/src/internal/variance.ts +5 -0
  61. package/src/namespace/namespace.test.ts +46 -0
  62. package/src/namespace/namespace.ts +85 -0
  63. package/src/procedure/procedure.ts +2 -24
  64. package/src/reducer/reducer.ts +3 -21
  65. package/src/remote/remoteState.test.ts +92 -75
  66. package/src/remote/remoteState.ts +99 -486
  67. package/src/remote/remoteState.typecheck.ts +0 -33
  68. package/src/remote/remoteStateDefinition.ts +135 -0
  69. package/src/remote/remoteStateLayers.ts +118 -0
  70. package/src/remote/remoteStateLiveTypes.ts +59 -0
  71. package/src/remote/remoteStateSend.ts +64 -0
  72. package/src/runtime/appRuntime.test.ts +197 -0
  73. package/src/runtime/appRuntime.ts +131 -0
  74. package/src/runtime/bus.ts +2 -15
  75. package/src/runtime/eventBudget.test.ts +155 -0
  76. package/src/runtime/eventBudget.ts +119 -0
  77. package/src/runtime/hardening.test.ts +74 -9
  78. package/src/runtime/instrumentation.test.ts +324 -0
  79. package/src/runtime/instrumentation.ts +44 -0
  80. package/src/runtime/loop.test.ts +0 -17
  81. package/src/runtime/loop.ts +41 -58
  82. package/src/runtime/queries.ts +0 -17
  83. package/src/scene/featureScene.test.ts +59 -0
  84. package/src/scene/scene.ts +51 -44
  85. package/src/scene/seedScene.test.ts +96 -116
  86. package/src/state/state.ts +12 -28
  87. package/src/state/stateFamily.test.ts +2 -11
  88. package/src/state/stateFamily.ts +20 -54
  89. package/src/state/stateGroup.ts +2 -49
  90. package/src/state/token.ts +4 -17
  91. package/src/synced/syncedStore.ts +7 -42
  92. package/src/ui/node.ts +0 -6
  93. package/src/ui/trigger.ts +0 -5
  94. package/src/wire/tree.test.ts +0 -3
  95. package/src/wire/tree.ts +0 -39
  96. package/src/wire/triggers.test.ts +0 -4
  97. package/src/wire/triggers.ts +5 -27
@@ -1,14 +1,9 @@
1
1
  import { Effect } from 'effect'
2
2
  import { attachInspectable } from '../internal/inspect'
3
3
 
4
- /** A definition's `toJSON`: its reflectable manifest, else its raw statics. */
5
4
  const describe = (statics: object): unknown =>
6
5
  ('manifest' in statics ? Reflect.get(statics, 'manifest') : undefined) ?? statics
7
6
 
8
- /**
9
- * The reflectable descriptor every primitive carries (principle #6). The editor
10
- * and proof packages read these without running any logic.
11
- */
12
7
  export type Kind =
13
8
  | 'State'
14
9
  | 'StateGroup'
@@ -35,49 +30,22 @@ export interface Manifest {
35
30
  readonly name: string
36
31
  }
37
32
 
38
- /**
39
- * Build a class whose *static* side is itself an Effect, so `yield* TheClass`
40
- * runs `read` and returns its value (the same mechanism `Context.Tag` uses).
41
- * Subclasses inherit the protocol through the prototype chain, so the user's
42
- * `class FeedState extends State.make(...) {}` is yieldable too.
43
- *
44
- * `statics` (manifest, `.live`, tag, …) are attached as own properties
45
- * and likewise inherited by the subclass.
46
- */
47
33
  export const yieldableClass = <A, E, R, Statics extends object>(
48
34
  read: Effect.Effect<A, E, R>,
49
35
  statics: Statics,
50
36
  ): { new (): {} } & Effect.Effect<A, E, R> & Statics => {
51
37
  class Base {}
52
38
  Object.setPrototypeOf(Base, read)
53
- // `defineProperty` over `Reflect.ownKeys`, not `Object.assign` over
54
- // `Object.entries`: a static can shadow a non-writable own property of the
55
- // class function (notably `name`, which a `Source`-shaped definition like a
56
- // Calc carries) and `assign` would throw on those; and `ownKeys` (unlike
57
- // `entries`) carries symbol-keyed statics — the `TypeId` brands.
58
39
  Reflect.ownKeys(statics).forEach((key) => {
59
40
  const staticValue = Reflect.get(statics, key)
60
41
  Object.defineProperty(Base, key, { value: staticValue, writable: true, enumerable: true, configurable: true })
61
42
  })
62
- // Print like an Effect value (the manifest), not `[object Object]`.
63
43
  attachInspectable(Base, () => describe(statics))
64
44
  // oxlint-disable-next-line reform-rules/no-type-assertion -- phantom-shaped static side cannot structurally satisfy the yieldable Effect & Statics type
65
45
  return Base as never
66
46
  }
67
47
 
68
- /**
69
- * Build a *definition class*: an empty constructible carrying static marker data
70
- * (manifest, tag, policy, members, …) that the editor and proof packages reflect
71
- * on. Definition interfaces declare phantom, type-only fields — a composition's
72
- * `Props`, a family instance's `Key` — that exist purely for inference and have
73
- * no runtime value, so the assembled object cannot *structurally* satisfy the
74
- * interface. The one assertion to `Class` lives here, documented, instead of a
75
- * scattered `as never` / `as unknown as` at every `*.make`.
76
- */
77
48
  export const definitionClass = <Class>(statics: object): Class => {
78
- // `defineProperty` over `Reflect.ownKeys`, not `Object.assign`: a static can
79
- // shadow a non-writable own property of the class function (notably `name`)
80
- // and `assign` would throw, and `ownKeys` carries the symbol-keyed `TypeId`.
81
49
  const Base = class {}
82
50
  Reflect.ownKeys(statics).forEach((key) => {
83
51
  const staticValue = Reflect.get(statics, key)
@@ -2,9 +2,6 @@ import { expect, test } from 'vitest'
2
2
  import { Schema as S } from 'effect'
3
3
  import { Event } from '../index'
4
4
 
5
- // An event is a pure data fact: `Event.construct` builds the tagged value the
6
- // bus carries, and the class carries the reflectable `manifest` + `tag`.
7
-
8
5
  test('Event.construct builds the tagged value `{ _tag, ...payload }`', () => {
9
6
  class Added extends Event.make('Added', S.Struct({ id: S.String })) {}
10
7
  expect(Event.construct(Added, { id: 'x' })).toEqual({ _tag: 'Added', id: 'x' })
@@ -1,42 +1,36 @@
1
- import { Effect, PubSub, Runtime, type Schema } from 'effect'
1
+ import { Effect, PubSub, Runtime } from 'effect'
2
2
  import { type Manifest, definitionClass } from '../definition/definition'
3
3
  import { Bus, publish, type Tagged } from '../runtime/bus'
4
4
  import type { Trigger } from '../ui/trigger'
5
+ import type { AnySchema, AnyValue } from '../internal/variance'
5
6
 
6
- /** The dispatched value of an event: `{ _tag, ...payload }`, matched with `Match.tags`. */
7
7
  export type EventOf<N extends string, P> = Tagged & { readonly _tag: N } & P
8
8
 
9
9
  export interface EventManifest<N extends string, P> extends Manifest {
10
10
  readonly kind: 'Event'
11
11
  readonly name: N
12
- readonly schema: Schema.Schema<P, any>
12
+ readonly schema: AnySchema<P>
13
13
  }
14
14
 
15
15
  export interface EventClass<out N extends string, in out P> {
16
16
  new (): {}
17
17
  readonly manifest: EventManifest<N, P>
18
18
  readonly tag: N
19
- /** Carrier for `Event.construct` — builds the tagged value `{ _tag, ...payload }`. */
20
19
  readonly build: (payload: P) => EventOf<N, P>
21
- /** Carrier for `Event.trigger` — the UI callback effect (High priority). */
22
20
  readonly trigger: Effect.Effect<Trigger<P>, never, Bus>
23
21
  }
24
22
 
25
- export type AnyEvent = EventClass<string, any>
23
+ export type AnyEvent = EventClass<string, AnyValue>
26
24
  export type EventType<E> = E extends EventClass<infer N, infer P> ? EventOf<N, P> : never
27
25
 
28
- /** A pure data fact. Events depend on nothing — only reducers tie them to state. */
29
26
  export const make = <const N extends string, P>(
30
27
  name: N,
31
- schema: Schema.Schema<P, any>,
28
+ schema: AnySchema<P>,
32
29
  ): EventClass<N, P> => {
33
30
  const build = (payload: P): EventOf<N, P> => ({ _tag: name, ...payload })
34
31
  const trigger = Effect.gen(function* () {
35
32
  const bus = yield* Bus
36
33
  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
34
  return (payload: P) => {
41
35
  Runtime.runSync(runtime)(PubSub.publish(bus, { priority: 'High', event: build(payload) }))
42
36
  }
@@ -49,17 +43,14 @@ export const make = <const N extends string, P>(
49
43
  })
50
44
  }
51
45
 
52
- /** Construct the tagged value for an event (used for `boot` events). */
53
46
  export const construct = <N extends string, P>(event: EventClass<N, P>, payload: P): EventOf<N, P> =>
54
47
  event.build(payload)
55
48
 
56
- /** Dispatch as a follow-up (Normal priority) from a procedure/effect. */
57
49
  export const dispatch = <N extends string, P>(
58
50
  event: EventClass<N, P>,
59
51
  payload: P,
60
52
  ): Effect.Effect<void, never, Bus> => publish('Normal', event.build(payload))
61
53
 
62
- /** A callback for the UI that dispatches at High priority via the runtime. */
63
54
  export const trigger = <N extends string, P>(
64
55
  event: EventClass<N, P>,
65
56
  ): Effect.Effect<Trigger<P>, never, Bus> => event.trigger
@@ -7,7 +7,6 @@ export interface EventGroupClass<Members extends ReadonlyArray<AnyEvent>> {
7
7
  readonly members: Members
8
8
  }
9
9
 
10
- /** Compose events into a group, the same variadic shape as `StateGroup`/`RpcGroup`. */
11
10
  export const make = <const Members extends ReadonlyArray<AnyEvent>>(
12
11
  ...members: Members
13
12
  ): EventGroupClass<Members> =>
@@ -1,33 +1,41 @@
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'
1
+ import { type Cause, Duration, Effect, Exit, Layer, Scope } from "effect";
2
+ import { Schema as S } from "effect";
3
+ import { expect, it } from "@effect/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
13
 
14
14
  // A self-contained feature: a Count state + a reducer that folds Tick. The base
15
15
  // seeds Count; the logic provides the reducer over it and leaves the engine
16
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] }) {}
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", {
20
+ states: [Count],
21
+ events: [Tick],
22
+ }) {}
20
23
 
21
24
  const mod = featureModule(
22
25
  [],
23
- Reducer.live(CountReducer, (n) => n + 1).pipe(Layer.provideMerge(State.live(Count, 0))),
24
- )
26
+ Reducer.live(CountReducer, (n) => n + 1).pipe(
27
+ Layer.provideMerge(State.live(Count, 0)),
28
+ ),
29
+ );
25
30
 
26
31
  // 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 }) {}
32
+ class CtrUi extends ui("CtrUi")<{ props: {}; events: {} }>() {}
33
+ class CtrComp extends Composition.make("CtrComp", {
34
+ title: "Counter",
35
+ ui: CtrUi,
36
+ }) {}
29
37
 
30
- class CounterFeature extends Feature.make('counter', {
38
+ class CounterFeature extends Feature.make("counter", {
31
39
  composition: CtrComp,
32
40
  module: mod,
33
41
  boot: [Event.construct(Tick, {})],
@@ -41,42 +49,46 @@ const waitUntil = <A, R>(
41
49
  Effect.flatMap(read, (a) =>
42
50
  pred(a)
43
51
  ? Effect.succeed(a)
44
- : Effect.flatMap(Effect.sleep(Duration.millis(1)), () => waitUntil(read, pred)),
45
- ).pipe(Effect.timeout(Duration.seconds(2)))
52
+ : Effect.flatMap(Effect.sleep(Duration.millis(1)), () =>
53
+ waitUntil(read, pred),
54
+ ),
55
+ ).pipe(Effect.timeout(Duration.seconds(2)));
46
56
 
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
57
+ it.scopedLive(
58
+ "a mounted feature registers on the shared loop, boots, and folds dispatched events",
59
+ () =>
60
+ Effect.gen(function* () {
61
+ const engineContext = yield* Effect.context<EngineServices>();
62
+ const reducers = yield* Reducers;
63
+ const baseline = reducers.entries.length;
55
64
 
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)
65
+ const scope = yield* Scope.make();
66
+ const ctx = yield* mountFeature(
67
+ CounterFeature.binding,
68
+ engineContext,
69
+ ).pipe(Effect.provideService(Scope.Scope, scope));
70
+ // Registered into the SHARED registry.
71
+ expect(reducers.entries.length).toBe(baseline + 1);
62
72
 
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)
73
+ // boot dispatched Tick → Count folds to 1 on the shared loop.
74
+ const booted = yield* waitUntil(
75
+ Count.pipe(Effect.provide(ctx)),
76
+ (n) => n === 1,
77
+ );
78
+ expect(booted).toBe(1);
66
79
 
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)
80
+ // A further dispatch on the shared bus folds again → 2.
81
+ yield* publish("High", Event.construct(Tick, {}));
82
+ const folded = yield* waitUntil(
83
+ Count.pipe(Effect.provide(ctx)),
84
+ (n) => n === 2,
85
+ );
86
+ expect(folded).toBe(2);
71
87
 
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
- })
88
+ // Unmount: closing the feature scope unregisters its reducer — the entry is
89
+ // reclaimed, not left folding onto an orphaned store.
90
+ yield* Scope.close(scope, Exit.succeed(undefined));
91
+ expect(reducers.entries.length).toBe(baseline);
92
+ expect(reducers.byTag.get("Tick")).toBeUndefined();
93
+ }).pipe(Effect.provide(Engine)),
94
+ );
@@ -1,60 +1,81 @@
1
- import { Effect, Layer, Option } 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'
1
+ import { Effect, Layer, Option } from "effect";
2
+ import { expect, it } from "@effect/vitest";
3
+ import * as Composition from "../compose/composition";
4
+ import { ui } from "../compose/ui";
5
+ import * as Feature from "./feature";
6
+ import {
7
+ featureModule,
8
+ isFeature,
9
+ isFeatureBinding,
10
+ lazyImport,
11
+ } from "./feature";
7
12
 
8
13
  // 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 }) {}
14
+ class TestUi extends ui("TestUi")<{ props: {}; events: {} }>() {}
15
+ class TestComp extends Composition.make("TestComp", {
16
+ title: "Test",
17
+ ui: TestUi,
18
+ }) {}
19
+ class FailComp extends Composition.make("FailComp", {
20
+ title: "Fail",
21
+ ui: TestUi,
22
+ }) {}
12
23
 
13
24
  // A trivial loaded module: empty layer, no shared requirements.
14
- const mod = featureModule([], Layer.empty)
25
+ const mod = featureModule([], Layer.empty);
15
26
 
16
- class LazyFeature extends Feature.make('lazyFeature', {
17
- loadingStrategy: 'lazy',
27
+ class LazyFeature extends Feature.make("lazyFeature", {
28
+ loadingStrategy: "lazy",
18
29
  composition: TestComp,
19
30
  load: lazyImport(() => Promise.resolve({ default: mod })),
20
31
  placeholder: { loading: TestComp, failed: FailComp },
21
32
  }) {}
22
33
 
23
- class EagerFeature extends Feature.make('eagerFeature', {
34
+ class EagerFeature extends Feature.make("eagerFeature", {
24
35
  composition: TestComp,
25
36
  module: mod,
26
37
  }) {}
27
38
 
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(Option.getOrThrow(LazyFeature.manifest.placeholder).loading.name).toBe('TestComp')
34
- expect(Option.getOrThrow(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(Option.isNone(EagerFeature.manifest.placeholder)).toBe(true)
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
- })
39
+ it("a lazy feature reflects as a Feature manifest with placeholders, no chunk loaded", () => {
40
+ expect(LazyFeature.manifest.kind).toBe("Feature");
41
+ expect(LazyFeature.manifest.name).toBe("lazyFeature");
42
+ expect(LazyFeature.manifest.strategy).toBe("lazy");
43
+ expect(LazyFeature.manifest.composition.name).toBe("TestComp");
44
+ expect(Option.getOrThrow(LazyFeature.manifest.placeholder).loading.name).toBe(
45
+ "TestComp",
46
+ );
47
+ expect(Option.getOrThrow(LazyFeature.manifest.placeholder).failed.name).toBe(
48
+ "FailComp",
49
+ );
50
+ expect(LazyFeature.eagerModule).toBeUndefined();
51
+ });
52
+
53
+ it("an eager feature reflects as default strategy with its module available", () => {
54
+ expect(EagerFeature.manifest.strategy).toBe("default");
55
+ expect(Option.isNone(EagerFeature.manifest.placeholder)).toBe(true);
56
+ expect(EagerFeature.eagerModule).toBe(mod);
57
+ });
58
+
59
+ it("isFeature / isFeatureBinding discriminate", () => {
60
+ expect(isFeature(LazyFeature)).toBe(true);
61
+ expect(isFeature(TestComp)).toBe(false);
62
+ expect(isFeature(null)).toBe(false);
63
+ expect(isFeatureBinding(LazyFeature.binding)).toBe(true);
64
+ expect(isFeatureBinding(TestComp)).toBe(false);
65
+ });
66
+
67
+ it.live(
68
+ "load is an Effect (not a thenable) that yields the module and its requires",
69
+ () =>
70
+ Effect.gen(function* () {
71
+ // No `.then` on `load` — it is an Effect, run on the Effect runtime.
72
+ expect(typeof (LazyFeature.load as { then?: unknown }).then).toBe(
73
+ "undefined",
74
+ );
75
+ const loaded = yield* LazyFeature.load;
76
+ expect(loaded.requires).toEqual([]);
77
+ expect(loaded).toBe(mod);
78
+ // default strategy resolves immediately to its module.
79
+ expect(yield* EagerFeature.load).toBe(mod);
80
+ }),
81
+ );