@playfast/reform 1.1.1 → 1.2.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 (106) hide show
  1. package/README.md +85 -55
  2. package/package.json +17 -17
  3. package/src/boundary/boundary.test.ts +82 -33
  4. package/src/boundary/boundary.ts +141 -32
  5. package/src/calc/asyncCalc.invalidate.test.ts +733 -32
  6. package/src/calc/asyncCalc.test.ts +397 -225
  7. package/src/calc/asyncCalc.ts +115 -100
  8. package/src/calc/asyncCalcTypes.ts +147 -0
  9. package/src/calc/calc.test.ts +13 -16
  10. package/src/calc/calc.ts +44 -18
  11. package/src/calc/calcFamily.test.ts +90 -32
  12. package/src/calc/calcFamily.ts +56 -25
  13. package/src/calc/compose.ts +1 -14
  14. package/src/channel/channel.ts +25 -6
  15. package/src/channel/{procedureRegistry.ts → procedures.ts} +41 -23
  16. package/src/compose/composition.test.ts +19 -0
  17. package/src/compose/composition.ts +168 -81
  18. package/src/compose/compositionTypes.ts +249 -0
  19. package/src/compose/props.ts +13 -6
  20. package/src/compose/provide.ts +187 -46
  21. package/src/compose/slot.ts +156 -19
  22. package/src/compose/structure.test.ts +6 -3
  23. package/src/compose/structure.ts +106 -13
  24. package/src/compose/ui.test.ts +19 -3
  25. package/src/compose/ui.ts +147 -54
  26. package/src/compose/ui.typecheck.ts +40 -4
  27. package/src/definition/definition.ts +22 -9
  28. package/src/event/event.fromSource.test.ts +172 -0
  29. package/src/event/event.test.ts +10 -0
  30. package/src/event/event.ts +102 -18
  31. package/src/feature/feature.mount.test.ts +123 -56
  32. package/src/feature/feature.test.ts +67 -63
  33. package/src/feature/feature.ts +222 -262
  34. package/src/feature/feature.typecheck.ts +224 -91
  35. package/src/feature/featureBinding.ts +130 -20
  36. package/src/feature/featureClass.ts +162 -0
  37. package/src/feature/featureConfig.ts +192 -0
  38. package/src/feature/featureFactory.ts +163 -0
  39. package/src/feature/featureModule.ts +136 -0
  40. package/src/feature/featureModule.typecheck.ts +63 -0
  41. package/src/feature/featureNativeTree.ts +152 -0
  42. package/src/feature/featureRequirement.ts +82 -0
  43. package/src/feature/featureVariants.ts +234 -0
  44. package/src/feature/mountFeature.ts +54 -0
  45. package/src/graph/closure.ts +222 -0
  46. package/src/graph/services.ts +95 -0
  47. package/src/graph/summary.ts +134 -0
  48. package/src/index.ts +171 -245
  49. package/src/internal/bucketCache.ts +39 -0
  50. package/src/internal/capture.ts +9 -4
  51. package/src/internal/env.ts +1 -3
  52. package/src/internal/errors.ts +21 -10
  53. package/src/internal/queryDriver.ts +61 -25
  54. package/src/internal/queryDriverHydrate.ts +45 -0
  55. package/src/internal/queryDriverStore.ts +8 -5
  56. package/src/internal/queryDriverTypes.ts +11 -11
  57. package/src/internal/reuse.test.ts +10 -3
  58. package/src/internal/reuse.ts +5 -2
  59. package/src/internal/scheduler.ts +4 -1
  60. package/src/internal/sources.ts +25 -11
  61. package/src/internal/store.test.ts +27 -6
  62. package/src/internal/track.ts +3 -2
  63. package/src/internal.ts +222 -0
  64. package/src/namespace/namespace.ts +46 -25
  65. package/src/procedure/procedure.ts +38 -11
  66. package/src/reducer/reducer.ts +78 -34
  67. package/src/remote/pendingQueue.ts +145 -0
  68. package/src/remote/remoteState.test.ts +536 -354
  69. package/src/remote/remoteState.ts +145 -106
  70. package/src/remote/remoteState.typecheck.ts +47 -23
  71. package/src/remote/remoteStateMake.ts +97 -0
  72. package/src/remote/remoteStateSend.ts +95 -37
  73. package/src/remote/remoteStateTypes.ts +155 -0
  74. package/src/runtime/appRuntime.activation.test.ts +104 -0
  75. package/src/runtime/appRuntime.test.ts +187 -111
  76. package/src/runtime/appRuntime.ts +36 -130
  77. package/src/runtime/capturedAppRuntime.ts +274 -0
  78. package/src/runtime/eventBudget.test.ts +39 -12
  79. package/src/runtime/eventBudget.ts +9 -8
  80. package/src/runtime/featureMount.ts +94 -0
  81. package/src/runtime/hardening.test.ts +22 -13
  82. package/src/runtime/instrumentation.test.ts +213 -194
  83. package/src/runtime/instrumentation.ts +6 -11
  84. package/src/runtime/loop.test.ts +84 -7
  85. package/src/runtime/loop.ts +69 -40
  86. package/src/runtime/runtimeHandle.ts +124 -0
  87. package/src/scene/featureScene.test.ts +4 -2
  88. package/src/scene/scene.ts +234 -64
  89. package/src/scene/seedScene.test.ts +69 -86
  90. package/src/state/state.nominal.typecheck.ts +68 -0
  91. package/src/state/state.test.ts +17 -0
  92. package/src/state/state.ts +79 -26
  93. package/src/state/stateFamily.test.ts +39 -8
  94. package/src/state/stateFamily.ts +55 -22
  95. package/src/state/stateGroup.ts +53 -17
  96. package/src/state/token.ts +40 -10
  97. package/src/synced/syncedStore.ts +46 -23
  98. package/src/testkit/flight.testkit.ts +84 -0
  99. package/src/wire/tree.test.ts +8 -4
  100. package/src/wire/triggers.test.ts +6 -2
  101. package/src/wire/triggers.ts +14 -10
  102. package/src/calc/asyncCalcDefinitions.ts +0 -48
  103. package/src/internal/ctx.ts +0 -9
  104. package/src/remote/remoteStateDefinition.ts +0 -135
  105. package/src/remote/remoteStateLayers.ts +0 -118
  106. package/src/remote/remoteStateLiveTypes.ts +0 -59
