@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.
- package/README.md +85 -55
- package/package.json +17 -17
- package/src/boundary/boundary.test.ts +62 -16
- package/src/boundary/boundary.ts +141 -32
- package/src/calc/asyncCalc.invalidate.test.ts +516 -18
- package/src/calc/asyncCalc.test.ts +207 -175
- package/src/calc/asyncCalc.ts +168 -39
- package/src/calc/calc.test.ts +3 -5
- package/src/calc/calc.ts +44 -18
- package/src/calc/calcFamily.test.ts +80 -22
- package/src/calc/calcFamily.ts +56 -25
- package/src/calc/compose.ts +1 -14
- package/src/channel/channel.ts +128 -11
- package/src/compose/composition.test.ts +19 -0
- package/src/compose/composition.ts +346 -62
- package/src/compose/props.ts +13 -6
- package/src/compose/provide.ts +187 -46
- package/src/compose/slot.ts +156 -19
- package/src/compose/structure.test.ts +6 -3
- package/src/compose/structure.ts +106 -13
- package/src/compose/ui.test.ts +19 -3
- package/src/compose/ui.ts +147 -54
- package/src/compose/ui.typecheck.ts +40 -4
- package/src/definition/definition.ts +22 -9
- package/src/event/event.fromSource.test.ts +172 -0
- package/src/event/event.test.ts +10 -0
- package/src/event/event.ts +102 -18
- package/src/feature/feature.mount.test.ts +123 -56
- package/src/feature/feature.test.ts +67 -63
- package/src/feature/feature.ts +1317 -197
- package/src/feature/feature.typecheck.ts +253 -61
- package/src/graph/closure.ts +403 -0
- package/src/index.ts +171 -245
- package/src/internal/bucketCache.ts +39 -0
- package/src/internal/capture.ts +9 -4
- package/src/internal/env.ts +1 -3
- package/src/internal/errors.ts +21 -10
- package/src/internal/queryDriver.ts +120 -15
- package/src/internal/queryDriverStore.ts +8 -5
- package/src/internal/queryDriverTypes.ts +3 -1
- package/src/internal/reuse.test.ts +10 -3
- package/src/internal/reuse.ts +5 -2
- package/src/internal/scheduler.ts +4 -1
- package/src/internal/sources.ts +25 -11
- package/src/internal/track.ts +3 -2
- package/src/internal.ts +222 -0
- package/src/namespace/namespace.ts +46 -25
- package/src/procedure/procedure.ts +38 -11
- package/src/reducer/reducer.ts +78 -34
- package/src/remote/remoteState.test.ts +515 -339
- package/src/remote/remoteState.ts +572 -107
- package/src/remote/remoteState.typecheck.ts +47 -23
- package/src/runtime/appRuntime.activation.test.ts +100 -0
- package/src/runtime/appRuntime.test.ts +157 -105
- package/src/runtime/appRuntime.ts +394 -33
- package/src/runtime/eventBudget.test.ts +1 -4
- package/src/runtime/eventBudget.ts +9 -8
- package/src/runtime/hardening.test.ts +4 -9
- package/src/runtime/instrumentation.test.ts +174 -192
- package/src/runtime/instrumentation.ts +6 -11
- package/src/runtime/loop.test.ts +36 -0
- package/src/runtime/loop.ts +69 -40
- package/src/scene/featureScene.test.ts +4 -2
- package/src/scene/scene.ts +234 -64
- package/src/scene/seedScene.test.ts +69 -86
- package/src/state/state.nominal.typecheck.ts +68 -0
- package/src/state/state.test.ts +17 -0
- package/src/state/state.ts +79 -26
- package/src/state/stateFamily.test.ts +14 -0
- package/src/state/stateFamily.ts +55 -22
- package/src/state/stateGroup.ts +53 -17
- package/src/state/token.ts +40 -10
- package/src/synced/syncedStore.ts +46 -23
- package/src/testkit/flight.testkit.ts +75 -0
- package/src/wire/tree.test.ts +8 -4
- package/src/wire/triggers.test.ts +6 -2
- package/src/wire/triggers.ts +14 -10
- package/src/calc/asyncCalcDefinitions.ts +0 -48
- package/src/channel/procedureRegistry.ts +0 -90
- package/src/feature/featureBinding.ts +0 -48
- package/src/internal/ctx.ts +0 -9
- package/src/remote/remoteStateDefinition.ts +0 -135
- package/src/remote/remoteStateLayers.ts +0 -118
- package/src/remote/remoteStateLiveTypes.ts +0 -59
- package/src/remote/remoteStateSend.ts +0 -64
package/src/state/stateGroup.ts
CHANGED
|
@@ -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
|
|
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
|
|
10
|
-
|
|
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 [
|
|
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<
|
|
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 = <
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.
|
|
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
|
}
|
package/src/state/token.ts
CHANGED
|
@@ -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<
|
|
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<
|
|
9
|
-
constructor(name: N, store: Context.Tag<
|
|
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,
|
|
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<
|
|
35
|
+
export interface Source<N extends string, A, Identifier> extends AnySource {
|
|
20
36
|
readonly name: N
|
|
21
|
-
readonly store: Context.Tag<
|
|
37
|
+
readonly store: Context.Tag<Identifier, Store<A>>
|
|
22
38
|
}
|
|
23
39
|
|
|
24
|
-
export type
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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:
|
|
22
|
+
readonly schema: SyncedStoreSchemaReflection
|
|
20
23
|
readonly title: Option.Option<string>
|
|
21
24
|
readonly description: Option.Option<string>
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
29
|
-
readonly store: Context.Tag<
|
|
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<
|
|
45
|
+
export interface StoreCarrier<Identifier, A> {
|
|
46
|
+
readonly store: Context.Tag<Identifier, Store<A>>
|
|
35
47
|
}
|
|
36
48
|
|
|
37
|
-
export type AnySyncedStore =
|
|
38
|
-
|
|
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,
|
|
54
|
+
export const make = <const N extends string, ValueSchema extends Schema.Schema.AnyNoContext>(
|
|
41
55
|
name: N,
|
|
42
|
-
schema:
|
|
56
|
+
schema: ValueSchema,
|
|
43
57
|
options: StateOptions = {},
|
|
44
|
-
): SyncedStoreClass<N,
|
|
45
|
-
|
|
46
|
-
const
|
|
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
|
-
|
|
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<
|
|
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
|
+
}
|
package/src/wire/tree.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect, test } from 'vitest'
|
|
2
|
-
import
|
|
3
|
-
import type { WireNode, WireTree } from '
|
|
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 = [
|
|
75
|
-
|
|
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
|
|
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(
|
|
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) }
|
package/src/wire/triggers.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
49
|
+
schema: Schema.Schema<P, Encoded, never>,
|
|
52
50
|
): Effect.Effect<void> =>
|
|
53
|
-
Ref.update(entries, (map) =>
|
|
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
|
-
|
|
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
|
package/src/internal/ctx.ts
DELETED
|
@@ -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
|