@playfast/reform 1.1.1 → 1.2.0

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 (85) hide show
  1. package/README.md +85 -55
  2. package/package.json +17 -17
  3. package/src/boundary/boundary.test.ts +62 -16
  4. package/src/boundary/boundary.ts +141 -32
  5. package/src/calc/asyncCalc.invalidate.test.ts +516 -18
  6. package/src/calc/asyncCalc.test.ts +207 -175
  7. package/src/calc/asyncCalc.ts +168 -39
  8. package/src/calc/calc.test.ts +3 -5
  9. package/src/calc/calc.ts +44 -18
  10. package/src/calc/calcFamily.test.ts +80 -22
  11. package/src/calc/calcFamily.ts +56 -25
  12. package/src/calc/compose.ts +1 -14
  13. package/src/channel/channel.ts +128 -11
  14. package/src/compose/composition.test.ts +19 -0
  15. package/src/compose/composition.ts +346 -62
  16. package/src/compose/props.ts +13 -6
  17. package/src/compose/provide.ts +187 -46
  18. package/src/compose/slot.ts +156 -19
  19. package/src/compose/structure.test.ts +6 -3
  20. package/src/compose/structure.ts +106 -13
  21. package/src/compose/ui.test.ts +19 -3
  22. package/src/compose/ui.ts +147 -54
  23. package/src/compose/ui.typecheck.ts +40 -4
  24. package/src/definition/definition.ts +22 -9
  25. package/src/event/event.fromSource.test.ts +172 -0
  26. package/src/event/event.test.ts +10 -0
  27. package/src/event/event.ts +102 -18
  28. package/src/feature/feature.mount.test.ts +123 -56
  29. package/src/feature/feature.test.ts +67 -63
  30. package/src/feature/feature.ts +1317 -197
  31. package/src/feature/feature.typecheck.ts +253 -61
  32. package/src/graph/closure.ts +403 -0
  33. package/src/index.ts +171 -245
  34. package/src/internal/bucketCache.ts +39 -0
  35. package/src/internal/capture.ts +9 -4
  36. package/src/internal/env.ts +1 -3
  37. package/src/internal/errors.ts +21 -10
  38. package/src/internal/queryDriver.ts +120 -15
  39. package/src/internal/queryDriverStore.ts +8 -5
  40. package/src/internal/queryDriverTypes.ts +3 -1
  41. package/src/internal/reuse.test.ts +10 -3
  42. package/src/internal/reuse.ts +5 -2
  43. package/src/internal/scheduler.ts +4 -1
  44. package/src/internal/sources.ts +25 -11
  45. package/src/internal/track.ts +3 -2
  46. package/src/internal.ts +222 -0
  47. package/src/namespace/namespace.ts +46 -25
  48. package/src/procedure/procedure.ts +38 -11
  49. package/src/reducer/reducer.ts +78 -34
  50. package/src/remote/remoteState.test.ts +515 -339
  51. package/src/remote/remoteState.ts +572 -107
  52. package/src/remote/remoteState.typecheck.ts +47 -23
  53. package/src/runtime/appRuntime.activation.test.ts +100 -0
  54. package/src/runtime/appRuntime.test.ts +157 -105
  55. package/src/runtime/appRuntime.ts +394 -33
  56. package/src/runtime/eventBudget.test.ts +1 -4
  57. package/src/runtime/eventBudget.ts +9 -8
  58. package/src/runtime/hardening.test.ts +4 -9
  59. package/src/runtime/instrumentation.test.ts +174 -192
  60. package/src/runtime/instrumentation.ts +6 -11
  61. package/src/runtime/loop.test.ts +36 -0
  62. package/src/runtime/loop.ts +69 -40
  63. package/src/scene/featureScene.test.ts +4 -2
  64. package/src/scene/scene.ts +234 -64
  65. package/src/scene/seedScene.test.ts +69 -86
  66. package/src/state/state.nominal.typecheck.ts +68 -0
  67. package/src/state/state.test.ts +17 -0
  68. package/src/state/state.ts +79 -26
  69. package/src/state/stateFamily.test.ts +14 -0
  70. package/src/state/stateFamily.ts +55 -22
  71. package/src/state/stateGroup.ts +53 -17
  72. package/src/state/token.ts +40 -10
  73. package/src/synced/syncedStore.ts +46 -23
  74. package/src/testkit/flight.testkit.ts +75 -0
  75. package/src/wire/tree.test.ts +8 -4
  76. package/src/wire/triggers.test.ts +6 -2
  77. package/src/wire/triggers.ts +14 -10
  78. package/src/calc/asyncCalcDefinitions.ts +0 -48
  79. package/src/channel/procedureRegistry.ts +0 -90
  80. package/src/feature/featureBinding.ts +0 -48
  81. package/src/internal/ctx.ts +0 -9
  82. package/src/remote/remoteStateDefinition.ts +0 -135
  83. package/src/remote/remoteStateLayers.ts +0 -118
  84. package/src/remote/remoteStateLiveTypes.ts +0 -59
  85. package/src/remote/remoteStateSend.ts +0 -64
