signalium 0.3.8 → 1.0.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.
Files changed (287) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/CHANGELOG.md +15 -0
  3. package/build/react.js +19 -0
  4. package/build/transform.js +19 -0
  5. package/dist/cjs/config.d.ts +8 -3
  6. package/dist/cjs/config.d.ts.map +1 -1
  7. package/dist/cjs/config.js +14 -8
  8. package/dist/cjs/config.js.map +1 -1
  9. package/dist/cjs/debug.d.ts +2 -2
  10. package/dist/cjs/debug.d.ts.map +1 -1
  11. package/dist/cjs/debug.js +3 -3
  12. package/dist/cjs/debug.js.map +1 -1
  13. package/dist/cjs/hooks.d.ts +14 -42
  14. package/dist/cjs/hooks.d.ts.map +1 -1
  15. package/dist/cjs/hooks.js +19 -240
  16. package/dist/cjs/hooks.js.map +1 -1
  17. package/dist/cjs/index.d.ts +5 -3
  18. package/dist/cjs/index.d.ts.map +1 -1
  19. package/dist/cjs/index.js +18 -18
  20. package/dist/cjs/index.js.map +1 -1
  21. package/dist/cjs/internals/async.d.ts +50 -0
  22. package/dist/cjs/internals/async.d.ts.map +1 -0
  23. package/dist/cjs/internals/async.js +390 -0
  24. package/dist/cjs/internals/async.js.map +1 -0
  25. package/dist/cjs/internals/connect.d.ts +4 -0
  26. package/dist/cjs/internals/connect.d.ts.map +1 -0
  27. package/dist/cjs/internals/connect.js +37 -0
  28. package/dist/cjs/internals/connect.js.map +1 -0
  29. package/dist/cjs/internals/consumer.d.ts +6 -0
  30. package/dist/cjs/internals/consumer.d.ts.map +1 -0
  31. package/dist/cjs/internals/consumer.js +13 -0
  32. package/dist/cjs/internals/consumer.js.map +1 -0
  33. package/dist/cjs/internals/contexts.d.ts +33 -0
  34. package/dist/cjs/internals/contexts.d.ts.map +1 -0
  35. package/dist/cjs/internals/contexts.js +103 -0
  36. package/dist/cjs/internals/contexts.js.map +1 -0
  37. package/dist/cjs/internals/derived.d.ts +66 -0
  38. package/dist/cjs/internals/derived.d.ts.map +1 -0
  39. package/dist/cjs/internals/derived.js +128 -0
  40. package/dist/cjs/internals/derived.js.map +1 -0
  41. package/dist/cjs/internals/dirty.d.ts +5 -0
  42. package/dist/cjs/internals/dirty.d.ts.map +1 -0
  43. package/dist/cjs/internals/dirty.js +79 -0
  44. package/dist/cjs/internals/dirty.js.map +1 -0
  45. package/dist/cjs/internals/edge.d.ts +32 -0
  46. package/dist/cjs/internals/edge.d.ts.map +1 -0
  47. package/dist/cjs/internals/edge.js +59 -0
  48. package/dist/cjs/internals/edge.js.map +1 -0
  49. package/dist/cjs/internals/get.d.ts +10 -0
  50. package/dist/cjs/internals/get.d.ts.map +1 -0
  51. package/dist/cjs/internals/get.js +255 -0
  52. package/dist/cjs/internals/get.js.map +1 -0
  53. package/dist/cjs/internals/scheduling.d.ts +12 -0
  54. package/dist/cjs/internals/scheduling.d.ts.map +1 -0
  55. package/dist/cjs/internals/scheduling.js +117 -0
  56. package/dist/cjs/internals/scheduling.js.map +1 -0
  57. package/dist/cjs/internals/state.d.ts +18 -0
  58. package/dist/cjs/internals/state.d.ts.map +1 -0
  59. package/dist/cjs/internals/state.js +88 -0
  60. package/dist/cjs/internals/state.js.map +1 -0
  61. package/dist/cjs/internals/utils/debug-name.d.ts +2 -0
  62. package/dist/cjs/internals/utils/debug-name.d.ts.map +1 -0
  63. package/dist/cjs/internals/utils/debug-name.js +14 -0
  64. package/dist/cjs/internals/utils/debug-name.js.map +1 -0
  65. package/dist/cjs/internals/utils/equals.d.ts +3 -0
  66. package/dist/cjs/internals/utils/equals.d.ts.map +1 -0
  67. package/dist/cjs/internals/utils/equals.js +13 -0
  68. package/dist/cjs/internals/utils/equals.js.map +1 -0
  69. package/dist/cjs/internals/utils/hash.d.ts +7 -0
  70. package/dist/cjs/internals/utils/hash.d.ts.map +1 -0
  71. package/dist/cjs/internals/utils/hash.js +181 -0
  72. package/dist/cjs/internals/utils/hash.js.map +1 -0
  73. package/dist/cjs/internals/utils/stringify.d.ts +3 -0
  74. package/dist/cjs/internals/utils/stringify.d.ts.map +1 -0
  75. package/dist/cjs/{utils.js → internals/utils/stringify.js} +5 -27
  76. package/dist/cjs/internals/utils/stringify.js.map +1 -0
  77. package/dist/cjs/internals/utils/type-utils.d.ts +6 -0
  78. package/dist/cjs/internals/utils/type-utils.d.ts.map +1 -0
  79. package/dist/cjs/internals/utils/type-utils.js +22 -0
  80. package/dist/cjs/internals/utils/type-utils.js.map +1 -0
  81. package/dist/cjs/react/context.d.ts +1 -1
  82. package/dist/cjs/react/context.d.ts.map +1 -1
  83. package/dist/cjs/react/provider.d.ts +4 -3
  84. package/dist/cjs/react/provider.d.ts.map +1 -1
  85. package/dist/cjs/react/provider.js +7 -3
  86. package/dist/cjs/react/provider.js.map +1 -1
  87. package/dist/cjs/react/setup.d.ts.map +1 -1
  88. package/dist/cjs/react/setup.js +2 -1
  89. package/dist/cjs/react/setup.js.map +1 -1
  90. package/dist/cjs/react/signal-value.d.ts +5 -1
  91. package/dist/cjs/react/signal-value.d.ts.map +1 -1
  92. package/dist/cjs/react/signal-value.js +35 -45
  93. package/dist/cjs/react/signal-value.js.map +1 -1
  94. package/dist/cjs/trace.d.ts +32 -28
  95. package/dist/cjs/trace.d.ts.map +1 -1
  96. package/dist/cjs/trace.js +14 -16
  97. package/dist/cjs/trace.js.map +1 -1
  98. package/dist/cjs/transform.d.ts +6 -0
  99. package/dist/cjs/transform.d.ts.map +1 -0
  100. package/dist/cjs/transform.js +92 -0
  101. package/dist/cjs/transform.js.map +1 -0
  102. package/dist/cjs/types.d.ts +32 -40
  103. package/dist/cjs/types.d.ts.map +1 -1
  104. package/dist/esm/config.d.ts +8 -3
  105. package/dist/esm/config.d.ts.map +1 -1
  106. package/dist/esm/config.js +12 -7
  107. package/dist/esm/config.js.map +1 -1
  108. package/dist/esm/debug.d.ts +2 -2
  109. package/dist/esm/debug.d.ts.map +1 -1
  110. package/dist/esm/debug.js +2 -2
  111. package/dist/esm/debug.js.map +1 -1
  112. package/dist/esm/hooks.d.ts +14 -42
  113. package/dist/esm/hooks.d.ts.map +1 -1
  114. package/dist/esm/hooks.js +17 -226
  115. package/dist/esm/hooks.js.map +1 -1
  116. package/dist/esm/index.d.ts +5 -3
  117. package/dist/esm/index.d.ts.map +1 -1
  118. package/dist/esm/index.js +5 -3
  119. package/dist/esm/index.js.map +1 -1
  120. package/dist/esm/internals/async.d.ts +50 -0
  121. package/dist/esm/internals/async.d.ts.map +1 -0
  122. package/dist/esm/internals/async.js +383 -0
  123. package/dist/esm/internals/async.js.map +1 -0
  124. package/dist/esm/internals/connect.d.ts +4 -0
  125. package/dist/esm/internals/connect.d.ts.map +1 -0
  126. package/dist/esm/internals/connect.js +33 -0
  127. package/dist/esm/internals/connect.js.map +1 -0
  128. package/dist/esm/internals/consumer.d.ts +6 -0
  129. package/dist/esm/internals/consumer.d.ts.map +1 -0
  130. package/dist/esm/internals/consumer.js +9 -0
  131. package/dist/esm/internals/consumer.js.map +1 -0
  132. package/dist/esm/internals/contexts.d.ts +33 -0
  133. package/dist/esm/internals/contexts.d.ts.map +1 -0
  134. package/dist/esm/internals/contexts.js +92 -0
  135. package/dist/esm/internals/contexts.js.map +1 -0
  136. package/dist/esm/internals/derived.d.ts +66 -0
  137. package/dist/esm/internals/derived.d.ts.map +1 -0
  138. package/dist/esm/internals/derived.js +118 -0
  139. package/dist/esm/internals/derived.js.map +1 -0
  140. package/dist/esm/internals/dirty.d.ts +5 -0
  141. package/dist/esm/internals/dirty.d.ts.map +1 -0
  142. package/dist/esm/internals/dirty.js +75 -0
  143. package/dist/esm/internals/dirty.js.map +1 -0
  144. package/dist/esm/internals/edge.d.ts +32 -0
  145. package/dist/esm/internals/edge.d.ts.map +1 -0
  146. package/dist/esm/internals/edge.js +54 -0
  147. package/dist/esm/internals/edge.js.map +1 -0
  148. package/dist/esm/internals/get.d.ts +10 -0
  149. package/dist/esm/internals/get.d.ts.map +1 -0
  150. package/dist/esm/internals/get.js +247 -0
  151. package/dist/esm/internals/get.js.map +1 -0
  152. package/dist/esm/internals/scheduling.d.ts +12 -0
  153. package/dist/esm/internals/scheduling.d.ts.map +1 -0
  154. package/dist/esm/internals/scheduling.js +106 -0
  155. package/dist/esm/internals/scheduling.js.map +1 -0
  156. package/dist/esm/internals/state.d.ts +18 -0
  157. package/dist/esm/internals/state.d.ts.map +1 -0
  158. package/dist/esm/internals/state.js +82 -0
  159. package/dist/esm/internals/state.js.map +1 -0
  160. package/dist/esm/internals/utils/debug-name.d.ts +2 -0
  161. package/dist/esm/internals/utils/debug-name.d.ts.map +1 -0
  162. package/dist/esm/internals/utils/debug-name.js +11 -0
  163. package/dist/esm/internals/utils/debug-name.js.map +1 -0
  164. package/dist/esm/internals/utils/equals.d.ts +3 -0
  165. package/dist/esm/internals/utils/equals.d.ts.map +1 -0
  166. package/dist/esm/internals/utils/equals.js +9 -0
  167. package/dist/esm/internals/utils/equals.js.map +1 -0
  168. package/dist/esm/internals/utils/hash.d.ts +7 -0
  169. package/dist/esm/internals/utils/hash.d.ts.map +1 -0
  170. package/dist/esm/internals/utils/hash.js +174 -0
  171. package/dist/esm/internals/utils/hash.js.map +1 -0
  172. package/dist/esm/internals/utils/stringify.d.ts +3 -0
  173. package/dist/esm/internals/utils/stringify.d.ts.map +1 -0
  174. package/dist/esm/{utils.js → internals/utils/stringify.js} +4 -25
  175. package/dist/esm/internals/utils/stringify.js.map +1 -0
  176. package/dist/esm/internals/utils/type-utils.d.ts +6 -0
  177. package/dist/esm/internals/utils/type-utils.d.ts.map +1 -0
  178. package/dist/esm/internals/utils/type-utils.js +15 -0
  179. package/dist/esm/internals/utils/type-utils.js.map +1 -0
  180. package/dist/esm/react/context.d.ts +1 -1
  181. package/dist/esm/react/context.d.ts.map +1 -1
  182. package/dist/esm/react/provider.d.ts +4 -3
  183. package/dist/esm/react/provider.d.ts.map +1 -1
  184. package/dist/esm/react/provider.js +6 -2
  185. package/dist/esm/react/provider.js.map +1 -1
  186. package/dist/esm/react/setup.d.ts.map +1 -1
  187. package/dist/esm/react/setup.js +3 -2
  188. package/dist/esm/react/setup.js.map +1 -1
  189. package/dist/esm/react/signal-value.d.ts +5 -1
  190. package/dist/esm/react/signal-value.d.ts.map +1 -1
  191. package/dist/esm/react/signal-value.js +34 -45
  192. package/dist/esm/react/signal-value.js.map +1 -1
  193. package/dist/esm/trace.d.ts +32 -28
  194. package/dist/esm/trace.d.ts.map +1 -1
  195. package/dist/esm/trace.js +13 -15
  196. package/dist/esm/trace.js.map +1 -1
  197. package/dist/esm/transform.d.ts +6 -0
  198. package/dist/esm/transform.d.ts.map +1 -0
  199. package/dist/esm/transform.js +89 -0
  200. package/dist/esm/transform.js.map +1 -0
  201. package/dist/esm/types.d.ts +32 -40
  202. package/dist/esm/types.d.ts.map +1 -1
  203. package/package.json +23 -4
  204. package/src/__tests__/__snapshots__/context.test.ts.snap +2101 -0
  205. package/src/__tests__/__snapshots__/nesting.test.ts.snap +16201 -0
  206. package/src/__tests__/__snapshots__/params-and-state.test.ts.snap +1879 -0
  207. package/src/__tests__/async-task.test.ts +327 -0
  208. package/src/__tests__/context.test.ts +517 -0
  209. package/src/__tests__/nesting.test.ts +298 -0
  210. package/src/__tests__/params-and-state.test.ts +230 -0
  211. package/src/__tests__/reactive-async.test.ts +548 -0
  212. package/src/__tests__/reactive-sync.test.ts +130 -0
  213. package/src/__tests__/subscription.test.ts +510 -0
  214. package/src/__tests__/utils/async.ts +1 -1
  215. package/src/__tests__/utils/instrumented-hooks.ts +229 -124
  216. package/src/__tests__/utils/permute.ts +25 -14
  217. package/src/config.ts +19 -9
  218. package/src/debug.ts +2 -2
  219. package/src/hooks.ts +46 -380
  220. package/src/index.ts +7 -24
  221. package/src/internals/async.ts +556 -0
  222. package/src/internals/connect.ts +41 -0
  223. package/src/internals/consumer.ts +13 -0
  224. package/src/internals/contexts.ts +133 -0
  225. package/src/internals/derived.ts +208 -0
  226. package/src/internals/dirty.ts +91 -0
  227. package/src/internals/edge.ts +109 -0
  228. package/src/internals/get.ts +298 -0
  229. package/src/internals/scheduling.ts +140 -0
  230. package/src/internals/state.ts +111 -0
  231. package/src/internals/utils/debug-name.ts +14 -0
  232. package/src/internals/utils/equals.ts +12 -0
  233. package/src/internals/utils/hash.ts +221 -0
  234. package/src/{utils.ts → internals/utils/stringify.ts} +3 -29
  235. package/src/internals/utils/type-utils.ts +19 -0
  236. package/src/react/__tests__/async.test.tsx +704 -0
  237. package/src/react/__tests__/basic.test.tsx +95 -0
  238. package/src/react/__tests__/contexts.test.tsx +99 -0
  239. package/src/react/__tests__/subscriptions.test.tsx +49 -0
  240. package/src/react/__tests__/utils.tsx +40 -0
  241. package/src/react/context.ts +1 -1
  242. package/src/react/provider.tsx +12 -4
  243. package/src/react/setup.ts +3 -2
  244. package/src/react/signal-value.ts +47 -67
  245. package/src/trace.ts +43 -38
  246. package/src/transform.ts +113 -0
  247. package/src/types.ts +56 -46
  248. package/transform.js +19 -0
  249. package/vitest.workspace.ts +38 -2
  250. package/dist/cjs/scheduling.d.ts +0 -11
  251. package/dist/cjs/scheduling.d.ts.map +0 -1
  252. package/dist/cjs/scheduling.js +0 -108
  253. package/dist/cjs/scheduling.js.map +0 -1
  254. package/dist/cjs/signals.d.ts +0 -73
  255. package/dist/cjs/signals.d.ts.map +0 -1
  256. package/dist/cjs/signals.js +0 -632
  257. package/dist/cjs/signals.js.map +0 -1
  258. package/dist/cjs/utils.d.ts +0 -4
  259. package/dist/cjs/utils.d.ts.map +0 -1
  260. package/dist/cjs/utils.js.map +0 -1
  261. package/dist/esm/scheduling.d.ts +0 -11
  262. package/dist/esm/scheduling.d.ts.map +0 -1
  263. package/dist/esm/scheduling.js +0 -97
  264. package/dist/esm/scheduling.js.map +0 -1
  265. package/dist/esm/signals.d.ts +0 -73
  266. package/dist/esm/signals.d.ts.map +0 -1
  267. package/dist/esm/signals.js +0 -614
  268. package/dist/esm/signals.js.map +0 -1
  269. package/dist/esm/utils.d.ts +0 -4
  270. package/dist/esm/utils.d.ts.map +0 -1
  271. package/dist/esm/utils.js.map +0 -1
  272. package/src/__tests__/hooks/async-computed.test.ts +0 -190
  273. package/src/__tests__/hooks/async-task.test.ts +0 -334
  274. package/src/__tests__/hooks/computed.test.ts +0 -126
  275. package/src/__tests__/hooks/context.test.ts +0 -527
  276. package/src/__tests__/hooks/nesting.test.ts +0 -303
  277. package/src/__tests__/hooks/params-and-state.test.ts +0 -168
  278. package/src/__tests__/hooks/subscription.test.ts +0 -97
  279. package/src/__tests__/signals/async.test.ts +0 -416
  280. package/src/__tests__/signals/basic.test.ts +0 -399
  281. package/src/__tests__/signals/subscription.test.ts +0 -632
  282. package/src/__tests__/signals/watcher.test.ts +0 -253
  283. package/src/__tests__/utils/builders.ts +0 -22
  284. package/src/__tests__/utils/instrumented-signals.ts +0 -291
  285. package/src/react/__tests__/react.test.tsx +0 -227
  286. package/src/scheduling.ts +0 -130
  287. package/src/signals.ts +0 -824
