@playfast/reform 1.3.0 → 1.4.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 +15 -6
- package/dist/calc/asyncCalc.d.ts +3 -1
- package/dist/calc/asyncCalc.d.ts.map +1 -1
- package/dist/calc/asyncCalc.js +15 -5
- package/dist/calc/asyncCalc.js.map +1 -1
- package/dist/calc/asyncCalcTypes.d.ts +2 -0
- package/dist/calc/asyncCalcTypes.d.ts.map +1 -1
- package/dist/calc/calcFamily.d.ts.map +1 -1
- package/dist/calc/calcFamily.js +77 -34
- package/dist/calc/calcFamily.js.map +1 -1
- package/dist/calc/queryState.d.ts +3 -1
- package/dist/calc/queryState.d.ts.map +1 -1
- package/dist/calc/queryState.js +31 -9
- package/dist/calc/queryState.js.map +1 -1
- package/dist/channel/channel.d.ts.map +1 -1
- package/dist/channel/channel.js +6 -1
- package/dist/channel/channel.js.map +1 -1
- package/dist/compose/composition.d.ts.map +1 -1
- package/dist/compose/composition.js +7 -1
- package/dist/compose/composition.js.map +1 -1
- package/dist/compose/structure.d.ts.map +1 -1
- package/dist/compose/structure.js +33 -8
- package/dist/compose/structure.js.map +1 -1
- package/dist/compose/ui.d.ts.map +1 -1
- package/dist/compose/ui.js +16 -5
- package/dist/compose/ui.js.map +1 -1
- package/dist/event/event.d.ts.map +1 -1
- package/dist/event/event.js +23 -2
- package/dist/event/event.js.map +1 -1
- package/dist/internal/familyKeys.d.ts +16 -0
- package/dist/internal/familyKeys.d.ts.map +1 -0
- package/dist/internal/familyKeys.js +35 -0
- package/dist/internal/familyKeys.js.map +1 -0
- package/dist/internal/platform.d.ts +43 -0
- package/dist/internal/platform.d.ts.map +1 -0
- package/dist/internal/platform.js +68 -0
- package/dist/internal/platform.js.map +1 -0
- package/dist/internal/queryDriver.d.ts.map +1 -1
- package/dist/internal/queryDriver.js +88 -64
- package/dist/internal/queryDriver.js.map +1 -1
- package/dist/internal/queryDriverConsume.d.ts +9 -0
- package/dist/internal/queryDriverConsume.d.ts.map +1 -0
- package/dist/internal/queryDriverConsume.js +16 -0
- package/dist/internal/queryDriverConsume.js.map +1 -0
- package/dist/internal/queryDriverHydrate.d.ts.map +1 -1
- package/dist/internal/queryDriverHydrate.js +9 -3
- package/dist/internal/queryDriverHydrate.js.map +1 -1
- package/dist/internal/queryDriverPersist.d.ts +16 -0
- package/dist/internal/queryDriverPersist.d.ts.map +1 -0
- package/dist/internal/queryDriverPersist.js +15 -0
- package/dist/internal/queryDriverPersist.js.map +1 -0
- package/dist/internal/queryDriverPersistWarn.d.ts +12 -0
- package/dist/internal/queryDriverPersistWarn.d.ts.map +1 -0
- package/dist/internal/queryDriverPersistWarn.js +31 -0
- package/dist/internal/queryDriverPersistWarn.js.map +1 -0
- package/dist/internal/queryDriverSettle.d.ts +23 -0
- package/dist/internal/queryDriverSettle.d.ts.map +1 -0
- package/dist/internal/queryDriverSettle.js +52 -0
- package/dist/internal/queryDriverSettle.js.map +1 -0
- package/dist/internal/queryDriverStore.d.ts +2 -28
- package/dist/internal/queryDriverStore.d.ts.map +1 -1
- package/dist/internal/queryDriverStore.js +5 -29
- package/dist/internal/queryDriverStore.js.map +1 -1
- package/dist/internal/queryDriverTypes.d.ts +3 -0
- package/dist/internal/queryDriverTypes.d.ts.map +1 -1
- package/dist/internal/scheduler.d.ts +1 -0
- package/dist/internal/scheduler.d.ts.map +1 -1
- package/dist/internal/scheduler.js +69 -6
- package/dist/internal/scheduler.js.map +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -0
- package/dist/internal.js.map +1 -1
- package/dist/reducer/reducer.js +3 -3
- package/dist/reducer/reducer.js.map +1 -1
- package/dist/remote/pendingQueue.d.ts +4 -2
- package/dist/remote/pendingQueue.d.ts.map +1 -1
- package/dist/remote/pendingQueue.js +20 -4
- package/dist/remote/pendingQueue.js.map +1 -1
- package/dist/remote/remoteState.d.ts.map +1 -1
- package/dist/remote/remoteState.js +27 -14
- package/dist/remote/remoteState.js.map +1 -1
- package/dist/remote/remoteStateSend.d.ts +3 -1
- package/dist/remote/remoteStateSend.d.ts.map +1 -1
- package/dist/remote/remoteStateSend.js +19 -3
- package/dist/remote/remoteStateSend.js.map +1 -1
- package/dist/remote/remoteStateTypes.d.ts +3 -0
- package/dist/remote/remoteStateTypes.d.ts.map +1 -1
- package/dist/runtime/bus.d.ts +5 -0
- package/dist/runtime/bus.d.ts.map +1 -1
- package/dist/runtime/bus.js +1 -1
- package/dist/runtime/bus.js.map +1 -1
- package/dist/runtime/capturedAppRuntime.d.ts.map +1 -1
- package/dist/runtime/capturedAppRuntime.js +37 -51
- package/dist/runtime/capturedAppRuntime.js.map +1 -1
- package/dist/runtime/eventBudget.d.ts.map +1 -1
- package/dist/runtime/eventBudget.js +2 -1
- package/dist/runtime/eventBudget.js.map +1 -1
- package/dist/runtime/featureMount.d.ts +34 -2
- package/dist/runtime/featureMount.d.ts.map +1 -1
- package/dist/runtime/featureMount.js +66 -2
- package/dist/runtime/featureMount.js.map +1 -1
- package/dist/runtime/loop.d.ts +12 -0
- package/dist/runtime/loop.d.ts.map +1 -1
- package/dist/runtime/loop.js +8 -5
- package/dist/runtime/loop.js.map +1 -1
- package/dist/state/state.d.ts +2 -2
- package/dist/state/state.d.ts.map +1 -1
- package/dist/state/state.js +2 -2
- package/dist/state/state.js.map +1 -1
- package/dist/state/stateFamily.d.ts +2 -2
- package/dist/state/stateFamily.d.ts.map +1 -1
- package/dist/state/stateFamily.js +80 -32
- package/dist/state/stateFamily.js.map +1 -1
- package/dist/state/stateGroup.d.ts +5 -5
- package/dist/state/stateGroup.d.ts.map +1 -1
- package/dist/state/stateGroup.js +1 -1
- package/dist/state/stateGroup.js.map +1 -1
- package/dist/testkit/flight.testkit.d.ts.map +1 -1
- package/dist/testkit/flight.testkit.js +2 -1
- package/dist/testkit/flight.testkit.js.map +1 -1
- package/dist/wire/tree.d.ts.map +1 -1
- package/dist/wire/tree.js +6 -1
- package/dist/wire/tree.js.map +1 -1
- package/package.json +1 -1
- package/src/calc/asyncCalc-key-attribution.test.ts +182 -0
- package/src/calc/asyncCalc-registry-name.test.ts +82 -0
- package/src/calc/asyncCalc.superseded-commit.test.ts +162 -0
- package/src/calc/asyncCalc.test.ts +35 -7
- package/src/calc/asyncCalc.ts +33 -10
- package/src/calc/asyncCalcTypes.ts +6 -0
- package/src/calc/calcFamily.ts +81 -34
- package/src/calc/family-identity.test.ts +231 -0
- package/src/calc/queryState.ts +36 -5
- package/src/channel/channel-throttle-frames.test.ts +163 -0
- package/src/channel/channel.ts +5 -0
- package/src/compose/compose-identity.test.ts +175 -0
- package/src/compose/composition.ts +8 -4
- package/src/compose/structure.ts +45 -13
- package/src/compose/ui.ts +22 -5
- package/src/event/event.ts +31 -2
- package/src/event/syncTrigger.test.ts +66 -0
- package/src/internal/familyKeys.ts +41 -0
- package/src/internal/platform.test.ts +55 -0
- package/src/internal/platform.ts +72 -0
- package/src/internal/queryDriver-defect-attribution.test.ts +107 -0
- package/src/internal/queryDriver-persist-key.test.ts +118 -0
- package/src/internal/queryDriver.persistence-staleness.test.ts +209 -0
- package/src/internal/queryDriver.ts +92 -80
- package/src/internal/queryDriverConsume.ts +42 -0
- package/src/internal/queryDriverHydrate.ts +9 -3
- package/src/internal/queryDriverPersist.ts +42 -0
- package/src/internal/queryDriverPersistWarn.ts +61 -0
- package/src/internal/queryDriverSettle.ts +85 -0
- package/src/internal/queryDriverStore.ts +9 -74
- package/src/internal/queryDriverTypes.ts +10 -0
- package/src/internal/queryState-and-scheduler-audit.test.ts +170 -0
- package/src/internal/scheduler-multi-source-wake.test.ts +111 -0
- package/src/internal/scheduler.ts +80 -6
- package/src/internal.ts +1 -0
- package/src/reducer/reducer.ts +3 -3
- package/src/remote/pendingQueue.ts +35 -5
- package/src/remote/remoteState.intent-key-stamp.test.ts +207 -0
- package/src/remote/remoteState.key-attribution.test.ts +174 -0
- package/src/remote/remoteState.ts +27 -15
- package/src/remote/remoteStateSend.ts +29 -8
- package/src/remote/remoteStateTypes.ts +7 -0
- package/src/runtime/bus.ts +6 -1
- package/src/runtime/capturedAppRuntime.ts +134 -110
- package/src/runtime/eventBudget.ts +2 -1
- package/src/runtime/feature-mount-ownership.test.ts +539 -0
- package/src/runtime/featureMount.ts +110 -3
- package/src/runtime/loop.ts +29 -6
- package/src/runtime/mount-inflight-ownership.test.ts +361 -0
- package/src/runtime/mount-scope-hardening.test.ts +440 -0
- package/src/runtime/runtime.schema-identity.test.ts +188 -0
- package/src/state/state.ts +8 -4
- package/src/state/stateFamily.ts +86 -36
- package/src/state/stateGroup.ts +24 -6
- package/src/testkit/flight.testkit.ts +2 -1
- package/src/wire/tree-key-presence.test.ts +46 -0
- package/src/wire/tree.ts +9 -1
package/dist/wire/tree.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../src/wire/tree.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,QAAQ,GAChB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACzE;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAA;CACxC;AAED,MAAM,MAAM,QAAQ,GAAG,mBAAmB,CAAA;AAE1C,MAAM,MAAM,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAA;AAE9C,MAAM,MAAM,SAAS,GACjB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../src/wire/tree.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,QAAQ,GAChB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACzE;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAA;CACxC;AAED,MAAM,MAAM,QAAQ,GAAG,mBAAmB,CAAA;AAE1C,MAAM,MAAM,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAA;AAE9C,MAAM,MAAM,SAAS,GACjB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAA;AA+DpD,eAAO,MAAM,IAAI,GAAI,UAAU,QAAQ,EAAE,MAAM,QAAQ,KAAG,aAAa,CAAC,SAAS,CAahF,CAAA;AAWD,eAAO,MAAM,KAAK,GAAI,OAAO,QAAQ,EAAE,SAAS,aAAa,CAAC,SAAS,CAAC,KAAG,QASxE,CAAA;AAIH,eAAO,MAAM,KAAK,GAAI,MAAM,QAAQ,KAAG,QAIpC,CAAA;AAEH,eAAO,MAAM,UAAU,GAAI,MAAM,QAAQ,EAAE,UAAU,MAAM,KAAG,QAI3D,CAAA"}
|
package/dist/wire/tree.js
CHANGED
|
@@ -7,7 +7,12 @@ const recordsEqual = (left, right) => {
|
|
|
7
7
|
const leftKeys = Rec.keys(left);
|
|
8
8
|
const rightKeys = Rec.keys(right);
|
|
9
9
|
return (leftKeys.length === rightKeys.length &&
|
|
10
|
-
|
|
10
|
+
// Presence, not just value: two records of the same size whose keys differ both
|
|
11
|
+
// read `undefined` for the other's key and would compare equal. An encoded
|
|
12
|
+
// `S.optional` field keeps its key when its value is undefined, so a frame can
|
|
13
|
+
// really swap one such key for another and diff to no patches at all.
|
|
14
|
+
leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) &&
|
|
15
|
+
deepEqual({ left: left[key], right: right[key] })));
|
|
11
16
|
};
|
|
12
17
|
const deepEqual = ({ left, right }) => {
|
|
13
18
|
if (left === right) {
|
package/dist/wire/tree.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree.js","sourceRoot":"","sources":["../../src/wire/tree.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,IAAI,GAAG,EAAE,MAAM,QAAQ,CAAA;AACnE,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,MAAM,cAAc,CAAA;AA6BhD,MAAM,QAAQ,GAAG,CAAC,SAAkB,EAAwC,EAAE,CAC5E,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,KAAK,IAAI,CAAA;AAErD,MAAM,WAAW,GAAG,CAAC,IAA4B,EAAE,KAA6B,EAAW,EAAE,CAC3F,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;IAC5B,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAA;AAEnF,MAAM,YAAY,GAAG,CAAC,IAA6B,EAAE,KAA8B,EAAW,EAAE;IAC9F,MAAM,QAAQ,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC/B,MAAM,SAAS,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACjC,OAAO,CACL,QAAQ,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM;QACpC,QAAQ,CAAC,KAAK,
|
|
1
|
+
{"version":3,"file":"tree.js","sourceRoot":"","sources":["../../src/wire/tree.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "The renderer-neutral core of the reform framework — typed, headless state, events, reducers, derived values, async/remote data, and compositions built on Effect.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect",
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { expect, it } from '@effect/vitest'
|
|
2
|
+
import { Data, Effect, Layer, Option, Schema as S } from 'effect'
|
|
3
|
+
import { AsyncCalc, Engine, State, StateGroup } from '../index'
|
|
4
|
+
import { makeFlightGate, until } from '../testkit/flight.testkit'
|
|
5
|
+
import { toAsyncData } from './queryState'
|
|
6
|
+
|
|
7
|
+
// github.com/springdotnew/reform/issues/22 — a retained payload must name the
|
|
8
|
+
// input key it answers, so a consumer can never read the previous key's fields
|
|
9
|
+
// as an answer to the key it asked with.
|
|
10
|
+
|
|
11
|
+
class Boom extends Data.TaggedError('Boom')<{ readonly chatId: string }> {}
|
|
12
|
+
|
|
13
|
+
// The portal's chat-history shape in miniature: readers take a write grant off
|
|
14
|
+
// this object, so mis-attributing it hands out an affordance, not a flicker.
|
|
15
|
+
const Chat = S.Struct({ chatId: S.String, canWrite: S.Boolean })
|
|
16
|
+
|
|
17
|
+
class ChatId extends State.make('chatId', S.String) {}
|
|
18
|
+
class Inputs extends StateGroup.make(ChatId) {}
|
|
19
|
+
const chatId = StateGroup.select(Inputs, 'chatId')
|
|
20
|
+
const seededAtA = StateGroup.live(Inputs, { chatId: 'A' })
|
|
21
|
+
|
|
22
|
+
it.live('a key change does not surface the previous key as Success', () => {
|
|
23
|
+
class History extends AsyncCalc.make('HistorySuccess', {
|
|
24
|
+
inputs: [chatId],
|
|
25
|
+
output: Chat,
|
|
26
|
+
alwaysOn: true,
|
|
27
|
+
}) {}
|
|
28
|
+
const gate = makeFlightGate()
|
|
29
|
+
const HistoryLive = AsyncCalc.live(History, {
|
|
30
|
+
query: ({ chatId: id }) => gate.through(Effect.succeed({ chatId: id, canWrite: id === 'A' })),
|
|
31
|
+
})
|
|
32
|
+
const TestLayer = HistoryLive.pipe(Layer.provideMerge(seededAtA))
|
|
33
|
+
|
|
34
|
+
return Effect.gen(function* () {
|
|
35
|
+
const source = yield* chatId.store
|
|
36
|
+
const store = yield* History.store
|
|
37
|
+
yield* until(
|
|
38
|
+
() => store.get(),
|
|
39
|
+
(v) => v._tag === 'Success',
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// Park B's flight so the window where A's payload is retained stays open.
|
|
43
|
+
yield* gate.hold
|
|
44
|
+
source.set('B')
|
|
45
|
+
yield* gate.awaitEntry(2)
|
|
46
|
+
|
|
47
|
+
expect(store.get()._tag).toBe('Loading')
|
|
48
|
+
|
|
49
|
+
yield* gate.release
|
|
50
|
+
const settled = yield* until(
|
|
51
|
+
() => store.get(),
|
|
52
|
+
(v) => v._tag === 'Success' && v.refetching === false,
|
|
53
|
+
)
|
|
54
|
+
expect(settled).toMatchObject({ _tag: 'Success', value: { chatId: 'B', canWrite: false } })
|
|
55
|
+
}).pipe(Effect.provide(TestLayer))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it.live('a key change does not surface the previous key as Error', () => {
|
|
59
|
+
class History extends AsyncCalc.make('HistoryFailure', {
|
|
60
|
+
inputs: [chatId],
|
|
61
|
+
output: Chat,
|
|
62
|
+
error: S.instanceOf(Boom),
|
|
63
|
+
alwaysOn: true,
|
|
64
|
+
}) {}
|
|
65
|
+
const gate = makeFlightGate()
|
|
66
|
+
const HistoryLive = AsyncCalc.live(History, {
|
|
67
|
+
query: ({ chatId: id }) =>
|
|
68
|
+
id === 'A'
|
|
69
|
+
? Effect.fail(new Boom({ chatId: id }))
|
|
70
|
+
: gate.through(Effect.succeed({ chatId: id, canWrite: false })),
|
|
71
|
+
})
|
|
72
|
+
const TestLayer = HistoryLive.pipe(Layer.provideMerge(seededAtA))
|
|
73
|
+
|
|
74
|
+
return Effect.gen(function* () {
|
|
75
|
+
const source = yield* chatId.store
|
|
76
|
+
const store = yield* History.store
|
|
77
|
+
yield* until(
|
|
78
|
+
() => store.get(),
|
|
79
|
+
(v) => v._tag === 'Error',
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
// A's failure is not an answer about B.
|
|
83
|
+
yield* gate.hold
|
|
84
|
+
source.set('B')
|
|
85
|
+
yield* gate.awaitEntry(1)
|
|
86
|
+
|
|
87
|
+
expect(store.get()._tag).toBe('Loading')
|
|
88
|
+
}).pipe(Effect.provide(TestLayer))
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it.live('keepPreviousData opts back into showing the previous key while refetching', () => {
|
|
92
|
+
class History extends AsyncCalc.make('HistoryKept', {
|
|
93
|
+
inputs: [chatId],
|
|
94
|
+
output: Chat,
|
|
95
|
+
alwaysOn: true,
|
|
96
|
+
}) {}
|
|
97
|
+
const gate = makeFlightGate()
|
|
98
|
+
const HistoryLive = AsyncCalc.live(History, {
|
|
99
|
+
query: ({ chatId: id }) => gate.through(Effect.succeed({ chatId: id, canWrite: id === 'A' })),
|
|
100
|
+
keepPreviousData: true,
|
|
101
|
+
})
|
|
102
|
+
const TestLayer = HistoryLive.pipe(Layer.provideMerge(seededAtA))
|
|
103
|
+
|
|
104
|
+
return Effect.gen(function* () {
|
|
105
|
+
const source = yield* chatId.store
|
|
106
|
+
const store = yield* History.store
|
|
107
|
+
yield* until(
|
|
108
|
+
() => store.get(),
|
|
109
|
+
(v) => v._tag === 'Success',
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
yield* gate.hold
|
|
113
|
+
source.set('B')
|
|
114
|
+
yield* gate.awaitEntry(2)
|
|
115
|
+
|
|
116
|
+
expect(store.get()).toMatchObject({
|
|
117
|
+
_tag: 'Success',
|
|
118
|
+
value: { chatId: 'A' },
|
|
119
|
+
refetching: true,
|
|
120
|
+
})
|
|
121
|
+
}).pipe(Effect.provide(TestLayer))
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it.live('a same-key refetch still keeps the settled value under refetching', () => {
|
|
125
|
+
class HistorySameKey extends AsyncCalc.make('HistorySameKey', {
|
|
126
|
+
inputs: [chatId],
|
|
127
|
+
output: Chat,
|
|
128
|
+
alwaysOn: true,
|
|
129
|
+
}) {}
|
|
130
|
+
const gate = makeFlightGate()
|
|
131
|
+
const HistoryLive = AsyncCalc.live(HistorySameKey, {
|
|
132
|
+
query: ({ chatId: id }) => gate.through(Effect.succeed({ chatId: id, canWrite: true })),
|
|
133
|
+
})
|
|
134
|
+
const TestLayer = HistoryLive.pipe(Layer.provideMerge(seededAtA), Layer.provideMerge(Engine))
|
|
135
|
+
|
|
136
|
+
return Effect.gen(function* () {
|
|
137
|
+
const store = yield* HistorySameKey.store
|
|
138
|
+
yield* until(
|
|
139
|
+
() => store.get(),
|
|
140
|
+
(v) => v._tag === 'Success',
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// Same key: the payload still answers the question being asked.
|
|
144
|
+
yield* gate.hold
|
|
145
|
+
yield* AsyncCalc.refetch(HistorySameKey)
|
|
146
|
+
yield* gate.awaitEntry(2)
|
|
147
|
+
|
|
148
|
+
expect(store.get()).toMatchObject({
|
|
149
|
+
_tag: 'Success',
|
|
150
|
+
value: { chatId: 'A' },
|
|
151
|
+
refetching: true,
|
|
152
|
+
})
|
|
153
|
+
}).pipe(Effect.provide(TestLayer))
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('toAsyncData reports data from a superseded key as loading', () => {
|
|
157
|
+
const retained = {
|
|
158
|
+
data: Option.some({ chatId: 'A', canWrite: true }),
|
|
159
|
+
error: Option.none(),
|
|
160
|
+
isFetching: true,
|
|
161
|
+
isStale: false,
|
|
162
|
+
isPreviousKey: true,
|
|
163
|
+
keepPreviousData: false,
|
|
164
|
+
}
|
|
165
|
+
expect(toAsyncData(retained, false)._tag).toBe('Loading')
|
|
166
|
+
// The opt-in travels on the state, so every reading of one query agrees.
|
|
167
|
+
expect(toAsyncData({ ...retained, keepPreviousData: true }, false)).toMatchObject({
|
|
168
|
+
_tag: 'Success',
|
|
169
|
+
refetching: true,
|
|
170
|
+
})
|
|
171
|
+
// …and it keeps a payload, not a verdict: with nothing retained there is nothing
|
|
172
|
+
// to opt into, so a previous key's error stays withheld.
|
|
173
|
+
const failedOnly = {
|
|
174
|
+
data: Option.none<{ readonly chatId: string }>(),
|
|
175
|
+
error: Option.some('boom'),
|
|
176
|
+
isFetching: true,
|
|
177
|
+
isStale: false,
|
|
178
|
+
isPreviousKey: true,
|
|
179
|
+
keepPreviousData: true,
|
|
180
|
+
}
|
|
181
|
+
expect(toAsyncData(failedOnly, false)._tag).toBe('Loading')
|
|
182
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { expect, it } from '@effect/vitest'
|
|
2
|
+
import { Effect, Layer, Schema as S } from 'effect'
|
|
3
|
+
import { AsyncCalc, Engine, State, StateGroup } from '../index'
|
|
4
|
+
import { Queries } from '../runtime/queries'
|
|
5
|
+
import { until } from '../testkit/flight.testkit'
|
|
6
|
+
|
|
7
|
+
// `AsyncCalc.invalidate`/`refetch` look the query up by `calc.name`. A class
|
|
8
|
+
// declaration defines its own `name` own-property, so `class ChatHistory extends
|
|
9
|
+
// AsyncCalc.make('chat/history', …)` reports 'ChatHistory' and shadows the
|
|
10
|
+
// registered 'chat/history'. The registry lookup misses and the call is silently
|
|
11
|
+
// swallowed by `?.` — the very naming the framework asks for (namespace the name
|
|
12
|
+
// per feature) is what breaks it.
|
|
13
|
+
|
|
14
|
+
class Seed extends State.make('seed', S.Number) {}
|
|
15
|
+
class Inputs extends StateGroup.make(Seed) {}
|
|
16
|
+
const seed = StateGroup.select(Inputs, 'seed')
|
|
17
|
+
|
|
18
|
+
const isStale = (name: string): Effect.Effect<boolean, never, Queries> =>
|
|
19
|
+
Effect.map(Queries, (queries) => queries.byName.get(name)?.snapshot().isStale ?? false)
|
|
20
|
+
|
|
21
|
+
it.live('refetch reaches a calc whose class binding differs from its registered name', () => {
|
|
22
|
+
const runs = { n: 0 }
|
|
23
|
+
class ChatHistory extends AsyncCalc.make('chat/history', {
|
|
24
|
+
inputs: [seed],
|
|
25
|
+
output: S.Number,
|
|
26
|
+
alwaysOn: true,
|
|
27
|
+
}) {}
|
|
28
|
+
const Live = AsyncCalc.live(ChatHistory, {
|
|
29
|
+
query: ({ seed: value }) =>
|
|
30
|
+
Effect.sync(() => {
|
|
31
|
+
runs.n += 1
|
|
32
|
+
return value
|
|
33
|
+
}),
|
|
34
|
+
})
|
|
35
|
+
const TestLayer = Live.pipe(
|
|
36
|
+
Layer.provideMerge(StateGroup.live(Inputs, { seed: 1 })),
|
|
37
|
+
Layer.provideMerge(Engine),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return Effect.gen(function* () {
|
|
41
|
+
const store = yield* ChatHistory.store
|
|
42
|
+
yield* until(
|
|
43
|
+
() => store.get(),
|
|
44
|
+
(v) => v._tag === 'Success',
|
|
45
|
+
)
|
|
46
|
+
expect(runs.n).toBe(1)
|
|
47
|
+
|
|
48
|
+
yield* AsyncCalc.refetch(ChatHistory)
|
|
49
|
+
yield* until(
|
|
50
|
+
() => runs.n,
|
|
51
|
+
(n) => n > 1,
|
|
52
|
+
20,
|
|
53
|
+
)
|
|
54
|
+
expect(runs.n).toBe(2)
|
|
55
|
+
}).pipe(Effect.provide(TestLayer))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it.live('invalidate reaches a calc whose class binding differs from its registered name', () => {
|
|
59
|
+
class ChatBranches extends AsyncCalc.make('chat/branches', {
|
|
60
|
+
inputs: [seed],
|
|
61
|
+
output: S.Number,
|
|
62
|
+
alwaysOn: true,
|
|
63
|
+
}) {}
|
|
64
|
+
const Live = AsyncCalc.live(ChatBranches, {
|
|
65
|
+
query: ({ seed: value }) => Effect.succeed(value),
|
|
66
|
+
})
|
|
67
|
+
const TestLayer = Live.pipe(
|
|
68
|
+
Layer.provideMerge(StateGroup.live(Inputs, { seed: 1 })),
|
|
69
|
+
Layer.provideMerge(Engine),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return Effect.gen(function* () {
|
|
73
|
+
const store = yield* ChatBranches.store
|
|
74
|
+
yield* until(
|
|
75
|
+
() => store.get(),
|
|
76
|
+
(v) => v._tag === 'Success' && v.refetching === false,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
yield* AsyncCalc.invalidate(ChatBranches)
|
|
80
|
+
expect(yield* isStale('chat/branches')).toBe(true)
|
|
81
|
+
}).pipe(Effect.provide(TestLayer))
|
|
82
|
+
})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { expect, it } from '@effect/vitest'
|
|
2
|
+
import { Data, Effect, Layer, Schema as S } from 'effect'
|
|
3
|
+
import { AsyncCalc, Engine, State, StateGroup } from '../index'
|
|
4
|
+
import { makeFlightGate, until } from '../testkit/flight.testkit'
|
|
5
|
+
|
|
6
|
+
// Class bindings match their registered names on purpose: `AsyncCalc.invalidate` /
|
|
7
|
+
// `refetch` resolve a query by `calc.name`, and a class declaration installs its
|
|
8
|
+
// own `name` own-property that shadows the static the framework wrote. That
|
|
9
|
+
// divergence is a separate finding — these tests keep the two aligned so they
|
|
10
|
+
// isolate the driver, not the lookup.
|
|
11
|
+
|
|
12
|
+
class Torn extends Data.TaggedError('Torn')<{ readonly reason: string }> {}
|
|
13
|
+
|
|
14
|
+
// A negative wait: long enough that the missing transition would have landed,
|
|
15
|
+
// short enough that the assertion — not the timeout — reports the failure.
|
|
16
|
+
const settleRounds = 40
|
|
17
|
+
|
|
18
|
+
// `commitSuccess` asks only `disabledNow()` — is the query enabled *now* — before
|
|
19
|
+
// writing. It never asks whether the request it is committing survived the gate.
|
|
20
|
+
// A run that was in flight when `disabled` flipped is dropped from the queue's
|
|
21
|
+
// point of view and its state wiped to Idle, yet it still lands as the answer once
|
|
22
|
+
// the query is re-enabled under a different key. `coalesce: 'trailing'` makes this
|
|
23
|
+
// deterministic: that consumer never cancels the run it is holding.
|
|
24
|
+
it.live('a flight disabled away commits over the query the gate reset', () => {
|
|
25
|
+
class Count extends State.make('supersedeDisableCount', S.Number) {}
|
|
26
|
+
class Inputs extends StateGroup.make(Count) {}
|
|
27
|
+
class SupersedeDisableResurrect extends AsyncCalc.make('SupersedeDisableResurrect', {
|
|
28
|
+
inputs: [StateGroup.select(Inputs, 'supersedeDisableCount')],
|
|
29
|
+
output: S.Number,
|
|
30
|
+
}) {}
|
|
31
|
+
const firstGate = makeFlightGate()
|
|
32
|
+
const laterGate = makeFlightGate()
|
|
33
|
+
// Armed before the layer builds, so the very first flight parks at the gate.
|
|
34
|
+
Effect.runSync(firstGate.hold)
|
|
35
|
+
Effect.runSync(laterGate.hold)
|
|
36
|
+
const TestLayer = AsyncCalc.live(SupersedeDisableResurrect, {
|
|
37
|
+
query: ({ supersedeDisableCount: count }) =>
|
|
38
|
+
count === 1
|
|
39
|
+
? firstGate.through(Effect.succeed(count * 2))
|
|
40
|
+
: laterGate.through(Effect.succeed(count * 2)),
|
|
41
|
+
disabled: ({ supersedeDisableCount: count }) => count < 0,
|
|
42
|
+
coalesce: 'trailing',
|
|
43
|
+
}).pipe(Layer.provideMerge(StateGroup.live(Inputs, { supersedeDisableCount: 1 })))
|
|
44
|
+
|
|
45
|
+
return Effect.gen(function* () {
|
|
46
|
+
const source = yield* StateGroup.select(Inputs, 'supersedeDisableCount').store
|
|
47
|
+
const store = yield* SupersedeDisableResurrect.store
|
|
48
|
+
yield* firstGate.awaitEntry(1)
|
|
49
|
+
|
|
50
|
+
// count=1's answer is held at the gate; the disable then wipes the query back
|
|
51
|
+
// to Idle and drops the key it was asked with.
|
|
52
|
+
source.set(-1)
|
|
53
|
+
yield* until(
|
|
54
|
+
() => store.get(),
|
|
55
|
+
(v) => v._tag === 'Idle',
|
|
56
|
+
)
|
|
57
|
+
expect(store.get()._tag).toBe('Idle')
|
|
58
|
+
|
|
59
|
+
// Re-enabled at a different key. The trailing consumer runs count=1 to
|
|
60
|
+
// completion first, then parks count=5's flight, so what the store reports in
|
|
61
|
+
// between is a stable state rather than a window to catch.
|
|
62
|
+
source.set(5)
|
|
63
|
+
yield* firstGate.release
|
|
64
|
+
yield* laterGate.awaitEntry(1)
|
|
65
|
+
|
|
66
|
+
// count=1's answer belongs to a key the gate already discarded: it is not an
|
|
67
|
+
// answer about count=5, and it is not permission to leave Idle behind.
|
|
68
|
+
expect(store.get()).toMatchObject({ _tag: 'Loading' })
|
|
69
|
+
}).pipe(Effect.provide(TestLayer))
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// The same hole on the failure side: `settleError` sits behind the identical
|
|
73
|
+
// `!disabledNow()` guard, so a disabled-away run's failure is published as the
|
|
74
|
+
// current key's error.
|
|
75
|
+
it.live('a failure disabled away commits over the query the gate reset', () => {
|
|
76
|
+
class Count extends State.make('supersedeDisableErrCount', S.Number) {}
|
|
77
|
+
class Inputs extends StateGroup.make(Count) {}
|
|
78
|
+
class SupersedeDisableResurrectError extends AsyncCalc.make('SupersedeDisableResurrectError', {
|
|
79
|
+
inputs: [StateGroup.select(Inputs, 'supersedeDisableErrCount')],
|
|
80
|
+
output: S.Number,
|
|
81
|
+
error: S.String,
|
|
82
|
+
}) {}
|
|
83
|
+
const firstGate = makeFlightGate()
|
|
84
|
+
const laterGate = makeFlightGate()
|
|
85
|
+
Effect.runSync(firstGate.hold)
|
|
86
|
+
Effect.runSync(laterGate.hold)
|
|
87
|
+
const TestLayer = AsyncCalc.live(SupersedeDisableResurrectError, {
|
|
88
|
+
query: ({ supersedeDisableErrCount: count }): Effect.Effect<number, string> =>
|
|
89
|
+
count === 1
|
|
90
|
+
? // Park first, then fail: the gate only wraps a success.
|
|
91
|
+
firstGate.through(Effect.void).pipe(Effect.zipRight(Effect.fail('boom-for-1')))
|
|
92
|
+
: laterGate.through(Effect.succeed(count * 2)),
|
|
93
|
+
disabled: ({ supersedeDisableErrCount: count }) => count < 0,
|
|
94
|
+
coalesce: 'trailing',
|
|
95
|
+
}).pipe(Layer.provideMerge(StateGroup.live(Inputs, { supersedeDisableErrCount: 1 })))
|
|
96
|
+
|
|
97
|
+
return Effect.gen(function* () {
|
|
98
|
+
const source = yield* StateGroup.select(Inputs, 'supersedeDisableErrCount').store
|
|
99
|
+
const store = yield* SupersedeDisableResurrectError.store
|
|
100
|
+
yield* firstGate.awaitEntry(1)
|
|
101
|
+
|
|
102
|
+
source.set(-1)
|
|
103
|
+
yield* until(
|
|
104
|
+
() => store.get(),
|
|
105
|
+
(v) => v._tag === 'Idle',
|
|
106
|
+
)
|
|
107
|
+
expect(store.get()._tag).toBe('Idle')
|
|
108
|
+
|
|
109
|
+
source.set(5)
|
|
110
|
+
yield* firstGate.release
|
|
111
|
+
yield* laterGate.awaitEntry(1)
|
|
112
|
+
|
|
113
|
+
// count=1's failure is not a failure of count=5.
|
|
114
|
+
expect(store.get()).toMatchObject({ _tag: 'Loading' })
|
|
115
|
+
}).pipe(Effect.provide(TestLayer))
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// `runQuery` folds the whole cause: a typed failure reaches `settleError`, but a
|
|
119
|
+
// defect is only logged and then swallowed as `Option.none()`. Nothing ever clears
|
|
120
|
+
// the `isFetching` flag `enqueue` raised, so the run that died stays advertised as
|
|
121
|
+
// in flight for the rest of the query's life.
|
|
122
|
+
it.live('a query that dies leaves the view refetching forever', () => {
|
|
123
|
+
const runs = { n: 0 }
|
|
124
|
+
class RegressDefectQ extends AsyncCalc.make('RegressDefectQ', {
|
|
125
|
+
inputs: [],
|
|
126
|
+
output: S.Number,
|
|
127
|
+
alwaysOn: true,
|
|
128
|
+
}) {}
|
|
129
|
+
const TestLayer = AsyncCalc.live(RegressDefectQ, {
|
|
130
|
+
query: (): Effect.Effect<number> =>
|
|
131
|
+
Effect.suspend(() => {
|
|
132
|
+
runs.n += 1
|
|
133
|
+
// A defect, not a typed failure: an exception escaping the query body.
|
|
134
|
+
return runs.n === 1 ? Effect.succeed(1) : Effect.die(new Torn({ reason: 'kaboom' }))
|
|
135
|
+
}),
|
|
136
|
+
}).pipe(Layer.provideMerge(Engine))
|
|
137
|
+
|
|
138
|
+
return Effect.gen(function* () {
|
|
139
|
+
const store = yield* RegressDefectQ.store
|
|
140
|
+
yield* until(
|
|
141
|
+
() => store.get(),
|
|
142
|
+
(v) => v._tag === 'Success' && v.refetching === false,
|
|
143
|
+
)
|
|
144
|
+
expect(store.get()).toMatchObject({ _tag: 'Success', value: 1, refetching: false })
|
|
145
|
+
|
|
146
|
+
yield* AsyncCalc.refetch(RegressDefectQ)
|
|
147
|
+
yield* until(
|
|
148
|
+
() => runs.n,
|
|
149
|
+
(n) => n > 1,
|
|
150
|
+
)
|
|
151
|
+
expect(runs.n).toBe(2)
|
|
152
|
+
|
|
153
|
+
// The run is over — it died. Nothing is in flight and nothing is queued, so
|
|
154
|
+
// the view must stop advertising a refetch.
|
|
155
|
+
yield* until(
|
|
156
|
+
() => store.get(),
|
|
157
|
+
(v) => v._tag === 'Success' && v.refetching === false,
|
|
158
|
+
settleRounds,
|
|
159
|
+
)
|
|
160
|
+
expect(store.get()).toMatchObject({ _tag: 'Success', value: 1, refetching: false })
|
|
161
|
+
}).pipe(Effect.provide(TestLayer))
|
|
162
|
+
})
|
|
@@ -104,6 +104,10 @@ it.live('a dependency change re-fetches, keeping the last value with refetching:
|
|
|
104
104
|
const gate = makeFlightGate()
|
|
105
105
|
const DoubledLive = AsyncCalc.live(Doubled, {
|
|
106
106
|
query: ({ count }) => gate.through(Effect.succeed(count * 2)),
|
|
107
|
+
// Opt-in: across an input-key change the previous key's payload is retained
|
|
108
|
+
// only under `keepPreviousData`. The default reads as Loading — see
|
|
109
|
+
// asyncCalc-key-attribution.test.ts.
|
|
110
|
+
keepPreviousData: true,
|
|
107
111
|
})
|
|
108
112
|
const TestLayer = DoubledLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { count: 5 })))
|
|
109
113
|
|
|
@@ -245,7 +249,10 @@ it.live('invalidateBy: an input change outside the key does not re-fetch', () =>
|
|
|
245
249
|
// Holding the gate means any flight the status churn triggered would still
|
|
246
250
|
// be marked fetching here, and could not have settled back out of view.
|
|
247
251
|
yield* gate.hold
|
|
248
|
-
yield* afterNotify(
|
|
252
|
+
yield* afterNotify(
|
|
253
|
+
status,
|
|
254
|
+
Effect.sync(() => status.set('b')),
|
|
255
|
+
)
|
|
249
256
|
expect(store.get()).toMatchObject({ _tag: 'Success', refetching: false })
|
|
250
257
|
expect(runs.n).toBe(1)
|
|
251
258
|
yield* gate.release
|
|
@@ -338,8 +345,14 @@ it.live(
|
|
|
338
345
|
yield* gate.hold
|
|
339
346
|
source.set(2)
|
|
340
347
|
yield* gate.awaitEntry(2)
|
|
341
|
-
yield* afterNotify(
|
|
342
|
-
|
|
348
|
+
yield* afterNotify(
|
|
349
|
+
source,
|
|
350
|
+
Effect.sync(() => source.set(3)),
|
|
351
|
+
)
|
|
352
|
+
yield* afterNotify(
|
|
353
|
+
source,
|
|
354
|
+
Effect.sync(() => source.set(4)),
|
|
355
|
+
)
|
|
343
356
|
yield* gate.release
|
|
344
357
|
const settled = yield* until(
|
|
345
358
|
() => store.get(),
|
|
@@ -371,6 +384,9 @@ it.live(
|
|
|
371
384
|
? trailingGate.through(Effect.succeed(count * 2))
|
|
372
385
|
: firstGate.through(Effect.succeed(count * 2)),
|
|
373
386
|
coalesce: 'trailing',
|
|
387
|
+
// The stale settle belongs to a superseded key, so it is visible only under
|
|
388
|
+
// `keepPreviousData`; by default it reads as Loading.
|
|
389
|
+
keepPreviousData: true,
|
|
374
390
|
})
|
|
375
391
|
const TestLayer = DoubledLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { count: 1 })))
|
|
376
392
|
|
|
@@ -386,7 +402,10 @@ it.live(
|
|
|
386
402
|
yield* trailingGate.hold
|
|
387
403
|
source.set(2)
|
|
388
404
|
yield* firstGate.awaitEntry(2)
|
|
389
|
-
yield* afterNotify(
|
|
405
|
+
yield* afterNotify(
|
|
406
|
+
source,
|
|
407
|
+
Effect.sync(() => source.set(3)),
|
|
408
|
+
)
|
|
390
409
|
|
|
391
410
|
yield* firstGate.release
|
|
392
411
|
yield* trailingGate.awaitEntry(1)
|
|
@@ -437,8 +456,14 @@ it.live('coalesce trailing: a disable while a request waits in the slot skips th
|
|
|
437
456
|
yield* gate.hold
|
|
438
457
|
source.set(2)
|
|
439
458
|
yield* gate.awaitEntry(2)
|
|
440
|
-
yield* afterNotify(
|
|
441
|
-
|
|
459
|
+
yield* afterNotify(
|
|
460
|
+
source,
|
|
461
|
+
Effect.sync(() => source.set(3)),
|
|
462
|
+
)
|
|
463
|
+
yield* afterNotify(
|
|
464
|
+
source,
|
|
465
|
+
Effect.sync(() => source.set(-1)),
|
|
466
|
+
)
|
|
442
467
|
yield* gate.release
|
|
443
468
|
expect(store.get()._tag).toBe('Idle')
|
|
444
469
|
expect(completed).toEqual([1, 2])
|
|
@@ -545,7 +570,10 @@ it.live(
|
|
|
545
570
|
expect(runs.n).toBe(1)
|
|
546
571
|
|
|
547
572
|
yield* gate.hold
|
|
548
|
-
yield* afterNotify(
|
|
573
|
+
yield* afterNotify(
|
|
574
|
+
status,
|
|
575
|
+
Effect.sync(() => status.set('b')),
|
|
576
|
+
)
|
|
549
577
|
expect(store.get()).toMatchObject({ _tag: 'Success', refetching: false })
|
|
550
578
|
expect(runs.n).toBe(1)
|
|
551
579
|
yield* gate.release
|
package/src/calc/asyncCalc.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Context, Effect, Layer, Match, Option, Schema } from 'effect'
|
|
2
2
|
import { yieldableClass } from '../definition/definition'
|
|
3
3
|
import { type AnyEvent } from '../event/event'
|
|
4
|
+
import { resolvePersistSlot } from '../internal/queryDriverPersistWarn'
|
|
4
5
|
import { type InputStores } from '../internal/sources'
|
|
5
6
|
import {
|
|
6
7
|
bumpRevision,
|
|
@@ -15,7 +16,7 @@ import { type Store } from '../internal/store'
|
|
|
15
16
|
import { readTracked } from '../internal/track'
|
|
16
17
|
import * as Reducer from '../reducer/reducer'
|
|
17
18
|
import { Reducers } from '../runtime/loop'
|
|
18
|
-
import { Queries } from '../runtime/queries'
|
|
19
|
+
import { Queries, type QueryHandle } from '../runtime/queries'
|
|
19
20
|
import * as State from '../state/state'
|
|
20
21
|
import { type AnySource, type SourceCapture } from '../state/token'
|
|
21
22
|
import { type AsyncData, narrowStore } from './asyncData'
|
|
@@ -44,7 +45,6 @@ export type {
|
|
|
44
45
|
DefinedAsyncCalcClass,
|
|
45
46
|
} from './asyncCalcTypes'
|
|
46
47
|
|
|
47
|
-
|
|
48
48
|
export function make<
|
|
49
49
|
const N extends string,
|
|
50
50
|
const Inputs extends ReadonlyArray<AnySource>,
|
|
@@ -154,10 +154,12 @@ export function live<
|
|
|
154
154
|
const cfg: AsyncCalcLiveView<Inputs, A, E, R> = config
|
|
155
155
|
|
|
156
156
|
const persistOption = cfg.persist
|
|
157
|
-
const persistKey =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
const persistKey = yield* resolvePersistSlot({
|
|
158
|
+
label: 'AsyncCalc',
|
|
159
|
+
name: calc.manifest.name,
|
|
160
|
+
inputs: calc.inputs.length,
|
|
161
|
+
option: persistOption,
|
|
162
|
+
})
|
|
161
163
|
const persistCodec: QueryCodec<A> = {
|
|
162
164
|
encode: Schema.encode(calc.manifest.output),
|
|
163
165
|
decode: Schema.decodeUnknown(calc.manifest.output),
|
|
@@ -188,6 +190,7 @@ export function live<
|
|
|
188
190
|
disabled: cfg.disabled,
|
|
189
191
|
coalesce: cfg.coalesce,
|
|
190
192
|
reuse: cfg.reuse,
|
|
193
|
+
keepPreviousData: cfg.keepPreviousData,
|
|
191
194
|
persist,
|
|
192
195
|
extraKey,
|
|
193
196
|
})
|
|
@@ -208,14 +211,34 @@ export function live<
|
|
|
208
211
|
)
|
|
209
212
|
}
|
|
210
213
|
|
|
214
|
+
// Resolved through `manifest`, never through `calc.name`: a `class X extends
|
|
215
|
+
// AsyncCalc.make('some/name')` declaration defines its own `name` own-property,
|
|
216
|
+
// which shadows the registered one — so a name-keyed lookup silently misses for
|
|
217
|
+
// exactly the namespaced names the framework asks authors to use.
|
|
211
218
|
export interface NamedCalc {
|
|
212
|
-
readonly name: string
|
|
219
|
+
readonly manifest: { readonly name: string }
|
|
213
220
|
}
|
|
214
221
|
|
|
215
|
-
|
|
222
|
+
const onHandle = (
|
|
223
|
+
calc: NamedCalc,
|
|
224
|
+
verb: string,
|
|
225
|
+
act: (handle: QueryHandle) => void,
|
|
226
|
+
): Effect.Effect<void, never, Queries> =>
|
|
216
227
|
Effect.flatMap(Queries, (queries) =>
|
|
217
|
-
Effect.
|
|
228
|
+
Effect.suspend(() =>
|
|
229
|
+
Option.match(Option.fromNullable(queries.byName.get(calc.manifest.name)), {
|
|
230
|
+
// Swallowing this is how a dead invalidation goes unnoticed for a release.
|
|
231
|
+
onNone: () =>
|
|
232
|
+
Effect.logWarning(
|
|
233
|
+
`reform: ${verb} skipped — no live query named '${calc.manifest.name}'`,
|
|
234
|
+
),
|
|
235
|
+
onSome: (handle) => Effect.sync(() => act(handle)),
|
|
236
|
+
}),
|
|
237
|
+
),
|
|
218
238
|
)
|
|
219
239
|
|
|
240
|
+
export const invalidate = (calc: NamedCalc): Effect.Effect<void, never, Queries> =>
|
|
241
|
+
onHandle(calc, 'invalidate', (handle) => handle.invalidate())
|
|
242
|
+
|
|
220
243
|
export const refetch = (calc: NamedCalc): Effect.Effect<void, never, Queries> =>
|
|
221
|
-
|
|
244
|
+
onHandle(calc, 'refetch', (handle) => handle.refetch())
|