@@ -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* () {
@@ -0,0 +1,75 @@
1
+ import { Deferred, Duration, Effect, MutableRef, Option } from 'effect'
2
+
3
+ // Coalescing/cancellation semantics are only meaningful while a query is still
4
+ // in flight. Tests used to build that overlap by racing wall-clock — a 25ms
5
+ // query delay against 5ms gaps between input writes — which silently stops
6
+ // overlapping on a loaded machine: the runs serialize, every key gets its own
7
+ // flight, and the assertion fails reporting a coalescing bug that isn't there.
8
+ // A gate makes the overlap explicit: hold the flight open, land the burst,
9
+ // release.
10
+
11
+ export const tick = (ms = 10): Effect.Effect<void> => Effect.sleep(Duration.millis(ms))
12
+
13
+ export const until = <A>(
14
+ read: () => A,
15
+ predicate: (value: A) => boolean,
16
+ rounds = 200,
17
+ ): Effect.Effect<A> =>
18
+ Effect.suspend(() => {
19
+ const value = read()
20
+ return predicate(value) || rounds <= 0
21
+ ? Effect.succeed(value)
22
+ : Effect.flatMap(tick(10), () => until(read, predicate, rounds - 1))
23
+ })
24
+
25
+ export interface FlightGate {
26
+ /** Wrap a query body: it records its entry, then parks while the gate holds. */
27
+ readonly through: <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
28
+ /** Park every flight that reaches the gate from now on. */
29
+ readonly hold: Effect.Effect<void>
30
+ /** Let the parked flights settle; later flights pass straight through. */
31
+ readonly release: Effect.Effect<void>
32
+ /** Resolve once `count` flights have reached the gate. */
33
+ readonly awaitEntry: (count: number) => Effect.Effect<void>
34
+ /** How many flights have reached the gate. */
35
+ readonly entered: () => number
36
+ }
37
+
38
+ export const makeFlightGate = (): FlightGate => {
39
+ const latch = MutableRef.make(Option.none<Deferred.Deferred<void>>())
40
+ const entered = MutableRef.make(0)
41
+
42
+ return {
43
+ through: (body) =>
44
+ body.pipe(
45
+ Effect.tap(() =>
46
+ Effect.suspend(() => {
47
+ MutableRef.update(entered, (count) => count + 1)
48
+ return Option.match(MutableRef.get(latch), {
49
+ onNone: () => Effect.void,
50
+ onSome: (parked) => Deferred.await(parked),
51
+ })
52
+ }),
53
+ ),
54
+ ),
55
+ hold: Effect.flatMap(Deferred.make<void>(), (parked) =>
56
+ Effect.sync(() => MutableRef.set(latch, Option.some(parked))),
57
+ ),
58
+ release: Effect.suspend(() => {
59
+ const parked = MutableRef.get(latch)
60
+ MutableRef.set(latch, Option.none())
61
+ return Option.match(parked, {
62
+ onNone: () => Effect.void,
63
+ onSome: (deferred) => Effect.asVoid(Deferred.succeed(deferred, undefined)),
64
+ })
65
+ }),
66
+ awaitEntry: (count) =>
67
+ Effect.asVoid(
68
+ until(
69
+ () => MutableRef.get(entered),
70
+ (reached) => reached >= count,
71
+ ),
72
+ ),
73
+ entered: () => MutableRef.get(entered),
74
+ }
75
+ }
@@ -1,6 +1,6 @@
1
1
  import { expect, test } from 'vitest'
2
- import { Wire } from '../index'
3
- import type { WireNode, WireTree } from '../index'
2
+ import * as Wire from './tree'
3
+ import type { WireNode, WireTree } from './tree'
4
4
 
5
5
  const node = (id: string, over: Partial<WireNode> = {}): WireNode => ({
6
6
  id,
@@ -71,8 +71,12 @@ test('roots and childrenOf order siblings by childIndex', () => {
71
71
  })
72
72
 
73
73
  test('nested deep prop changes are detected', () => {
74
- const prev: WireTree = [node('a', { props: [{ _tag: 'Data', name: 'rows', value: [{ id: 1 }, { id: 2 }] }] })]
75
- const next: WireTree = [node('a', { props: [{ _tag: 'Data', name: 'rows', value: [{ id: 1 }, { id: 3 }] }] })]
74
+ const prev: WireTree = [
75
+ node('a', { props: [{ _tag: 'Data', name: 'rows', value: [{ id: 1 }, { id: 2 }] }] }),
76
+ ]
77
+ const next: WireTree = [
78
+ node('a', { props: [{ _tag: 'Data', name: 'rows', value: [{ id: 1 }, { id: 3 }] }] }),
79
+ ]
76
80
  expect(Wire.diff(prev, next)).toHaveLength(1)
77
81
  expect(Wire.diff(prev, prev)).toHaveLength(0)
78
82
  })
@@ -1,6 +1,6 @@
1
1
  import { expect, test } from 'vitest'
2
2
  import { Effect, Exit, Schema as S } from 'effect'
3
- import { Triggers } from '../index'
3
+ import * as Triggers from './triggers'
4
4
  import type { Trigger } from '../index'
5
5
 
6
6
  const ByPayload = S.Struct({ by: S.Number })
@@ -61,7 +61,11 @@ test('a revoked handle no longer fires', () => {
61
61
  Effect.gen(function* () {
62
62
  const registry = yield* Triggers.make
63
63
  const calls: number[] = []
64
- yield* registry.register('0:bump', ((p: { by: number }) => void calls.push(p.by)) as Trigger<{ by: number }>, ByPayload)
64
+ yield* registry.register(
65
+ '0:bump',
66
+ ((p: { by: number }) => void calls.push(p.by)) as Trigger<{ by: number }>,
67
+ ByPayload,
68
+ )
65
69
  yield* registry.revoke('0:bump')
66
70
  const exit = yield* Effect.exit(registry.invoke('0:bump', { by: 1 }))
67
71
  return { calls, failed: Exit.isFailure(exit) }
@@ -1,12 +1,10 @@
1
1
  import { type Cause, Context, Data, Effect, Layer, type ParseResult, Ref, Schema } from 'effect'
2
2
  import type { Trigger } from '../ui/trigger'
3
- import type { AnySchema, AnyValue } from '../internal/variance'
4
3
 
5
4
  export type TriggerHandle = string
6
5
 
7
6
  interface TriggerEntry {
8
- readonly trigger: Trigger<AnyValue>
9
- readonly schema: Schema.Schema.AnyNoContext
7
+ readonly invoke: (encodedPayload: unknown) => Effect.Effect<void, ParseResult.ParseError, never>
10
8
  }
11
9
 
12
10
  const UnknownTriggerBase: new (args: { readonly handle: TriggerHandle }) => Cause.YieldableError & {
@@ -22,10 +20,10 @@ export class UnknownTrigger extends UnknownTriggerBase {
22
20
  }
23
21
 
24
22
  export interface TriggerRegistryApi {
25
- readonly register: <P>(
23
+ readonly register: <P, Encoded>(
26
24
  handle: TriggerHandle,
27
25
  trigger: Trigger<P>,
28
- schema: AnySchema<P>,
26
+ schema: Schema.Schema<P, Encoded, never>,
29
27
  ) => Effect.Effect<void>
30
28
  readonly invoke: (
31
29
  handle: TriggerHandle,
@@ -45,12 +43,19 @@ export class TriggerRegistry extends TriggerRegistryBase {}
45
43
  export const make: Effect.Effect<TriggerRegistryApi> = Effect.gen(function* () {
46
44
  const entries = yield* Ref.make(new Map<TriggerHandle, TriggerEntry>())
47
45
 
48
- const register = <P>(
46
+ const register = <P, Encoded>(
49
47
  handle: TriggerHandle,
50
48
  trigger: Trigger<P>,
51
- schema: AnySchema<P>,
49
+ schema: Schema.Schema<P, Encoded, never>,
52
50
  ): Effect.Effect<void> =>
53
- Ref.update(entries, (map) => new Map(map).set(handle, { trigger, schema }))
51
+ Ref.update(entries, (map) =>
52
+ new Map(map).set(handle, {
53
+ invoke: (encodedPayload) =>
54
+ Schema.decodeUnknown(schema)(encodedPayload).pipe(
55
+ Effect.map((payload) => trigger(payload)),
56
+ ),
57
+ }),
58
+ )
54
59
 
55
60
  const invoke = Effect.fn('invoke')(function* (
56
61
  handle: TriggerHandle,
@@ -60,8 +65,7 @@ export const make: Effect.Effect<TriggerRegistryApi> = Effect.gen(function* () {
60
65
  if (entry === undefined) {
61
66
  return yield* Effect.fail(new UnknownTrigger({ handle }))
62
67
  }
63
- const payload = yield* Schema.decodeUnknown(entry.schema)(encodedPayload)
64
- entry.trigger(payload)
68
+ yield* entry.invoke(encodedPayload)
65
69
  })
66
70
 
67
71
  const revoke = (handle: TriggerHandle): Effect.Effect<void> =>
@@ -1,48 +0,0 @@
1
- import { Context, Effect } from 'effect'
2
- import { yieldableClass } from '../definition/definition'
3
- import { type GatedOf, gatedFlag } from '../internal/queryDriver'
4
- import type { Store } from '../internal/store'
5
- import { readTracked } from '../internal/track'
6
- import { Queries } from '../runtime/queries'
7
- import type { AnySource } from '../state/token'
8
- import type { AsyncData } from './asyncData'
9
- import type { AsyncCalcClass, AsyncCalcConfig, AsyncCalcManifest } from './asyncCalc'
10
-
11
- export const make = <
12
- const N extends string,
13
- const Inputs extends ReadonlyArray<AnySource>,
14
- A,
15
- E = never,
16
- const AlwaysOn extends boolean = false,
17
- >(
18
- name: N,
19
- config: AsyncCalcConfig<Inputs, A, E, AlwaysOn>,
20
- ): AsyncCalcClass<N, Inputs, A, E, GatedOf<AlwaysOn>> => {
21
- const store = Context.GenericTag<Store<AsyncData<A, E, GatedOf<AlwaysOn>>>>(
22
- `reform/asyncCalc/${name}`,
23
- )
24
- const gated = gatedFlag(config.alwaysOn)
25
- const manifest: AsyncCalcManifest<N, A, E> = {
26
- kind: 'AsyncCalc',
27
- name,
28
- output: config.output,
29
- gated,
30
- ...(config.error !== undefined ? { error: config.error } : {}),
31
- }
32
- const read = Effect.flatMap(store, readTracked)
33
- return yieldableClass(read, { manifest, store, name, inputs: config.inputs, gated })
34
- }
35
-
36
- export interface NamedCalc {
37
- readonly name: string
38
- }
39
-
40
- export const invalidate = (calc: NamedCalc): Effect.Effect<void, never, Queries> =>
41
- Effect.flatMap(Queries, (queries) =>
42
- Effect.sync(() => queries.byName.get(calc.name)?.invalidate()),
43
- )
44
-
45
- export const refetch = (calc: NamedCalc): Effect.Effect<void, never, Queries> =>
46
- Effect.flatMap(Queries, (queries) =>
47
- Effect.sync(() => queries.byName.get(calc.name)?.refetch()),
48
- )
@@ -1,90 +0,0 @@
1
- import { Context, Effect, Layer } from 'effect'
2
- import type { Tagged } from '../runtime/bus'
3
-
4
- export interface ProcedureEntry {
5
- readonly name: string
6
- readonly channelName: string
7
- readonly handles: ReadonlySet<string>
8
- readonly run: (event: Tagged) => Effect.Effect<unknown, unknown, never>
9
- }
10
-
11
- export interface ProcedureRegistry {
12
- readonly entries: Array<ProcedureEntry>
13
- readonly byTag: Map<string, Array<ProcedureEntry>>
14
- readonly channelsByTag: Map<string, Array<string>>
15
- readonly byChannelTag: Map<string, Map<string, Array<ProcedureEntry>>>
16
- readonly register: (entry: ProcedureEntry) => void
17
- readonly unregister: (entry: ProcedureEntry) => void
18
- }
19
-
20
- const ProceduresBase: Context.TagClass<Procedures, 'reform/Procedures', ProcedureRegistry> =
21
- Context.Tag('reform/Procedures')<Procedures, ProcedureRegistry>()
22
- export class Procedures extends ProceduresBase {}
23
-
24
- const getOrCreate = <K, V>(map: Map<K, V>, key: K, make: () => V): V => {
25
- const existing = map.get(key)
26
- if (existing !== undefined) {
27
- return existing
28
- }
29
- const created = make()
30
- map.set(key, created)
31
- return created
32
- }
33
-
34
- const dropFrom = <K, T>(map: Map<K, Array<T>>, key: K, target: T): void => {
35
- const bucket = map.get(key)
36
- if (bucket === undefined) {
37
- return
38
- }
39
- const next = bucket.filter((candidate) => candidate !== target)
40
- if (next.length === 0) {
41
- map.delete(key)
42
- } else {
43
- map.set(key, next)
44
- }
45
- }
46
-
47
- const makeProcedureRegistry = (): ProcedureRegistry => {
48
- const state: { entries: Array<ProcedureEntry> } = { entries: [] }
49
- const byTag = new Map<string, Array<ProcedureEntry>>()
50
- const channelsByTag = new Map<string, Array<string>>()
51
- const byChannelTag = new Map<string, Map<string, Array<ProcedureEntry>>>()
52
- return {
53
- get entries() {
54
- return state.entries
55
- },
56
- byTag,
57
- channelsByTag,
58
- byChannelTag,
59
- register: (entry) => {
60
- state.entries = [...state.entries, entry]
61
- entry.handles.forEach((tag) => {
62
- byTag.set(tag, [...getOrCreate(byTag, tag, () => []), entry])
63
- const channels = getOrCreate(channelsByTag, tag, () => [])
64
- if (!channels.includes(entry.channelName)) {
65
- channelsByTag.set(tag, [...channels, entry.channelName])
66
- }
67
- const channelMap = getOrCreate(byChannelTag, entry.channelName, () => new Map())
68
- channelMap.set(tag, [...getOrCreate(channelMap, tag, () => []), entry])
69
- })
70
- },
71
- unregister: (entry) => {
72
- state.entries = state.entries.filter((existing) => existing !== entry)
73
- entry.handles.forEach((tag) => {
74
- dropFrom(byTag, tag, entry)
75
- const channelMap = byChannelTag.get(entry.channelName)
76
- if (channelMap !== undefined) {
77
- dropFrom(channelMap, tag, entry)
78
- if (channelMap.get(tag) === undefined) {
79
- dropFrom(channelsByTag, tag, entry.channelName)
80
- }
81
- if (channelMap.size === 0) {
82
- byChannelTag.delete(entry.channelName)
83
- }
84
- }
85
- })
86
- },
87
- }
88
- }
89
-
90
- export const proceduresLayer: Layer.Layer<Procedures> = Layer.sync(Procedures, makeProcedureRegistry)
@@ -1,48 +0,0 @@
1
- import { Effect, Layer, Option } from 'effect'
2
- import type { Manifest } from '../definition/definition'
3
- import type { CompositionClass } from '../compose/composition'
4
- import type { Tagged } from '../runtime/bus'
5
- import type { FeatureLoadFailed } from '../internal/errors'
6
- import type { FeatureRequirement } from './feature'
7
-
8
- export interface FailedProps {
9
- readonly error: FeatureLoadFailed
10
- readonly retry: () => void
11
- }
12
-
13
- export interface FeatureManifest extends Manifest {
14
- readonly kind: 'Feature'
15
- readonly name: string
16
- readonly strategy: 'lazy' | 'default'
17
- readonly composition: Manifest & { readonly kind: 'Composition' }
18
- readonly placeholder: Option.Option<{
19
- readonly loading: Manifest & { readonly kind: 'Composition' }
20
- readonly failed: Manifest & { readonly kind: 'Composition' }
21
- }>
22
- }
23
-
24
- export interface LoadedModule {
25
- readonly requires: ReadonlyArray<FeatureRequirement>
26
- readonly usesInput: boolean
27
- readonly layer: Layer.Layer<never, never, unknown>
28
- }
29
-
30
- export const FeatureBindingTypeId: unique symbol = Symbol.for('reform/FeatureBinding')
31
- export type FeatureBindingTypeId = typeof FeatureBindingTypeId
32
-
33
- export interface FeatureBindingExternalApi {
34
- readonly [FeatureBindingTypeId]: FeatureBindingTypeId
35
- readonly manifest: FeatureManifest
36
- readonly composition: CompositionClass<unknown>
37
- readonly strategy: 'lazy' | 'default'
38
- readonly load: Effect.Effect<LoadedModule, FeatureLoadFailed>
39
- readonly placeholder?: {
40
- readonly loading: CompositionClass<unknown>
41
- readonly failed: CompositionClass<unknown>
42
- }
43
- readonly boot: ReadonlyArray<Tagged>
44
- }
45
- export type FeatureBinding = FeatureBindingExternalApi
46
-
47
- export const isFeatureBinding = (candidate: unknown): candidate is FeatureBinding =>
48
- typeof candidate === 'object' && candidate !== null && FeatureBindingTypeId in candidate
@@ -1,9 +0,0 @@
1
- import type { Effect } from 'effect'
2
- import type { YieldWrap } from 'effect/Utils'
3
- import type { AnyValue } from './variance'
4
-
5
- export type Ctx<Eff> = [Eff] extends [never]
6
- ? never
7
- : [Eff] extends [YieldWrap<Effect.Effect<AnyValue, AnyValue, infer R>>]
8
- ? R
9
- : never