@playfast/reform 0.0.5 → 0.0.8
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/package.json +31 -10
- package/src/boundary/boundary.test.ts +301 -0
- package/src/calc/asyncCalc.test.ts +556 -0
- package/src/calc/asyncData.ts +9 -1
- package/src/calc/calc.test.ts +287 -0
- package/src/calc/calcFamily.test.ts +206 -0
- package/src/calc/compose.test.ts +68 -0
- package/src/channel/channel.ts +9 -5
- package/src/compose/host.ts +4 -1
- package/src/compose/props.ts +5 -1
- package/src/compose/ui.test.ts +62 -0
- package/src/compose/ui.ts +173 -44
- package/src/compose/ui.typecheck.ts +56 -0
- package/src/definition/definition.ts +2 -0
- package/src/event/event.test.ts +23 -0
- package/src/feature/feature.mount.test.ts +82 -0
- package/src/feature/feature.test.ts +60 -0
- package/src/feature/feature.typecheck.ts +108 -0
- package/src/index.ts +38 -2
- package/src/internal/capture.ts +4 -4
- package/src/internal/errors.test.ts +33 -0
- package/src/internal/errors.ts +92 -16
- package/src/internal/inspect.test.ts +28 -0
- package/src/internal/queryDriver.ts +1 -1
- package/src/internal/reuse.test.ts +116 -0
- package/src/internal/scheduler.ts +3 -1
- package/src/internal/sources.ts +2 -2
- package/src/internal/stateRegistry.ts +32 -0
- package/src/internal/store.test.ts +80 -0
- package/src/internal/track.ts +3 -1
- package/src/remote/remoteState.test.ts +695 -0
- package/src/remote/remoteState.typecheck.ts +195 -0
- package/src/runtime/bus.ts +6 -2
- package/src/runtime/hardening.test.ts +69 -0
- package/src/runtime/loop.test.ts +178 -0
- package/src/runtime/loop.ts +4 -2
- package/src/scene/seedScene.test.ts +169 -0
- package/src/state/state.ts +4 -1
- package/src/state/stateFamily.test.ts +138 -0
- package/src/state/stateFamily.ts +4 -1
- package/src/state/stateGroup.ts +31 -4
- package/src/state/token.ts +5 -4
- package/src/synced/syncedStore.ts +99 -0
- package/src/wire/tree.test.ts +81 -0
- package/src/wire/tree.ts +129 -0
- package/src/wire/triggers.test.ts +76 -0
- package/src/wire/triggers.ts +98 -0
package/package.json
CHANGED
|
@@ -1,24 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform",
|
|
3
|
-
"
|
|
3
|
+
"playbook": "./playbook",
|
|
4
|
+
"version": "0.0.8",
|
|
4
5
|
"type": "module",
|
|
5
|
-
"
|
|
6
|
+
"description": "The renderer-neutral core of the reform framework — typed, headless state, events, reducers, derived values, async/remote data, and compositions built on Effect.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"reform",
|
|
9
|
+
"effect",
|
|
10
|
+
"state-management",
|
|
11
|
+
"headless",
|
|
12
|
+
"framework",
|
|
13
|
+
"reactive"
|
|
14
|
+
],
|
|
6
15
|
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/playfast/reform.git",
|
|
19
|
+
"directory": "packages/reform"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/playfast/reform/issues"
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": false,
|
|
7
25
|
"exports": {
|
|
8
26
|
"./package.json": "./package.json",
|
|
9
|
-
".": "./src/index.ts"
|
|
27
|
+
".": "./src/index.ts",
|
|
28
|
+
"./*": "./src/*.ts"
|
|
10
29
|
},
|
|
11
30
|
"files": [
|
|
12
31
|
"src",
|
|
13
|
-
"README.md"
|
|
14
|
-
"!src/**/*.test.ts",
|
|
15
|
-
"!src/**/*.test.tsx",
|
|
16
|
-
"!src/**/*.typecheck.ts",
|
|
17
|
-
"!src/**/*.typecheck.tsx"
|
|
32
|
+
"README.md"
|
|
18
33
|
],
|
|
19
34
|
"scripts": {
|
|
20
|
-
"
|
|
21
|
-
"
|
|
35
|
+
"clean": "rm -rf dist .tsbuildinfo",
|
|
36
|
+
"check": "tsc --noEmit",
|
|
37
|
+
"build": "tsc -p tsconfig.build.json",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest",
|
|
40
|
+
"coverage": "vitest run --coverage",
|
|
41
|
+
"lint": "oxlint src",
|
|
42
|
+
"lint:fix": "oxlint --fix src"
|
|
22
43
|
},
|
|
23
44
|
"peerDependencies": {
|
|
24
45
|
"effect": "*",
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { expect, it } from '@effect/vitest'
|
|
2
|
+
import { Data, Duration, Effect, Layer, Schema as S } from 'effect'
|
|
3
|
+
import { AsyncCalc, Boundary, Calc, Engine, Event, State, StateGroup } from '../index'
|
|
4
|
+
|
|
5
|
+
// Boundary merges several async lifecycles into one Pending/Errored/Ready value
|
|
6
|
+
// — the Suspense-boundary analog, value-level. The behaviours under test: the
|
|
7
|
+
// arm mapping (Loading pends, Idle and SWR-refetching count Ready, Error
|
|
8
|
+
// surfaces), the chained-gate settle-gap pin, the `once` latch, and the
|
|
9
|
+
// notification economy (arm-stable churn wakes nobody).
|
|
10
|
+
|
|
11
|
+
const tick = (ms = 10) => Effect.sleep(Duration.millis(ms))
|
|
12
|
+
|
|
13
|
+
const until = <A>(read: () => A, pred: (a: A) => boolean, rounds = 200): Effect.Effect<A> =>
|
|
14
|
+
Effect.suspend(() => {
|
|
15
|
+
const a = read()
|
|
16
|
+
return pred(a) || rounds <= 0
|
|
17
|
+
? Effect.succeed(a)
|
|
18
|
+
: Effect.flatMap(tick(10), () => until(read, pred, rounds - 1))
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
class Boom extends Data.TaggedError('Boom')<{ readonly message: string }> {}
|
|
22
|
+
|
|
23
|
+
it.live('pending while ANY covered query is on its first load; one Ready when all settle', () => {
|
|
24
|
+
class Seed extends State.make('seed', S.Number) {}
|
|
25
|
+
class Inputs extends StateGroup.make(Seed) {}
|
|
26
|
+
class Fast extends AsyncCalc.make('Fast', {
|
|
27
|
+
inputs: [StateGroup.select(Inputs, 'seed')],
|
|
28
|
+
output: S.Number,
|
|
29
|
+
alwaysOn: true,
|
|
30
|
+
}) {}
|
|
31
|
+
class Slow extends AsyncCalc.make('Slow', {
|
|
32
|
+
inputs: [StateGroup.select(Inputs, 'seed')],
|
|
33
|
+
output: S.Number,
|
|
34
|
+
alwaysOn: true,
|
|
35
|
+
}) {}
|
|
36
|
+
class Gate extends Boundary.make('Gate', { over: [Fast, Slow] }) {}
|
|
37
|
+
const TestLayer = Boundary.live(Gate).pipe(
|
|
38
|
+
Layer.provideMerge(
|
|
39
|
+
Layer.merge(
|
|
40
|
+
AsyncCalc.live(Fast, { query: ({ seed }) => Effect.succeed(seed).pipe(Effect.delay('10 millis')) }),
|
|
41
|
+
AsyncCalc.live(Slow, { query: ({ seed }) => Effect.succeed(seed).pipe(Effect.delay('40 millis')) }),
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
Layer.provideMerge(StateGroup.live(Inputs, { seed: 1 })),
|
|
45
|
+
)
|
|
46
|
+
return Effect.gen(function* () {
|
|
47
|
+
const store = yield* Gate.store
|
|
48
|
+
const observed: Array<string> = []
|
|
49
|
+
store.subscribe(() => {
|
|
50
|
+
observed.push(store.get()._tag)
|
|
51
|
+
})
|
|
52
|
+
expect(store.get()._tag).toBe('Pending')
|
|
53
|
+
|
|
54
|
+
// The fast query settles first — still Pending (the slow one holds it).
|
|
55
|
+
yield* tick(20)
|
|
56
|
+
expect(store.get()._tag).toBe('Pending')
|
|
57
|
+
|
|
58
|
+
yield* until(() => store.get()._tag, (t) => t === 'Ready')
|
|
59
|
+
// Exactly ONE notification fired across the whole boot: Pending→Pending
|
|
60
|
+
// churn (Fast settling) was absorbed by the stable arm reference.
|
|
61
|
+
expect(observed).toEqual(['Ready'])
|
|
62
|
+
}).pipe(Effect.provide(TestLayer))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it.live('a gated query that is OFF counts Ready — the off arm must not hold the gate', () => {
|
|
66
|
+
class Flag extends State.make('flag', S.Boolean) {}
|
|
67
|
+
class Inputs extends StateGroup.make(Flag) {}
|
|
68
|
+
class Always extends AsyncCalc.make('Always', {
|
|
69
|
+
inputs: [StateGroup.select(Inputs, 'flag')],
|
|
70
|
+
output: S.Number,
|
|
71
|
+
alwaysOn: true,
|
|
72
|
+
}) {}
|
|
73
|
+
class Gated extends AsyncCalc.make('Gated', {
|
|
74
|
+
inputs: [StateGroup.select(Inputs, 'flag')],
|
|
75
|
+
output: S.Number,
|
|
76
|
+
}) {}
|
|
77
|
+
class Gate extends Boundary.make('Gate', { over: [Always, Gated] }) {}
|
|
78
|
+
const TestLayer = Boundary.live(Gate).pipe(
|
|
79
|
+
Layer.provideMerge(
|
|
80
|
+
Layer.merge(
|
|
81
|
+
AsyncCalc.live(Always, { query: () => Effect.succeed(1) }),
|
|
82
|
+
AsyncCalc.live(Gated, { query: () => Effect.succeed(2), disabled: ({ flag }) => !flag }),
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
Layer.provideMerge(StateGroup.live(Inputs, { flag: false })),
|
|
86
|
+
)
|
|
87
|
+
return Effect.gen(function* () {
|
|
88
|
+
const store = yield* Gate.store
|
|
89
|
+
// The gated query is Idle (deliberately off): only the always-on first
|
|
90
|
+
// load pends, and Ready arrives without ever waiting on the Idle arm.
|
|
91
|
+
yield* until(() => store.get()._tag, (t) => t === 'Ready')
|
|
92
|
+
expect(store.get()._tag).toBe('Ready')
|
|
93
|
+
}).pipe(Effect.provide(TestLayer))
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it.live('an SWR refetch never re-pends — Success with refetching: true is Ready', () => {
|
|
97
|
+
class Seed extends State.make('seed', S.Number) {}
|
|
98
|
+
class Inputs extends StateGroup.make(Seed) {}
|
|
99
|
+
class Q extends AsyncCalc.make('Q', {
|
|
100
|
+
inputs: [StateGroup.select(Inputs, 'seed')],
|
|
101
|
+
output: S.Number,
|
|
102
|
+
alwaysOn: true,
|
|
103
|
+
}) {}
|
|
104
|
+
class Gate extends Boundary.make('Gate', { over: [Q] }) {}
|
|
105
|
+
const TestLayer = Boundary.live(Gate).pipe(
|
|
106
|
+
Layer.provideMerge(
|
|
107
|
+
AsyncCalc.live(Q, {
|
|
108
|
+
query: ({ seed }) => Effect.succeed(seed * 2).pipe(Effect.delay('15 millis')),
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
Layer.provideMerge(StateGroup.live(Inputs, { seed: 1 })),
|
|
112
|
+
)
|
|
113
|
+
return Effect.gen(function* () {
|
|
114
|
+
const seed = yield* StateGroup.select(Inputs, 'seed').store
|
|
115
|
+
const qStore = yield* Q.store
|
|
116
|
+
const store = yield* Gate.store
|
|
117
|
+
yield* until(() => store.get()._tag, (t) => t === 'Ready')
|
|
118
|
+
|
|
119
|
+
// Move the input: the query goes Success+refetching (SWR) — Ready holds.
|
|
120
|
+
seed.set(2)
|
|
121
|
+
yield* until(() => qStore.get(), (q) => q._tag === 'Success' && q.refetching)
|
|
122
|
+
expect(store.get()._tag).toBe('Ready')
|
|
123
|
+
yield* until(() => qStore.get(), (q) => q._tag === 'Success' && !q.refetching)
|
|
124
|
+
expect(store.get()._tag).toBe('Ready')
|
|
125
|
+
}).pipe(Effect.provide(TestLayer))
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it.live('a first-load failure surfaces as Errored with the failure, and a retry recovers', () => {
|
|
129
|
+
class Seed extends State.make('seed', S.Number) {}
|
|
130
|
+
class Inputs extends StateGroup.make(Seed) {}
|
|
131
|
+
class Poke extends Event.make('Poke', S.Struct({})) {}
|
|
132
|
+
const healthy = { now: false }
|
|
133
|
+
class Q extends AsyncCalc.make('Q', {
|
|
134
|
+
inputs: [StateGroup.select(Inputs, 'seed')],
|
|
135
|
+
output: S.Number,
|
|
136
|
+
error: S.instanceOf(Boom),
|
|
137
|
+
alwaysOn: true,
|
|
138
|
+
}) {}
|
|
139
|
+
class Gate extends Boundary.make('Gate', { over: [Q] }) {}
|
|
140
|
+
const TestLayer = Boundary.live(Gate).pipe(
|
|
141
|
+
Layer.provideMerge(
|
|
142
|
+
AsyncCalc.live(Q, {
|
|
143
|
+
query: () =>
|
|
144
|
+
healthy.now ? Effect.succeed(1) : Effect.fail(new Boom({ message: 'server down' })),
|
|
145
|
+
invalidateOn: [Poke],
|
|
146
|
+
}),
|
|
147
|
+
),
|
|
148
|
+
Layer.provideMerge(StateGroup.live(Inputs, { seed: 1 })),
|
|
149
|
+
Layer.provideMerge(Engine),
|
|
150
|
+
)
|
|
151
|
+
return Effect.gen(function* () {
|
|
152
|
+
const store = yield* Gate.store
|
|
153
|
+
const errored = yield* until(() => store.get(), (s) => s._tag === 'Errored')
|
|
154
|
+
if (errored._tag === 'Errored') {
|
|
155
|
+
expect(errored.errors).toHaveLength(1)
|
|
156
|
+
const failure = errored.errors[0]
|
|
157
|
+
expect(failure instanceof Boom && failure.message).toBe('server down')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The retry path: the server heals, a listed fact refetches, Ready lands.
|
|
161
|
+
healthy.now = true
|
|
162
|
+
yield* Event.dispatch(Poke, {})
|
|
163
|
+
yield* until(() => store.get()._tag, (t) => t === 'Ready')
|
|
164
|
+
expect(store.get()._tag).toBe('Ready')
|
|
165
|
+
}).pipe(Effect.provide(TestLayer))
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it.live('settle-gap pin: a chained gate never leaks Ready between hops', () => {
|
|
169
|
+
// B enables only when A has succeeded — the app's Session → Bootstrap shape.
|
|
170
|
+
// The dangerous instant: A flips Success while B is still Idle (Idle counts
|
|
171
|
+
// Ready), so a naive merge could notify Ready before B's driver flips it to
|
|
172
|
+
// Loading. Layer dependency order (B's driver subscribes to A's store before
|
|
173
|
+
// the boundary does) plus the one-microtask fixpoint flush close the gap;
|
|
174
|
+
// this records every notification to prove no intermediate Ready escapes.
|
|
175
|
+
class Seed extends State.make('seed', S.Number) {}
|
|
176
|
+
class Inputs extends StateGroup.make(Seed) {}
|
|
177
|
+
class A extends AsyncCalc.make('A', {
|
|
178
|
+
inputs: [StateGroup.select(Inputs, 'seed')],
|
|
179
|
+
output: S.Number,
|
|
180
|
+
alwaysOn: true,
|
|
181
|
+
}) {}
|
|
182
|
+
class B extends AsyncCalc.make('B', {
|
|
183
|
+
inputs: [A],
|
|
184
|
+
output: S.Number,
|
|
185
|
+
}) {}
|
|
186
|
+
class Gate extends Boundary.make('Gate', { over: [A, B] }) {}
|
|
187
|
+
const TestLayer = Boundary.live(Gate).pipe(
|
|
188
|
+
Layer.provideMerge(
|
|
189
|
+
AsyncCalc.live(B, {
|
|
190
|
+
query: () => Effect.succeed(2).pipe(Effect.delay('15 millis')),
|
|
191
|
+
disabled: ({ A: a }) => a._tag !== 'Success',
|
|
192
|
+
}),
|
|
193
|
+
),
|
|
194
|
+
Layer.provideMerge(
|
|
195
|
+
AsyncCalc.live(A, { query: () => Effect.succeed(1).pipe(Effect.delay('15 millis')) }),
|
|
196
|
+
),
|
|
197
|
+
Layer.provideMerge(StateGroup.live(Inputs, { seed: 1 })),
|
|
198
|
+
)
|
|
199
|
+
return Effect.gen(function* () {
|
|
200
|
+
const store = yield* Gate.store
|
|
201
|
+
const observed: Array<string> = []
|
|
202
|
+
store.subscribe(() => {
|
|
203
|
+
observed.push(store.get()._tag)
|
|
204
|
+
})
|
|
205
|
+
expect(store.get()._tag).toBe('Pending')
|
|
206
|
+
|
|
207
|
+
yield* until(() => store.get()._tag, (t) => t === 'Ready')
|
|
208
|
+
yield* tick(30) // let any straggler notification land before judging
|
|
209
|
+
// ONE Ready, at the end — never a Ready while B's hop was still pending.
|
|
210
|
+
expect(observed).toEqual(['Ready'])
|
|
211
|
+
}).pipe(Effect.provide(TestLayer))
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it.live('once + a Calc hop in the chain: the latch never freezes a mid-flush Ready', () => {
|
|
215
|
+
// The app's boot shape: AsyncCalc A → plain Calc projecting it → AsyncCalc B
|
|
216
|
+
// gated on the projection. Propagation crosses TWO scheduler-flush rounds,
|
|
217
|
+
// so when A settles there is a mid-flush frame where B has not yet flipped
|
|
218
|
+
// to Loading — a recompute-time latch would freeze that intermediate Ready
|
|
219
|
+
// and the boundary would abandon B's whole first load (caught live by the
|
|
220
|
+
// boot-frames walk: nav in, boards card still spinning).
|
|
221
|
+
class Seed extends State.make('seed', S.Number) {}
|
|
222
|
+
class Inputs extends StateGroup.make(Seed) {}
|
|
223
|
+
class A extends AsyncCalc.make('A', {
|
|
224
|
+
inputs: [StateGroup.select(Inputs, 'seed')],
|
|
225
|
+
output: S.Number,
|
|
226
|
+
alwaysOn: true,
|
|
227
|
+
}) {}
|
|
228
|
+
class Hop extends Calc.make('Hop', { inputs: [A], output: S.Number }) {}
|
|
229
|
+
class B extends AsyncCalc.make('B', {
|
|
230
|
+
inputs: [Hop],
|
|
231
|
+
output: S.Number,
|
|
232
|
+
}) {}
|
|
233
|
+
class Gate extends Boundary.make('Gate', { over: [A, B] }) {}
|
|
234
|
+
const TestLayer = Boundary.live(Gate, { once: true }).pipe(
|
|
235
|
+
Layer.provideMerge(
|
|
236
|
+
AsyncCalc.live(B, {
|
|
237
|
+
query: () => Effect.succeed(2).pipe(Effect.delay('25 millis')),
|
|
238
|
+
disabled: ({ Hop: hop }) => hop < 0,
|
|
239
|
+
}),
|
|
240
|
+
),
|
|
241
|
+
Layer.provideMerge(Calc.live(Hop, ({ A: a }) => (a._tag === 'Success' ? a.value : -1))),
|
|
242
|
+
Layer.provideMerge(
|
|
243
|
+
AsyncCalc.live(A, { query: () => Effect.succeed(1).pipe(Effect.delay('15 millis')) }),
|
|
244
|
+
),
|
|
245
|
+
Layer.provideMerge(StateGroup.live(Inputs, { seed: 1 })),
|
|
246
|
+
)
|
|
247
|
+
return Effect.gen(function* () {
|
|
248
|
+
const bStore = yield* B.store
|
|
249
|
+
const store = yield* Gate.store
|
|
250
|
+
// While B's first load is in flight the boundary MUST still be Pending —
|
|
251
|
+
// a poisoned latch would already report Ready here.
|
|
252
|
+
yield* until(() => bStore.get()._tag, (t) => t === 'Loading')
|
|
253
|
+
expect(store.get()._tag).toBe('Pending')
|
|
254
|
+
yield* until(() => bStore.get()._tag, (t) => t === 'Success')
|
|
255
|
+
yield* until(() => store.get()._tag, (t) => t === 'Ready')
|
|
256
|
+
expect(store.get()._tag).toBe('Ready')
|
|
257
|
+
}).pipe(Effect.provide(TestLayer))
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it.live('once: a covered gate enabling AFTER the latch never re-pends the boundary', () => {
|
|
261
|
+
// The boot-boundary semantics: a route-gated query (the board) first loads
|
|
262
|
+
// on a later navigation — its own screen owns that loading state, the shell
|
|
263
|
+
// splash must not come back.
|
|
264
|
+
class Route extends State.make('routeFlag', S.Boolean) {}
|
|
265
|
+
class Inputs extends StateGroup.make(Route) {}
|
|
266
|
+
class Always extends AsyncCalc.make('Always', {
|
|
267
|
+
inputs: [StateGroup.select(Inputs, 'routeFlag')],
|
|
268
|
+
output: S.Number,
|
|
269
|
+
alwaysOn: true,
|
|
270
|
+
}) {}
|
|
271
|
+
class Routed extends AsyncCalc.make('Routed', {
|
|
272
|
+
inputs: [StateGroup.select(Inputs, 'routeFlag')],
|
|
273
|
+
output: S.Number,
|
|
274
|
+
}) {}
|
|
275
|
+
class Gate extends Boundary.make('Gate', { over: [Always, Routed] }) {}
|
|
276
|
+
const TestLayer = Boundary.live(Gate, { once: true }).pipe(
|
|
277
|
+
Layer.provideMerge(
|
|
278
|
+
Layer.merge(
|
|
279
|
+
AsyncCalc.live(Always, { query: () => Effect.succeed(1) }),
|
|
280
|
+
AsyncCalc.live(Routed, {
|
|
281
|
+
query: () => Effect.succeed(2).pipe(Effect.delay('20 millis')),
|
|
282
|
+
disabled: ({ routeFlag }) => !routeFlag,
|
|
283
|
+
}),
|
|
284
|
+
),
|
|
285
|
+
),
|
|
286
|
+
Layer.provideMerge(StateGroup.live(Inputs, { routeFlag: false })),
|
|
287
|
+
)
|
|
288
|
+
return Effect.gen(function* () {
|
|
289
|
+
const route = yield* StateGroup.select(Inputs, 'routeFlag').store
|
|
290
|
+
const routedStore = yield* Routed.store
|
|
291
|
+
const store = yield* Gate.store
|
|
292
|
+
yield* until(() => store.get()._tag, (t) => t === 'Ready')
|
|
293
|
+
|
|
294
|
+
// "Navigate": the routed query enables and goes Loading — latched Ready holds.
|
|
295
|
+
route.set(true)
|
|
296
|
+
yield* until(() => routedStore.get()._tag, (t) => t === 'Loading')
|
|
297
|
+
expect(store.get()._tag).toBe('Ready')
|
|
298
|
+
yield* until(() => routedStore.get()._tag, (t) => t === 'Success')
|
|
299
|
+
expect(store.get()._tag).toBe('Ready')
|
|
300
|
+
}).pipe(Effect.provide(TestLayer))
|
|
301
|
+
})
|