@playfast/reform 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/README.md +15 -6
  2. package/dist/calc/asyncCalc.d.ts +3 -1
  3. package/dist/calc/asyncCalc.d.ts.map +1 -1
  4. package/dist/calc/asyncCalc.js +15 -5
  5. package/dist/calc/asyncCalc.js.map +1 -1
  6. package/dist/calc/asyncCalcTypes.d.ts +2 -0
  7. package/dist/calc/asyncCalcTypes.d.ts.map +1 -1
  8. package/dist/calc/calcFamily.d.ts.map +1 -1
  9. package/dist/calc/calcFamily.js +77 -34
  10. package/dist/calc/calcFamily.js.map +1 -1
  11. package/dist/calc/queryState.d.ts +3 -1
  12. package/dist/calc/queryState.d.ts.map +1 -1
  13. package/dist/calc/queryState.js +31 -9
  14. package/dist/calc/queryState.js.map +1 -1
  15. package/dist/channel/channel.d.ts.map +1 -1
  16. package/dist/channel/channel.js +6 -1
  17. package/dist/channel/channel.js.map +1 -1
  18. package/dist/compose/composition.d.ts.map +1 -1
  19. package/dist/compose/composition.js +7 -1
  20. package/dist/compose/composition.js.map +1 -1
  21. package/dist/compose/structure.d.ts.map +1 -1
  22. package/dist/compose/structure.js +33 -8
  23. package/dist/compose/structure.js.map +1 -1
  24. package/dist/compose/ui.d.ts.map +1 -1
  25. package/dist/compose/ui.js +16 -5
  26. package/dist/compose/ui.js.map +1 -1
  27. package/dist/event/event.d.ts +1 -1
  28. package/dist/event/event.d.ts.map +1 -1
  29. package/dist/event/event.js +26 -4
  30. package/dist/event/event.js.map +1 -1
  31. package/dist/internal/familyKeys.d.ts +16 -0
  32. package/dist/internal/familyKeys.d.ts.map +1 -0
  33. package/dist/internal/familyKeys.js +35 -0
  34. package/dist/internal/familyKeys.js.map +1 -0
  35. package/dist/internal/platform.d.ts +43 -0
  36. package/dist/internal/platform.d.ts.map +1 -0
  37. package/dist/internal/platform.js +68 -0
  38. package/dist/internal/platform.js.map +1 -0
  39. package/dist/internal/queryDriver.d.ts.map +1 -1
  40. package/dist/internal/queryDriver.js +88 -64
  41. package/dist/internal/queryDriver.js.map +1 -1
  42. package/dist/internal/queryDriverConsume.d.ts +9 -0
  43. package/dist/internal/queryDriverConsume.d.ts.map +1 -0
  44. package/dist/internal/queryDriverConsume.js +16 -0
  45. package/dist/internal/queryDriverConsume.js.map +1 -0
  46. package/dist/internal/queryDriverHydrate.d.ts.map +1 -1
  47. package/dist/internal/queryDriverHydrate.js +9 -3
  48. package/dist/internal/queryDriverHydrate.js.map +1 -1
  49. package/dist/internal/queryDriverPersist.d.ts +16 -0
  50. package/dist/internal/queryDriverPersist.d.ts.map +1 -0
  51. package/dist/internal/queryDriverPersist.js +15 -0
  52. package/dist/internal/queryDriverPersist.js.map +1 -0
  53. package/dist/internal/queryDriverPersistWarn.d.ts +12 -0
  54. package/dist/internal/queryDriverPersistWarn.d.ts.map +1 -0
  55. package/dist/internal/queryDriverPersistWarn.js +31 -0
  56. package/dist/internal/queryDriverPersistWarn.js.map +1 -0
  57. package/dist/internal/queryDriverSettle.d.ts +23 -0
  58. package/dist/internal/queryDriverSettle.d.ts.map +1 -0
  59. package/dist/internal/queryDriverSettle.js +52 -0
  60. package/dist/internal/queryDriverSettle.js.map +1 -0
  61. package/dist/internal/queryDriverStore.d.ts +2 -28
  62. package/dist/internal/queryDriverStore.d.ts.map +1 -1
  63. package/dist/internal/queryDriverStore.js +5 -29
  64. package/dist/internal/queryDriverStore.js.map +1 -1
  65. package/dist/internal/queryDriverTypes.d.ts +3 -0
  66. package/dist/internal/queryDriverTypes.d.ts.map +1 -1
  67. package/dist/internal/scheduler.d.ts +1 -0
  68. package/dist/internal/scheduler.d.ts.map +1 -1
  69. package/dist/internal/scheduler.js +69 -6
  70. package/dist/internal/scheduler.js.map +1 -1
  71. package/dist/internal.d.ts +2 -0
  72. package/dist/internal.d.ts.map +1 -1
  73. package/dist/internal.js +4 -0
  74. package/dist/internal.js.map +1 -1
  75. package/dist/reducer/reducer.js +3 -3
  76. package/dist/reducer/reducer.js.map +1 -1
  77. package/dist/remote/pendingQueue.d.ts +4 -2
  78. package/dist/remote/pendingQueue.d.ts.map +1 -1
  79. package/dist/remote/pendingQueue.js +20 -4
  80. package/dist/remote/pendingQueue.js.map +1 -1
  81. package/dist/remote/remoteState.d.ts.map +1 -1
  82. package/dist/remote/remoteState.js +27 -14
  83. package/dist/remote/remoteState.js.map +1 -1
  84. package/dist/remote/remoteStateSend.d.ts +3 -1
  85. package/dist/remote/remoteStateSend.d.ts.map +1 -1
  86. package/dist/remote/remoteStateSend.js +19 -3
  87. package/dist/remote/remoteStateSend.js.map +1 -1
  88. package/dist/remote/remoteStateTypes.d.ts +3 -0
  89. package/dist/remote/remoteStateTypes.d.ts.map +1 -1
  90. package/dist/runtime/bus.d.ts +5 -0
  91. package/dist/runtime/bus.d.ts.map +1 -1
  92. package/dist/runtime/bus.js +1 -1
  93. package/dist/runtime/bus.js.map +1 -1
  94. package/dist/runtime/capturedAppRuntime.d.ts.map +1 -1
  95. package/dist/runtime/capturedAppRuntime.js +37 -51
  96. package/dist/runtime/capturedAppRuntime.js.map +1 -1
  97. package/dist/runtime/eventBudget.d.ts.map +1 -1
  98. package/dist/runtime/eventBudget.js +2 -1
  99. package/dist/runtime/eventBudget.js.map +1 -1
  100. package/dist/runtime/featureMount.d.ts +34 -2
  101. package/dist/runtime/featureMount.d.ts.map +1 -1
  102. package/dist/runtime/featureMount.js +66 -2
  103. package/dist/runtime/featureMount.js.map +1 -1
  104. package/dist/runtime/loop.d.ts +12 -0
  105. package/dist/runtime/loop.d.ts.map +1 -1
  106. package/dist/runtime/loop.js +8 -5
  107. package/dist/runtime/loop.js.map +1 -1
  108. package/dist/state/state.d.ts +2 -2
  109. package/dist/state/state.d.ts.map +1 -1
  110. package/dist/state/state.js +2 -2
  111. package/dist/state/state.js.map +1 -1
  112. package/dist/state/stateFamily.d.ts +2 -2
  113. package/dist/state/stateFamily.d.ts.map +1 -1
  114. package/dist/state/stateFamily.js +80 -32
  115. package/dist/state/stateFamily.js.map +1 -1
  116. package/dist/state/stateGroup.d.ts +5 -5
  117. package/dist/state/stateGroup.d.ts.map +1 -1
  118. package/dist/state/stateGroup.js +1 -1
  119. package/dist/state/stateGroup.js.map +1 -1
  120. package/dist/testkit/flight.testkit.d.ts.map +1 -1
  121. package/dist/testkit/flight.testkit.js +2 -1
  122. package/dist/testkit/flight.testkit.js.map +1 -1
  123. package/dist/ui/trigger.d.ts +3 -0
  124. package/dist/ui/trigger.d.ts.map +1 -1
  125. package/dist/ui/trigger.js +14 -1
  126. package/dist/ui/trigger.js.map +1 -1
  127. package/dist/wire/tree.d.ts.map +1 -1
  128. package/dist/wire/tree.js +6 -1
  129. package/dist/wire/tree.js.map +1 -1
  130. package/package.json +1 -1
  131. package/src/calc/asyncCalc-key-attribution.test.ts +182 -0
  132. package/src/calc/asyncCalc-registry-name.test.ts +82 -0
  133. package/src/calc/asyncCalc.superseded-commit.test.ts +162 -0
  134. package/src/calc/asyncCalc.test.ts +35 -7
  135. package/src/calc/asyncCalc.ts +33 -10
  136. package/src/calc/asyncCalcTypes.ts +6 -0
  137. package/src/calc/calcFamily.ts +81 -34
  138. package/src/calc/family-identity.test.ts +231 -0
  139. package/src/calc/queryState.ts +36 -5
  140. package/src/channel/channel-throttle-frames.test.ts +163 -0
  141. package/src/channel/channel.ts +5 -0
  142. package/src/compose/compose-identity.test.ts +175 -0
  143. package/src/compose/composition.ts +8 -4
  144. package/src/compose/structure.ts +45 -13
  145. package/src/compose/ui.ts +22 -5
  146. package/src/event/event.ts +34 -5
  147. package/src/event/syncTrigger.test.ts +66 -0
  148. package/src/internal/familyKeys.ts +41 -0
  149. package/src/internal/platform.test.ts +55 -0
  150. package/src/internal/platform.ts +72 -0
  151. package/src/internal/queryDriver-defect-attribution.test.ts +107 -0
  152. package/src/internal/queryDriver-persist-key.test.ts +118 -0
  153. package/src/internal/queryDriver.persistence-staleness.test.ts +209 -0
  154. package/src/internal/queryDriver.ts +92 -80
  155. package/src/internal/queryDriverConsume.ts +42 -0
  156. package/src/internal/queryDriverHydrate.ts +9 -3
  157. package/src/internal/queryDriverPersist.ts +42 -0
  158. package/src/internal/queryDriverPersistWarn.ts +61 -0
  159. package/src/internal/queryDriverSettle.ts +85 -0
  160. package/src/internal/queryDriverStore.ts +9 -74
  161. package/src/internal/queryDriverTypes.ts +10 -0
  162. package/src/internal/queryState-and-scheduler-audit.test.ts +170 -0
  163. package/src/internal/scheduler-multi-source-wake.test.ts +111 -0
  164. package/src/internal/scheduler.ts +80 -6
  165. package/src/internal.ts +4 -0
  166. package/src/reducer/reducer.ts +3 -3
  167. package/src/remote/pendingQueue.ts +35 -5
  168. package/src/remote/remoteState.intent-key-stamp.test.ts +207 -0
  169. package/src/remote/remoteState.key-attribution.test.ts +174 -0
  170. package/src/remote/remoteState.ts +27 -15
  171. package/src/remote/remoteStateSend.ts +29 -8
  172. package/src/remote/remoteStateTypes.ts +7 -0
  173. package/src/runtime/bus.ts +6 -1
  174. package/src/runtime/capturedAppRuntime.ts +134 -110
  175. package/src/runtime/eventBudget.ts +2 -1
  176. package/src/runtime/feature-mount-ownership.test.ts +539 -0
  177. package/src/runtime/featureMount.ts +110 -3
  178. package/src/runtime/loop.ts +29 -6
  179. package/src/runtime/mount-inflight-ownership.test.ts +361 -0
  180. package/src/runtime/mount-scope-hardening.test.ts +440 -0
  181. package/src/runtime/runtime.schema-identity.test.ts +188 -0
  182. package/src/state/state.ts +8 -4
  183. package/src/state/stateFamily.ts +86 -36
  184. package/src/state/stateGroup.ts +24 -6
  185. package/src/testkit/flight.testkit.ts +2 -1
  186. package/src/ui/trigger.ts +18 -0
  187. package/src/wire/tree-key-presence.test.ts +46 -0
  188. package/src/wire/tree.ts +9 -1
