@playfast/reform-proof 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 +12 -12
- package/package.json +18 -18
- package/src/engine.ts +1657 -8
- package/src/fingerprint.test.ts +24 -0
- package/src/fingerprint.ts +73 -0
- package/src/index.ts +352 -87
- package/src/nested-feature.test.ts +174 -0
- package/src/runner.ts +5 -1
- package/src/structure-events.test.ts +9 -3
- package/src/structure-locators.test.ts +19 -9
- package/src/typed-seams.test.ts +6 -3
- package/src/assertions.ts +0 -65
- package/src/driverTypes.ts +0 -19
- package/src/execution.ts +0 -100
- package/src/facade.ts +0 -258
- package/src/product.ts +0 -77
- package/src/runtimeFacade.ts +0 -269
- package/src/runtimeTreeDispose.ts +0 -21
- package/src/runtimeTreeDriver.ts +0 -285
- package/src/runtimeTreeFacade.ts +0 -134
- package/src/runtimeTreeSettle.ts +0 -34
- package/src/runtimeTreeTypes.ts +0 -99
- package/src/sink.ts +0 -73
- package/src/treeBindings.ts +0 -121
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { expect, it } from '@effect/vitest'
|
|
2
|
+
import { Effect, Layer, Schema as S } from 'effect'
|
|
3
|
+
import {
|
|
4
|
+
Composition,
|
|
5
|
+
Engine,
|
|
6
|
+
Feature,
|
|
7
|
+
type FeatureSlotTarget,
|
|
8
|
+
featureModule,
|
|
9
|
+
mount,
|
|
10
|
+
one,
|
|
11
|
+
provide,
|
|
12
|
+
scene,
|
|
13
|
+
slot,
|
|
14
|
+
State,
|
|
15
|
+
ui,
|
|
16
|
+
} from '@playfast/reform'
|
|
17
|
+
import { FeatureInput, featureModuleFromInput } from '@playfast/reform/internal'
|
|
18
|
+
import { Product, Proof, ProductRequirement } from './index'
|
|
19
|
+
|
|
20
|
+
const NestedFeatureInput = S.Struct({ value: S.Number })
|
|
21
|
+
|
|
22
|
+
class NestedFeatureValue extends State.make('proof.nestedFeature.value', S.Number) {}
|
|
23
|
+
|
|
24
|
+
class NestedUi extends ui('ProofNestedFeature')<{
|
|
25
|
+
props: { mountValue: number; featureValue: number }
|
|
26
|
+
}>() {}
|
|
27
|
+
|
|
28
|
+
class Nested extends Composition.make('ProofNestedFeature', {
|
|
29
|
+
title: 'Nested feature',
|
|
30
|
+
props: NestedFeatureInput,
|
|
31
|
+
states: [NestedFeatureValue],
|
|
32
|
+
ui: NestedUi,
|
|
33
|
+
})<Nested>() {}
|
|
34
|
+
|
|
35
|
+
const NestedLive = Composition.live(Nested, function* () {
|
|
36
|
+
const props = yield* Nested.props
|
|
37
|
+
const featureValue = yield* NestedFeatureValue
|
|
38
|
+
return mount({
|
|
39
|
+
props: { mountValue: props.value, featureValue },
|
|
40
|
+
slots: {},
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const NestedModule = featureModuleFromInput(
|
|
45
|
+
NestedFeatureInput,
|
|
46
|
+
[],
|
|
47
|
+
NestedLive.pipe(
|
|
48
|
+
Layer.provideMerge(
|
|
49
|
+
Layer.unwrapEffect(
|
|
50
|
+
Effect.map(FeatureInput, (input) =>
|
|
51
|
+
State.live(NestedFeatureValue, S.decodeUnknownSync(NestedFeatureInput)(input).value),
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
class PlaceholderUi extends ui('ProofNestedFeaturePlaceholder')<{
|
|
59
|
+
props: { phase: string }
|
|
60
|
+
}>() {}
|
|
61
|
+
|
|
62
|
+
class Placeholder extends Composition.make('ProofNestedFeaturePlaceholder', {
|
|
63
|
+
title: 'Nested feature placeholder',
|
|
64
|
+
ui: PlaceholderUi,
|
|
65
|
+
})<Placeholder>() {}
|
|
66
|
+
|
|
67
|
+
const PlaceholderLive = Composition.live(Placeholder, function* () {
|
|
68
|
+
return mount({ props: { phase: 'loading' }, slots: {} })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
class NestedFeature extends Feature.make('proof.nestedFeature', {
|
|
72
|
+
loadingStrategy: 'lazy',
|
|
73
|
+
input: NestedFeatureInput,
|
|
74
|
+
composition: Nested,
|
|
75
|
+
load: Effect.succeed(NestedModule),
|
|
76
|
+
placeholder: { loading: Placeholder, failed: Placeholder },
|
|
77
|
+
}) {}
|
|
78
|
+
|
|
79
|
+
class NestedSlot extends slot('ProofNestedFeatureSlot')<
|
|
80
|
+
NestedSlot,
|
|
81
|
+
FeatureSlotTarget<typeof NestedFeature>
|
|
82
|
+
>() {}
|
|
83
|
+
|
|
84
|
+
class RootUi extends ui('ProofNestedFeatureRoot')<{
|
|
85
|
+
props: { ready: boolean }
|
|
86
|
+
slots: { Nested: NestedSlot }
|
|
87
|
+
}>() {}
|
|
88
|
+
|
|
89
|
+
class Root extends Composition.make('ProofNestedFeatureRoot', {
|
|
90
|
+
title: 'Nested feature root',
|
|
91
|
+
slots: { Nested: NestedSlot },
|
|
92
|
+
ui: RootUi,
|
|
93
|
+
})<Root>() {}
|
|
94
|
+
|
|
95
|
+
const RootLive = Composition.live(Root, function* () {
|
|
96
|
+
return mount({
|
|
97
|
+
props: { ready: true },
|
|
98
|
+
slots: { Nested: one({ value: 42 }) },
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const RootModule = featureModule(
|
|
103
|
+
[],
|
|
104
|
+
Layer.mergeAll(RootLive, PlaceholderLive, provide(NestedSlot, NestedFeature)),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
class RootFeature extends Feature.make('proof.rootFeature', {
|
|
108
|
+
composition: Root,
|
|
109
|
+
module: RootModule,
|
|
110
|
+
}) {}
|
|
111
|
+
|
|
112
|
+
class HarnessRootSlot extends slot('ProofHarnessRootSlot')<
|
|
113
|
+
HarnessRootSlot,
|
|
114
|
+
FeatureSlotTarget<typeof RootFeature>
|
|
115
|
+
>() {}
|
|
116
|
+
|
|
117
|
+
class HarnessUi extends ui('ProofNestedFeatureHarness')<{
|
|
118
|
+
props: { ready: boolean }
|
|
119
|
+
slots: { Root: HarnessRootSlot }
|
|
120
|
+
}>() {}
|
|
121
|
+
|
|
122
|
+
class Harness extends Composition.make('ProofNestedFeatureHarness', {
|
|
123
|
+
title: 'Nested feature harness',
|
|
124
|
+
slots: { Root: HarnessRootSlot },
|
|
125
|
+
ui: HarnessUi,
|
|
126
|
+
})<Harness>() {}
|
|
127
|
+
|
|
128
|
+
const HarnessLive = Composition.live(Harness, function* () {
|
|
129
|
+
return mount({
|
|
130
|
+
props: { ready: true },
|
|
131
|
+
slots: { Root: one({}) },
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const RootScene = scene(Harness, {
|
|
136
|
+
provide: [
|
|
137
|
+
Layer.mergeAll(HarnessLive, provide(HarnessRootSlot, RootFeature)).pipe(
|
|
138
|
+
Layer.provideMerge(Engine),
|
|
139
|
+
),
|
|
140
|
+
],
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
class MountsNestedFeature extends ProductRequirement.make(
|
|
144
|
+
Root,
|
|
145
|
+
'mounts a nested native Feature with its slot input',
|
|
146
|
+
) {}
|
|
147
|
+
|
|
148
|
+
class RootProduct extends Product.make(Root, {
|
|
149
|
+
requirements: [MountsNestedFeature],
|
|
150
|
+
}) {}
|
|
151
|
+
|
|
152
|
+
const mountsNestedFeature = Proof.implementVia(MountsNestedFeature, RootScene, function* (app) {
|
|
153
|
+
const root = yield* app.slots.Root.first
|
|
154
|
+
const nested = yield* root.slots.Nested.first
|
|
155
|
+
yield* nested.expectProps({ mountValue: 42, featureValue: 42 })
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const NestedFeatureProofs = Proof.suite(RootProduct, {
|
|
159
|
+
proofs: [mountsNestedFeature],
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it.live('proves a Feature that mounts a nested native Feature', () =>
|
|
163
|
+
Effect.gen(function* () {
|
|
164
|
+
const result = yield* Proof.runEffect(NestedFeatureProofs)
|
|
165
|
+
expect(result.results).toEqual([
|
|
166
|
+
{
|
|
167
|
+
requirement: 'mounts a nested native Feature with its slot input',
|
|
168
|
+
ok: true,
|
|
169
|
+
error: undefined,
|
|
170
|
+
},
|
|
171
|
+
])
|
|
172
|
+
expect(result.ok).toBe(true)
|
|
173
|
+
}),
|
|
174
|
+
)
|
package/src/runner.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from 'effect'
|
|
2
|
-
import { driveProof, executeProof } from './
|
|
2
|
+
import { driveProof, driveProofEffect, executeProof, executeProofEffect } from './engine'
|
|
3
3
|
import type { DriveResult, Proof, ProofResult } from './index'
|
|
4
4
|
|
|
5
5
|
export interface ProofRunnerApi {
|
|
6
|
+
readonly executeProofEffect: (proof: Proof) => Effect.Effect<ProofResult, never, never>
|
|
7
|
+
readonly driveProofEffect: (proof: Proof) => Effect.Effect<DriveResult, never, never>
|
|
6
8
|
readonly executeProof: (proof: Proof) => Promise<ProofResult>
|
|
7
9
|
readonly driveProof: (proof: Proof) => Promise<DriveResult>
|
|
8
10
|
}
|
|
@@ -13,6 +15,8 @@ const ProofRunnerBase: Context.TagClass<ProofRunner, 'reform-proof/ProofRunner',
|
|
|
13
15
|
export class ProofRunner extends ProofRunnerBase {}
|
|
14
16
|
|
|
15
17
|
export const proofRunnerLayer: Layer.Layer<ProofRunner> = Layer.succeed(ProofRunner, {
|
|
18
|
+
executeProofEffect,
|
|
19
|
+
driveProofEffect,
|
|
16
20
|
executeProof,
|
|
17
21
|
driveProof,
|
|
18
22
|
})
|
|
@@ -19,7 +19,10 @@ import { Product, Proof, ProductRequirement, expect as proofExpect } from './ind
|
|
|
19
19
|
class CountState extends State.make('scount', S.Number) {}
|
|
20
20
|
class MiniStates extends StateGroup.make(CountState) {}
|
|
21
21
|
class Bumped extends Event.make('SBumped', S.Struct({})) {}
|
|
22
|
-
class BumpReducer extends Reducer.make('SBumpReducer', {
|
|
22
|
+
class BumpReducer extends Reducer.make('SBumpReducer', {
|
|
23
|
+
states: [CountState],
|
|
24
|
+
events: [Bumped],
|
|
25
|
+
}) {}
|
|
23
26
|
const BumpReducerLive = Reducer.live(BumpReducer, (n) => n + 1)
|
|
24
27
|
|
|
25
28
|
class CounterUi extends ui('SCounter')<{
|
|
@@ -31,14 +34,17 @@ class Counter extends Composition.make('SCounter', {
|
|
|
31
34
|
states: [MiniStates],
|
|
32
35
|
events: [Bumped],
|
|
33
36
|
ui: CounterUi,
|
|
34
|
-
}) {}
|
|
37
|
+
})<Counter>() {}
|
|
35
38
|
const CounterLive = Composition.live(Counter, function* () {
|
|
36
39
|
const count = yield* StateGroup.select(MiniStates, 'scount')
|
|
37
40
|
const bump = yield* Event.trigger(Bumped)
|
|
38
41
|
return mount({ props: { count }, slots: {}, events: { bump } })
|
|
39
42
|
})
|
|
40
43
|
|
|
41
|
-
const presentations = provide(
|
|
44
|
+
const presentations = provide(
|
|
45
|
+
CounterUi,
|
|
46
|
+
Ui.make(CounterUi, () => null),
|
|
47
|
+
)
|
|
42
48
|
const Views = CounterLive.pipe(Layer.provideMerge(presentations))
|
|
43
49
|
const Logic = BumpReducerLive.pipe(
|
|
44
50
|
Layer.provideMerge(Layer.mergeAll(Engine, StateGroup.live(MiniStates, { scount: 5 }))),
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
Engine,
|
|
5
5
|
each,
|
|
6
6
|
mount,
|
|
7
|
-
Props,
|
|
8
7
|
provide,
|
|
9
8
|
scene,
|
|
10
9
|
slot,
|
|
@@ -22,13 +21,15 @@ class TodosState extends State.make(
|
|
|
22
21
|
) {}
|
|
23
22
|
class TodoStates extends StateGroup.make(TodosState) {}
|
|
24
23
|
|
|
25
|
-
class ItemUi extends ui('LocItem')<{
|
|
24
|
+
class ItemUi extends ui('LocItem')<{
|
|
25
|
+
props: { id: string; done: boolean }
|
|
26
|
+
}>() {}
|
|
26
27
|
class Item extends Composition.make('LocItem', {
|
|
27
28
|
title: 'LocItem',
|
|
28
29
|
props: S.Struct({ id: S.String, done: S.Boolean }),
|
|
29
30
|
ui: ItemUi,
|
|
30
|
-
}) {}
|
|
31
|
-
class ItemSlot extends slot('LocItem')<typeof Item>() {}
|
|
31
|
+
})<Item>() {}
|
|
32
|
+
class ItemSlot extends slot('LocItem')<ItemSlot, typeof Item>() {}
|
|
32
33
|
|
|
33
34
|
class ListUi extends ui('LocList')<{
|
|
34
35
|
props: { total: number }
|
|
@@ -39,25 +40,34 @@ class List extends Composition.make('LocList', {
|
|
|
39
40
|
states: [TodoStates],
|
|
40
41
|
slots: { Item: ItemSlot },
|
|
41
42
|
ui: ListUi,
|
|
42
|
-
}) {}
|
|
43
|
+
})<List>() {}
|
|
43
44
|
|
|
44
45
|
const ListLive = Composition.live(List, function* () {
|
|
45
46
|
const todos = yield* StateGroup.select(TodoStates, 'locTodos')
|
|
46
47
|
return mount({
|
|
47
48
|
props: { total: todos.length },
|
|
48
49
|
slots: {
|
|
49
|
-
Item: each(todos, {
|
|
50
|
+
Item: each(todos, {
|
|
51
|
+
key: (t) => t.id,
|
|
52
|
+
props: (t) => ({ id: t.id, done: t.done }),
|
|
53
|
+
}),
|
|
50
54
|
},
|
|
51
55
|
})
|
|
52
56
|
})
|
|
53
57
|
const ItemLive = Composition.live(Item, function* () {
|
|
54
|
-
const props
|
|
58
|
+
const props = yield* Item.props
|
|
55
59
|
return mount({ props, slots: {} })
|
|
56
60
|
})
|
|
57
61
|
|
|
58
62
|
const presentations = Layer.mergeAll(
|
|
59
|
-
provide(
|
|
60
|
-
|
|
63
|
+
provide(
|
|
64
|
+
ListUi,
|
|
65
|
+
Ui.make(ListUi, () => null),
|
|
66
|
+
),
|
|
67
|
+
provide(
|
|
68
|
+
ItemUi,
|
|
69
|
+
Ui.make(ItemUi, () => null),
|
|
70
|
+
),
|
|
61
71
|
)
|
|
62
72
|
const wiring = provide(ItemSlot, Item)
|
|
63
73
|
const Views = Layer.mergeAll(ListLive, ItemLive, wiring).pipe(Layer.provideMerge(presentations))
|
package/src/typed-seams.test.ts
CHANGED
|
@@ -25,24 +25,27 @@ class BumpReducer extends Reducer.make('BumpReducer', { states: [CountState], ev
|
|
|
25
25
|
const BumpReducerLive = Reducer.live(BumpReducer, (n) => n + 1)
|
|
26
26
|
|
|
27
27
|
class LabelUi extends ui('Label')<{ props: { text: string } }>() {}
|
|
28
|
-
|
|
28
|
+
|
|
29
|
+
class Label extends Composition.make('Label', { title: 'Label', ui: LabelUi })<Label>() {}
|
|
29
30
|
const LabelLive = Composition.live(Label, function* () {
|
|
30
31
|
return mount({ props: { text: 'count' }, slots: {} })
|
|
31
32
|
})
|
|
32
|
-
|
|
33
|
+
|
|
34
|
+
class LabelSlot extends slot('Label')<LabelSlot, typeof Label>() {}
|
|
33
35
|
|
|
34
36
|
class CounterUi extends ui('Counter')<{
|
|
35
37
|
props: { count: number }
|
|
36
38
|
slots: { Label: LabelSlot }
|
|
37
39
|
events: { bump: Trigger<Record<string, never>> }
|
|
38
40
|
}>() {}
|
|
41
|
+
|
|
39
42
|
class Counter extends Composition.make('Counter', {
|
|
40
43
|
title: 'Counter',
|
|
41
44
|
states: [MiniStates],
|
|
42
45
|
events: [Bumped],
|
|
43
46
|
slots: { Label: LabelSlot },
|
|
44
47
|
ui: CounterUi,
|
|
45
|
-
}) {}
|
|
48
|
+
})<Counter>() {}
|
|
46
49
|
const CounterLive = Composition.live(Counter, function* () {
|
|
47
50
|
const count = yield* StateGroup.select(MiniStates, 'count')
|
|
48
51
|
const bump = yield* Event.trigger(Bumped)
|
package/src/assertions.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { Array as Arr, Effect, Option, Record as Rec } from 'effect'
|
|
2
|
-
import { AssertionFailed } from './errors'
|
|
3
|
-
|
|
4
|
-
interface ComparePair {
|
|
5
|
-
readonly left: unknown
|
|
6
|
-
readonly right: unknown
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
// oxlint-disable-next-line reform-rules/no-json-parse-stringify -- arbitrary unknown assertion values, not Schema-typed data
|
|
10
|
-
const show = (subject: unknown): string => JSON.stringify(subject)
|
|
11
|
-
|
|
12
|
-
const deepEqual = ({ left, right }: ComparePair): boolean =>
|
|
13
|
-
Object.is(left, right) || show(left) === show(right)
|
|
14
|
-
|
|
15
|
-
const fail = (message: string): Effect.Effect<never> =>
|
|
16
|
-
Effect.dieMessage(new AssertionFailed({ detail: message }).message)
|
|
17
|
-
|
|
18
|
-
export const expect = <A>(actual: A) => ({
|
|
19
|
-
toBe: (expected: A): Effect.Effect<void> =>
|
|
20
|
-
Object.is(actual, expected)
|
|
21
|
-
? Effect.void
|
|
22
|
-
: fail(`expected ${show(actual)} to be ${show(expected)}`),
|
|
23
|
-
toEqual: (expected: A): Effect.Effect<void> =>
|
|
24
|
-
deepEqual({ left: actual, right: expected })
|
|
25
|
-
? Effect.void
|
|
26
|
-
: fail(`expected ${show(actual)} to equal ${show(expected)}`),
|
|
27
|
-
toContain: (expected: A extends ReadonlyArray<infer E> ? E : unknown): Effect.Effect<void> =>
|
|
28
|
-
Array.isArray(actual) && actual.some((element) => deepEqual({ left: element, right: expected }))
|
|
29
|
-
? Effect.void
|
|
30
|
-
: fail(`expected ${show(actual)} to contain ${show(expected)}`),
|
|
31
|
-
toMatchObject: (expected: Partial<A>): Effect.Effect<void> => {
|
|
32
|
-
const mismatch = matchPartial({ actual, expected })
|
|
33
|
-
return mismatch === undefined ? Effect.void : fail(mismatch)
|
|
34
|
-
},
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
const isRecord = (candidate: unknown): candidate is Record<string, unknown> =>
|
|
38
|
-
typeof candidate === 'object' && candidate !== null
|
|
39
|
-
|
|
40
|
-
interface MatchInput {
|
|
41
|
-
readonly actual: unknown
|
|
42
|
-
readonly expected: unknown
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const matchPartial = ({ actual, expected }: MatchInput): string | undefined => {
|
|
46
|
-
if (!isRecord(expected)) {
|
|
47
|
-
return deepEqual({ left: actual, right: expected })
|
|
48
|
-
? undefined
|
|
49
|
-
: `expected ${show(actual)} to match ${show(expected)}`
|
|
50
|
-
}
|
|
51
|
-
if (!isRecord(actual)) {
|
|
52
|
-
return `expected ${show(actual)} to be an object matching ${show(expected)}`
|
|
53
|
-
}
|
|
54
|
-
const mismatch = Arr.findFirst(
|
|
55
|
-
Rec.keys(expected),
|
|
56
|
-
(key) => !deepEqual({ left: actual[key], right: expected[key] }),
|
|
57
|
-
)
|
|
58
|
-
return Option.match(mismatch, {
|
|
59
|
-
onNone: () => undefined,
|
|
60
|
-
onSome: (key) =>
|
|
61
|
-
`expected key ${show(key)} to match ${show(expected[key])}, got ${show(actual[key])}`,
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export const matchProps: (input: MatchInput) => string | undefined = matchPartial
|
package/src/driverTypes.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { UiCapture } from '@playfast/reform'
|
|
2
|
-
|
|
3
|
-
export interface StepFrame {
|
|
4
|
-
readonly index: number
|
|
5
|
-
readonly captures: ReadonlyArray<UiCapture>
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface DriveResult {
|
|
9
|
-
readonly requirement: string
|
|
10
|
-
readonly frames: ReadonlyArray<StepFrame>
|
|
11
|
-
readonly ok: boolean
|
|
12
|
-
// oxlint-disable-next-line reform-rules/no-optional-fields -- serialized result DTO read as `error ?? …` by the editor timeline (external); optional kept for wire compat
|
|
13
|
-
readonly error?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ProofDriver {
|
|
17
|
-
readonly requirement: string
|
|
18
|
-
readonly run: () => Promise<DriveResult>
|
|
19
|
-
}
|
package/src/execution.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { Cause, Effect, Option, Ref } from 'effect'
|
|
2
|
-
import { yieldWrapGet } from 'effect/Utils'
|
|
3
|
-
import {
|
|
4
|
-
Bus,
|
|
5
|
-
type CompositionService,
|
|
6
|
-
publish,
|
|
7
|
-
type Scene,
|
|
8
|
-
sceneInstrumentation,
|
|
9
|
-
} from '@playfast/reform'
|
|
10
|
-
import type { DriveResult, Proof, ProofResult, StepFrame } from './index'
|
|
11
|
-
import { makeFacadeEffect } from './facade'
|
|
12
|
-
import { makeSink, messageOf, proofLayer, type RuntimeServices, type Sink } from './sink'
|
|
13
|
-
import type { PumpStep } from './treeBindings'
|
|
14
|
-
|
|
15
|
-
const missingCoverage = (proof: Proof, dispatched: Set<string>): ReadonlyArray<string> =>
|
|
16
|
-
Option.match(proof.requirement.manifest.events, {
|
|
17
|
-
onNone: () => [],
|
|
18
|
-
onSome: (declared) => declared.map(String).filter((event) => !dispatched.has(event)),
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
const bootScene = (scene: Scene): Effect.Effect<void, never, Bus> =>
|
|
22
|
-
Effect.forEach(
|
|
23
|
-
Option.getOrElse(Option.fromNullable(scene.boot), () => []),
|
|
24
|
-
(event) => publish('High', event),
|
|
25
|
-
).pipe(Effect.asVoid)
|
|
26
|
-
|
|
27
|
-
const executeProofEffect = Effect.fn('executeProofEffect')(function* (
|
|
28
|
-
proof: Proof,
|
|
29
|
-
sink: Sink,
|
|
30
|
-
): Effect.fn.Return<ProofResult, never, RuntimeServices> {
|
|
31
|
-
const dispatched = new Set<string>()
|
|
32
|
-
const statement = proof.requirement.manifest.statement
|
|
33
|
-
return yield* Effect.gen(function* () {
|
|
34
|
-
const { facade, settle } = yield* makeFacadeEffect(proof.scene.composition, sink, dispatched)
|
|
35
|
-
yield* bootScene(proof.scene)
|
|
36
|
-
yield* settle
|
|
37
|
-
yield* Effect.gen(() => proof.body(facade))
|
|
38
|
-
const missing = missingCoverage(proof, dispatched)
|
|
39
|
-
const coverageError = `requirement declares events never dispatched: ${missing.join(', ')}`
|
|
40
|
-
return missing.length > 0
|
|
41
|
-
? { requirement: statement, ok: false, error: coverageError }
|
|
42
|
-
: { requirement: statement, ok: true }
|
|
43
|
-
}).pipe(
|
|
44
|
-
Effect.catchAllCause((cause) =>
|
|
45
|
-
Effect.succeed({ requirement: statement, ok: false, error: messageOf(Cause.squash(cause)) }),
|
|
46
|
-
),
|
|
47
|
-
)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
export const executeProof = async (proof: Proof): Promise<ProofResult> => {
|
|
51
|
-
const sink = makeSink(sceneInstrumentation(proof.scene))
|
|
52
|
-
return Effect.runPromise(executeProofEffect(proof, sink).pipe(Effect.provide(proofLayer(proof.scene, sink))))
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const driveProofEffect = Effect.fn('driveProofEffect')(function* (
|
|
56
|
-
proof: Proof,
|
|
57
|
-
sink: Sink,
|
|
58
|
-
): Effect.fn.Return<DriveResult, never, RuntimeServices> {
|
|
59
|
-
const frames = yield* Ref.make<ReadonlyArray<StepFrame>>([])
|
|
60
|
-
const statement = proof.requirement.manifest.statement
|
|
61
|
-
const dispatched = new Set<string>()
|
|
62
|
-
const record = (index: number): Effect.Effect<void> =>
|
|
63
|
-
Ref.update(frames, (current) => [...current, { index, captures: [...sink.captures] }])
|
|
64
|
-
const pump = Effect.fn('pump')(function* (
|
|
65
|
-
generator: ReturnType<Proof['body']>,
|
|
66
|
-
step: PumpStep,
|
|
67
|
-
): Effect.fn.Return<void, unknown, CompositionService> {
|
|
68
|
-
const next = generator.next(step.input)
|
|
69
|
-
if (next.done === true) {
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
const output = yield* yieldWrapGet(next.value)
|
|
73
|
-
yield* record(step.index)
|
|
74
|
-
yield* pump(generator, { input: output, index: step.index + 1 })
|
|
75
|
-
})
|
|
76
|
-
return yield* Effect.gen(function* () {
|
|
77
|
-
const { facade, settle } = yield* makeFacadeEffect(proof.scene.composition, sink, dispatched)
|
|
78
|
-
yield* bootScene(proof.scene)
|
|
79
|
-
yield* settle
|
|
80
|
-
yield* record(0)
|
|
81
|
-
yield* pump(proof.body(facade), { input: undefined, index: 1 })
|
|
82
|
-
return { requirement: statement, frames: yield* Ref.get(frames), ok: true }
|
|
83
|
-
}).pipe(
|
|
84
|
-
Effect.catchAllCause((cause) =>
|
|
85
|
-
Ref.get(frames).pipe(
|
|
86
|
-
Effect.map((collected) => ({
|
|
87
|
-
requirement: statement,
|
|
88
|
-
frames: collected,
|
|
89
|
-
ok: false,
|
|
90
|
-
error: messageOf(Cause.squash(cause)),
|
|
91
|
-
})),
|
|
92
|
-
),
|
|
93
|
-
),
|
|
94
|
-
)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
export const driveProof = async (proof: Proof): Promise<DriveResult> => {
|
|
98
|
-
const sink = makeSink(sceneInstrumentation(proof.scene))
|
|
99
|
-
return Effect.runPromise(driveProofEffect(proof, sink).pipe(Effect.provide(proofLayer(proof.scene, sink))))
|
|
100
|
-
}
|