@@ -1,4 +1,4 @@
1
- import { Context, Effect, FiberRef, Layer, Option, Schema } from 'effect'
1
+ import { Context, Effect, FiberRef, Layer, Option, type ParseResult, Schema } from 'effect'
2
2
  import type { Effect as EffectType } from 'effect/Effect'
3
3
  import { type Manifest, yieldableClass } from '../definition/definition'
4
4
  import { resolveScheduler } from '../internal/scheduler'
@@ -7,7 +7,6 @@ import { resolveInstrumentation, stateUpdateHook } from '../runtime/instrumentat
7
7
  import { claimStateTag } from '../internal/stateRegistry'
8
8
  import { makeStore, type Store } from '../internal/store'
9
9
  import { readTracked } from '../internal/track'
10
- import type { AnySchema, AnyValue } from '../internal/variance'
11
10
 
12
11
  export interface StateOptionsExternalApi {
13
12
  readonly title?: string
@@ -15,34 +14,72 @@ export interface StateOptionsExternalApi {
15
14
  }
16
15
  export type StateOptions = StateOptionsExternalApi
17
16
 
18
- export interface StateManifestExternalApi<N extends string, A> extends Manifest {
17
+ // Full Schema parameters are invariant; heterogeneous manifests expose only their AST.
18
+ export interface StateSchemaReflection {
19
+ readonly ast: Schema.Schema<unknown, unknown, unknown>['ast']
20
+ }
21
+
22
+ export interface StateManifestExternalApi<N extends string> extends Manifest {
19
23
  readonly kind: 'State'
20
24
  readonly name: N
21
- readonly schema: AnySchema<A>
25
+ readonly schema: StateSchemaReflection
22
26
  readonly title?: string
23
27
  readonly description?: string
24
28
  }
25
- export type StateManifest<N extends string, A> = StateManifestExternalApi<N, A>
29
+ export type StateManifest<N extends string> = StateManifestExternalApi<N>
30
+
31
+ export const StateStoreTypeId: unique symbol = Symbol.for('reform/StateStore')
32
+ export type StateStoreTypeId = typeof StateStoreTypeId
33
+
34
+ export interface StateStore<N extends string, in out A> extends Store<A> {
35
+ readonly [StateStoreTypeId]: N
36
+ }
26
37
 
27
- export interface StateClass<out N extends string, in out A> extends EffectType<A, never, Store<A>> {
38
+ export type StateCapture<Result> = <N extends string, A>(state: StateClass<N, A>) => Result
39
+
40
+ export interface AnyState {
28
41
  new (): {}
29
- readonly manifest: StateManifest<N, A>
30
- readonly store: Context.Tag<Store<A>, Store<A>>
42
+ readonly manifest: StateManifest<string>
43
+ readonly capture: <Result>(visit: StateCapture<Result>) => Result
31
44
  }
32
45
 
33
- export type AnyState = StateClass<string, AnyValue>
34
- export type StateValue<S> = S extends StateClass<string, infer A> ? A : never
35
- export type StateName<S> = S extends StateClass<infer N, AnyValue> ? N : never
46
+ interface StateStatics<N extends string, A> {
47
+ readonly manifest: StateManifest<N>
48
+ // Subclasses change the class function name; this preserves the authored source name.
49
+ readonly name: N
50
+ readonly capture: <Result>(visit: StateCapture<Result>) => Result
51
+ readonly store: Context.Tag<StateStore<N, A>, Store<A>>
52
+ readonly decodeUnknown: (input: unknown) => Effect.Effect<A, ParseResult.ParseError, never>
53
+ readonly decodeUnknownSync: (input: unknown) => A
54
+ readonly decodeUnknownOption: (input: unknown) => Option.Option<A>
55
+ }
36
56
 
37
- export const make = <const N extends string, A>(
57
+ export interface StateClass<N extends string, A>
58
+ extends EffectType<A, never, StateStore<N, A>>, StateStatics<N, A> {
59
+ new (): {}
60
+ }
61
+
62
+ export type StateDefinition<
63
+ N extends string,
64
+ A,
65
+ StateSchema extends Schema.Schema.AnyNoContext,
66
+ > = StateClass<N, A> & {
67
+ readonly manifest: StateManifest<N> & { readonly schema: StateSchema }
68
+ }
69
+
70
+ export type StateValue<S> = S extends StateClass<infer _N, infer A> ? A : never
71
+ export type StateName<S> = S extends StateClass<infer N, infer _A> ? N : never
72
+
73
+ export const make = <const N extends string, StateSchema extends Schema.Schema.AnyNoContext>(
38
74
  name: N,
39
- schema: AnySchema<A>,
75
+ schema: StateSchema,
40
76
  options: StateOptions = {},
41
- ): StateClass<N, A> => {
77
+ ): StateDefinition<N, Schema.Schema.Type<StateSchema>, StateSchema> => {
78
+ type A = Schema.Schema.Type<StateSchema>
42
79
  const identifier = `reform/state/${name}`
43
80
  claimStateTag(identifier)
44
- const store = Context.GenericTag<Store<A>, Store<A>>(identifier)
45
- const manifest: StateManifest<N, A> = {
81
+ const store = Context.GenericTag<StateStore<N, A>, Store<A>>(identifier)
82
+ const manifest: StateManifest<N> & { readonly schema: StateSchema } = {
46
83
  kind: 'State',
47
84
  name,
48
85
  schema,
@@ -50,25 +87,41 @@ export const make = <const N extends string, A>(
50
87
  ...(options.description !== undefined ? { description: options.description } : {}),
51
88
  }
52
89
  const read = Effect.flatMap(store, readTracked)
53
- return yieldableClass(read, { manifest, store })
90
+ const state: StateDefinition<N, A, StateSchema> = yieldableClass<
91
+ A,
92
+ never,
93
+ StateStore<N, A>,
94
+ StateStatics<N, A> & {
95
+ readonly manifest: StateManifest<N> & { readonly schema: StateSchema }
96
+ }
97
+ >(read, {
98
+ manifest,
99
+ name,
100
+ store,
101
+ decodeUnknown: Schema.decodeUnknown(schema),
102
+ decodeUnknownSync: Schema.decodeUnknownSync(schema),
103
+ decodeUnknownOption: Schema.decodeUnknownOption(schema),
104
+ capture: <Result>(visit: StateCapture<Result>): Result => visit(state),
105
+ })
106
+ return state
54
107
  }
55
108
 
56
- const resolveSeed = <S extends AnyState>(
57
- state: S,
58
- initial: StateValue<S>,
59
- ): Effect.Effect<StateValue<S>> =>
109
+ const resolveSeed = <N extends string, A>(
110
+ state: StateClass<N, A>,
111
+ initial: A,
112
+ ): Effect.Effect<A, never, never> =>
60
113
  Effect.map(FiberRef.get(CurrentSeedOverrides), (overrides) => {
61
114
  if (!(state.manifest.name in overrides)) {
62
115
  return initial
63
116
  }
64
- const override = Schema.decodeUnknownOption(state.manifest.schema)(overrides[state.manifest.name])
117
+ const override = state.decodeUnknownOption(overrides[state.manifest.name])
65
118
  return Option.getOrElse(override, () => initial)
66
119
  })
67
120
 
68
- export const live = <S extends AnyState>(
69
- state: S,
70
- initial: StateValue<S>,
71
- ): Layer.Layer<Store<StateValue<S>>> =>
121
+ export const live = <N extends string, A>(
122
+ state: StateClass<N, A>,
123
+ initial: A,
124
+ ): Layer.Layer<StateStore<N, A>> =>
72
125
  Layer.effect(
73
126
  state.store,
74
127
  Effect.all([resolveScheduler, resolveSeed(state, initial), resolveInstrumentation]).pipe(
@@ -1,8 +1,26 @@
1
1
  import { expect, it } from '@effect/vitest'
2
- import { Duration, Effect, Layer, Schema as S } from 'effect'
2
+ import { Effect, Layer, Schema as S } from 'effect'
3
3
  import { Engine, Event, publish, Reducer, StateFamily } from '../index'
4
+ import { until } from '../testkit/flight.testkit'
4
5
 
5
- const tick = Effect.sleep(Duration.millis(1))
6
+ // Eviction is queued with queueMicrotask; one turn of the queue provably drains it.
7
+ const drainMicrotasks = Effect.promise(
8
+ () => new Promise<void>((resolve) => queueMicrotask(resolve)),
9
+ )
10
+
11
+ it.live('same-shaped families resolve their own stores', () => {
12
+ class First extends StateFamily.make('family.nominal.first', S.String, S.Number) {}
13
+ class Second extends StateFamily.make('family.nominal.second', S.String, S.Number) {}
14
+ const Live = Layer.mergeAll(StateFamily.live(First, 1), StateFamily.live(Second, 2))
15
+
16
+ return Effect.gen(function* () {
17
+ const first = yield* First.store
18
+ const second = yield* Second.store
19
+
20
+ expect(first.at('key').get()).toBe(1)
21
+ expect(second.at('key').get()).toBe(2)
22
+ }).pipe(Effect.provide(Live))
23
+ })
6
24
 
7
25
  it.live("writing one key does not notify another key's subscribers", () => {
8
26
  class Items extends StateFamily.make('items', S.String, S.Number) {}
@@ -22,7 +40,10 @@ it.live("writing one key does not notify another key's subscribers", () => {
22
40
  })
23
41
 
24
42
  a.set(1)
25
- yield* tick
43
+ yield* until(
44
+ () => aWakes.n,
45
+ (n) => n > 0,
46
+ )
26
47
  expect(aWakes.n).toBe(1)
27
48
  expect(bWakes.n).toBe(0)
28
49
  expect(b.get()).toBe(0)
@@ -67,12 +88,16 @@ it.live('evictWhenUnused drops a key once its last subscriber leaves', () => {
67
88
  const offB = store.subscribe(() => {})
68
89
  expect(family.size()).toBe(1)
69
90
 
91
+ // Releasing one of two subscribers queues no eviction at all — assert with no window.
70
92
  offA()
71
- yield* tick
93
+ yield* drainMicrotasks
72
94
  expect(family.size()).toBe(1)
73
95
 
74
96
  offB()
75
- yield* tick
97
+ yield* until(
98
+ () => family.size(),
99
+ (size) => size === 0,
100
+ )
76
101
  expect(family.size()).toBe(0)
77
102
  expect(family.at('k').get()).toBe(0)
78
103
  }).pipe(Effect.provide(TestLayer))
@@ -89,7 +114,7 @@ it.live('evictWhenUnused: a same-tick re-subscribe cancels eviction', () => {
89
114
  const off = store.subscribe(() => {})
90
115
  off()
91
116
  store.subscribe(() => {})
92
- yield* tick
117
+ yield* drainMicrotasks
93
118
  expect(family.size()).toBe(1)
94
119
  expect(family.at('k').get()).toBe(7)
95
120
  }).pipe(Effect.provide(TestLayer))
@@ -116,12 +141,18 @@ it.live('a family reducer fold returning Tombstone evicts the key', () => {
116
141
  const family = yield* Items.store
117
142
 
118
143
  yield* publish('High', Event.construct(Bumped, { id: 'a' }))
119
- yield* Effect.sleep(Duration.millis(5))
144
+ yield* until(
145
+ () => family.at('a').get(),
146
+ (value) => value === 1,
147
+ )
120
148
  const before = family.at('a')
121
149
  expect(before.get()).toBe(1)
122
150
 
123
151
  yield* publish('High', Event.construct(Removed, { id: 'a' }))
124
- yield* Effect.sleep(Duration.millis(5))
152
+ yield* until(
153
+ () => family.at('a'),
154
+ (store) => store !== before,
155
+ )
125
156
  const after = family.at('a')
126
157
  expect(after).not.toBe(before)
127
158
  expect(after.get()).toBe(0)
@@ -1,11 +1,10 @@
1
- import { Context, Effect, Layer, Option } from 'effect'
1
+ import { Context, Effect, Layer, Option, Schema } from 'effect'
2
2
  import { type Manifest, definitionClass } from '../definition/definition'
3
3
  import { resolveScheduler, type Scheduler } from '../internal/scheduler'
4
4
  import { claimStateTag } from '../internal/stateRegistry'
5
5
  import { resolveInstrumentation, stateUpdateHook } from '../runtime/instrumentation'
6
6
  import { makeStore, type Store } from '../internal/store'
7
7
  import { readTracked } from '../internal/track'
8
- import type { AnySchema, AnyValue } from '../internal/variance'
9
8
 
10
9
  export const Tombstone: unique symbol = Symbol.for('reform/StateFamily/Tombstone')
11
10
  export type Tombstone = typeof Tombstone
@@ -84,41 +83,66 @@ const makeFamilyStore = <K, V>(
84
83
  }
85
84
  }
86
85
 
87
- export interface StateFamilyManifestExternalApi<N extends string, K, V> extends Manifest {
86
+ // Full Schema parameters are invariant; heterogeneous manifests expose only their AST.
87
+ export interface FamilySchemaReflection {
88
+ readonly ast: Schema.Schema<unknown, unknown, unknown>['ast']
89
+ }
90
+
91
+ export interface StateFamilyManifestExternalApi<N extends string> extends Manifest {
88
92
  readonly kind: 'StateFamily'
89
93
  readonly name: N
90
- readonly key: AnySchema<K>
91
- readonly value: AnySchema<V>
94
+ readonly key: FamilySchemaReflection
95
+ readonly value: FamilySchemaReflection
92
96
  readonly title?: string
93
97
  readonly description?: string
94
98
  }
95
- export type StateFamilyManifest<N extends string, K, V> = StateFamilyManifestExternalApi<N, K, V>
99
+ export type StateFamilyManifest<N extends string> = StateFamilyManifestExternalApi<N>
100
+
101
+ export const StateFamilyStoreTypeId: unique symbol = Symbol.for('reform/StateFamilyStore')
102
+ export type StateFamilyStoreTypeId = typeof StateFamilyStoreTypeId
103
+
104
+ export interface StateFamilyStore<N extends string, in out K, in out V> extends FamilyStore<K, V> {
105
+ readonly [StateFamilyStoreTypeId]: N
106
+ }
96
107
 
97
- export interface StateFamilyClass<out N extends string, in out K, in out V> {
108
+ export type StateFamilyCapture<Result> = <N extends string, K, V>(
109
+ family: StateFamilyClass<N, K, V>,
110
+ ) => Result
111
+
112
+ export interface AnyFamily {
113
+ new (): {}
114
+ readonly manifest: StateFamilyManifest<string>
115
+ readonly capture: <Result>(visit: StateFamilyCapture<Result>) => Result
116
+ }
117
+
118
+ export interface StateFamilyClass<N extends string, K, V> {
98
119
  new (): { readonly Key: K }
99
- readonly manifest: StateFamilyManifest<N, K, V>
100
- readonly store: Context.Tag<FamilyStore<K, V>, FamilyStore<K, V>>
120
+ readonly manifest: StateFamilyManifest<N>
121
+ readonly capture: <Result>(visit: StateFamilyCapture<Result>) => Result
122
+ readonly store: Context.Tag<StateFamilyStore<N, K, V>, FamilyStore<K, V>>
123
+ readonly decodeKeySync: (input: unknown) => K
124
+ readonly decodeValueSync: (input: unknown) => V
101
125
  }
102
126
 
103
- export type AnyFamily = StateFamilyClass<string, AnyValue, AnyValue>
104
- export type FamilyKey<F> = F extends StateFamilyClass<string, infer K, AnyValue> ? K : never
105
- export type FamilyValue<F> = F extends StateFamilyClass<string, AnyValue, infer V> ? V : never
127
+ export type FamilyName<F> = F extends StateFamilyClass<infer N, infer _K, infer _V> ? N : never
128
+ export type FamilyKey<F> = F extends StateFamilyClass<infer _N, infer K, infer _V> ? K : never
129
+ export type FamilyValue<F> = F extends StateFamilyClass<infer _N, infer _K, infer V> ? V : never
106
130
 
107
131
  export interface StateFamilyMakeOptionsExternalApi {
108
132
  readonly title?: string
109
133
  readonly description?: string
110
134
  }
111
135
 
112
- export const make = <const N extends string, K, V>(
136
+ export const make = <const N extends string, K, KeyEncoded, V, ValueEncoded>(
113
137
  name: N,
114
- key: AnySchema<K>,
115
- valueSchema: AnySchema<V>,
138
+ key: Schema.Schema<K, KeyEncoded, never>,
139
+ valueSchema: Schema.Schema<V, ValueEncoded, never>,
116
140
  options: StateFamilyMakeOptionsExternalApi = {},
117
141
  ): StateFamilyClass<N, K, V> => {
118
142
  const identifier = `reform/family/${name}`
119
143
  claimStateTag(identifier)
120
- const store = Context.GenericTag<FamilyStore<K, V>, FamilyStore<K, V>>(identifier)
121
- const manifest: StateFamilyManifest<N, K, V> = {
144
+ const store = Context.GenericTag<StateFamilyStore<N, K, V>, FamilyStore<K, V>>(identifier)
145
+ const manifest: StateFamilyManifest<N> = {
122
146
  kind: 'StateFamily',
123
147
  name,
124
148
  key,
@@ -126,22 +150,31 @@ export const make = <const N extends string, K, V>(
126
150
  ...(options.title !== undefined ? { title: options.title } : {}),
127
151
  ...(options.description !== undefined ? { description: options.description } : {}),
128
152
  }
129
- return definitionClass<StateFamilyClass<N, K, V>>({ manifest, store })
153
+ const family: StateFamilyClass<N, K, V> = definitionClass<StateFamilyClass<N, K, V>>({
154
+ manifest,
155
+ store,
156
+ decodeKeySync: Schema.decodeUnknownSync(key),
157
+ decodeValueSync: Schema.decodeUnknownSync(valueSchema),
158
+ capture: <Result>(visit: StateFamilyCapture<Result>): Result => visit(family),
159
+ })
160
+ return family
130
161
  }
131
162
 
132
163
  export const read = <N extends string, K, V>(
133
164
  family: StateFamilyClass<N, K, V>,
134
165
  key: K,
135
- ): Effect.Effect<V, never, FamilyStore<K, V>> =>
166
+ ): Effect.Effect<V, never, StateFamilyStore<N, K, V>> =>
136
167
  Effect.flatMap(family.store, (familyStore) => readTracked(familyStore.at(key)))
137
168
 
138
169
  export const live = <N extends string, K, V>(
139
170
  family: StateFamilyClass<N, K, V>,
140
171
  initial: V | ((key: K) => V),
141
172
  options: FamilyOptions = {},
142
- ): Layer.Layer<FamilyStore<K, V>> => {
143
- // oxlint-disable-next-line reform-rules/no-type-assertion -- value-or-factory union can't be discriminated for a generic V without a cast
144
- const seed: (key: K) => V = typeof initial === 'function' ? (initial as (key: K) => V) : () => initial
173
+ ): Layer.Layer<StateFamilyStore<N, K, V>> => {
174
+ const seed = (key: K): V =>
175
+ typeof initial === 'function'
176
+ ? family.decodeValueSync(Reflect.apply(initial, undefined, [key]))
177
+ : initial
145
178
  return Layer.effect(
146
179
  family.store,
147
180
  Effect.zipWith(resolveScheduler, resolveInstrumentation, (scheduler, instrumentation) =>
@@ -1,13 +1,25 @@
1
- import { Array as Arr, Layer, Option } from 'effect'
1
+ import { Array as Arr, Context, Effect, Layer, Option } from 'effect'
2
2
  import { definitionClass } from '../definition/definition'
3
3
  import { DuplicateRegistration, UnknownGroupState } from '../internal/errors'
4
4
  import { type StoresOf } from '../internal/sources'
5
- import { type AnyState, live as stateLive, type StateClass, type StateName } from './state'
5
+ import type { Store } from '../internal/store'
6
+ import {
7
+ type AnyState,
8
+ live as stateLive,
9
+ type StateName,
10
+ type StateStore,
11
+ type StateValue,
12
+ } from './state'
6
13
  import { StateToken } from './token'
7
- import type { AnyValue } from '../internal/variance'
8
14
 
9
- type ValueForName<Members extends ReadonlyArray<AnyState>, N extends string> =
10
- Extract<Members[number], StateClass<N, AnyValue>> extends StateClass<string, infer A> ? A : never
15
+ type MemberForName<Members extends ReadonlyArray<AnyState>, N extends string> = Extract<
16
+ Members[number],
17
+ { readonly manifest: { readonly name: N } }
18
+ >
19
+
20
+ type ValueForName<Members extends ReadonlyArray<AnyState>, N extends string> = StateValue<
21
+ MemberForName<Members, N>
22
+ >
11
23
 
12
24
  export interface StateGroupClass<Members extends ReadonlyArray<AnyState>> {
13
25
  new (): {}
@@ -23,7 +35,10 @@ type DuplicateStateName<N extends string> = readonly ['reform: duplicate state n
23
35
  type NoDuplicateNames<
24
36
  Members extends ReadonlyArray<AnyState>,
25
37
  Seen extends string = never,
26
- > = Members extends readonly [infer Head extends AnyState, ...infer Tail extends ReadonlyArray<AnyState>]
38
+ > = Members extends readonly [
39
+ infer Head extends AnyState,
40
+ ...infer Tail extends ReadonlyArray<AnyState>,
41
+ ]
27
42
  ? readonly [
28
43
  StateName<Head> extends Seen ? DuplicateStateName<StateName<Head>> : Head,
29
44
  ...NoDuplicateNames<Tail, Seen | StateName<Head>>,
@@ -43,7 +58,9 @@ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) exten
43
58
 
44
59
  type EntrySeeds<G> = G extends AnyStateGroup ? GroupSeeds<G> : never
45
60
 
46
- export type SeedsOf<St extends ReadonlyArray<unknown>> = Partial<UnionToIntersection<EntrySeeds<St[number]>>>
61
+ export type SeedsOf<St extends ReadonlyArray<unknown>> = Partial<
62
+ UnionToIntersection<EntrySeeds<St[number]>>
63
+ >
47
64
 
48
65
  export const make = <const Members extends ReadonlyArray<AnyState>>(
49
66
  ...members: Members & NoDuplicateNames<Members>
@@ -66,7 +83,10 @@ export const make = <const Members extends ReadonlyArray<AnyState>>(
66
83
  })
67
84
  }
68
85
 
69
- export const select = <Members extends ReadonlyArray<AnyState>, N extends StateName<Members[number]>>(
86
+ export const select = <
87
+ Members extends ReadonlyArray<AnyState>,
88
+ N extends StateName<Members[number]>,
89
+ >(
70
90
  group: StateGroupClass<Members>,
71
91
  name: N,
72
92
  ): StateToken<N, ValueForName<Members, N>> => {
@@ -75,17 +95,33 @@ export const select = <Members extends ReadonlyArray<AnyState>, N extends StateN
75
95
  // oxlint-disable-next-line reform-rules/no-throw -- definition-time invariant; sync lookup has no Effect context (see internal/errors.ts)
76
96
  throw new UnknownGroupState({ name })
77
97
  }
78
- return new StateToken(name, member.store)
98
+ const store = Context.GenericTag<
99
+ StateStore<N, ValueForName<Members, N>>,
100
+ Store<ValueForName<Members, N>>
101
+ >(`reform/state/${name}`)
102
+ return new StateToken(name, store)
79
103
  }
80
104
 
81
- export const live = <Members extends ReadonlyArray<AnyState>>(
105
+ export function live<Members extends ReadonlyArray<AnyState>>(
82
106
  group: StateGroupClass<Members>,
83
107
  seeds: GroupSeeds<StateGroupClass<Members>>,
84
- ): Layer.Layer<StoresOf<Members>> => {
85
- // oxlint-disable-next-line reform-rules/no-type-assertion -- reflection boundary: index a mapped seed type by runtime member name
86
- const seedRecord = seeds as Record<string, unknown>
87
- // oxlint-disable-next-line reform-rules/no-type-assertion -- union-of-stores Rout can't be expressed by reduce's single-layer accumulator
88
- return group.members
89
- .map((member) => stateLive(member, seedRecord[member.manifest.name]))
90
- .reduce((accumulator, layer) => Layer.merge(accumulator, layer)) as Layer.Layer<StoresOf<Members>>
108
+ ): Layer.Layer<StoresOf<Members>>
109
+ export function live(
110
+ group: AnyStateGroup,
111
+ seeds: Readonly<Record<string, unknown>>,
112
+ ): Layer.Layer<never> {
113
+ const layers = group.members.map((member) =>
114
+ member.capture<Layer.Layer<never>>((state) =>
115
+ Layer.unwrapEffect(
116
+ state.decodeUnknown(seeds[state.manifest.name]).pipe(
117
+ Effect.orDie,
118
+ Effect.map((seed) => stateLive(state, seed)),
119
+ ),
120
+ ),
121
+ ),
122
+ )
123
+ return layers.reduce<Layer.Layer<never>>(
124
+ (merged, memberLayer) => Layer.merge(merged, memberLayer),
125
+ Layer.empty,
126
+ )
91
127
  }
@@ -1,26 +1,56 @@
1
1
  import { Context, Effect, Effectable } from 'effect'
2
2
  import type { Store } from '../internal/store'
3
- import type { AnyValue } from '../internal/variance'
4
3
  import { readTracked } from '../internal/track'
4
+ import type { StateStore } from './state'
5
5
 
6
- export class StateToken<out N extends string, in out A> extends Effectable.Class<A, never, Store<A>> {
6
+ export class StateToken<
7
+ N extends string,
8
+ in out A,
9
+ Identifier = StateStore<N, A>,
10
+ > extends Effectable.Class<A, never, Identifier> {
7
11
  readonly name: N
8
- readonly store: Context.Tag<Store<A>, Store<A>>
9
- constructor(name: N, store: Context.Tag<Store<A>, Store<A>>) {
12
+ readonly store: Context.Tag<Identifier, Store<A>>
13
+ constructor(name: N, store: Context.Tag<Identifier, Store<A>>) {
10
14
  super()
11
15
  this.name = name
12
16
  this.store = store
13
17
  }
14
- commit(): Effect.Effect<A, never, Store<A>> {
18
+ commit(): Effect.Effect<A, never, Identifier> {
15
19
  return Effect.flatMap(this.store, readTracked)
16
20
  }
21
+ capture<Result>(visit: SourceCapture<Result>): Result {
22
+ return visit(this)
23
+ }
24
+ }
25
+
26
+ export type SourceCapture<Result> = <N extends string, A, Identifier>(
27
+ source: Source<N, A, Identifier>,
28
+ ) => Result
29
+
30
+ export interface AnySource {
31
+ readonly name: string
32
+ readonly store: { readonly key: string }
17
33
  }
18
34
 
19
- export interface Source<out N extends string, in out A> {
35
+ export interface Source<N extends string, A, Identifier> extends AnySource {
20
36
  readonly name: N
21
- readonly store: Context.Tag<Store<A>, Store<A>>
37
+ readonly store: Context.Tag<Identifier, Store<A>>
22
38
  }
23
39
 
24
- export type AnySource = Source<string, AnyValue>
25
- export type SourceName<S> = S extends Source<infer N, AnyValue> ? N : never
26
- export type SourceValue<S> = S extends Source<AnyValue, infer A> ? A : never
40
+ export type SourceName<S> = S extends {
41
+ readonly manifest: { readonly name: infer N extends string }
42
+ }
43
+ ? N
44
+ : S extends { readonly name: infer N extends string }
45
+ ? N
46
+ : never
47
+ export type SourceValue<S> = S extends {
48
+ readonly store: Context.Tag<infer _Identifier, Store<infer A>>
49
+ }
50
+ ? A
51
+ : never
52
+ export type SourceIdentifier<S> = S extends {
53
+ readonly store: Context.Tag<infer Identifier, infer _Service>
54
+ }
55
+ ? Identifier
56
+ : never
@@ -1,49 +1,66 @@
1
- import { Context, Effect, Layer, Option, type Scope } from 'effect'
1
+ import { Context, Effect, Layer, Option, type Schema, type Scope } from 'effect'
2
2
  import type { Effect as EffectType } from 'effect/Effect'
3
3
  import { type Manifest, yieldableClass } from '../definition/definition'
4
4
  import { resolveScheduler } from '../internal/scheduler'
5
5
  import { makeDerivedStore, type Store } from '../internal/store'
6
6
  import { readTracked } from '../internal/track'
7
- import type { Source } from '../state/token'
7
+ import type { AnySource, Source, SourceCapture } from '../state/token'
8
8
  import type { StateOptions } from '../state/state'
9
- import type { AnySchema, AnyValue } from '../internal/variance'
10
9
 
11
10
  export interface SyncedSource<A> {
12
11
  readonly snapshot: () => A
13
12
  readonly subscribe: (onChange: () => void) => () => void
14
13
  }
15
14
 
16
- export interface SyncedStoreManifest<N extends string, A> extends Manifest {
15
+ export interface SyncedStoreSchemaReflection {
16
+ readonly ast: Schema.Schema<unknown, unknown, unknown>['ast']
17
+ }
18
+
19
+ export interface SyncedStoreManifest<N extends string> extends Manifest {
17
20
  readonly kind: 'SyncedStore'
18
21
  readonly name: N
19
- readonly schema: AnySchema<A>
22
+ readonly schema: SyncedStoreSchemaReflection
20
23
  readonly title: Option.Option<string>
21
24
  readonly description: Option.Option<string>
22
25
  }
23
26
 
24
- export interface SyncedStoreClass<out N extends string, in out A>
25
- extends EffectType<A, never, Store<A>>,
26
- Source<N, A> {
27
+ export const SyncedStoreIdentifierTypeId: unique symbol = Symbol.for('reform/SyncedStoreIdentifier')
28
+ export type SyncedStoreIdentifierTypeId = typeof SyncedStoreIdentifierTypeId
29
+
30
+ export interface SyncedStoreIdentifier<N extends string, in out A> extends Store<A> {
31
+ readonly [SyncedStoreIdentifierTypeId]: N
32
+ }
33
+
34
+ export interface SyncedStoreClass<N extends string, in out A>
35
+ extends
36
+ EffectType<A, never, SyncedStoreIdentifier<N, A>>,
37
+ Source<N, A, SyncedStoreIdentifier<N, A>> {
27
38
  new (): {}
28
- readonly manifest: SyncedStoreManifest<N, A>
29
- readonly store: Context.Tag<Store<A>, Store<A>>
39
+ readonly manifest: SyncedStoreManifest<N>
40
+ readonly store: Context.Tag<SyncedStoreIdentifier<N, A>, Store<A>>
30
41
  readonly name: N
42
+ readonly capture: <Result>(visit: SourceCapture<Result>) => Result
31
43
  }
32
44
 
33
- export interface StoreCarrier<A> {
34
- readonly store: Context.Tag<Store<A>, Store<A>>
45
+ export interface StoreCarrier<Identifier, A> {
46
+ readonly store: Context.Tag<Identifier, Store<A>>
35
47
  }
36
48
 
37
- export type AnySyncedStore = SyncedStoreClass<string, AnyValue>
38
- export type SyncedValue<S> = S extends SyncedStoreClass<string, infer A> ? A : never
49
+ export type AnySyncedStore = AnySource & {
50
+ readonly manifest: Manifest & { readonly kind: 'SyncedStore' }
51
+ }
52
+ export type SyncedValue<S> = S extends SyncedStoreClass<infer _N, infer A> ? A : never
39
53
 
40
- export const make = <const N extends string, A>(
54
+ export const make = <const N extends string, ValueSchema extends Schema.Schema.AnyNoContext>(
41
55
  name: N,
42
- schema: AnySchema<A>,
56
+ schema: ValueSchema,
43
57
  options: StateOptions = {},
44
- ): SyncedStoreClass<N, A> => {
45
- const store = Context.GenericTag<Store<A>, Store<A>>(`reform/syncedStore/${name}`)
46
- const manifest: SyncedStoreManifest<N, A> = {
58
+ ): SyncedStoreClass<N, Schema.Schema.Type<ValueSchema>> => {
59
+ type A = Schema.Schema.Type<ValueSchema>
60
+ const store = Context.GenericTag<SyncedStoreIdentifier<N, A>, Store<A>>(
61
+ `reform/syncedStore/${name}`,
62
+ )
63
+ const manifest: SyncedStoreManifest<N> = {
47
64
  kind: 'SyncedStore',
48
65
  name,
49
66
  schema,
@@ -51,13 +68,19 @@ export const make = <const N extends string, A>(
51
68
  description: Option.fromNullable(options.description),
52
69
  }
53
70
  const read = Effect.flatMap(store, readTracked)
54
- return yieldableClass(read, { manifest, store, name })
71
+ const synced: SyncedStoreClass<N, A> = yieldableClass(read, {
72
+ manifest,
73
+ store,
74
+ name,
75
+ capture: <Result>(visit: SourceCapture<Result>): Result => visit(synced),
76
+ })
77
+ return synced
55
78
  }
56
79
 
57
- export const live = <A, R>(
58
- def: StoreCarrier<A>,
80
+ export const live = <Identifier, A, R>(
81
+ def: StoreCarrier<Identifier, A>,
59
82
  acquire: Effect.Effect<SyncedSource<A>, never, R>,
60
- ): Layer.Layer<Store<A>, never, Exclude<R, Scope.Scope>> =>
83
+ ): Layer.Layer<Identifier, never, Exclude<R, Scope.Scope>> =>
61
84
  Layer.scoped(
62
85
  def.store,
63
86
  Effect.gen(function* () {