signalium 0.2.7 → 0.3.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 (152) hide show
  1. package/.turbo/turbo-build.log +12 -0
  2. package/CHANGELOG.md +12 -0
  3. package/dist/cjs/config.d.ts +14 -5
  4. package/dist/cjs/config.d.ts.map +1 -1
  5. package/dist/cjs/config.js +23 -14
  6. package/dist/cjs/config.js.map +1 -1
  7. package/dist/cjs/debug.d.ts +3 -0
  8. package/dist/cjs/debug.d.ts.map +1 -0
  9. package/dist/cjs/debug.js +16 -0
  10. package/dist/cjs/debug.js.map +1 -0
  11. package/dist/cjs/hooks.d.ts +45 -0
  12. package/dist/cjs/hooks.d.ts.map +1 -0
  13. package/dist/cjs/hooks.js +260 -0
  14. package/dist/cjs/hooks.js.map +1 -0
  15. package/dist/cjs/index.d.ts +5 -3
  16. package/dist/cjs/index.d.ts.map +1 -1
  17. package/dist/cjs/index.js +21 -8
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/react/context.d.ts +4 -0
  20. package/dist/cjs/react/context.d.ts.map +1 -0
  21. package/dist/cjs/react/context.js +10 -0
  22. package/dist/cjs/react/context.js.map +1 -0
  23. package/dist/cjs/react/index.d.ts +5 -0
  24. package/dist/cjs/react/index.d.ts.map +1 -0
  25. package/dist/cjs/react/index.js +12 -0
  26. package/dist/cjs/react/index.js.map +1 -0
  27. package/dist/cjs/react/provider.d.ts +7 -0
  28. package/dist/cjs/react/provider.d.ts.map +1 -0
  29. package/dist/cjs/react/provider.js +13 -0
  30. package/dist/cjs/react/provider.js.map +1 -0
  31. package/dist/cjs/react/signal-value.d.ts +3 -0
  32. package/dist/cjs/react/signal-value.d.ts.map +1 -0
  33. package/dist/cjs/react/signal-value.js +42 -0
  34. package/dist/cjs/react/signal-value.js.map +1 -0
  35. package/dist/cjs/react/state.d.ts +3 -0
  36. package/dist/cjs/react/state.d.ts.map +1 -0
  37. package/dist/cjs/react/state.js +13 -0
  38. package/dist/cjs/react/state.js.map +1 -0
  39. package/dist/cjs/scheduling.d.ts +5 -0
  40. package/dist/cjs/scheduling.d.ts.map +1 -1
  41. package/dist/cjs/scheduling.js +59 -5
  42. package/dist/cjs/scheduling.js.map +1 -1
  43. package/dist/cjs/signals.d.ts +28 -65
  44. package/dist/cjs/signals.d.ts.map +1 -1
  45. package/dist/cjs/signals.js +223 -65
  46. package/dist/cjs/signals.js.map +1 -1
  47. package/dist/cjs/trace.d.ts +127 -0
  48. package/dist/cjs/trace.d.ts.map +1 -0
  49. package/dist/cjs/trace.js +319 -0
  50. package/dist/cjs/trace.js.map +1 -0
  51. package/dist/cjs/types.d.ts +66 -0
  52. package/dist/cjs/types.d.ts.map +1 -0
  53. package/dist/cjs/types.js +3 -0
  54. package/dist/cjs/types.js.map +1 -0
  55. package/dist/cjs/utils.d.ts +4 -0
  56. package/dist/cjs/utils.d.ts.map +1 -0
  57. package/dist/cjs/utils.js +80 -0
  58. package/dist/cjs/utils.js.map +1 -0
  59. package/dist/esm/config.d.ts +14 -5
  60. package/dist/esm/config.d.ts.map +1 -1
  61. package/dist/esm/config.js +19 -11
  62. package/dist/esm/config.js.map +1 -1
  63. package/dist/esm/debug.d.ts +3 -0
  64. package/dist/esm/debug.d.ts.map +1 -0
  65. package/dist/esm/debug.js +3 -0
  66. package/dist/esm/debug.js.map +1 -0
  67. package/dist/esm/hooks.d.ts +45 -0
  68. package/dist/esm/hooks.d.ts.map +1 -0
  69. package/dist/esm/hooks.js +243 -0
  70. package/dist/esm/hooks.js.map +1 -0
  71. package/dist/esm/index.d.ts +5 -3
  72. package/dist/esm/index.d.ts.map +1 -1
  73. package/dist/esm/index.js +4 -2
  74. package/dist/esm/index.js.map +1 -1
  75. package/dist/esm/react/context.d.ts +4 -0
  76. package/dist/esm/react/context.d.ts.map +1 -0
  77. package/dist/esm/react/context.js +6 -0
  78. package/dist/esm/react/context.js.map +1 -0
  79. package/dist/esm/react/index.d.ts +5 -0
  80. package/dist/esm/react/index.d.ts.map +1 -0
  81. package/dist/esm/react/index.js +5 -0
  82. package/dist/esm/react/index.js.map +1 -0
  83. package/dist/esm/react/provider.d.ts +7 -0
  84. package/dist/esm/react/provider.d.ts.map +1 -0
  85. package/dist/esm/react/provider.js +10 -0
  86. package/dist/esm/react/provider.js.map +1 -0
  87. package/dist/esm/react/signal-value.d.ts +3 -0
  88. package/dist/esm/react/signal-value.d.ts.map +1 -0
  89. package/dist/esm/react/signal-value.js +38 -0
  90. package/dist/esm/react/signal-value.js.map +1 -0
  91. package/dist/esm/react/state.d.ts +3 -0
  92. package/dist/esm/react/state.d.ts.map +1 -0
  93. package/dist/esm/react/state.js +10 -0
  94. package/dist/esm/react/state.js.map +1 -0
  95. package/dist/esm/scheduling.d.ts +5 -0
  96. package/dist/esm/scheduling.d.ts.map +1 -1
  97. package/dist/esm/scheduling.js +51 -1
  98. package/dist/esm/scheduling.js.map +1 -1
  99. package/dist/esm/signals.d.ts +28 -65
  100. package/dist/esm/signals.d.ts.map +1 -1
  101. package/dist/esm/signals.js +215 -61
  102. package/dist/esm/signals.js.map +1 -1
  103. package/dist/esm/trace.d.ts +127 -0
  104. package/dist/esm/trace.d.ts.map +1 -0
  105. package/dist/esm/trace.js +311 -0
  106. package/dist/esm/trace.js.map +1 -0
  107. package/dist/esm/types.d.ts +66 -0
  108. package/dist/esm/types.d.ts.map +1 -0
  109. package/dist/esm/types.js +2 -0
  110. package/dist/esm/types.js.map +1 -0
  111. package/dist/esm/utils.d.ts +4 -0
  112. package/dist/esm/utils.d.ts.map +1 -0
  113. package/dist/esm/utils.js +75 -0
  114. package/dist/esm/utils.js.map +1 -0
  115. package/package.json +43 -2
  116. package/src/__tests__/hooks/async-computed.test.ts +190 -0
  117. package/src/__tests__/hooks/async-task.test.ts +227 -0
  118. package/src/__tests__/hooks/computed.test.ts +126 -0
  119. package/src/__tests__/hooks/context.test.ts +527 -0
  120. package/src/__tests__/hooks/nesting.test.ts +303 -0
  121. package/src/__tests__/hooks/params-and-state.test.ts +168 -0
  122. package/src/__tests__/hooks/subscription.test.ts +97 -0
  123. package/src/__tests__/signals/async.test.ts +416 -0
  124. package/src/__tests__/signals/basic.test.ts +399 -0
  125. package/src/__tests__/signals/subscription.test.ts +632 -0
  126. package/src/__tests__/signals/watcher.test.ts +253 -0
  127. package/src/__tests__/utils/async.ts +6 -0
  128. package/src/__tests__/utils/builders.ts +22 -0
  129. package/src/__tests__/utils/instrumented-hooks.ts +309 -0
  130. package/src/__tests__/utils/instrumented-signals.ts +281 -0
  131. package/src/__tests__/utils/permute.ts +74 -0
  132. package/src/config.ts +32 -17
  133. package/src/debug.ts +14 -0
  134. package/src/hooks.ts +429 -0
  135. package/src/index.ts +28 -3
  136. package/src/react/__tests__/react.test.tsx +135 -0
  137. package/src/react/context.ts +8 -0
  138. package/src/react/index.ts +4 -0
  139. package/src/react/provider.tsx +18 -0
  140. package/src/react/signal-value.ts +56 -0
  141. package/src/react/state.ts +13 -0
  142. package/src/scheduling.ts +69 -1
  143. package/src/signals.ts +331 -157
  144. package/src/trace.ts +449 -0
  145. package/src/types.ts +86 -0
  146. package/src/utils.ts +83 -0
  147. package/tsconfig.json +2 -1
  148. package/vitest.workspace.ts +24 -0
  149. package/src/__tests__/async.test.ts +0 -426
  150. package/src/__tests__/basic.test.ts +0 -378
  151. package/src/__tests__/subscription.test.ts +0 -645
  152. package/src/__tests__/utils/instrumented.ts +0 -326