@@ -1,2 +1,5 @@
1
+ import { Option } from 'effect';
1
2
  export type Trigger<P> = (payload: P) => void;
3
+ export declare const taggedTrigger: <P>(tag: string, trigger: Trigger<P>) => Trigger<P>;
4
+ export declare const triggerEventTag: (trigger: Trigger<never>) => Option.Option<string>;
2
5
  //# sourceMappingURL=trigger.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"trigger.d.ts","sourceRoot":"","sources":["../../src/ui/trigger.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAA"}
1
+ {"version":3,"file":"trigger.d.ts","sourceRoot":"","sources":["../../src/ui/trigger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAA;AAQ7C,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,KAAK,MAAM,EAAE,SAAS,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAG5E,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,SAAS,OAAO,CAAC,KAAK,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAG7E,CAAA"}
@@ -1,2 +1,15 @@
1
- export {};
1
+ import { Option } from 'effect';
2
+ // A trigger is an opaque closure by the time it reaches a UI contract, so the domain
3
+ // Event it publishes is otherwise unrecoverable. Proofs need it: two compositions can
4
+ // expose an action of the same name, and only the Event says whether they are the same
5
+ // behaviour or a coincidence of spelling.
6
+ const EventTag = Symbol.for('reform/Trigger/event');
7
+ export const taggedTrigger = (tag, trigger) => {
8
+ Object.defineProperty(trigger, EventTag, { value: tag, enumerable: false });
9
+ return trigger;
10
+ };
11
+ export const triggerEventTag = (trigger) => {
12
+ const tag = Reflect.get(trigger, EventTag);
13
+ return typeof tag === 'string' ? Option.some(tag) : Option.none();
14
+ };
2
15
  //# sourceMappingURL=trigger.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"trigger.js","sourceRoot":"","sources":["../../src/ui/trigger.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"trigger.js","sourceRoot":"","sources":["../../src/ui/trigger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAI/B,qFAAqF;AACrF,sFAAsF;AACtF,uFAAuF;AACvF,0CAA0C;AAC1C,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAA;AAEnD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAI,GAAW,EAAE,OAAmB,EAAc,EAAE;IAC/E,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAA;IAC3E,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAuB,EAAyB,EAAE;IAChF,MAAM,GAAG,GAAY,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IACnD,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,CAAA;AACnE,CAAC,CAAA"}
@@ -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;AAuDpD,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"}
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
- leftKeys.every((key) => deepEqual({ left: left[key], right: right[key] })));
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) {
@@ -1 +1 @@
1
- {"version":3,"file":"tree.js","sourceRoot":"","sources":["../../src/wire/tree.ts"],"names":[],"mappings":"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"}
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.0",
3
+ "version": "1.4.1",
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(status, Effect.sync(() => status.set('b')))
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(source, Effect.sync(() => source.set(3)))
342
- yield* afterNotify(source, Effect.sync(() => source.set(4)))
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(source, Effect.sync(() => source.set(3)))
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(source, Effect.sync(() => source.set(3)))
441
- yield* afterNotify(source, Effect.sync(() => source.set(-1)))
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(status, Effect.sync(() => status.set('b')))
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