@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
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { Context, Effect, Schema } from 'effect'
|
|
2
|
-
import type { Effect as EffectType } from 'effect/Effect'
|
|
3
|
-
import { type Manifest, yieldableClass } from '../definition/definition'
|
|
4
|
-
import * as Event from '../event/event'
|
|
5
|
-
import { readTracked } from '../internal/track'
|
|
6
|
-
import type { AnySchema } from '../internal/variance'
|
|
7
|
-
import type { GatedOf } from '../internal/queryDriver'
|
|
8
|
-
import { gatedFlag } from '../internal/queryDriver'
|
|
9
|
-
import type { AnySource } from '../state/token'
|
|
10
|
-
import { StateToken } from '../state/token'
|
|
11
|
-
import type { Store } from '../internal/store'
|
|
12
|
-
import type { AsyncData, AsyncError, AsyncSuccess } from '../calc/asyncData'
|
|
13
|
-
|
|
14
|
-
export interface PendingIntent<I> {
|
|
15
|
-
readonly opId: string
|
|
16
|
-
readonly intent: I
|
|
17
|
-
readonly status: 'sending' | 'confirmed'
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface FailedIntent<I> {
|
|
21
|
-
readonly intent: I
|
|
22
|
-
readonly error: unknown
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export type RemoteStateSettled<A, E> = AsyncSuccess<A> | AsyncError<E>
|
|
26
|
-
|
|
27
|
-
// ExternalApi: optional `intent` only on Queued envelopes
|
|
28
|
-
export interface QueueFoldEventExternalApi<I> {
|
|
29
|
-
readonly _tag: string
|
|
30
|
-
readonly opId: string
|
|
31
|
-
readonly intent?: I
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface RemoteStateManifestExternalApi<N extends string, A, E> extends Manifest {
|
|
35
|
-
readonly kind: 'RemoteState'
|
|
36
|
-
readonly name: N
|
|
37
|
-
readonly output: AnySchema<A>
|
|
38
|
-
readonly error?: AnySchema<E>
|
|
39
|
-
readonly gated: boolean
|
|
40
|
-
readonly intents: ReadonlyArray<string>
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface RemoteStateClass<
|
|
44
|
-
out N extends string,
|
|
45
|
-
out Inputs extends ReadonlyArray<AnySource>,
|
|
46
|
-
in out Intents extends ReadonlyArray<Event.AnyEvent>,
|
|
47
|
-
in out A,
|
|
48
|
-
in out E,
|
|
49
|
-
in out Gated extends boolean,
|
|
50
|
-
> extends EffectType<AsyncData<A, E, Gated>, never, Store<AsyncData<A, E, Gated>>> {
|
|
51
|
-
new (): {}
|
|
52
|
-
readonly manifest: RemoteStateManifestExternalApi<N, A, E>
|
|
53
|
-
readonly store: Context.Tag<Store<AsyncData<A, E, Gated>>, Store<AsyncData<A, E, Gated>>>
|
|
54
|
-
readonly name: N
|
|
55
|
-
readonly inputs: Inputs
|
|
56
|
-
readonly intents: Intents
|
|
57
|
-
readonly gated: Gated
|
|
58
|
-
readonly truth: StateToken<`${N}/truth`, AsyncData<A, E, Gated>>
|
|
59
|
-
// pending is a Source (not StateClass) so user reducers cannot target it
|
|
60
|
-
readonly pending: StateToken<
|
|
61
|
-
`${N}/pending`,
|
|
62
|
-
ReadonlyArray<PendingIntent<Event.EventType<Intents[number]>>>
|
|
63
|
-
>
|
|
64
|
-
readonly Failed: Event.EventClass<`${N}/Failed`, FailedIntent<Event.EventType<Intents[number]>>>
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface RemoteStateConfigExternalApi<
|
|
68
|
-
Inputs extends ReadonlyArray<AnySource>,
|
|
69
|
-
Intents extends ReadonlyArray<Event.AnyEvent>,
|
|
70
|
-
A,
|
|
71
|
-
E,
|
|
72
|
-
AlwaysOn extends boolean,
|
|
73
|
-
> {
|
|
74
|
-
readonly inputs: Inputs
|
|
75
|
-
readonly output: AnySchema<A>
|
|
76
|
-
readonly error?: AnySchema<E>
|
|
77
|
-
readonly alwaysOn?: AlwaysOn
|
|
78
|
-
readonly intents: Intents
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Schema.Unknown in intent slot: Intents is open at make-time; event schemas are reflective only
|
|
82
|
-
export const looseSchema = <P>(schema: Schema.Schema.AnyNoContext): AnySchema<P> => schema
|
|
83
|
-
|
|
84
|
-
export const make = <
|
|
85
|
-
const N extends string,
|
|
86
|
-
const Inputs extends ReadonlyArray<AnySource>,
|
|
87
|
-
const Intents extends ReadonlyArray<Event.AnyEvent>,
|
|
88
|
-
A,
|
|
89
|
-
E = never,
|
|
90
|
-
const AlwaysOn extends boolean = false,
|
|
91
|
-
>(
|
|
92
|
-
name: N,
|
|
93
|
-
config: RemoteStateConfigExternalApi<Inputs, Intents, A, E, AlwaysOn>,
|
|
94
|
-
): RemoteStateClass<N, Inputs, Intents, A, E, GatedOf<AlwaysOn>> => {
|
|
95
|
-
type I = Event.EventType<Intents[number]>
|
|
96
|
-
const store = Context.GenericTag<Store<AsyncData<A, E, GatedOf<AlwaysOn>>>>(
|
|
97
|
-
`reform/remoteState/${name}`,
|
|
98
|
-
)
|
|
99
|
-
const truthTag = Context.GenericTag<Store<AsyncData<A, E, GatedOf<AlwaysOn>>>>(
|
|
100
|
-
`reform/remoteState/${name}/truth`,
|
|
101
|
-
)
|
|
102
|
-
const pendingTag = Context.GenericTag<Store<ReadonlyArray<PendingIntent<I>>>>(
|
|
103
|
-
`reform/remoteState/${name}/pending`,
|
|
104
|
-
)
|
|
105
|
-
const gated = gatedFlag(config.alwaysOn)
|
|
106
|
-
const manifest: RemoteStateManifestExternalApi<N, A, E> = {
|
|
107
|
-
kind: 'RemoteState',
|
|
108
|
-
name,
|
|
109
|
-
output: config.output,
|
|
110
|
-
gated,
|
|
111
|
-
intents: config.intents.map((event) => event.tag),
|
|
112
|
-
...(config.error !== undefined ? { error: config.error } : {}),
|
|
113
|
-
}
|
|
114
|
-
// Annotated consts preserve template-literal types for interface matching
|
|
115
|
-
const truthName: `${N}/truth` = `${name}/truth`
|
|
116
|
-
const pendingName: `${N}/pending` = `${name}/pending`
|
|
117
|
-
const failedName: `${N}/Failed` = `${name}/Failed`
|
|
118
|
-
const read = Effect.flatMap(store, readTracked)
|
|
119
|
-
return yieldableClass(read, {
|
|
120
|
-
manifest,
|
|
121
|
-
store,
|
|
122
|
-
name,
|
|
123
|
-
inputs: config.inputs,
|
|
124
|
-
intents: config.intents,
|
|
125
|
-
gated,
|
|
126
|
-
truth: new StateToken(truthName, truthTag),
|
|
127
|
-
pending: new StateToken(pendingName, pendingTag),
|
|
128
|
-
Failed: Event.make(
|
|
129
|
-
failedName,
|
|
130
|
-
looseSchema<FailedIntent<I>>(
|
|
131
|
-
Schema.Struct({ intent: Schema.Unknown, error: Schema.Unknown }),
|
|
132
|
-
),
|
|
133
|
-
),
|
|
134
|
-
})
|
|
135
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { Context, Effect, Layer, MutableRef } from 'effect'
|
|
2
|
-
import { type AnyAsyncData, AsyncData, narrowStore, widenStore } from '../calc/asyncData'
|
|
3
|
-
import { reuse as shareStructure } from '../internal/reuse'
|
|
4
|
-
import { resolveScheduler } from '../internal/scheduler'
|
|
5
|
-
import { sameKey } from '../internal/sources'
|
|
6
|
-
import { makeDerivedStore, type Store } from '../internal/store'
|
|
7
|
-
import { narrowHandled } from '../runtime/bus'
|
|
8
|
-
import { type ReducerEntry, Reducers } from '../runtime/loop'
|
|
9
|
-
import type { PendingIntent, QueueFoldEventExternalApi } from './remoteStateDefinition'
|
|
10
|
-
|
|
11
|
-
interface OverlayLayerOptions<I, A, E, Gated extends boolean> {
|
|
12
|
-
readonly apply: (current: A, intent: I) => A
|
|
13
|
-
readonly pendingTag: Context.Tag<Store<ReadonlyArray<PendingIntent<I>>>, Store<ReadonlyArray<PendingIntent<I>>>>
|
|
14
|
-
readonly reuse: boolean | undefined
|
|
15
|
-
readonly storeTag: Context.Tag<Store<AsyncData<A, E, Gated>>, Store<AsyncData<A, E, Gated>>>
|
|
16
|
-
readonly truthTag: Context.Tag<Store<AsyncData<A, E, Gated>>, Store<AsyncData<A, E, Gated>>>
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const makeOverlayLayer = <I, A, E, Gated extends boolean>(
|
|
20
|
-
options: OverlayLayerOptions<I, A, E, Gated>,
|
|
21
|
-
): Layer.Layer<Store<AsyncData<A, E, Gated>>, never, Store<AsyncData<A, E, Gated>> | Store<ReadonlyArray<PendingIntent<I>>>> =>
|
|
22
|
-
Layer.scoped(
|
|
23
|
-
options.storeTag,
|
|
24
|
-
Effect.gen(function* () {
|
|
25
|
-
const scheduler = yield* resolveScheduler
|
|
26
|
-
const feedStore = widenStore<A, E, Gated>(yield* options.truthTag)
|
|
27
|
-
const queueStore = yield* options.pendingTag
|
|
28
|
-
const memo = MutableRef.make<
|
|
29
|
-
{ readonly key: ReadonlyArray<unknown>; readonly output: AnyAsyncData<A, E> } | undefined
|
|
30
|
-
>(undefined)
|
|
31
|
-
const recompute = (): AnyAsyncData<A, E> => {
|
|
32
|
-
const feed = feedStore.getSnapshot()
|
|
33
|
-
const pending = queueStore.getSnapshot()
|
|
34
|
-
const key = [feed, pending]
|
|
35
|
-
const previous = MutableRef.get(memo)
|
|
36
|
-
if (previous !== undefined && sameKey(key, previous.key)) {
|
|
37
|
-
return previous.output
|
|
38
|
-
}
|
|
39
|
-
const applyPending = (base: A): A =>
|
|
40
|
-
pending.reduce((applied, entry) => options.apply(applied, entry.intent), base)
|
|
41
|
-
const overlaid: AnyAsyncData<A, E> =
|
|
42
|
-
feed._tag === 'Success' ? AsyncData.success(applyPending(feed.value), feed.refetching) : feed
|
|
43
|
-
const output =
|
|
44
|
-
options.reuse === true &&
|
|
45
|
-
previous !== undefined &&
|
|
46
|
-
previous.output._tag === 'Success' &&
|
|
47
|
-
overlaid._tag === 'Success'
|
|
48
|
-
? AsyncData.success(shareStructure(previous.output.value, overlaid.value), overlaid.refetching)
|
|
49
|
-
: overlaid
|
|
50
|
-
MutableRef.set(memo, { key, output })
|
|
51
|
-
return output
|
|
52
|
-
}
|
|
53
|
-
const subscribe = (listener: () => void): (() => void) => {
|
|
54
|
-
const offFeed = feedStore.subscribe(listener)
|
|
55
|
-
const offQueue = queueStore.subscribe(listener)
|
|
56
|
-
return () => {
|
|
57
|
-
offFeed()
|
|
58
|
-
offQueue()
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
const derived = makeDerivedStore(recompute, subscribe, scheduler)
|
|
62
|
-
yield* Effect.addFinalizer(() => Effect.sync(derived.unsubscribe))
|
|
63
|
-
return narrowStore<A, E, Gated>(derived.store)
|
|
64
|
-
}),
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
interface QueueReducerOptions<I> {
|
|
68
|
-
readonly ackedTag: string
|
|
69
|
-
readonly name: string
|
|
70
|
-
readonly pendingTag: Context.Tag<Store<ReadonlyArray<PendingIntent<I>>>, Store<ReadonlyArray<PendingIntent<I>>>>
|
|
71
|
-
readonly queuedTag: string
|
|
72
|
-
readonly settledTag: string
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export const makeQueueReducerLayer = <I>(
|
|
76
|
-
options: QueueReducerOptions<I>,
|
|
77
|
-
): Layer.Layer<never, never, Reducers | Store<ReadonlyArray<PendingIntent<I>>>> => {
|
|
78
|
-
const handles: ReadonlySet<string> = new Set([options.queuedTag, options.ackedTag, options.settledTag])
|
|
79
|
-
const fold = (
|
|
80
|
-
queue: ReadonlyArray<PendingIntent<I>>,
|
|
81
|
-
event: QueueFoldEventExternalApi<I>,
|
|
82
|
-
): ReadonlyArray<PendingIntent<I>> => {
|
|
83
|
-
if (event._tag === options.queuedTag) {
|
|
84
|
-
return event.intent === undefined
|
|
85
|
-
? queue
|
|
86
|
-
: [...queue, { opId: event.opId, intent: event.intent, status: 'sending' }]
|
|
87
|
-
}
|
|
88
|
-
if (event._tag === options.ackedTag) {
|
|
89
|
-
const sending = queue.some((entry) => entry.opId === event.opId && entry.status === 'sending')
|
|
90
|
-
if (!sending) {
|
|
91
|
-
return queue
|
|
92
|
-
}
|
|
93
|
-
return queue.map((entry) =>
|
|
94
|
-
entry.opId === event.opId ? { ...entry, status: 'confirmed' as const } : entry,
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
return queue.some((entry) => entry.opId === event.opId)
|
|
98
|
-
? queue.filter((entry) => entry.opId !== event.opId)
|
|
99
|
-
: queue
|
|
100
|
-
}
|
|
101
|
-
return Layer.scopedDiscard(
|
|
102
|
-
Effect.gen(function* () {
|
|
103
|
-
const reducers = yield* Reducers
|
|
104
|
-
if (reducers.entries.some((entry) => entry.name === options.name)) {
|
|
105
|
-
yield* Effect.logWarning(`reform: duplicate reducer name '${options.name}' registered`)
|
|
106
|
-
}
|
|
107
|
-
const store = yield* options.pendingTag
|
|
108
|
-
const entry: ReducerEntry = {
|
|
109
|
-
name: options.name,
|
|
110
|
-
handles,
|
|
111
|
-
apply: (event) => store.set(fold(store.get(), narrowHandled(event))),
|
|
112
|
-
}
|
|
113
|
-
yield* Effect.acquireRelease(Effect.sync(() => reducers.register(entry)), () =>
|
|
114
|
-
Effect.sync(() => reducers.unregister(entry)),
|
|
115
|
-
)
|
|
116
|
-
}),
|
|
117
|
-
)
|
|
118
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { Effect } from 'effect'
|
|
2
|
-
import * as Event from '../event/event'
|
|
3
|
-
import * as Channel from '../channel/channel'
|
|
4
|
-
import type { InputsObject, InvalidateBy } from '../internal/sources'
|
|
5
|
-
import type { AnySource } from '../state/token'
|
|
6
|
-
import type { RemoteStateSettled } from './remoteStateDefinition'
|
|
7
|
-
|
|
8
|
-
type EventPayload<Ev extends Event.AnyEvent> = Ev extends Event.EventClass<string, infer P>
|
|
9
|
-
? P
|
|
10
|
-
: never
|
|
11
|
-
|
|
12
|
-
export type RemoteStateLiveExternalApi<
|
|
13
|
-
Inputs extends ReadonlyArray<AnySource>,
|
|
14
|
-
Intents extends ReadonlyArray<Event.AnyEvent>,
|
|
15
|
-
A,
|
|
16
|
-
E,
|
|
17
|
-
Gated extends boolean,
|
|
18
|
-
R,
|
|
19
|
-
R2,
|
|
20
|
-
SettledEvent extends Event.AnyEvent = never,
|
|
21
|
-
> = {
|
|
22
|
-
readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
|
|
23
|
-
// Any failure settles + Failed; response value ignored — truth via invalidated query
|
|
24
|
-
readonly send: (intent: Event.EventType<Intents[number]>) => Effect.Effect<unknown, unknown, R2>
|
|
25
|
-
// Must be idempotent against truth that already carries the intent
|
|
26
|
-
readonly apply: (value: A, intent: Event.EventType<Intents[number]>) => A
|
|
27
|
-
readonly channel?: Channel.ChannelClass
|
|
28
|
-
readonly invalidateBy?: InvalidateBy<Inputs>
|
|
29
|
-
readonly invalidateOn?: ReadonlyArray<Event.AnyEvent>
|
|
30
|
-
readonly coalesce?: 'switch' | 'trailing'
|
|
31
|
-
readonly reuse?: boolean
|
|
32
|
-
readonly settled?: {
|
|
33
|
-
readonly event: SettledEvent
|
|
34
|
-
readonly payload: (result: RemoteStateSettled<A, E>) => EventPayload<SettledEvent>
|
|
35
|
-
}
|
|
36
|
-
// Only settled server truth is persisted (overlay is a separate store)
|
|
37
|
-
readonly persist?:
|
|
38
|
-
| boolean
|
|
39
|
-
| { readonly key?: string | ((inputs: InputsObject<Inputs>) => string) }
|
|
40
|
-
} & (Gated extends true
|
|
41
|
-
? { readonly disabled?: (inputs: InputsObject<Inputs>) => boolean }
|
|
42
|
-
: { readonly disabled?: never })
|
|
43
|
-
|
|
44
|
-
// Runtime view: `disabled` is `never` at type level for non-gated definitions
|
|
45
|
-
export type RemoteStateLiveRuntimeExternalApi<Inputs extends ReadonlyArray<AnySource>, A, E, R> = {
|
|
46
|
-
readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
|
|
47
|
-
readonly invalidateBy?: InvalidateBy<Inputs>
|
|
48
|
-
readonly disabled?: (inputs: InputsObject<Inputs>) => boolean
|
|
49
|
-
readonly coalesce?: 'switch' | 'trailing'
|
|
50
|
-
readonly reuse?: boolean
|
|
51
|
-
readonly persist?:
|
|
52
|
-
| boolean
|
|
53
|
-
| { readonly key?: string | ((inputs: InputsObject<Inputs>) => string) }
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface SettleLink {
|
|
57
|
-
readonly requested: () => number
|
|
58
|
-
readonly register: (opId: string, waitFor: number) => void
|
|
59
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { Context, Effect, Either, Layer } from 'effect'
|
|
2
|
-
import * as Channel from '../channel/channel'
|
|
3
|
-
import * as Event from '../event/event'
|
|
4
|
-
import { Bus, narrowHandled } from '../runtime/bus'
|
|
5
|
-
import type { SettleLink } from './remoteStateLiveTypes'
|
|
6
|
-
|
|
7
|
-
interface SendProcedureOptions<I, R> {
|
|
8
|
-
readonly acked: Event.EventClass<string, { readonly opId: string }>
|
|
9
|
-
readonly channel: Channel.ChannelClass
|
|
10
|
-
readonly failed: Event.EventClass<string, { readonly intent: I; readonly error: unknown }>
|
|
11
|
-
readonly handles: ReadonlySet<string>
|
|
12
|
-
readonly invalidated: Event.EventClass<string, {}>
|
|
13
|
-
readonly linkTag: Context.Tag<SettleLink, SettleLink>
|
|
14
|
-
readonly name: string
|
|
15
|
-
readonly queued: Event.EventClass<string, { readonly opId: string; readonly intent: I }>
|
|
16
|
-
readonly send: (intent: I) => Effect.Effect<unknown, unknown, R>
|
|
17
|
-
readonly settled: Event.EventClass<string, { readonly opId: string }>
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const makeSendProcedureLayer = <I, R>(
|
|
21
|
-
options: SendProcedureOptions<I, R>,
|
|
22
|
-
): Layer.Layer<never, never, Channel.Procedures | SettleLink | Bus | R> =>
|
|
23
|
-
Layer.scopedDiscard(
|
|
24
|
-
Effect.gen(function* () {
|
|
25
|
-
const procedures = yield* Channel.Procedures
|
|
26
|
-
if (procedures.entries.some((entry) => entry.name === options.name)) {
|
|
27
|
-
yield* Effect.logWarning(`reform: duplicate procedure name '${options.name}' registered`)
|
|
28
|
-
}
|
|
29
|
-
const link = yield* options.linkTag
|
|
30
|
-
const runtime = yield* Effect.runtime<Bus | R>()
|
|
31
|
-
const deliver = Effect.fn('deliver')(
|
|
32
|
-
function* (intent: I): Effect.fn.Return<void, never, Bus | R> {
|
|
33
|
-
const opId = yield* Effect.sync(() => crypto.randomUUID())
|
|
34
|
-
yield* Event.dispatch(options.queued, { opId, intent })
|
|
35
|
-
const outcome = yield* options.send(intent).pipe(
|
|
36
|
-
Effect.catchAllDefect((defect) => Effect.fail(defect)),
|
|
37
|
-
Effect.either,
|
|
38
|
-
Effect.onInterrupt(() => Event.dispatch(options.settled, { opId })),
|
|
39
|
-
)
|
|
40
|
-
yield* Either.match(outcome, {
|
|
41
|
-
onLeft: (error) =>
|
|
42
|
-
Effect.zipRight(
|
|
43
|
-
Event.dispatch(options.failed, { intent, error }),
|
|
44
|
-
Event.dispatch(options.settled, { opId }),
|
|
45
|
-
),
|
|
46
|
-
onRight: () =>
|
|
47
|
-
Effect.sync(() => link.register(opId, link.requested() + 1)).pipe(
|
|
48
|
-
Effect.zipRight(Event.dispatch(options.acked, { opId })),
|
|
49
|
-
Effect.zipRight(Event.dispatch(options.invalidated, {})),
|
|
50
|
-
),
|
|
51
|
-
})
|
|
52
|
-
},
|
|
53
|
-
)
|
|
54
|
-
const entry: Channel.ProcedureEntry = {
|
|
55
|
-
name: options.name,
|
|
56
|
-
channelName: options.channel.manifest.name,
|
|
57
|
-
handles: options.handles,
|
|
58
|
-
run: (event) => Effect.provide(deliver(narrowHandled<I>(event)), runtime),
|
|
59
|
-
}
|
|
60
|
-
yield* Effect.acquireRelease(Effect.sync(() => procedures.register(entry)), () =>
|
|
61
|
-
Effect.sync(() => procedures.unregister(entry)),
|
|
62
|
-
)
|
|
63
|
-
}),
|
|
64
|
-
)
|