@@ -0,0 +1,75 @@
1
+ import { createComputedSignal, createSubscriptionSignal } from './signals.js';
2
+ const objectToIdMap = new WeakMap();
3
+ let nextId = 1;
4
+ export function getObjectId(obj) {
5
+ let id = objectToIdMap.get(obj);
6
+ if (id === undefined) {
7
+ id = `obj-${nextId++}`;
8
+ objectToIdMap.set(obj, id);
9
+ }
10
+ return id;
11
+ }
12
+ // Handle basic POJOs and arrays recursively
13
+ function isPOJO(obj) {
14
+ return Object.getPrototypeOf(obj) === Object.prototype;
15
+ }
16
+ function isPlainArray(arr) {
17
+ return Array.isArray(arr);
18
+ }
19
+ export function hashValue(value) {
20
+ if (value === null)
21
+ return 'null';
22
+ if (value === undefined)
23
+ return 'undefined';
24
+ switch (typeof value) {
25
+ case 'number':
26
+ case 'boolean':
27
+ case 'string':
28
+ return String(value);
29
+ case 'bigint':
30
+ return value.toString();
31
+ case 'symbol':
32
+ return String(value);
33
+ case 'object': {
34
+ if (value instanceof Date) {
35
+ return value.toISOString();
36
+ }
37
+ if (isPlainArray(value)) {
38
+ return `[${value.map(hashValue).join(',')}]`;
39
+ }
40
+ if (isPOJO(value)) {
41
+ const entries = [
42
+ ...Object.entries(value),
43
+ ...Object.getOwnPropertySymbols(value).map(sym => [sym, value[sym]]),
44
+ ].sort(([a], [b]) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0));
45
+ return `{ ${entries.map(([k, v]) => `${String(k)}: ${hashValue(v)}`).join(', ')} }`;
46
+ }
47
+ return getObjectId(value);
48
+ }
49
+ case 'function':
50
+ return getObjectId(value);
51
+ default:
52
+ return getObjectId(value);
53
+ }
54
+ }
55
+ let UNKNOWN_SUBSCRIPTION_ID = 0;
56
+ let UNKNOWN_COMPUTED_ID = 0;
57
+ let UNKNOWN_ASYNC_COMPUTED_ID = 0;
58
+ const UNKNOWN_SIGNAL_NAMES = new Map();
59
+ export function getUnknownSignalFnName(fn, makeSignal) {
60
+ let name = UNKNOWN_SIGNAL_NAMES.get(fn);
61
+ if (name === undefined) {
62
+ if (makeSignal === createSubscriptionSignal) {
63
+ name = `unknownSubscription${UNKNOWN_SUBSCRIPTION_ID++}`;
64
+ }
65
+ else if (makeSignal === createComputedSignal) {
66
+ name = `unknownComputed${UNKNOWN_COMPUTED_ID++}`;
67
+ }
68
+ else {
69
+ name = `unknownAsyncComputed${UNKNOWN_ASYNC_COMPUTED_ID++}`;
70
+ }
71
+ UNKNOWN_SIGNAL_NAMES.set(fn, name);
72
+ }
73
+ return name;
74
+ }
75
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalium",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "repository": "https://github.com/pzuraq/signalium",
6
6
  "description": "Chain-reactivity at critical mass",
