@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,13 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
Effect,
|
|
4
|
-
Layer,
|
|
5
|
-
MutableRef,
|
|
6
|
-
Option,
|
|
7
|
-
Runtime,
|
|
8
|
-
Schema,
|
|
9
|
-
} from 'effect'
|
|
10
|
-
import { type AsyncData, narrowStore, widenStore } from '../calc/asyncData'
|
|
1
|
+
import { Context, Effect, Iterable, Layer, MutableRef, Option, Runtime, Schema } from 'effect'
|
|
2
|
+
import { type AnyAsyncData, AsyncData, narrowStore, widenStore } from '../calc/asyncData'
|
|
11
3
|
import * as Channel from '../channel/channel'
|
|
12
4
|
import * as Event from '../event/event'
|
|
13
5
|
import {
|
|
@@ -16,42 +8,48 @@ import {
|
|
|
16
8
|
RevisionSchema,
|
|
17
9
|
revisionZero,
|
|
18
10
|
} from '../internal/queryDriver'
|
|
11
|
+
import { reuse as shareStructure } from '../internal/reuse'
|
|
19
12
|
import { resolveScheduler } from '../internal/scheduler'
|
|
20
|
-
import { type InputStores } from '../internal/sources'
|
|
21
|
-
import {
|
|
13
|
+
import { type InputStores, sameKey } from '../internal/sources'
|
|
14
|
+
import { makeDerivedStore } from '../internal/store'
|
|
22
15
|
import * as Reducer from '../reducer/reducer'
|
|
23
16
|
import { Bus, publish } from '../runtime/bus'
|
|
24
17
|
import { Reducers } from '../runtime/loop'
|
|
25
18
|
import * as State from '../state/state'
|
|
26
19
|
import { type AnySource } from '../state/token'
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
type RemoteStateClass,
|
|
31
|
-
type RemoteStateSettled,
|
|
32
|
-
} from './remoteStateDefinition'
|
|
20
|
+
import { makePendingQueue, type PendingQueue } from './pendingQueue'
|
|
21
|
+
import { looseSchema } from './remoteStateMake'
|
|
22
|
+
import { queueReducerLayer, sendProcedureLayer } from './remoteStateSend'
|
|
33
23
|
import type {
|
|
24
|
+
RemoteStateClass,
|
|
34
25
|
RemoteStateLiveExternalApi,
|
|
35
|
-
|
|
26
|
+
RemoteStatePendingStore,
|
|
27
|
+
RemoteStateSettled,
|
|
28
|
+
RemoteStateTruthStore,
|
|
29
|
+
RemoteStateVisibleStore,
|
|
36
30
|
SettleLink,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
import { makeSendProcedureLayer } from './remoteStateSend'
|
|
40
|
-
|
|
41
|
-
// Generation rule: intent acked at gen g settles only on Success with gen > g (no flicker).
|
|
42
|
-
// apply must be idempotent: between refetch landing and settle, intent is applied over truth that already has it.
|
|
31
|
+
WaiterRegistration,
|
|
32
|
+
} from './remoteStateTypes'
|
|
43
33
|
|
|
34
|
+
export { make } from './remoteStateMake'
|
|
44
35
|
export {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
36
|
+
RemoteStatePendingStoreTypeId,
|
|
37
|
+
RemoteStateTruthStoreTypeId,
|
|
38
|
+
RemoteStateVisibleStoreTypeId,
|
|
39
|
+
} from './remoteStateTypes'
|
|
40
|
+
export type {
|
|
41
|
+
FailedIntent,
|
|
42
|
+
PendingIntent,
|
|
43
|
+
RemoteStateClass,
|
|
44
|
+
RemoteStateConfigExternalApi,
|
|
45
|
+
RemoteStateLiveExternalApi,
|
|
46
|
+
RemoteStateManifestExternalApi,
|
|
47
|
+
RemoteStatePendingStore,
|
|
48
|
+
RemoteStateSchemaReflection,
|
|
49
|
+
RemoteStateSettled,
|
|
50
|
+
RemoteStateTruthStore,
|
|
51
|
+
RemoteStateVisibleStore,
|
|
52
|
+
} from './remoteStateTypes'
|
|
55
53
|
|
|
56
54
|
export const live = <
|
|
57
55
|
N extends string,
|
|
@@ -67,21 +65,17 @@ export const live = <
|
|
|
67
65
|
remote: RemoteStateClass<N, Inputs, Intents, A, E, Gated>,
|
|
68
66
|
config: RemoteStateLiveExternalApi<Inputs, Intents, A, E, Gated, R, R2, SettledEvent>,
|
|
69
67
|
): Layer.Layer<
|
|
70
|
-
|
|
|
71
|
-
| Store<ReadonlyArray<PendingIntent<Event.EventType<Intents[number]>>>>,
|
|
68
|
+
RemoteStateVisibleStore<N> | RemoteStateTruthStore<N> | RemoteStatePendingStore<N>,
|
|
72
69
|
never,
|
|
73
|
-
|
|
74
|
-
| R
|
|
75
|
-
| R2
|
|
76
|
-
| Reducers
|
|
77
|
-
| Channel.Procedures
|
|
78
|
-
| Channel.Channels
|
|
79
|
-
| Bus
|
|
70
|
+
InputStores<Inputs> | R | R2 | Reducers | Channel.Procedures | Channel.Channels | Bus
|
|
80
71
|
> => {
|
|
81
72
|
type I = Event.EventType<Intents[number]>
|
|
82
73
|
const name = remote.manifest.name
|
|
83
74
|
const truthTag = remote.truth.store
|
|
84
75
|
const pendingTag = remote.pending.store
|
|
76
|
+
const pendingQueueTag = Context.GenericTag<PendingQueue<I>>(
|
|
77
|
+
`reform/remoteState/${name}/pendingQueue`,
|
|
78
|
+
)
|
|
85
79
|
|
|
86
80
|
const Queued = Event.make(
|
|
87
81
|
`${name}/Queued`,
|
|
@@ -107,53 +101,57 @@ export const live = <
|
|
|
107
101
|
|
|
108
102
|
const driverLayer = Layer.scopedContext(
|
|
109
103
|
Effect.gen(function* () {
|
|
110
|
-
// oxlint-disable-next-line reform-rules/no-type-assertion -- type-level erasure seam: `disabled` is `never` for non-gated definitions; read it back through the runtime view
|
|
111
|
-
const cfg = config as RemoteStateLiveRuntimeExternalApi<Inputs, A, E, R>
|
|
112
104
|
const revision = Option.getOrUndefined(yield* Effect.serviceOption(revisionState.store))
|
|
113
105
|
const runtime = yield* Effect.runtime<Bus>()
|
|
114
106
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
>([])
|
|
107
|
+
// Buckets, not cancellable fibers, keep waiters alive under latest/exclusive policies.
|
|
108
|
+
// Monotonic waitFor values make insertion order match generation order.
|
|
109
|
+
const waiters = new Map<number, Set<{ readonly opId: string }>>()
|
|
119
110
|
const onSettled = (generation: number) => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
),
|
|
111
|
+
Iterable.forEach(
|
|
112
|
+
Iterable.takeWhile(waiters, ([waitFor]) => waitFor <= generation),
|
|
113
|
+
([waitFor, generationWaiters]) => {
|
|
114
|
+
waiters.delete(waitFor)
|
|
115
|
+
// runSync enqueues settles in the same flush as converged truth.
|
|
116
|
+
generationWaiters.forEach(({ opId }) =>
|
|
117
|
+
Runtime.runSync(runtime)(publish('Normal', Event.construct(Settled, { opId }))),
|
|
118
|
+
)
|
|
119
|
+
},
|
|
130
120
|
)
|
|
131
121
|
}
|
|
122
|
+
const registerWaiter = ({ opId, waitFor }: WaiterRegistration): void => {
|
|
123
|
+
const generationWaiters = waiters.get(waitFor)
|
|
124
|
+
if (generationWaiters === undefined) {
|
|
125
|
+
waiters.set(waitFor, new Set([{ opId }]))
|
|
126
|
+
} else {
|
|
127
|
+
generationWaiters.add({ opId })
|
|
128
|
+
}
|
|
129
|
+
}
|
|
132
130
|
|
|
133
|
-
|
|
134
|
-
const persistOption = cfg.persist
|
|
131
|
+
const persistOption = config.persist
|
|
135
132
|
const persistKey =
|
|
136
133
|
persistOption === true || persistOption === undefined || persistOption === false
|
|
137
134
|
? name
|
|
138
|
-
: persistOption.key ?? name
|
|
135
|
+
: (persistOption.key ?? name)
|
|
139
136
|
const persist =
|
|
140
137
|
persistOption === undefined || persistOption === false
|
|
141
138
|
? undefined
|
|
142
|
-
: { key: persistKey,
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
139
|
+
: { key: persistKey, codec: remote.codec }
|
|
140
|
+
const revisionKey = (source: NonNullable<typeof revision>) => ({
|
|
141
|
+
read: () => source.getSnapshot(),
|
|
142
|
+
subscribe: (listener: () => void) => source.subscribe(listener),
|
|
143
|
+
})
|
|
144
|
+
const extraKey = revision === undefined ? undefined : revisionKey(revision)
|
|
147
145
|
const driver = yield* makeQueryDriver({
|
|
148
146
|
name,
|
|
149
147
|
label: 'RemoteState',
|
|
150
148
|
gated: remote.gated,
|
|
151
149
|
inputs: remote.inputs,
|
|
152
|
-
query:
|
|
153
|
-
invalidateBy:
|
|
154
|
-
disabled:
|
|
155
|
-
coalesce:
|
|
156
|
-
reuse:
|
|
150
|
+
query: config.query,
|
|
151
|
+
invalidateBy: config.invalidateBy,
|
|
152
|
+
disabled: config.disabled,
|
|
153
|
+
coalesce: config.coalesce,
|
|
154
|
+
reuse: config.reuse,
|
|
157
155
|
persist,
|
|
158
156
|
extraKey,
|
|
159
157
|
onSettled,
|
|
@@ -173,7 +171,7 @@ export const live = <
|
|
|
173
171
|
Runtime.runSync(runtime)(
|
|
174
172
|
publish(
|
|
175
173
|
'Normal',
|
|
176
|
-
|
|
174
|
+
settledConfig.event.buildUnknown(settledConfig.payload(settledTruth)),
|
|
177
175
|
),
|
|
178
176
|
)
|
|
179
177
|
}
|
|
@@ -185,8 +183,7 @@ export const live = <
|
|
|
185
183
|
}
|
|
186
184
|
const link: SettleLink = {
|
|
187
185
|
requested: driver.requested,
|
|
188
|
-
register: (opId, waitFor) =>
|
|
189
|
-
MutableRef.update(waiters, (all) => [...all, { opId, waitFor }]),
|
|
186
|
+
register: (opId, waitFor) => registerWaiter({ opId, waitFor }),
|
|
190
187
|
}
|
|
191
188
|
return Context.make(truthTag, narrowStore<A, E, Gated>(driver.store)).pipe(
|
|
192
189
|
Context.add(linkTag, link),
|
|
@@ -194,48 +191,90 @@ export const live = <
|
|
|
194
191
|
}),
|
|
195
192
|
)
|
|
196
193
|
|
|
197
|
-
const overlayLayer =
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
194
|
+
const overlayLayer = Layer.scoped(
|
|
195
|
+
remote.store,
|
|
196
|
+
Effect.gen(function* () {
|
|
197
|
+
const scheduler = yield* resolveScheduler
|
|
198
|
+
const feedStore = widenStore<A, E, Gated>(yield* truthTag)
|
|
199
|
+
const queue = yield* pendingQueueTag
|
|
200
|
+
const memo = MutableRef.make<
|
|
201
|
+
{ readonly key: ReadonlyArray<unknown>; readonly output: AnyAsyncData<A, E> } | undefined
|
|
202
|
+
>(undefined)
|
|
203
|
+
const recompute = (): AnyAsyncData<A, E> => {
|
|
204
|
+
const feed = feedStore.getSnapshot()
|
|
205
|
+
const key = [feed, queue.revision()]
|
|
206
|
+
const prev = MutableRef.get(memo)
|
|
207
|
+
if (prev !== undefined && sameKey(key, prev.key)) {
|
|
208
|
+
return prev.output
|
|
209
|
+
}
|
|
210
|
+
// Full replay preserves observable value identity when apply allocates and reuse is off.
|
|
211
|
+
const applyPending = (base: A): A =>
|
|
212
|
+
queue.fold(base, (applied, entry) => config.apply(applied, entry.intent))
|
|
213
|
+
const overlaid: AnyAsyncData<A, E> =
|
|
214
|
+
feed._tag === 'Success'
|
|
215
|
+
? AsyncData.success(applyPending(feed.value), feed.refetching)
|
|
216
|
+
: feed
|
|
217
|
+
const reusable =
|
|
218
|
+
config.reuse === true &&
|
|
219
|
+
prev !== undefined &&
|
|
220
|
+
prev.output._tag === 'Success' &&
|
|
221
|
+
overlaid._tag === 'Success'
|
|
222
|
+
? shareStructure(prev.output.value, overlaid.value)
|
|
223
|
+
: undefined
|
|
224
|
+
const output: AnyAsyncData<A, E> =
|
|
225
|
+
reusable === undefined || overlaid._tag !== 'Success'
|
|
226
|
+
? overlaid
|
|
227
|
+
: AsyncData.success(reusable, overlaid.refetching)
|
|
228
|
+
MutableRef.set(memo, { key, output })
|
|
229
|
+
return output
|
|
230
|
+
}
|
|
231
|
+
const subscribe = (listener: () => void): (() => void) => {
|
|
232
|
+
const offFeed = feedStore.subscribe(listener)
|
|
233
|
+
const offQueue = queue.store.subscribe(listener)
|
|
234
|
+
return () => {
|
|
235
|
+
offFeed()
|
|
236
|
+
offQueue()
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const derived = makeDerivedStore(recompute, subscribe, scheduler)
|
|
240
|
+
yield* Effect.addFinalizer(() => Effect.sync(derived.unsubscribe))
|
|
241
|
+
return narrowStore<A, E, Gated>(derived.store)
|
|
242
|
+
}),
|
|
243
|
+
)
|
|
204
244
|
|
|
205
|
-
const queueReducerLayer = makeQueueReducerLayer<I>({
|
|
206
|
-
ackedTag: Acked.tag,
|
|
207
|
-
name: `${name}/pending`,
|
|
208
|
-
pendingTag,
|
|
209
|
-
queuedTag: Queued.tag,
|
|
210
|
-
settledTag: Settled.tag,
|
|
211
|
-
})
|
|
212
245
|
const channel = config.channel ?? Channel.make(`${name}/sends`, { policy: { _tag: 'merge' } })
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
const sendProcedureLayer = makeSendProcedureLayer<I, R2>({
|
|
246
|
+
const intentEvents = {
|
|
247
|
+
queued: Queued,
|
|
216
248
|
acked: Acked,
|
|
217
|
-
|
|
218
|
-
failed: remote.Failed,
|
|
219
|
-
handles: sendHandles,
|
|
249
|
+
settled: Settled,
|
|
220
250
|
invalidated: Invalidated,
|
|
251
|
+
}
|
|
252
|
+
const pendingReducer = queueReducerLayer({
|
|
253
|
+
name: `${name}/pending`,
|
|
254
|
+
queueTag: pendingQueueTag,
|
|
255
|
+
events: intentEvents,
|
|
256
|
+
})
|
|
257
|
+
const sendProcedure = sendProcedureLayer({
|
|
258
|
+
name: `${name}/send`,
|
|
259
|
+
channel,
|
|
260
|
+
handles: new Set(remote.intents.map((event) => event.tag)),
|
|
221
261
|
linkTag,
|
|
222
|
-
|
|
223
|
-
queued: Queued,
|
|
262
|
+
failed: remote.Failed,
|
|
224
263
|
send: config.send,
|
|
225
|
-
|
|
264
|
+
events: intentEvents,
|
|
226
265
|
})
|
|
227
266
|
|
|
228
|
-
const pendingStoreLayer = Layer.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
),
|
|
267
|
+
const pendingStoreLayer = Layer.scopedContext(
|
|
268
|
+
Effect.map(resolveScheduler, (scheduler) => {
|
|
269
|
+
const queue = makePendingQueue<I>(scheduler)
|
|
270
|
+
return Context.make(pendingTag, queue.store).pipe(Context.add(pendingQueueTag, queue))
|
|
271
|
+
}),
|
|
233
272
|
)
|
|
234
273
|
return Layer.mergeAll(
|
|
235
274
|
overlayLayer,
|
|
236
|
-
|
|
275
|
+
pendingReducer,
|
|
237
276
|
Reducer.live(revisionReducer, bumpRevision),
|
|
238
|
-
|
|
277
|
+
sendProcedure,
|
|
239
278
|
Channel.live(channel),
|
|
240
279
|
).pipe(
|
|
241
280
|
Layer.provideMerge(driverLayer),
|
|
@@ -3,7 +3,6 @@ import * as Boundary from '../boundary/boundary'
|
|
|
3
3
|
import type { AsyncData } from '../calc/asyncData'
|
|
4
4
|
import { type Channels, type Procedures } from '../channel/channel'
|
|
5
5
|
import * as Event from '../event/event'
|
|
6
|
-
import type { Store } from '../internal/store'
|
|
7
6
|
import * as Reducer from '../reducer/reducer'
|
|
8
7
|
import type { Bus } from '../runtime/bus'
|
|
9
8
|
import { type Reducers } from '../runtime/loop'
|
|
@@ -12,8 +11,10 @@ import * as StateGroup from '../state/stateGroup'
|
|
|
12
11
|
import { type AnySource, type Source } from '../state/token'
|
|
13
12
|
import * as RemoteState from './remoteState'
|
|
14
13
|
|
|
15
|
-
const AddIntentBase: Event.EventClass<
|
|
16
|
-
|
|
14
|
+
const AddIntentBase: Event.EventClass<
|
|
15
|
+
'TAddIntent',
|
|
16
|
+
{ readonly id: string; readonly name: string }
|
|
17
|
+
> = Event.make('TAddIntent', S.Struct({ id: S.String, name: S.String }))
|
|
17
18
|
class AddIntent extends AddIntentBase {}
|
|
18
19
|
const RenameIntentBase: Event.EventClass<
|
|
19
20
|
'TRenameIntent',
|
|
@@ -27,7 +28,10 @@ const PageBase: State.StateClass<'tcPage', number> = State.make('tcPage', S.Numb
|
|
|
27
28
|
class Page extends PageBase {}
|
|
28
29
|
const InputsBase: StateGroup.StateGroupClass<readonly [typeof Page]> = StateGroup.make(Page)
|
|
29
30
|
class Inputs extends InputsBase {}
|
|
30
|
-
const page: Source<'tcPage', number
|
|
31
|
+
const page: Source<'tcPage', number, State.StateStore<'tcPage', number>> = StateGroup.select(
|
|
32
|
+
Inputs,
|
|
33
|
+
'tcPage',
|
|
34
|
+
)
|
|
31
35
|
|
|
32
36
|
const Row: S.Struct<{ id: typeof S.String; name: typeof S.String }> = S.Struct({
|
|
33
37
|
id: S.String,
|
|
@@ -58,10 +62,11 @@ const ItemsBase: RemoteState.RemoteStateClass<
|
|
|
58
62
|
class Items extends ItemsBase {}
|
|
59
63
|
|
|
60
64
|
export const okLive: Layer.Layer<
|
|
61
|
-
|
|
|
62
|
-
|
|
|
65
|
+
| RemoteState.RemoteStateVisibleStore<'TItems'>
|
|
66
|
+
| RemoteState.RemoteStateTruthStore<'TItems'>
|
|
67
|
+
| RemoteState.RemoteStatePendingStore<'TItems'>,
|
|
63
68
|
never,
|
|
64
|
-
|
|
69
|
+
State.StateStore<'tcPage', number> | SendDep | Reducers | Procedures | Channels | Bus
|
|
65
70
|
> = RemoteState.live(Items, {
|
|
66
71
|
query: ({ tcPage }) =>
|
|
67
72
|
tcPage > 0 ? Effect.succeed<ReadonlyArray<Row>>([]) : Effect.fail('empty page'),
|
|
@@ -78,10 +83,11 @@ export const okLive: Layer.Layer<
|
|
|
78
83
|
})
|
|
79
84
|
|
|
80
85
|
export const badIntent: Layer.Layer<
|
|
81
|
-
|
|
|
82
|
-
|
|
|
86
|
+
| RemoteState.RemoteStateVisibleStore<'TItems'>
|
|
87
|
+
| RemoteState.RemoteStateTruthStore<'TItems'>
|
|
88
|
+
| RemoteState.RemoteStatePendingStore<'TItems'>,
|
|
83
89
|
never,
|
|
84
|
-
|
|
90
|
+
State.StateStore<'tcPage', number> | Reducers | Procedures | Channels | Bus
|
|
85
91
|
> = RemoteState.live(Items, {
|
|
86
92
|
query: () => Effect.succeed<ReadonlyArray<Row>>([]),
|
|
87
93
|
send: () => Effect.void,
|
|
@@ -90,16 +96,20 @@ export const badIntent: Layer.Layer<
|
|
|
90
96
|
})
|
|
91
97
|
|
|
92
98
|
export const okLayer: Layer.Layer<
|
|
93
|
-
|
|
99
|
+
| RemoteState.RemoteStateVisibleStore<'TItems'>
|
|
100
|
+
| RemoteState.RemoteStateTruthStore<'TItems'>
|
|
101
|
+
| RemoteState.RemoteStatePendingStore<'TItems'>,
|
|
94
102
|
never,
|
|
95
|
-
|
|
103
|
+
State.StateStore<'tcPage', number> | SendDep | Reducers | Procedures | Channels | Bus
|
|
96
104
|
> = okLive
|
|
97
105
|
|
|
98
106
|
// @ts-expect-error the send body's R2 must appear in the layer's requirements
|
|
99
107
|
export const badLayer: Layer.Layer<
|
|
100
|
-
|
|
108
|
+
| RemoteState.RemoteStateVisibleStore<'TItems'>
|
|
109
|
+
| RemoteState.RemoteStateTruthStore<'TItems'>
|
|
110
|
+
| RemoteState.RemoteStatePendingStore<'TItems'>,
|
|
101
111
|
never,
|
|
102
|
-
|
|
112
|
+
State.StateStore<'tcPage', number> | Reducers | Procedures | Channels | Bus
|
|
103
113
|
> = okLive
|
|
104
114
|
|
|
105
115
|
const AlwaysItemsBase: RemoteState.RemoteStateClass<
|
|
@@ -118,10 +128,11 @@ const AlwaysItemsBase: RemoteState.RemoteStateClass<
|
|
|
118
128
|
class AlwaysItems extends AlwaysItemsBase {}
|
|
119
129
|
|
|
120
130
|
export const badDisabled: Layer.Layer<
|
|
121
|
-
|
|
|
122
|
-
|
|
|
131
|
+
| RemoteState.RemoteStateVisibleStore<'TAlwaysItems'>
|
|
132
|
+
| RemoteState.RemoteStateTruthStore<'TAlwaysItems'>
|
|
133
|
+
| RemoteState.RemoteStatePendingStore<'TAlwaysItems'>,
|
|
123
134
|
never,
|
|
124
|
-
|
|
135
|
+
State.StateStore<'tcPage', number> | Reducers | Procedures | Channels | Bus
|
|
125
136
|
> = RemoteState.live(AlwaysItems, {
|
|
126
137
|
query: () => Effect.succeed<ReadonlyArray<Row>>([]),
|
|
127
138
|
send: () => Effect.void,
|
|
@@ -145,19 +156,32 @@ export const noIdle: Effect.Effect.Success<typeof AlwaysItems>['_tag'] = idleArm
|
|
|
145
156
|
// @ts-expect-error no `error` schema ⇒ no Error arm
|
|
146
157
|
export const noError: Effect.Effect.Success<typeof AlwaysItems>['_tag'] = errorArm
|
|
147
158
|
|
|
148
|
-
export const classAsSource: Source<
|
|
159
|
+
export const classAsSource: Source<
|
|
160
|
+
'TItems',
|
|
161
|
+
AsyncData<ReadonlyArray<Row>, string, true>,
|
|
162
|
+
RemoteState.RemoteStateVisibleStore<'TItems'>
|
|
163
|
+
> = Items
|
|
149
164
|
export const classAsAnySource: AnySource = Items
|
|
150
165
|
|
|
151
|
-
export const pendingAsSource: Source<
|
|
152
|
-
|
|
166
|
+
export const pendingAsSource: Source<
|
|
167
|
+
'TItems/pending',
|
|
168
|
+
ReadonlyArray<RemoteState.PendingIntent<Intent>>,
|
|
169
|
+
RemoteState.RemoteStatePendingStore<'TItems'>
|
|
170
|
+
> = Items.pending
|
|
153
171
|
|
|
154
|
-
export const truthAsSource: Source<
|
|
155
|
-
|
|
172
|
+
export const truthAsSource: Source<
|
|
173
|
+
'TItems/truth',
|
|
174
|
+
AsyncData<ReadonlyArray<Row>, string, true>,
|
|
175
|
+
RemoteState.RemoteStateTruthStore<'TItems'>
|
|
176
|
+
> = Items.truth
|
|
156
177
|
|
|
157
178
|
export const covered: Boundary.BoundaryClass<'TItemsBoundary', readonly [typeof Items]> =
|
|
158
179
|
Boundary.make('TItemsBoundary', { over: [Items] })
|
|
159
180
|
|
|
160
|
-
export const badReducerTarget: Reducer.StateReducerClass<
|
|
181
|
+
export const badReducerTarget: Reducer.StateReducerClass<
|
|
182
|
+
State.AnyState,
|
|
183
|
+
readonly [typeof AddIntent]
|
|
184
|
+
> =
|
|
161
185
|
// @ts-expect-error the pending queue is not a reducible state
|
|
162
186
|
Reducer.make('TBadReducer', {
|
|
163
187
|
states: [Items.pending],
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Context, Effect, Schema } from 'effect'
|
|
2
|
+
import type { AsyncData } from '../calc/asyncData'
|
|
3
|
+
import { yieldableClass } from '../definition/definition'
|
|
4
|
+
import * as Event from '../event/event'
|
|
5
|
+
import { type GatedOf, gatedFlag, type QueryCodec } from '../internal/queryDriver'
|
|
6
|
+
import type { Store } from '../internal/store'
|
|
7
|
+
import { readTracked } from '../internal/track'
|
|
8
|
+
import { type AnySource, type SourceCapture, StateToken } from '../state/token'
|
|
9
|
+
import type {
|
|
10
|
+
FailedIntent,
|
|
11
|
+
PendingIntent,
|
|
12
|
+
RemoteStateClass,
|
|
13
|
+
RemoteStateConfigExternalApi,
|
|
14
|
+
RemoteStateManifestExternalApi,
|
|
15
|
+
RemoteStatePendingStore,
|
|
16
|
+
RemoteStateTruthStore,
|
|
17
|
+
RemoteStateVisibleStore,
|
|
18
|
+
} from './remoteStateTypes'
|
|
19
|
+
|
|
20
|
+
// Schema.Unknown is structural because Intents is open at make-time; the declaration preserves its authored payload type.
|
|
21
|
+
export const looseSchema = <P>(schema: Schema.Schema.AnyNoContext): Schema.Schema<P, P, never> =>
|
|
22
|
+
Schema.declare((input): input is P => Schema.is(schema)(input))
|
|
23
|
+
|
|
24
|
+
export const make = <
|
|
25
|
+
const N extends string,
|
|
26
|
+
const Inputs extends ReadonlyArray<AnySource>,
|
|
27
|
+
const Intents extends ReadonlyArray<Event.AnyEvent>,
|
|
28
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
29
|
+
ErrorSchema extends Schema.Schema.AnyNoContext | undefined = undefined,
|
|
30
|
+
const AlwaysOn extends boolean = false,
|
|
31
|
+
>(
|
|
32
|
+
name: N,
|
|
33
|
+
config: RemoteStateConfigExternalApi<Inputs, Intents, OutputSchema, ErrorSchema, AlwaysOn>,
|
|
34
|
+
): RemoteStateClass<
|
|
35
|
+
N,
|
|
36
|
+
Inputs,
|
|
37
|
+
Intents,
|
|
38
|
+
Schema.Schema.Type<OutputSchema>,
|
|
39
|
+
ErrorSchema extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<ErrorSchema> : never,
|
|
40
|
+
GatedOf<AlwaysOn>
|
|
41
|
+
> => {
|
|
42
|
+
type A = Schema.Schema.Type<OutputSchema>
|
|
43
|
+
type E = ErrorSchema extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<ErrorSchema> : never
|
|
44
|
+
type I = Event.EventType<Intents[number]>
|
|
45
|
+
const store = Context.GenericTag<
|
|
46
|
+
RemoteStateVisibleStore<N>,
|
|
47
|
+
Store<AsyncData<A, E, GatedOf<AlwaysOn>>>
|
|
48
|
+
>(`reform/remoteState/${name}`)
|
|
49
|
+
const truthTag = Context.GenericTag<
|
|
50
|
+
RemoteStateTruthStore<N>,
|
|
51
|
+
Store<AsyncData<A, E, GatedOf<AlwaysOn>>>
|
|
52
|
+
>(`reform/remoteState/${name}/truth`)
|
|
53
|
+
const pendingTag = Context.GenericTag<
|
|
54
|
+
RemoteStatePendingStore<N>,
|
|
55
|
+
Store<ReadonlyArray<PendingIntent<I>>>
|
|
56
|
+
>(`reform/remoteState/${name}/pending`)
|
|
57
|
+
const gated = gatedFlag(config.alwaysOn)
|
|
58
|
+
const manifest: RemoteStateManifestExternalApi<N> = {
|
|
59
|
+
kind: 'RemoteState',
|
|
60
|
+
name,
|
|
61
|
+
output: config.output,
|
|
62
|
+
gated,
|
|
63
|
+
intents: config.intents.map((event) => event.tag),
|
|
64
|
+
...(config.error !== undefined ? { error: config.error } : {}),
|
|
65
|
+
}
|
|
66
|
+
const codec: QueryCodec<A> = {
|
|
67
|
+
encode: Schema.encode(config.output),
|
|
68
|
+
decode: Schema.decodeUnknown(config.output),
|
|
69
|
+
}
|
|
70
|
+
// Annotations preserve template-literal types for the public statics.
|
|
71
|
+
const truthName: `${N}/truth` = `${name}/truth`
|
|
72
|
+
const pendingName: `${N}/pending` = `${name}/pending`
|
|
73
|
+
const failedName: `${N}/Failed` = `${name}/Failed`
|
|
74
|
+
const read = Effect.flatMap(store, readTracked)
|
|
75
|
+
const remote: RemoteStateClass<N, Inputs, Intents, A, E, GatedOf<AlwaysOn>> = yieldableClass(
|
|
76
|
+
read,
|
|
77
|
+
{
|
|
78
|
+
manifest,
|
|
79
|
+
codec,
|
|
80
|
+
store,
|
|
81
|
+
name,
|
|
82
|
+
inputs: config.inputs,
|
|
83
|
+
intents: config.intents,
|
|
84
|
+
gated,
|
|
85
|
+
capture: <Result>(visit: SourceCapture<Result>): Result => visit(remote),
|
|
86
|
+
truth: new StateToken(truthName, truthTag),
|
|
87
|
+
pending: new StateToken(pendingName, pendingTag),
|
|
88
|
+
Failed: Event.make(
|
|
89
|
+
failedName,
|
|
90
|
+
looseSchema<FailedIntent<I>>(
|
|
91
|
+
Schema.Struct({ intent: Schema.Unknown, error: Schema.Unknown }),
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
return remote
|
|
97
|
+
}
|