@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.
- package/README.md +85 -55
- package/package.json +17 -17
- package/src/boundary/boundary.test.ts +82 -33
- package/src/boundary/boundary.ts +141 -32
- package/src/calc/asyncCalc.invalidate.test.ts +733 -32
- package/src/calc/asyncCalc.test.ts +397 -225
- package/src/calc/asyncCalc.ts +115 -100
- package/src/calc/asyncCalcTypes.ts +147 -0
- package/src/calc/calc.test.ts +13 -16
- package/src/calc/calc.ts +44 -18
- package/src/calc/calcFamily.test.ts +90 -32
- package/src/calc/calcFamily.ts +56 -25
- package/src/calc/compose.ts +1 -14
- package/src/channel/channel.ts +25 -6
- package/src/channel/{procedureRegistry.ts → procedures.ts} +41 -23
- package/src/compose/composition.test.ts +19 -0
- package/src/compose/composition.ts +168 -81
- package/src/compose/compositionTypes.ts +249 -0
- 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 +222 -262
- package/src/feature/feature.typecheck.ts +224 -91
- package/src/feature/featureBinding.ts +130 -20
- package/src/feature/featureClass.ts +162 -0
- package/src/feature/featureConfig.ts +192 -0
- package/src/feature/featureFactory.ts +163 -0
- package/src/feature/featureModule.ts +136 -0
- package/src/feature/featureModule.typecheck.ts +63 -0
- package/src/feature/featureNativeTree.ts +152 -0
- package/src/feature/featureRequirement.ts +82 -0
- package/src/feature/featureVariants.ts +234 -0
- package/src/feature/mountFeature.ts +54 -0
- package/src/graph/closure.ts +222 -0
- package/src/graph/services.ts +95 -0
- package/src/graph/summary.ts +134 -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 +61 -25
- package/src/internal/queryDriverHydrate.ts +45 -0
- package/src/internal/queryDriverStore.ts +8 -5
- package/src/internal/queryDriverTypes.ts +11 -11
- 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/store.test.ts +27 -6
- 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/pendingQueue.ts +145 -0
- package/src/remote/remoteState.test.ts +536 -354
- package/src/remote/remoteState.ts +145 -106
- package/src/remote/remoteState.typecheck.ts +47 -23
- package/src/remote/remoteStateMake.ts +97 -0
- package/src/remote/remoteStateSend.ts +95 -37
- package/src/remote/remoteStateTypes.ts +155 -0
- package/src/runtime/appRuntime.activation.test.ts +104 -0
- package/src/runtime/appRuntime.test.ts +187 -111
- package/src/runtime/appRuntime.ts +36 -130
- package/src/runtime/capturedAppRuntime.ts +274 -0
- package/src/runtime/eventBudget.test.ts +39 -12
- package/src/runtime/eventBudget.ts +9 -8
- package/src/runtime/featureMount.ts +94 -0
- package/src/runtime/hardening.test.ts +22 -13
- package/src/runtime/instrumentation.test.ts +213 -194
- package/src/runtime/instrumentation.ts +6 -11
- package/src/runtime/loop.test.ts +84 -7
- package/src/runtime/loop.ts +69 -40
- package/src/runtime/runtimeHandle.ts +124 -0
- 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 +39 -8
- 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 +84 -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/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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { expect, it } from '@effect/vitest'
|
|
2
|
-
import { Context,
|
|
2
|
+
import { Context, Effect, Exit, Layer, Schema as S, Scope } from 'effect'
|
|
3
3
|
import { CalcFamily, State, StateGroup } from '../index'
|
|
4
|
+
import { flush } from '../testkit/flight.testkit'
|
|
4
5
|
|
|
5
|
-
const tick = Effect.sleep(Duration.millis(1))
|
|
6
6
|
|
|
7
7
|
const Board = S.Record({ key: S.String, value: S.Number })
|
|
8
8
|
|
|
@@ -33,7 +33,7 @@ it.live('a member derives from the shared inputs and recomputes when they change
|
|
|
33
33
|
expect(yield* CalcFamily.read(Slice, 'b')).toBe(10)
|
|
34
34
|
|
|
35
35
|
feed.set({ a: 2, b: 10 })
|
|
36
|
-
yield*
|
|
36
|
+
yield* flush()
|
|
37
37
|
expect(yield* CalcFamily.read(Slice, 'a')).toBe(2)
|
|
38
38
|
}).pipe(Effect.provide(TestLayer))
|
|
39
39
|
})
|
|
@@ -55,28 +55,88 @@ it.live('a member only notifies when its own slice of the projection moves', ()
|
|
|
55
55
|
})
|
|
56
56
|
|
|
57
57
|
feed.set({ a: 2, b: 10 })
|
|
58
|
-
yield*
|
|
58
|
+
yield* flush()
|
|
59
59
|
expect(wakes).toEqual({ a: 1, b: 0 })
|
|
60
60
|
|
|
61
61
|
feed.set({ a: 2, b: 11 })
|
|
62
|
-
yield*
|
|
62
|
+
yield* flush()
|
|
63
63
|
expect(wakes).toEqual({ a: 1, b: 1 })
|
|
64
64
|
}).pipe(Effect.provide(TestLayer))
|
|
65
65
|
})
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
const reuseFixture = () => {
|
|
68
|
+
class Feed extends State.make('reuse-feed', Board) {}
|
|
69
|
+
class Revision extends State.make('reuse-revision', S.Number) {}
|
|
70
|
+
class Inputs extends StateGroup.make(Feed, Revision) {}
|
|
71
|
+
class Presentation extends CalcFamily.make('ReusePresentation', {
|
|
72
|
+
key: S.String,
|
|
73
|
+
inputs: [StateGroup.select(Inputs, 'reuse-feed'), StateGroup.select(Inputs, 'reuse-revision')],
|
|
74
|
+
output: S.Struct({ nested: S.Array(S.Struct({ value: S.Number })) }),
|
|
75
|
+
}) {}
|
|
76
|
+
const PresentationLive = CalcFamily.live(
|
|
77
|
+
Presentation,
|
|
78
|
+
(key) =>
|
|
79
|
+
({ 'reuse-feed': feed }) => ({
|
|
80
|
+
nested: [{ value: feed[key] ?? 0 }],
|
|
81
|
+
}),
|
|
82
|
+
{ reuse: true },
|
|
83
|
+
)
|
|
84
|
+
return { Inputs, Presentation, PresentationLive }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
it.live('reuse preserves each member output when equal and replaces it when changed', () => {
|
|
88
|
+
const { Inputs, Presentation, PresentationLive } = reuseFixture()
|
|
89
|
+
const TestLayer = PresentationLive.pipe(
|
|
90
|
+
Layer.provideMerge(
|
|
91
|
+
StateGroup.live(Inputs, { 'reuse-feed': { a: 1, b: 1 }, 'reuse-revision': 0 }),
|
|
92
|
+
),
|
|
71
93
|
)
|
|
72
94
|
return Effect.gen(function* () {
|
|
73
|
-
yield*
|
|
74
|
-
yield*
|
|
75
|
-
yield*
|
|
76
|
-
|
|
95
|
+
const feed = yield* StateGroup.select(Inputs, 'reuse-feed').store
|
|
96
|
+
const revision = yield* StateGroup.select(Inputs, 'reuse-revision').store
|
|
97
|
+
const family = yield* Presentation.store
|
|
98
|
+
const initialA = family.at('a').get()
|
|
99
|
+
const initialB = family.at('b').get()
|
|
100
|
+
const wakes = { a: 0, b: 0 }
|
|
101
|
+
family.at('a').subscribe(() => {
|
|
102
|
+
wakes.a += 1
|
|
103
|
+
})
|
|
104
|
+
family.at('b').subscribe(() => {
|
|
105
|
+
wakes.b += 1
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(Object.is(initialA.nested, initialB.nested)).toBe(false)
|
|
109
|
+
revision.set(1)
|
|
110
|
+
yield* flush()
|
|
111
|
+
expect(Object.is(family.at('a').get().nested, initialA.nested)).toBe(true)
|
|
112
|
+
expect(Object.is(family.at('b').get().nested, initialB.nested)).toBe(true)
|
|
113
|
+
expect(wakes).toEqual({ a: 0, b: 0 })
|
|
114
|
+
|
|
115
|
+
feed.set({ a: 2, b: 1 })
|
|
116
|
+
yield* flush()
|
|
117
|
+
expect(family.at('a').get().nested).toEqual([{ value: 2 }])
|
|
118
|
+
expect(Object.is(family.at('a').get().nested, initialA.nested)).toBe(false)
|
|
119
|
+
expect(Object.is(family.at('b').get().nested, initialB.nested)).toBe(true)
|
|
120
|
+
expect(wakes).toEqual({ a: 1, b: 0 })
|
|
77
121
|
}).pipe(Effect.provide(TestLayer))
|
|
78
122
|
})
|
|
79
123
|
|
|
124
|
+
it.live(
|
|
125
|
+
'reading does not recompute while the shared inputs are unchanged (memoized per member)',
|
|
126
|
+
() => {
|
|
127
|
+
const { Inputs, Slice, SliceLive, runs } = fixture()
|
|
128
|
+
const TestLayer = SliceLive.pipe(
|
|
129
|
+
Layer.provideMerge(StateGroup.live(Inputs, { feed: { a: 1 } })),
|
|
130
|
+
)
|
|
131
|
+
return Effect.gen(function* () {
|
|
132
|
+
yield* CalcFamily.read(Slice, 'a')
|
|
133
|
+
yield* CalcFamily.read(Slice, 'a')
|
|
134
|
+
yield* CalcFamily.read(Slice, 'a')
|
|
135
|
+
expect(runs.get('a')).toBe(1)
|
|
136
|
+
}).pipe(Effect.provide(TestLayer))
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
|
|
80
140
|
it.live('invalidateBy: shared-input churn outside the key recomputes no member', () => {
|
|
81
141
|
class Feed extends State.make('feed', Board) {}
|
|
82
142
|
class Noise extends State.make('noise', S.Number) {}
|
|
@@ -89,10 +149,11 @@ it.live('invalidateBy: shared-input churn outside the key recomputes no member',
|
|
|
89
149
|
}) {}
|
|
90
150
|
const SliceLive = CalcFamily.live(
|
|
91
151
|
Slice,
|
|
92
|
-
(key) =>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
152
|
+
(key) =>
|
|
153
|
+
({ feed }) => {
|
|
154
|
+
runs.set(key, (runs.get(key) ?? 0) + 1)
|
|
155
|
+
return feed[key] ?? 0
|
|
156
|
+
},
|
|
96
157
|
{ invalidateBy: ({ feed }) => [feed] },
|
|
97
158
|
)
|
|
98
159
|
const TestLayer = SliceLive.pipe(
|
|
@@ -104,7 +165,7 @@ it.live('invalidateBy: shared-input churn outside the key recomputes no member',
|
|
|
104
165
|
expect(runs.get('a')).toBe(1)
|
|
105
166
|
|
|
106
167
|
noise.set(99)
|
|
107
|
-
yield*
|
|
168
|
+
yield* flush()
|
|
108
169
|
expect(yield* CalcFamily.read(Slice, 'a')).toBe(1)
|
|
109
170
|
expect(runs.get('a')).toBe(1)
|
|
110
171
|
}).pipe(Effect.provide(TestLayer))
|
|
@@ -112,9 +173,7 @@ it.live('invalidateBy: shared-input churn outside the key recomputes no member',
|
|
|
112
173
|
|
|
113
174
|
it.live('forget releases the member and its upstream subscription', () => {
|
|
114
175
|
const { Inputs, Slice, SliceLive, runs } = fixture()
|
|
115
|
-
const TestLayer = SliceLive.pipe(
|
|
116
|
-
Layer.provideMerge(StateGroup.live(Inputs, { feed: { a: 1 } })),
|
|
117
|
-
)
|
|
176
|
+
const TestLayer = SliceLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { feed: { a: 1 } })))
|
|
118
177
|
return Effect.gen(function* () {
|
|
119
178
|
const feed = yield* StateGroup.select(Inputs, 'feed').store
|
|
120
179
|
const family = yield* Slice.store
|
|
@@ -125,7 +184,7 @@ it.live('forget releases the member and its upstream subscription', () => {
|
|
|
125
184
|
expect(family.size()).toBe(0)
|
|
126
185
|
const before = runs.get('a') ?? 0
|
|
127
186
|
feed.set({ a: 2 })
|
|
128
|
-
yield*
|
|
187
|
+
yield* flush()
|
|
129
188
|
expect(runs.get('a') ?? 0).toBe(before)
|
|
130
189
|
|
|
131
190
|
expect(family.at('a').get()).toBe(2)
|
|
@@ -143,15 +202,14 @@ it.live('evictWhenUnused: an idle member is dropped on the next microtask and un
|
|
|
143
202
|
}) {}
|
|
144
203
|
const SliceLive = CalcFamily.live(
|
|
145
204
|
Slice,
|
|
146
|
-
(key) =>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
205
|
+
(key) =>
|
|
206
|
+
({ feed }) => {
|
|
207
|
+
runs.set(key, (runs.get(key) ?? 0) + 1)
|
|
208
|
+
return feed[key] ?? 0
|
|
209
|
+
},
|
|
150
210
|
{ evictWhenUnused: true },
|
|
151
211
|
)
|
|
152
|
-
const TestLayer = SliceLive.pipe(
|
|
153
|
-
Layer.provideMerge(StateGroup.live(Inputs, { feed: { a: 1 } })),
|
|
154
|
-
)
|
|
212
|
+
const TestLayer = SliceLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { feed: { a: 1 } })))
|
|
155
213
|
return Effect.gen(function* () {
|
|
156
214
|
const feed = yield* StateGroup.select(Inputs, 'feed').store
|
|
157
215
|
const family = yield* Slice.store
|
|
@@ -159,11 +217,11 @@ it.live('evictWhenUnused: an idle member is dropped on the next microtask and un
|
|
|
159
217
|
expect(family.size()).toBe(1)
|
|
160
218
|
|
|
161
219
|
off()
|
|
162
|
-
yield*
|
|
220
|
+
yield* flush()
|
|
163
221
|
expect(family.size()).toBe(0)
|
|
164
222
|
const before = runs.get('a') ?? 0
|
|
165
223
|
feed.set({ a: 2 })
|
|
166
|
-
yield*
|
|
224
|
+
yield* flush()
|
|
167
225
|
expect(runs.get('a') ?? 0).toBe(before)
|
|
168
226
|
}).pipe(Effect.provide(TestLayer))
|
|
169
227
|
})
|
|
@@ -187,7 +245,7 @@ it.live('the layer finalizer clears every member (no live subscriptions leak)',
|
|
|
187
245
|
expect(family.size()).toBe(0)
|
|
188
246
|
const before = [runs.get('a') ?? 0, runs.get('b') ?? 0]
|
|
189
247
|
feed.set({ a: 9, b: 9 })
|
|
190
|
-
yield*
|
|
248
|
+
yield* flush()
|
|
191
249
|
expect([runs.get('a') ?? 0, runs.get('b') ?? 0]).toEqual(before)
|
|
192
250
|
}),
|
|
193
251
|
)
|
package/src/calc/calcFamily.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Effect, Layer, MutableRef } from 'effect'
|
|
1
|
+
import { Context, Effect, Layer, MutableRef, type Schema } from 'effect'
|
|
2
2
|
import { type Manifest, definitionClass } from '../definition/definition'
|
|
3
3
|
import {
|
|
4
4
|
type InputsObject,
|
|
@@ -9,45 +9,70 @@ import {
|
|
|
9
9
|
} from '../internal/sources'
|
|
10
10
|
import { resolveScheduler } from '../internal/scheduler'
|
|
11
11
|
import { resolveInstrumentation, stateUpdateHook } from '../runtime/instrumentation'
|
|
12
|
+
import { reuse } from '../internal/reuse'
|
|
12
13
|
import { makeDerivedStore, type Store } from '../internal/store'
|
|
13
14
|
import { readTracked } from '../internal/track'
|
|
14
15
|
import { type FamilyOptions, type FamilyStore } from '../state/stateFamily'
|
|
15
16
|
import { type AnySource } from '../state/token'
|
|
16
|
-
import type { AnySchema } from '../internal/variance'
|
|
17
17
|
|
|
18
|
-
export interface
|
|
18
|
+
export interface CalcFamilySchemaReflection {
|
|
19
|
+
readonly ast: Schema.Schema<unknown, unknown, unknown>['ast']
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CalcFamilyManifest<N extends string> extends Manifest {
|
|
19
23
|
readonly kind: 'CalcFamily'
|
|
20
24
|
readonly name: N
|
|
21
|
-
readonly key:
|
|
22
|
-
readonly output:
|
|
25
|
+
readonly key: CalcFamilySchemaReflection
|
|
26
|
+
readonly output: CalcFamilySchemaReflection
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const CalcFamilyStoreTypeId: unique symbol = Symbol.for('reform/CalcFamilyStore')
|
|
30
|
+
export type CalcFamilyStoreTypeId = typeof CalcFamilyStoreTypeId
|
|
31
|
+
|
|
32
|
+
export interface CalcFamilyStore<N extends string, in out K, in out Out> extends FamilyStore<
|
|
33
|
+
K,
|
|
34
|
+
Out
|
|
35
|
+
> {
|
|
36
|
+
readonly [CalcFamilyStoreTypeId]: N
|
|
23
37
|
}
|
|
24
38
|
|
|
25
39
|
export interface CalcFamilyClass<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
N extends string,
|
|
41
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
42
|
+
K,
|
|
43
|
+
Out,
|
|
30
44
|
> {
|
|
31
45
|
new (): { readonly Key: K }
|
|
32
|
-
readonly manifest: CalcFamilyManifest<N
|
|
33
|
-
readonly store: Context.Tag<
|
|
46
|
+
readonly manifest: CalcFamilyManifest<N>
|
|
47
|
+
readonly store: Context.Tag<CalcFamilyStore<N, K, Out>, FamilyStore<K, Out>>
|
|
34
48
|
readonly inputs: Inputs
|
|
35
49
|
}
|
|
36
50
|
|
|
37
|
-
export interface CalcFamilyConfig<
|
|
38
|
-
|
|
51
|
+
export interface CalcFamilyConfig<
|
|
52
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
53
|
+
KeySchema extends Schema.Schema.AnyNoContext,
|
|
54
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
55
|
+
> {
|
|
56
|
+
readonly key: KeySchema
|
|
39
57
|
readonly inputs: Inputs
|
|
40
|
-
readonly output:
|
|
58
|
+
readonly output: OutputSchema
|
|
41
59
|
}
|
|
42
60
|
|
|
43
|
-
export const make = <
|
|
61
|
+
export const make = <
|
|
62
|
+
const N extends string,
|
|
63
|
+
const Inputs extends ReadonlyArray<AnySource>,
|
|
64
|
+
KeySchema extends Schema.Schema.AnyNoContext,
|
|
65
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
66
|
+
>(
|
|
44
67
|
name: N,
|
|
45
|
-
config: CalcFamilyConfig<Inputs,
|
|
46
|
-
): CalcFamilyClass<N, Inputs,
|
|
47
|
-
|
|
68
|
+
config: CalcFamilyConfig<Inputs, KeySchema, OutputSchema>,
|
|
69
|
+
): CalcFamilyClass<N, Inputs, Schema.Schema.Type<KeySchema>, Schema.Schema.Type<OutputSchema>> => {
|
|
70
|
+
type K = Schema.Schema.Type<KeySchema>
|
|
71
|
+
type Out = Schema.Schema.Type<OutputSchema>
|
|
72
|
+
const store = Context.GenericTag<CalcFamilyStore<N, K, Out>, FamilyStore<K, Out>>(
|
|
48
73
|
`reform/calcFamily/${name}`,
|
|
49
74
|
)
|
|
50
|
-
const manifest: CalcFamilyManifest<N
|
|
75
|
+
const manifest: CalcFamilyManifest<N> = {
|
|
51
76
|
kind: 'CalcFamily',
|
|
52
77
|
name,
|
|
53
78
|
key: config.key,
|
|
@@ -63,19 +88,21 @@ export const make = <const N extends string, const Inputs extends ReadonlyArray<
|
|
|
63
88
|
export const read = <N extends string, Inputs extends ReadonlyArray<AnySource>, K, Out>(
|
|
64
89
|
family: CalcFamilyClass<N, Inputs, K, Out>,
|
|
65
90
|
key: K,
|
|
66
|
-
): Effect.Effect<Out, never,
|
|
91
|
+
): Effect.Effect<Out, never, CalcFamilyStore<N, K, Out>> =>
|
|
67
92
|
Effect.flatMap(family.store, (familyStore) => readTracked(familyStore.at(key)))
|
|
68
93
|
|
|
69
|
-
export interface CalcFamilyLiveOptionsExternalApi<
|
|
70
|
-
extends
|
|
94
|
+
export interface CalcFamilyLiveOptionsExternalApi<
|
|
95
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
96
|
+
> extends FamilyOptions {
|
|
71
97
|
readonly invalidateBy?: InvalidateBy<Inputs>
|
|
98
|
+
readonly reuse?: boolean
|
|
72
99
|
}
|
|
73
100
|
|
|
74
101
|
export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>, K, Out>(
|
|
75
102
|
family: CalcFamilyClass<N, Inputs, K, Out>,
|
|
76
103
|
compute: (key: K) => (inputs: InputsObject<Inputs>) => Out,
|
|
77
104
|
options: CalcFamilyLiveOptionsExternalApi<Inputs> = {},
|
|
78
|
-
): Layer.Layer<
|
|
105
|
+
): Layer.Layer<CalcFamilyStore<N, K, Out>, never, InputStores<Inputs>> =>
|
|
79
106
|
Layer.scoped(
|
|
80
107
|
family.store,
|
|
81
108
|
Effect.gen(function* () {
|
|
@@ -96,7 +123,9 @@ export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>,
|
|
|
96
123
|
subscribers.delete(key)
|
|
97
124
|
}
|
|
98
125
|
|
|
99
|
-
const createMember = (
|
|
126
|
+
const createMember = (
|
|
127
|
+
key: K,
|
|
128
|
+
): { readonly store: Store<Out>; readonly unsubscribe: () => void } => {
|
|
100
129
|
const body = compute(key)
|
|
101
130
|
const memo = MutableRef.make<
|
|
102
131
|
{ readonly key: ReadonlyArray<unknown>; readonly output: Out } | undefined
|
|
@@ -109,7 +138,9 @@ export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>,
|
|
|
109
138
|
return prev.output
|
|
110
139
|
}
|
|
111
140
|
const end = instrumentation.calcRecomputed(family.manifest.name)
|
|
112
|
-
const
|
|
141
|
+
const fresh = body(args)
|
|
142
|
+
const output =
|
|
143
|
+
options.reuse === true && prev !== undefined ? reuse(prev.output, fresh) : fresh
|
|
113
144
|
end()
|
|
114
145
|
MutableRef.set(memo, { key: memoKey, output })
|
|
115
146
|
return output
|
package/src/calc/compose.ts
CHANGED
|
@@ -13,20 +13,7 @@ export function composeCalcs<LOut, LErr, LIn, AOut, AErr, AIn, BOut, BErr, BIn>(
|
|
|
13
13
|
LErr | AErr | BErr,
|
|
14
14
|
BIn | Exclude<AIn | Exclude<LIn, AOut>, BOut>
|
|
15
15
|
>
|
|
16
|
-
export function composeCalcs<
|
|
17
|
-
LOut,
|
|
18
|
-
LErr,
|
|
19
|
-
LIn,
|
|
20
|
-
AOut,
|
|
21
|
-
AErr,
|
|
22
|
-
AIn,
|
|
23
|
-
BOut,
|
|
24
|
-
BErr,
|
|
25
|
-
BIn,
|
|
26
|
-
COut,
|
|
27
|
-
CErr,
|
|
28
|
-
CIn,
|
|
29
|
-
>(
|
|
16
|
+
export function composeCalcs<LOut, LErr, LIn, AOut, AErr, AIn, BOut, BErr, BIn, COut, CErr, CIn>(
|
|
30
17
|
leaf: Layer.Layer<LOut, LErr, LIn>,
|
|
31
18
|
upstream1: Layer.Layer<AOut, AErr, AIn>,
|
|
32
19
|
upstream2: Layer.Layer<BOut, BErr, BIn>,
|
package/src/channel/channel.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Chunk,
|
|
3
|
+
Context,
|
|
4
|
+
Duration,
|
|
5
|
+
Effect,
|
|
6
|
+
Exit,
|
|
7
|
+
Layer,
|
|
8
|
+
Match,
|
|
9
|
+
Option,
|
|
10
|
+
Queue,
|
|
11
|
+
Scope,
|
|
12
|
+
Stream,
|
|
13
|
+
} from 'effect'
|
|
2
14
|
import { type Manifest, definitionClass } from '../definition/definition'
|
|
3
15
|
import type { Tagged } from '../runtime/bus'
|
|
4
16
|
import { resolveInstrumentation } from '../runtime/instrumentation'
|
|
5
|
-
import { type ProcedureEntry, Procedures } from './
|
|
17
|
+
import { type ProcedureEntry, Procedures } from './procedures'
|
|
18
|
+
|
|
6
19
|
export {
|
|
7
20
|
type ProcedureEntry,
|
|
8
21
|
type ProcedureRegistry,
|
|
9
22
|
Procedures,
|
|
10
23
|
proceduresLayer,
|
|
11
|
-
} from './
|
|
24
|
+
} from './procedures'
|
|
12
25
|
|
|
13
26
|
export type ChannelPolicy =
|
|
14
27
|
| { readonly _tag: 'merge' }
|
|
@@ -53,7 +66,9 @@ const ChannelsBase: Context.TagClass<
|
|
|
53
66
|
> = Context.Tag('reform/Channels')<Channels, { readonly byName: Map<string, ChannelRuntime> }>()
|
|
54
67
|
export class Channels extends ChannelsBase {}
|
|
55
68
|
|
|
56
|
-
export const channelsLayer: Layer.Layer<Channels> = Layer.sync(Channels, () => ({
|
|
69
|
+
export const channelsLayer: Layer.Layer<Channels> = Layer.sync(Channels, () => ({
|
|
70
|
+
byName: new Map(),
|
|
71
|
+
}))
|
|
57
72
|
|
|
58
73
|
export const make = (name: string, config: ChannelConfig): ChannelClass =>
|
|
59
74
|
definitionClass<ChannelClass>({
|
|
@@ -155,9 +170,13 @@ export const live = (channel: ChannelClass): Layer.Layer<never, never, Channels
|
|
|
155
170
|
|
|
156
171
|
const events = Stream.fromQueue(queue)
|
|
157
172
|
const driven: Stream.Stream<unknown> = Match.value(channel.policy).pipe(
|
|
158
|
-
Match.tag('merge', () =>
|
|
173
|
+
Match.tag('merge', () =>
|
|
174
|
+
Stream.mapEffect(events, runMatching, { concurrency: 'unbounded' }),
|
|
175
|
+
),
|
|
159
176
|
Match.tag('latest', () =>
|
|
160
|
-
Stream.flatMap(events, (event) => Stream.fromEffect(runMatching(event)), {
|
|
177
|
+
Stream.flatMap(events, (event) => Stream.fromEffect(runMatching(event)), {
|
|
178
|
+
switch: true,
|
|
179
|
+
}),
|
|
161
180
|
),
|
|
162
181
|
Match.tag('debounce', (policy) =>
|
|
163
182
|
events.pipe(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Context, Effect, Layer } from 'effect'
|
|
1
|
+
import { Context, type Effect, Layer, Option } from 'effect'
|
|
2
|
+
import { makeBucketCache } from '../internal/bucketCache'
|
|
2
3
|
import type { Tagged } from '../runtime/bus'
|
|
3
4
|
|
|
4
5
|
export interface ProcedureEntry {
|
|
@@ -10,9 +11,11 @@ export interface ProcedureEntry {
|
|
|
10
11
|
|
|
11
12
|
export interface ProcedureRegistry {
|
|
12
13
|
readonly entries: Array<ProcedureEntry>
|
|
13
|
-
readonly
|
|
14
|
-
readonly
|
|
15
|
-
readonly
|
|
14
|
+
readonly byName: Map<string, Set<ProcedureEntry>>
|
|
15
|
+
readonly byTag: Map<string, Set<ProcedureEntry>>
|
|
16
|
+
readonly channelsByTag: Map<string, Set<string>>
|
|
17
|
+
readonly channelsFor: (tag: string) => ReadonlyArray<string>
|
|
18
|
+
readonly byChannelTag: Map<string, Map<string, Set<ProcedureEntry>>>
|
|
16
19
|
readonly register: (entry: ProcedureEntry) => void
|
|
17
20
|
readonly unregister: (entry: ProcedureEntry) => void
|
|
18
21
|
}
|
|
@@ -31,45 +34,56 @@ const getOrCreate = <K, V>(map: Map<K, V>, key: K, make: () => V): V => {
|
|
|
31
34
|
return created
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
const dropFrom = <K, T>(map: Map<K,
|
|
37
|
+
const dropFrom = <K, T>(map: Map<K, Set<T>>, key: K, target: T): void => {
|
|
35
38
|
const bucket = map.get(key)
|
|
36
39
|
if (bucket === undefined) {
|
|
37
40
|
return
|
|
38
41
|
}
|
|
39
|
-
|
|
40
|
-
if (
|
|
42
|
+
bucket.delete(target)
|
|
43
|
+
if (bucket.size === 0) {
|
|
41
44
|
map.delete(key)
|
|
42
|
-
} else {
|
|
43
|
-
map.set(key, next)
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
const makeProcedureRegistry = (): ProcedureRegistry => {
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
49
|
+
const entries = new Set<ProcedureEntry>()
|
|
50
|
+
const byName = new Map<string, Set<ProcedureEntry>>()
|
|
51
|
+
const byTag = new Map<string, Set<ProcedureEntry>>()
|
|
52
|
+
const channelsByTag = new Map<string, Set<string>>()
|
|
53
|
+
const channelsFor = makeBucketCache(channelsByTag)
|
|
54
|
+
const byChannelTag = new Map<string, Map<string, Set<ProcedureEntry>>>()
|
|
55
|
+
const snapshot: { value: Option.Option<Array<ProcedureEntry>> } = {
|
|
56
|
+
value: Option.some([]),
|
|
57
|
+
}
|
|
52
58
|
return {
|
|
53
59
|
get entries() {
|
|
54
|
-
return
|
|
60
|
+
return Option.getOrElse(snapshot.value, () => {
|
|
61
|
+
const current = Array.from(entries)
|
|
62
|
+
snapshot.value = Option.some(current)
|
|
63
|
+
return current
|
|
64
|
+
})
|
|
55
65
|
},
|
|
66
|
+
byName,
|
|
56
67
|
byTag,
|
|
57
68
|
channelsByTag,
|
|
69
|
+
channelsFor: channelsFor.read,
|
|
58
70
|
byChannelTag,
|
|
59
71
|
register: (entry) => {
|
|
60
|
-
|
|
72
|
+
entries.add(entry)
|
|
73
|
+
snapshot.value = Option.none()
|
|
74
|
+
getOrCreate(byName, entry.name, () => new Set()).add(entry)
|
|
61
75
|
entry.handles.forEach((tag) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
channelsByTag.set(tag, [...channels, entry.channelName])
|
|
66
|
-
}
|
|
76
|
+
getOrCreate(byTag, tag, () => new Set()).add(entry)
|
|
77
|
+
getOrCreate(channelsByTag, tag, () => new Set()).add(entry.channelName)
|
|
78
|
+
channelsFor.invalidate(tag)
|
|
67
79
|
const channelMap = getOrCreate(byChannelTag, entry.channelName, () => new Map())
|
|
68
|
-
|
|
80
|
+
getOrCreate(channelMap, tag, () => new Set()).add(entry)
|
|
69
81
|
})
|
|
70
82
|
},
|
|
71
83
|
unregister: (entry) => {
|
|
72
|
-
|
|
84
|
+
entries.delete(entry)
|
|
85
|
+
snapshot.value = Option.none()
|
|
86
|
+
dropFrom(byName, entry.name, entry)
|
|
73
87
|
entry.handles.forEach((tag) => {
|
|
74
88
|
dropFrom(byTag, tag, entry)
|
|
75
89
|
const channelMap = byChannelTag.get(entry.channelName)
|
|
@@ -77,6 +91,7 @@ const makeProcedureRegistry = (): ProcedureRegistry => {
|
|
|
77
91
|
dropFrom(channelMap, tag, entry)
|
|
78
92
|
if (channelMap.get(tag) === undefined) {
|
|
79
93
|
dropFrom(channelsByTag, tag, entry.channelName)
|
|
94
|
+
channelsFor.invalidate(tag)
|
|
80
95
|
}
|
|
81
96
|
if (channelMap.size === 0) {
|
|
82
97
|
byChannelTag.delete(entry.channelName)
|
|
@@ -87,4 +102,7 @@ const makeProcedureRegistry = (): ProcedureRegistry => {
|
|
|
87
102
|
}
|
|
88
103
|
}
|
|
89
104
|
|
|
90
|
-
export const proceduresLayer: Layer.Layer<Procedures> = Layer.sync(
|
|
105
|
+
export const proceduresLayer: Layer.Layer<Procedures> = Layer.sync(
|
|
106
|
+
Procedures,
|
|
107
|
+
makeProcedureRegistry,
|
|
108
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Option, Schema } from 'effect'
|
|
2
|
+
import { expect, it } from '@effect/vitest'
|
|
3
|
+
import { make } from './composition'
|
|
4
|
+
import { ui } from './ui'
|
|
5
|
+
|
|
6
|
+
class ItemUi extends ui('composition.parser.ItemUi')<{
|
|
7
|
+
props: { readonly label: string }
|
|
8
|
+
}>() {}
|
|
9
|
+
|
|
10
|
+
class Item extends make('composition.parser.Item', {
|
|
11
|
+
title: 'Item',
|
|
12
|
+
props: Schema.Struct({ label: Schema.String }),
|
|
13
|
+
ui: ItemUi,
|
|
14
|
+
})<Item>() {}
|
|
15
|
+
|
|
16
|
+
it('validates reflected props against the composition schema', () => {
|
|
17
|
+
expect(Item.parseProps({ label: 'ready' })).toEqual(Option.some({ label: 'ready' }))
|
|
18
|
+
expect(Option.isNone(Item.parseProps({ label: 1 }))).toBe(true)
|
|
19
|
+
})
|