@@ -21,11 +21,48 @@
21
21
  "require": "./dist/cjs/index.d.ts"
22
22
  }
23
23
  },
24
+ "./debug": {
25
+ "import": {
26
+ "development": "./src/debug.ts",
27
+ "default": "./dist/esm/debug.js"
28
+ },
29
+ "require": {
30
+ "default": "./dist/cjs/debug.js"
31
+ },
32
+ "types": {
33
+ "development": "./src/debug.ts",
34
+ "import": "./dist/esm/debug.d.ts",
35
+ "require": "./dist/cjs/debug.d.ts"
36
+ }
37
+ },
38
+ "./react": {
39
+ "import": {
40
+ "development": "./src/react/index.ts",
41
+ "default": "./dist/esm/react/index.js"
42
+ },
43
+ "require": {
44
+ "default": "./dist/cjs/react/index.js"
45
+ },
46
+ "types": {
47
+ "development": "./src/react/index.ts",
48
+ "import": "./dist/esm/react/index.d.ts",
49
+ "require": "./dist/cjs/react/index.d.ts"
50
+ }
51
+ },
24
52
  "./package.json": "./package.json"
25
53
  },
54
+ "peerDependencies": {
55
+ "react": ">=18.3.1"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "react": {
59
+ "optional": true
60
+ }
61
+ },
26
62
  "scripts": {
27
63
  "dev": "vitest",
28
64
  "test": "vitest run",
65
+ "check-types": "tsc --noEmit",
29
66
  "build": "npm run build:esm && npm run build:cjs",
30
67
  "build:esm": "tsc",
31
68
  "build:cjs": "tsc --module commonjs --outDir dist/cjs --moduleResolution node"
@@ -33,7 +70,11 @@
33
70
  "author": "",
34
71
  "license": "ISC",
35
72
  "devDependencies": {
73
+ "@vitejs/plugin-react": "^4.3.4",
74
+ "@vitest/browser": "^3.0.6",
36
75
  "vite": "^5.4.8",
37
- "vitest": "^2.1.2"
76
+ "vitest": "^3.0.6",
77
+ "vitest-browser-react": "^0.1.1",
78
+ "playwright": "^1.50.1"
38
79
  }
39
80
  }