@@ -0,0 +1,704 @@
1
+ import { beforeEach, describe, expect, test } from 'vitest';
2
+ import { render } from 'vitest-browser-react';
3
+ import { state, reactive, ReactivePromise, subscription } from 'signalium';
4
+ import { setupReact } from '../index.js';
5
+ import React, { memo } from 'react';
6
+ import { Locator } from '@vitest/browser/context';
7
+ import { sleep } from '../../__tests__/utils/async.js';
8
+ import { createRenderCounter, HOC, RenderCounter } from './utils.js';
9
+
10
+ setupReact();
11
+
12
+ const PROMISE_PROPS: (keyof ReactivePromise<string>)[] = [
13
+ 'value',
14
+ 'error',
15
+ 'isPending',
16
+ 'isRejected',
17
+ 'isResolved',
18
+ 'isSettled',
19
+ 'isReady',
20
+ ];
21
+
22
+ function createPromisePropCounter(prop: keyof ReactivePromise<string>, wrapper?: HOC) {
23
+ return createRenderCounter(({ promise }: { promise: ReactivePromise<string> }) => {
24
+ return <>{String(promise[prop])}</>;
25
+ }, wrapper);
26
+ }
27
+
28
+ type PromisePropsKey = keyof ReactivePromise<string> | 'parent';
29
+
30
+ type PromisePropsRenderers = Record<PromisePropsKey, RenderCounter<{ promise: ReactivePromise<string> }>>;
31
+
32
+ export const createPromisePropsCounter = (
33
+ propWrapper?: HOC,
34
+ parentWrapper?: HOC,
35
+ ): [RenderCounter<{ promise: ReactivePromise<string> }>, PromisePropsRenderers] => {
36
+ const PropRenderers = PROMISE_PROPS.reduce((acc, prop) => {
37
+ acc[prop] = createPromisePropCounter(prop, propWrapper);
38
+ return acc;
39
+ }, {} as PromisePropsRenderers);
40
+
41
+ const ParentRenderer = createRenderCounter(({ promise }: { promise: ReactivePromise<string> }) => {
42
+ return (
43
+ <>
44
+ {PROMISE_PROPS.map(prop => {
45
+ const PropRenderer = PropRenderers[prop];
46
+
47
+ return <PropRenderer key={String(prop)} promise={promise} />;
48
+ })}
49
+ </>
50
+ );
51
+ }, parentWrapper);
52
+
53
+ PropRenderers.parent = ParentRenderer;
54
+
55
+ return [ParentRenderer, PropRenderers];
56
+ };
57
+
58
+ const getPromiseValuesAndCounts = (
59
+ getByTestId: (id: string | RegExp) => Locator,
60
+ PropRenderers: PromisePropsRenderers,
61
+ ) => {
62
+ return Object.fromEntries(
63
+ Object.entries(PropRenderers).map(([prop, renderer]) => {
64
+ const value = getByTestId(renderer.testId.toString());
65
+
66
+ return prop === 'parent'
67
+ ? [prop, renderer.renderCount]
68
+ : [prop, [value.element().textContent, renderer.renderCount]];
69
+ }),
70
+ );
71
+ };
72
+
73
+ describe('React > async', () => {
74
+ describe('reactive functions', () => {
75
+ test('results can be passed down to children and grandchildren, and are updated when reactive promise resolves', async () => {
76
+ const value = state('Hello');
77
+
78
+ const derived = reactive(async () => {
79
+ const v = value.get();
80
+ await sleep(100);
81
+ return `${v}, World`;
82
+ });
83
+
84
+ function GrandChild({ text }: { text: string }): React.ReactNode {
85
+ return <span>{text}</span>;
86
+ }
87
+
88
+ function Child({
89
+ asyncValue,
90
+ }: {
91
+ asyncValue: { isPending: boolean; value: string | undefined };
92
+ }): React.ReactNode {
93
+ return <div>{asyncValue.isPending ? 'Loading...' : <GrandChild text={asyncValue.value!} />}</div>;
94
+ }
95
+
96
+ function Parent(): React.ReactNode {
97
+ const d = derived();
98
+ return <Child asyncValue={d} />;
99
+ }
100
+
101
+ const { getByText } = render(<Parent />);
102
+
103
+ await expect.element(getByText('Loading...')).toBeInTheDocument();
104
+ await expect.element(getByText('Hello, World')).toBeInTheDocument();
105
+
106
+ value.set('Hey');
107
+
108
+ await expect.element(getByText('Loading...')).toBeInTheDocument();
109
+ await expect.element(getByText('Hey, World')).toBeInTheDocument();
110
+ });
111
+
112
+ test('results will update all promise props together when used in unmemoized functions', async () => {
113
+ const content = state('World');
114
+
115
+ const derived1 = reactive(async () => {
116
+ const v = `Hello, ${content.get()}`;
117
+ await sleep(100);
118
+ return v;
119
+ });
120
+
121
+ const [ParentRenderer, Renderers] = createPromisePropsCounter();
122
+
123
+ const Parent = () => <ParentRenderer promise={derived1()} />;
124
+
125
+ const { getByTestId } = render(<Parent />);
126
+
127
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
128
+ parent: 1,
129
+
130
+ isPending: ['true', 1],
131
+ isReady: ['false', 1],
132
+ isRejected: ['false', 1],
133
+ isResolved: ['false', 1],
134
+ isSettled: ['false', 1],
135
+ value: ['undefined', 1],
136
+ error: ['undefined', 1],
137
+ });
138
+
139
+ await sleep(200);
140
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
141
+ parent: 2,
142
+
143
+ isPending: ['false', 2],
144
+ isReady: ['true', 2],
145
+ isRejected: ['false', 2],
146
+ isResolved: ['true', 2],
147
+ isSettled: ['true', 2],
148
+ value: ['Hello, World', 2],
149
+ error: ['undefined', 2],
150
+ });
151
+
152
+ content.set('Galaxy');
153
+ await sleep(0);
154
+
155
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
156
+ parent: 3,
157
+
158
+ isPending: ['true', 3],
159
+ isReady: ['true', 3],
160
+ isRejected: ['false', 3],
161
+ isResolved: ['true', 3],
162
+ isSettled: ['true', 3],
163
+ value: ['Hello, World', 3],
164
+ error: ['undefined', 3],
165
+ });
166
+
167
+ await sleep(200);
168
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
169
+ parent: 4,
170
+
171
+ isPending: ['false', 4],
172
+ isReady: ['true', 4],
173
+ isRejected: ['false', 4],
174
+ isResolved: ['true', 4],
175
+ isSettled: ['true', 4],
176
+ value: ['Hello, Galaxy', 4],
177
+ error: ['undefined', 4],
178
+ });
179
+ });
180
+
181
+ test('it can transition back and forth between error and success states', async () => {
182
+ const content = state('World');
183
+
184
+ const derived1 = reactive(async () => {
185
+ const value = content.get();
186
+ const v = `Hello, ${value}`;
187
+ await sleep(100);
188
+ if (value === 'Galaxy') {
189
+ throw new Error('Galaxy is not allowed');
190
+ }
191
+ return v;
192
+ });
193
+
194
+ const [ParentRenderer, Renderers] = createPromisePropsCounter();
195
+
196
+ const Parent = () => <ParentRenderer promise={derived1()} />;
197
+
198
+ const { getByTestId } = render(<Parent />);
199
+
200
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
201
+ parent: 1,
202
+
203
+ isPending: ['true', 1],
204
+ isReady: ['false', 1],
205
+ isRejected: ['false', 1],
206
+ isResolved: ['false', 1],
207
+ isSettled: ['false', 1],
208
+ value: ['undefined', 1],
209
+ error: ['undefined', 1],
210
+ });
211
+
212
+ await sleep(200);
213
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
214
+ parent: 2,
215
+
216
+ isPending: ['false', 2],
217
+ isReady: ['true', 2],
218
+ isRejected: ['false', 2],
219
+ isResolved: ['true', 2],
220
+ isSettled: ['true', 2],
221
+ value: ['Hello, World', 2],
222
+ error: ['undefined', 2],
223
+ });
224
+
225
+ content.set('Galaxy');
226
+ await sleep(0);
227
+
228
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
229
+ parent: 3,
230
+
231
+ isPending: ['true', 3],
232
+ isReady: ['true', 3],
233
+ isRejected: ['false', 3],
234
+ isResolved: ['true', 3],
235
+ isSettled: ['true', 3],
236
+ value: ['Hello, World', 3],
237
+ error: ['undefined', 3],
238
+ });
239
+
240
+ await sleep(200);
241
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
242
+ parent: 4,
243
+
244
+ isPending: ['false', 4],
245
+ isReady: ['true', 4],
246
+ isRejected: ['true', 4],
247
+ isResolved: ['false', 4],
248
+ isSettled: ['true', 4],
249
+ value: ['Hello, World', 4],
250
+ error: ['Error: Galaxy is not allowed', 4],
251
+ });
252
+
253
+ content.set('Universe');
254
+ await sleep(0);
255
+
256
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
257
+ parent: 5,
258
+
259
+ isPending: ['true', 5],
260
+ isReady: ['true', 5],
261
+ isRejected: ['true', 5],
262
+ isResolved: ['false', 5],
263
+ isSettled: ['true', 5],
264
+ value: ['Hello, World', 5],
265
+ error: ['Error: Galaxy is not allowed', 5],
266
+ });
267
+
268
+ await sleep(200);
269
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
270
+ parent: 6,
271
+
272
+ isPending: ['false', 6],
273
+ isReady: ['true', 6],
274
+ isRejected: ['false', 6],
275
+ isResolved: ['true', 6],
276
+ isSettled: ['true', 6],
277
+ value: ['Hello, Universe', 6],
278
+ error: ['undefined', 6],
279
+ });
280
+ });
281
+
282
+ test('results can update when used in reactive functions', async () => {
283
+ const value1 = state('Hello');
284
+ let parentRenderCount = 0;
285
+ let childRenderCount = 0;
286
+
287
+ const derived1 = reactive(async () => {
288
+ const v = value1.get();
289
+ await sleep(100);
290
+ return v;
291
+ });
292
+
293
+ const Child = reactive(({ promise }: { promise: ReactivePromise<string> }): React.ReactNode => {
294
+ childRenderCount++;
295
+ return <span data-testid="child">{promise.value}</span>;
296
+ });
297
+
298
+ const Parent = reactive((): React.ReactNode => {
299
+ parentRenderCount++;
300
+ const d1 = derived1();
301
+ return (
302
+ <div data-testid="parent">
303
+ <Child promise={d1} />
304
+ </div>
305
+ );
306
+ });
307
+
308
+ const { getByTestId } = render(<Parent />);
309
+
310
+ // Wait for both promises to resolve
311
+ await sleep(200);
312
+ await expect.element(getByTestId('parent')).toBeInTheDocument();
313
+ await expect.element(getByTestId('child')).toBeInTheDocument();
314
+
315
+ expect(parentRenderCount).toBe(1);
316
+ expect(childRenderCount).toBe(2);
317
+
318
+ // Update only value1, should re-render only the child
319
+ value1.set('World');
320
+ await sleep(200);
321
+
322
+ expect(parentRenderCount).toBe(1);
323
+ expect(childRenderCount).toBe(3);
324
+ });
325
+
326
+ test('results do not update when used in React.memo components when passed down directly', async () => {
327
+ const value1 = state('Hello');
328
+ let parentRenderCount = 0;
329
+ let childRenderCount = 0;
330
+
331
+ const derived1 = reactive(async () => {
332
+ const v = value1.get();
333
+ await sleep(100);
334
+ return v;
335
+ });
336
+
337
+ const Child = memo(({ promise }: { promise: ReactivePromise<string> }): React.ReactNode => {
338
+ childRenderCount++;
339
+ return <span data-testid="child">{promise.value}</span>;
340
+ });
341
+
342
+ const Parent = memo((): React.ReactNode => {
343
+ parentRenderCount++;
344
+ const d1 = derived1();
345
+ return (
346
+ <div data-testid="parent">
347
+ <Child promise={d1} />
348
+ </div>
349
+ );
350
+ });
351
+
352
+ const { getByTestId } = render(<Parent />);
353
+
354
+ // Wait for both promises to resolve
355
+ await sleep(200);
356
+ await expect.element(getByTestId('parent')).toBeInTheDocument();
357
+ await expect.element(getByTestId('child')).toBeInTheDocument();
358
+
359
+ expect(parentRenderCount).toBe(2);
360
+ expect(childRenderCount).toBe(1);
361
+
362
+ // Update only value1, should re-render only the child
363
+ value1.set('World');
364
+ await sleep(200);
365
+
366
+ expect(parentRenderCount).toBe(4);
367
+ expect(childRenderCount).toBe(1);
368
+ });
369
+ });
370
+
371
+ describe('subscriptions', () => {
372
+ test('results can be passed down to children and grandchildren, and are updated when reactive promise resolves', async () => {
373
+ const value = state('Hello');
374
+
375
+ const derived = reactive(() => {
376
+ const greeting = value.get();
377
+
378
+ return subscription<string>(({ set }) => {
379
+ const run = async () => {
380
+ await sleep(100);
381
+
382
+ return `${greeting}, World`;
383
+ };
384
+
385
+ set(run());
386
+ });
387
+ });
388
+
389
+ function GrandChild({ text }: { text: string }): React.ReactNode {
390
+ return <span>{text}</span>;
391
+ }
392
+
393
+ function Child({
394
+ asyncValue,
395
+ }: {
396
+ asyncValue: { isPending: boolean; value: string | undefined };
397
+ }): React.ReactNode {
398
+ return <div>{asyncValue.isPending ? 'Loading...' : <GrandChild text={asyncValue.value!} />}</div>;
399
+ }
400
+
401
+ function Parent(): React.ReactNode {
402
+ const d = derived();
403
+ return <Child asyncValue={d} />;
404
+ }
405
+
406
+ const { getByText } = render(<Parent />);
407
+
408
+ await expect.element(getByText('Loading...')).toBeInTheDocument();
409
+ await expect.element(getByText('Hello, World')).toBeInTheDocument();
410
+
411
+ value.set('Hey');
412
+
413
+ await expect.element(getByText('Loading...')).toBeInTheDocument();
414
+ await expect.element(getByText('Hey, World')).toBeInTheDocument();
415
+ });
416
+
417
+ test('results will update all promise props together when used in unmemoized functions', async () => {
418
+ const content = state('World');
419
+
420
+ const derived1 = reactive(() => {
421
+ return subscription<string>(({ set }) => {
422
+ const v = content.get();
423
+
424
+ const run = async () => {
425
+ await sleep(100);
426
+ return `Hello, ${v}`;
427
+ };
428
+
429
+ set(run());
430
+ });
431
+ });
432
+
433
+ const [ParentRenderer, Renderers] = createPromisePropsCounter();
434
+
435
+ const Parent = () => <ParentRenderer promise={derived1()} />;
436
+
437
+ const { getByTestId } = render(<Parent />);
438
+
439
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
440
+ parent: 1,
441
+
442
+ isPending: ['true', 1],
443
+ isReady: ['false', 1],
444
+ isRejected: ['false', 1],
445
+ isResolved: ['false', 1],
446
+ isSettled: ['false', 1],
447
+ value: ['undefined', 1],
448
+ error: ['undefined', 1],
449
+ });
450
+
451
+ await sleep(200);
452
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
453
+ parent: 2,
454
+
455
+ isPending: ['false', 2],
456
+ isReady: ['true', 2],
457
+ isRejected: ['false', 2],
458
+ isResolved: ['true', 2],
459
+ isSettled: ['true', 2],
460
+ value: ['Hello, World', 2],
461
+ error: ['undefined', 2],
462
+ });
463
+
464
+ content.set('Galaxy');
465
+ // await sleep(10);
466
+
467
+ // expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
468
+ // parent: 3,
469
+
470
+ // isPending: ['true', 3],
471
+ // isReady: ['true', 3],
472
+ // isRejected: ['false', 3],
473
+ // isResolved: ['true', 3],
474
+ // isSettled: ['true', 3],
475
+ // value: ['Hello, World', 3],
476
+ // error: ['undefined', 3],
477
+ // });
478
+
479
+ await sleep(200);
480
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
481
+ parent: 4,
482
+
483
+ isPending: ['false', 4],
484
+ isReady: ['true', 4],
485
+ isRejected: ['false', 4],
486
+ isResolved: ['true', 4],
487
+ isSettled: ['true', 4],
488
+ value: ['Hello, Galaxy', 4],
489
+ error: ['undefined', 4],
490
+ });
491
+ });
492
+
493
+ test('it can transition back and forth between error and success states', async () => {
494
+ const content = state('World');
495
+
496
+ const derived1 = reactive(() => {
497
+ return subscription<string>(({ set }) => {
498
+ const value = content.get();
499
+
500
+ const run = async () => {
501
+ await sleep(100);
502
+
503
+ if (value === 'Galaxy') {
504
+ throw new Error('Galaxy is not allowed');
505
+ }
506
+
507
+ return `Hello, ${value}`;
508
+ };
509
+
510
+ set(run());
511
+ });
512
+ });
513
+
514
+ const [ParentRenderer, Renderers] = createPromisePropsCounter();
515
+
516
+ const Parent = () => <ParentRenderer promise={derived1()} />;
517
+
518
+ const { getByTestId } = render(<Parent />);
519
+
520
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
521
+ parent: 1,
522
+
523
+ isPending: ['true', 1],
524
+ isReady: ['false', 1],
525
+ isRejected: ['false', 1],
526
+ isResolved: ['false', 1],
527
+ isSettled: ['false', 1],
528
+ value: ['undefined', 1],
529
+ error: ['undefined', 1],
530
+ });
531
+
532
+ await sleep(200);
533
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
534
+ parent: 2,
535
+
536
+ isPending: ['false', 2],
537
+ isReady: ['true', 2],
538
+ isRejected: ['false', 2],
539
+ isResolved: ['true', 2],
540
+ isSettled: ['true', 2],
541
+ value: ['Hello, World', 2],
542
+ error: ['undefined', 2],
543
+ });
544
+
545
+ content.set('Galaxy');
546
+ await sleep(0);
547
+
548
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
549
+ parent: 3,
550
+
551
+ isPending: ['true', 3],
552
+ isReady: ['true', 3],
553
+ isRejected: ['false', 3],
554
+ isResolved: ['true', 3],
555
+ isSettled: ['true', 3],
556
+ value: ['Hello, World', 3],
557
+ error: ['undefined', 3],
558
+ });
559
+
560
+ await sleep(200);
561
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
562
+ parent: 4,
563
+
564
+ isPending: ['false', 4],
565
+ isReady: ['true', 4],
566
+ isRejected: ['true', 4],
567
+ isResolved: ['false', 4],
568
+ isSettled: ['true', 4],
569
+ value: ['Hello, World', 4],
570
+ error: ['Error: Galaxy is not allowed', 4],
571
+ });
572
+
573
+ content.set('Universe');
574
+ await sleep(0);
575
+
576
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
577
+ parent: 5,
578
+
579
+ isPending: ['true', 5],
580
+ isReady: ['true', 5],
581
+ isRejected: ['true', 5],
582
+ isResolved: ['false', 5],
583
+ isSettled: ['true', 5],
584
+ value: ['Hello, World', 5],
585
+ error: ['Error: Galaxy is not allowed', 5],
586
+ });
587
+
588
+ await sleep(200);
589
+ expect(getPromiseValuesAndCounts(getByTestId, Renderers)).toEqual({
590
+ parent: 6,
591
+
592
+ isPending: ['false', 6],
593
+ isReady: ['true', 6],
594
+ isRejected: ['false', 6],
595
+ isResolved: ['true', 6],
596
+ isSettled: ['true', 6],
597
+ value: ['Hello, Universe', 6],
598
+ error: ['undefined', 6],
599
+ });
600
+ });
601
+
602
+ test('results can update when used in reactive functions', async () => {
603
+ const value1 = state('Hello');
604
+ let parentRenderCount = 0;
605
+ let childRenderCount = 0;
606
+
607
+ const derived1 = reactive(() => {
608
+ return subscription<string>(({ set }) => {
609
+ const v = value1.get();
610
+
611
+ const run = async () => {
612
+ await sleep(100);
613
+ return v;
614
+ };
615
+
616
+ set(run());
617
+ });
618
+ });
619
+
620
+ const Child = reactive(({ promise }: { promise: ReactivePromise<string> }): React.ReactNode => {
621
+ childRenderCount++;
622
+ return <span data-testid="child">{promise.value}</span>;
623
+ });
624
+
625
+ const Parent = reactive((): React.ReactNode => {
626
+ parentRenderCount++;
627
+ const d1 = derived1();
628
+ return (
629
+ <div data-testid="parent">
630
+ <Child promise={d1} />
631
+ </div>
632
+ );
633
+ });
634
+
635
+ const { getByTestId } = render(<Parent />);
636
+
637
+ // Wait for both promises to resolve
638
+ await sleep(200);
639
+ await expect.element(getByTestId('parent')).toBeInTheDocument();
640
+ await expect.element(getByTestId('child')).toBeInTheDocument();
641
+
642
+ expect(parentRenderCount).toBe(1);
643
+ expect(childRenderCount).toBe(2);
644
+
645
+ // Update only value1, should re-render only the child
646
+ value1.set('World');
647
+ await sleep(200);
648
+
649
+ expect(parentRenderCount).toBe(1);
650
+ expect(childRenderCount).toBe(3);
651
+ });
652
+
653
+ test('results do not update when used in React.memo components when passed down directly', async () => {
654
+ const value1 = state('Hello');
655
+ let parentRenderCount = 0;
656
+ let childRenderCount = 0;
657
+
658
+ const derived1 = reactive(() => {
659
+ return subscription<string>(({ set }) => {
660
+ const v = value1.get();
661
+
662
+ const run = async () => {
663
+ await sleep(100);
664
+ return v;
665
+ };
666
+
667
+ set(run());
668
+ });
669
+ });
670
+
671
+ const Child = memo(({ promise }: { promise: ReactivePromise<string> }): React.ReactNode => {
672
+ childRenderCount++;
673
+ return <span data-testid="child">{promise.value}</span>;
674
+ });
675
+
676
+ const Parent = memo((): React.ReactNode => {
677
+ parentRenderCount++;
678
+ const d1 = derived1();
679
+ return (
680
+ <div data-testid="parent">
681
+ <Child promise={d1} />
682
+ </div>
683
+ );
684
+ });
685
+
686
+ const { getByTestId } = render(<Parent />);
687
+
688
+ // Wait for both promises to resolve
689
+ await sleep(200);
690
+ await expect.element(getByTestId('parent')).toBeInTheDocument();
691
+ await expect.element(getByTestId('child')).toBeInTheDocument();
692
+
693
+ expect(parentRenderCount).toBe(2);
694
+ expect(childRenderCount).toBe(1);
695
+
696
+ // Update only value1, should re-render only the child
697
+ value1.set('World');
698
+ await sleep(200);
699
+
700
+ expect(parentRenderCount).toBe(4);
701
+ expect(childRenderCount).toBe(1);
702
+ });
703
+ });
704
+ });