@@ -0,0 +1,190 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { state } from '../../index.js';
3
+ import { asyncComputed } from '../utils/instrumented-hooks.js';
4
+ import { nextTick } from '../utils/async.js';
5
+
6
+ describe('async computeds', () => {
7
+ test('Basic async computed works', async () => {
8
+ const getC = asyncComputed(async (a: number, b: number) => {
9
+ return a + b;
10
+ });
11
+
12
+ const result1 = getC(1, 2);
13
+ expect(result1.isPending).toBe(true);
14
+ expect(result1.result).toBe(undefined);
15
+ await nextTick();
16
+ expect(result1.isSuccess).toBe(true);
17
+ expect(result1.result).toBe(3);
18
+
19
+ const result2 = getC(2, 2);
20
+ expect(result2.isPending).toBe(true);
21
+ expect(result2.result).toBe(undefined);
22
+ await nextTick();
23
+ expect(result2.isSuccess).toBe(true);
24
+ expect(result2.result).toBe(4);
25
+ });
26
+
27
+ test('Async computed is not recomputed when the same arguments are passed', async () => {
28
+ let computeCount = 0;
29
+ const getC = asyncComputed(async (a: number, b: number) => {
30
+ computeCount++;
31
+ return a + b;
32
+ });
33
+
34
+ const result1 = getC(1, 2);
35
+ await nextTick();
36
+ expect(result1.result).toBe(3);
37
+ expect(computeCount).toBe(1);
38
+
39
+ const result2 = getC(1, 2);
40
+ await nextTick();
41
+ expect(result2.result).toBe(3);
42
+ expect(computeCount).toBe(1);
43
+ });
44
+
45
+ test('Async computed is recomputed when the arguments change', async () => {
46
+ let computeCount = 0;
47
+ const getC = asyncComputed(async (a: number, b: number) => {
48
+ computeCount++;
49
+ return a + b;
50
+ });
51
+
52
+ const result1 = getC(1, 2);
53
+ await nextTick();
54
+ expect(result1.result).toBe(3);
55
+ expect(computeCount).toBe(1);
56
+
57
+ const result2 = getC(2, 2);
58
+ await nextTick();
59
+ expect(result2.result).toBe(4);
60
+ expect(computeCount).toBe(2);
61
+ });
62
+
63
+ test('Async computed is recomputed when state changes', async () => {
64
+ let computeCount = 0;
65
+ const stateValue = state(1);
66
+
67
+ const getC = asyncComputed(async (a: number) => {
68
+ computeCount++;
69
+ return a + stateValue.get();
70
+ });
71
+
72
+ const result1 = getC(1);
73
+ await nextTick();
74
+ expect(result1.result).toBe(2);
75
+ expect(computeCount).toBe(1);
76
+
77
+ stateValue.set(2);
78
+ const result2 = getC(1);
79
+ await nextTick();
80
+ expect(result2.result).toBe(3);
81
+ expect(computeCount).toBe(2);
82
+ });
83
+
84
+ test('Async computed handles errors', async () => {
85
+ const getC = asyncComputed(async (shouldError: boolean) => {
86
+ if (shouldError) {
87
+ throw new Error('Test error');
88
+ }
89
+ return 'success';
90
+ });
91
+
92
+ const result1 = getC(false);
93
+ await nextTick();
94
+ expect(result1.isSuccess).toBe(true);
95
+ expect(result1.result).toBe('success');
96
+
97
+ const result2 = getC(true);
98
+ await nextTick();
99
+ expect(result2.isError).toBe(true);
100
+ expect(result2.error as Error).toBeInstanceOf(Error);
101
+ expect((result2.error as Error).message).toBe('Test error');
102
+ });
103
+
104
+ test('Async computed with init value starts ready', () => {
105
+ const getC = asyncComputed(async () => 'updated', { initValue: 'initial' });
106
+
107
+ const result = getC();
108
+ expect(result.isReady).toBe(true);
109
+ expect(result.result).toBe('initial');
110
+ expect(result.isPending).toBe(true);
111
+ });
112
+
113
+ test('Nested async computeds work correctly', async () => {
114
+ let innerCount = 0;
115
+ let outerCount = 0;
116
+
117
+ const inner = asyncComputed(async (x: number) => {
118
+ innerCount++;
119
+ await nextTick();
120
+ return x * 2;
121
+ });
122
+
123
+ const outer = asyncComputed(async (x: number) => {
124
+ outerCount++;
125
+ const innerResult = inner(x);
126
+ const result = innerResult.await();
127
+ return result + 1;
128
+ });
129
+
130
+ const result1 = outer(2);
131
+ expect(result1.result).toBe(undefined);
132
+ expect(innerCount).toBe(1);
133
+ expect(outerCount).toBe(1);
134
+
135
+ await new Promise(resolve => setTimeout(resolve, 10));
136
+ const result2 = outer(2);
137
+ expect(result2.result).toBe(5);
138
+ expect(innerCount).toBe(1);
139
+ expect(outerCount).toBe(2);
140
+ });
141
+
142
+ test('Nested async computeds handle errors correctly', async () => {
143
+ const inner = asyncComputed(async (shouldError: boolean) => {
144
+ if (shouldError) throw new Error('Inner error');
145
+ await nextTick();
146
+ return 'inner success';
147
+ });
148
+
149
+ const outer = asyncComputed(async (shouldError: boolean) => {
150
+ const innerResult = inner(shouldError);
151
+ await innerResult.await();
152
+ return 'outer: ' + innerResult.result;
153
+ });
154
+
155
+ // Test success case
156
+ const successResult = outer(false);
157
+ await new Promise(resolve => setTimeout(resolve, 10));
158
+ expect(successResult.isSuccess).toBe(true);
159
+ expect(successResult.result).toBe('outer: inner success');
160
+
161
+ // Test error case
162
+ const errorResult = outer(true);
163
+ await new Promise(resolve => setTimeout(resolve, 10));
164
+ expect(errorResult.isError).toBe(true);
165
+ expect(errorResult.error).toBeInstanceOf(Error);
166
+ expect((errorResult.error as Error).message).toBe('Inner error');
167
+ });
168
+
169
+ test('Nested async computeds with init values work correctly', async () => {
170
+ const inner = asyncComputed(async (x: number) => x * 2, { initValue: 0 });
171
+ const outer = asyncComputed(
172
+ async (x: number) => {
173
+ const innerResult = inner(x);
174
+ await innerResult.await();
175
+ return innerResult.result! + 1;
176
+ },
177
+ { initValue: -1 },
178
+ );
179
+
180
+ const result = outer(2);
181
+ expect(result.isReady).toBe(true);
182
+ expect(result.result).toBe(-1); // Initial value
183
+ expect(result.isPending).toBe(true);
184
+
185
+ await new Promise(resolve => setTimeout(resolve, 10));
186
+ expect(result.result).toBe(5); // (2 * 2) + 1
187
+ expect(result.isPending).toBe(false);
188
+ expect(result.isSuccess).toBe(true);
189
+ });
190
+ });
@@ -0,0 +1,227 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { asyncComputed, asyncTask, computed } from '../utils/instrumented-hooks.js';
3
+ import { nextTick } from '../utils/async.js';
4
+
5
+ describe('async tasks', () => {
6
+ test('Basic async task works', async () => {
7
+ const getC = asyncTask(async (a: number, b: number) => {
8
+ return a + b;
9
+ });
10
+
11
+ // First set of args
12
+ const task1 = getC(1, 2);
13
+ expect(task1.isPending).toBe(false);
14
+ expect(task1.result).toBe(undefined);
15
+ expect(getC).toHaveCounts({ compute: 0 });
16
+
17
+ await nextTick();
18
+ expect(task1.isPending).toBe(false);
19
+ expect(task1.result).toBe(undefined);
20
+ expect(getC).toHaveCounts({ compute: 0 });
21
+
22
+ const result1 = task1.run();
23
+ expect(task1.isPending).toBe(true);
24
+ expect(task1.result).toBe(undefined);
25
+ expect(getC).toHaveCounts({ compute: 1 });
26
+
27
+ expect(await result1).toBe(3);
28
+ expect(task1.isSuccess).toBe(true);
29
+ expect(task1.result).toBe(3);
30
+ expect(getC).toHaveCounts({ compute: 1 });
31
+
32
+ const result2 = task1.run();
33
+ expect(task1.isPending).toBe(true);
34
+ expect(task1.result).toBe(3);
35
+ expect(getC).toHaveCounts({ compute: 2 });
36
+
37
+ expect(await result2).toBe(3);
38
+ expect(task1.isSuccess).toBe(true);
39
+ expect(task1.result).toBe(3);
40
+ expect(getC).toHaveCounts({ compute: 2 });
41
+
42
+ // Second set of args
43
+ const task2 = getC(2, 2);
44
+ expect(task2.isPending).toBe(false);
45
+ expect(task2.result).toBe(undefined);
46
+ expect(getC).toHaveCounts({ compute: 2 });
47
+
48
+ await nextTick();
49
+ expect(task2.isPending).toBe(false);
50
+ expect(task2.result).toBe(undefined);
51
+ expect(getC).toHaveCounts({ compute: 2 });
52
+
53
+ const result3 = task2.run();
54
+ expect(task2.isPending).toBe(true);
55
+ expect(task2.result).toBe(undefined);
56
+ expect(getC).toHaveCounts({ compute: 3 });
57
+
58
+ expect(await result3).toBe(4);
59
+ expect(task2.isSuccess).toBe(true);
60
+ expect(task2.result).toBe(4);
61
+ expect(getC).toHaveCounts({ compute: 3 });
62
+
63
+ const result4 = task2.run();
64
+ expect(task2.isPending).toBe(true);
65
+ expect(task2.result).toBe(4);
66
+ expect(getC).toHaveCounts({ compute: 4 });
67
+
68
+ expect(await result4).toBe(4);
69
+ expect(task2.isSuccess).toBe(true);
70
+ expect(task2.result).toBe(4);
71
+ expect(getC).toHaveCounts({ compute: 4 });
72
+ });
73
+
74
+ test('Separate tasks are created for different arguments', async () => {
75
+ const getC = asyncTask(async (a: number, b: number) => {
76
+ return a + b;
77
+ });
78
+
79
+ const task1 = getC(1, 2);
80
+ const task2 = getC(2, 2);
81
+
82
+ expect(task1.isPending).toBe(false);
83
+ expect(task1.result).toBe(undefined);
84
+ expect(task2.isPending).toBe(false);
85
+ expect(task2.result).toBe(undefined);
86
+ expect(getC).toHaveCounts({ compute: 0 });
87
+
88
+ await nextTick();
89
+ expect(task1.isPending).toBe(false);
90
+ expect(task1.result).toBe(undefined);
91
+ expect(task2.isPending).toBe(false);
92
+ expect(task2.result).toBe(undefined);
93
+ expect(getC).toHaveCounts({ compute: 0 });
94
+
95
+ const result1 = task1.run();
96
+ expect(task1.isPending).toBe(true);
97
+ expect(task1.result).toBe(undefined);
98
+ expect(task2.isPending).toBe(false);
99
+ expect(task2.result).toBe(undefined);
100
+ expect(getC).toHaveCounts({ compute: 1 });
101
+
102
+ expect(await result1).toBe(3);
103
+ expect(task1.isSuccess).toBe(true);
104
+ expect(task1.result).toBe(3);
105
+ expect(task2.isPending).toBe(false);
106
+ expect(task2.result).toBe(undefined);
107
+ expect(getC).toHaveCounts({ compute: 1 });
108
+
109
+ const result2 = task2.run();
110
+ expect(task2.isPending).toBe(true);
111
+ expect(task2.result).toBe(undefined);
112
+ expect(task1.isPending).toBe(false);
113
+ expect(task1.result).toBe(3);
114
+ expect(getC).toHaveCounts({ compute: 2 });
115
+
116
+ expect(await result2).toBe(4);
117
+ expect(task1.isPending).toBe(false);
118
+ expect(task1.result).toBe(3);
119
+ expect(task2.isSuccess).toBe(true);
120
+ expect(task2.result).toBe(4);
121
+ expect(getC).toHaveCounts({ compute: 2 });
122
+ });
123
+
124
+ test('Separate tasks notify separately', async () => {
125
+ const getC = asyncTask(async (a: number, b: number) => {
126
+ return a + b;
127
+ });
128
+
129
+ const computed1 = computed(() => getC(1, 2));
130
+ const computed2 = computed(() => getC(2, 2));
131
+
132
+ const task1 = computed1();
133
+ const task2 = computed2();
134
+
135
+ expect(task1.isPending).toBe(false);
136
+ expect(task1.result).toBe(undefined);
137
+ expect(task2.isPending).toBe(false);
138
+ expect(task2.result).toBe(undefined);
139
+ expect(getC).toHaveCounts({ compute: 0 });
140
+ expect(computed1).toHaveCounts({ compute: 1 });
141
+ expect(computed2).toHaveCounts({ compute: 1 });
142
+
143
+ await nextTick();
144
+ expect(task1.isPending).toBe(false);
145
+ expect(task1.result).toBe(undefined);
146
+ expect(task2.isPending).toBe(false);
147
+ expect(task2.result).toBe(undefined);
148
+ expect(getC).toHaveCounts({ compute: 0 });
149
+ expect(computed1).toHaveCounts({ compute: 1 });
150
+ expect(computed2).toHaveCounts({ compute: 1 });
151
+
152
+ const result1 = task1.run();
153
+
154
+ computed1();
155
+ computed2();
156
+
157
+ expect(task1.isPending).toBe(true);
158
+ expect(task1.result).toBe(undefined);
159
+ expect(task2.isPending).toBe(false);
160
+ expect(task2.result).toBe(undefined);
161
+ expect(getC).toHaveCounts({ compute: 1 });
162
+ expect(computed1).toHaveCounts({ compute: 2 });
163
+ expect(computed2).toHaveCounts({ compute: 1 });
164
+
165
+ computed1();
166
+ computed2();
167
+
168
+ expect(await result1).toBe(3);
169
+ expect(task1.isSuccess).toBe(true);
170
+ expect(task1.result).toBe(3);
171
+ expect(task2.isPending).toBe(false);
172
+ expect(task2.result).toBe(undefined);
173
+ expect(getC).toHaveCounts({ compute: 1 });
174
+
175
+ computed1();
176
+ computed2();
177
+
178
+ expect(computed1).toHaveCounts({ compute: 3 });
179
+ expect(computed2).toHaveCounts({ compute: 1 });
180
+
181
+ const result2 = task2.run();
182
+ expect(task2.isPending).toBe(true);
183
+ expect(task2.result).toBe(undefined);
184
+ expect(task1.isPending).toBe(false);
185
+ expect(task1.result).toBe(3);
186
+ expect(getC).toHaveCounts({ compute: 2 });
187
+
188
+ computed1();
189
+ computed2();
190
+
191
+ expect(computed1).toHaveCounts({ compute: 3 });
192
+ expect(computed2).toHaveCounts({ compute: 2 });
193
+
194
+ expect(await result2).toBe(4);
195
+ expect(task1.isPending).toBe(false);
196
+ expect(task1.result).toBe(3);
197
+ expect(task2.isSuccess).toBe(true);
198
+ expect(task2.result).toBe(4);
199
+ expect(getC).toHaveCounts({ compute: 2 });
200
+
201
+ computed1();
202
+ computed2();
203
+
204
+ expect(computed1).toHaveCounts({ compute: 3 });
205
+ expect(computed2).toHaveCounts({ compute: 3 });
206
+ });
207
+
208
+ test('Async computed handles errors', async () => {
209
+ const getC = asyncComputed(async (shouldError: boolean) => {
210
+ if (shouldError) {
211
+ throw new Error('Test error');
212
+ }
213
+ return 'success';
214
+ });
215
+
216
+ const result1 = getC(false);
217
+ await nextTick();
218
+ expect(result1.isSuccess).toBe(true);
219
+ expect(result1.result).toBe('success');
220
+
221
+ const result2 = getC(true);
222
+ await nextTick();
223
+ expect(result2.isError).toBe(true);
224
+ expect(result2.error as Error).toBeInstanceOf(Error);
225
+ expect((result2.error as Error).message).toBe('Test error');
226
+ });
227
+ });
@@ -0,0 +1,126 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { state } from '../../index.js';
3
+ import { computed } from '../utils/instrumented-hooks.js';
4
+
5
+ describe('computeds', () => {
6
+ test('Basic computed works', () => {
7
+ const getC = computed((a: number, b: number) => {
8
+ return a + b;
9
+ });
10
+
11
+ expect(getC).withParams(1, 2).toHaveValueAndCounts(3, { compute: 1 });
12
+ expect(getC).withParams(2, 2).toHaveValueAndCounts(4, { compute: 2 });
13
+ });
14
+
15
+ test('Computed can throw errors', () => {
16
+ const getC = computed((a: number) => {
17
+ if (a < 0) throw new Error('negative number');
18
+ return a * 2;
19
+ });
20
+
21
+ expect(getC).withParams(2).toHaveValueAndCounts(4, { compute: 1 });
22
+ expect(() => getC(-1)).toThrow('negative number');
23
+ });
24
+
25
+ describe('nesting behavior', () => {
26
+ test('Nested computeds work', () => {
27
+ const getInner = computed((a: number, b: number) => {
28
+ return a + b;
29
+ });
30
+
31
+ const getOuter = computed((x: number) => {
32
+ return getInner(x, 2) * 2;
33
+ });
34
+
35
+ expect(getOuter).withParams(1).toHaveValueAndCounts(6, { compute: 1 });
36
+ expect(getOuter).withParams(1).toHaveValueAndCounts(6, { compute: 1 });
37
+ expect(getOuter).withParams(2).toHaveValueAndCounts(8, { compute: 2 });
38
+ });
39
+
40
+ test('Nested computeds with shared state', () => {
41
+ const sharedState = state(1);
42
+
43
+ const getInner = computed((a: number) => {
44
+ return a + sharedState.get();
45
+ });
46
+
47
+ const getOuter = computed((x: number) => {
48
+ return getInner(x) * 2;
49
+ });
50
+
51
+ expect(getOuter).withParams(1).toHaveValueAndCounts(4, { compute: 1 });
52
+ sharedState.set(2);
53
+ expect(getOuter).withParams(1).toHaveValueAndCounts(6, { compute: 2 });
54
+ });
55
+
56
+ test('Deeply nested computeds maintain independence', () => {
57
+ const getA = computed((x: number) => {
58
+ return x + 1;
59
+ });
60
+
61
+ const getB = computed((x: number) => {
62
+ return getA(x) * 2 + getA(x * 2);
63
+ });
64
+
65
+ const getC = computed((x: number) => {
66
+ return getB(x) + getA(x);
67
+ });
68
+
69
+ expect(getC).withParams(1).toHaveValueAndCounts(9, { compute: 1 });
70
+ expect(getC).withParams(1).toHaveValueAndCounts(9, { compute: 1 });
71
+ expect(getC).withParams(2).toHaveValueAndCounts(14, { compute: 2 });
72
+ });
73
+
74
+ test('Nested computeds work with state signals', () => {
75
+ const stateA = state(1);
76
+ const stateB = state(2);
77
+
78
+ const getInner = computed((x: number) => {
79
+ return x + stateA.get();
80
+ });
81
+
82
+ const getOuter = computed((x: number) => {
83
+ return getInner(x) * stateB.get();
84
+ });
85
+
86
+ expect(getOuter).withParams(3).toHaveValueAndCounts(8, { compute: 1 });
87
+
88
+ stateA.set(2);
89
+ expect(getOuter).withParams(3).toHaveValueAndCounts(10, { compute: 2 });
90
+
91
+ stateB.set(3);
92
+ expect(getOuter).withParams(3).toHaveValueAndCounts(15, { compute: 3 });
93
+
94
+ expect(getOuter).withParams(3).toHaveValueAndCounts(15, { compute: 3 });
95
+ });
96
+
97
+ test('Nested computeds work with both state and arguments', () => {
98
+ const stateA = state(1);
99
+ const stateB = state(2);
100
+
101
+ const getInner = computed((x: number, y: number) => {
102
+ return x + y + stateA.get();
103
+ });
104
+
105
+ const getMiddle = computed((x: number) => {
106
+ return getInner(x, stateB.get()) * 2;
107
+ });
108
+
109
+ const getOuter = computed((x: number, y: number) => {
110
+ return getMiddle(x) + y;
111
+ });
112
+
113
+ expect(getOuter).withParams(1, 3).toHaveValueAndCounts(11, { compute: 1 });
114
+
115
+ stateB.set(3);
116
+ expect(getOuter).withParams(1, 3).toHaveValueAndCounts(13, { compute: 2 });
117
+
118
+ stateA.set(2);
119
+ expect(getOuter).withParams(1, 3).toHaveValueAndCounts(15, { compute: 3 });
120
+
121
+ expect(getOuter).withParams(1, 4).toHaveValueAndCounts(16, { compute: 4 });
122
+
123
+ expect(getOuter).withParams(1, 4).toHaveValueAndCounts(16, { compute: 4 });
124
+ });
125
+ });
126
+ });