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,253 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import {
3
+ createStateSignal,
4
+ createComputedSignal,
5
+ createAsyncComputedSignal,
6
+ createSubscriptionSignal,
7
+ createWatcherSignal,
8
+ } from '../utils/instrumented-signals.js';
9
+ import { result } from '../utils/builders.js';
10
+ import { nextTick, sleep } from '../utils/async.js';
11
+
12
+ describe('Watcher functionality', () => {
13
+ describe('with computeds', () => {
14
+ test('watches computed values', async () => {
15
+ const a = createStateSignal(1);
16
+ const b = createStateSignal(2);
17
+
18
+ const c = createComputedSignal(() => {
19
+ return a.get() + b.get();
20
+ });
21
+
22
+ let value;
23
+ const w = createWatcherSignal(() => {
24
+ value = c.get();
25
+ });
26
+
27
+ w.addListener(() => {
28
+ // do something
29
+ });
30
+
31
+ expect(value).toBe(undefined);
32
+ expect(w).toHaveSignalCounts({ compute: 0, effect: 0 });
33
+
34
+ await nextTick();
35
+
36
+ expect(value).toBe(3);
37
+ expect(w).toHaveSignalCounts({ compute: 1, effect: 1 });
38
+
39
+ a.set(2);
40
+
41
+ await nextTick();
42
+
43
+ expect(value).toBe(4);
44
+ expect(w).toHaveSignalCounts({ compute: 2, effect: 2 });
45
+ });
46
+
47
+ test('immediate option works', async () => {
48
+ const a = createStateSignal(1);
49
+ const b = createStateSignal(2);
50
+
51
+ const c = createComputedSignal(() => {
52
+ return a.get() + b.get();
53
+ });
54
+
55
+ let value;
56
+ const w = createWatcherSignal(() => {
57
+ value = c.get();
58
+ });
59
+
60
+ w.addListener(
61
+ () => {
62
+ // do something
63
+ },
64
+ { immediate: true },
65
+ );
66
+
67
+ expect(value).toBe(3);
68
+ expect(w).toHaveSignalCounts({ compute: 1, effect: 1 });
69
+
70
+ a.set(2);
71
+
72
+ await nextTick();
73
+
74
+ expect(value).toBe(4);
75
+ expect(w).toHaveSignalCounts({ compute: 2, effect: 2 });
76
+ });
77
+ });
78
+
79
+ describe('with async computeds', () => {
80
+ test('watches async computed values', async () => {
81
+ const a = createStateSignal(1);
82
+ const b = createStateSignal(2);
83
+
84
+ const c = createAsyncComputedSignal(async () => {
85
+ return a.get() + b.get();
86
+ });
87
+
88
+ let value;
89
+ const w = createWatcherSignal(() => {
90
+ return c.get();
91
+ });
92
+
93
+ const unsub = w.addListener(v => {
94
+ value = { ...v };
95
+ // do something
96
+ });
97
+
98
+ expect(value).toEqual(undefined);
99
+ expect(w).toHaveSignalCounts({ compute: 0, effect: 0 });
100
+
101
+ await sleep(); // First tick to start async computation
102
+
103
+ expect(value).toEqual(result(undefined, 'pending', 'initial'));
104
+ expect(w).toHaveSignalCounts({ compute: 1, effect: 1 });
105
+
106
+ await nextTick(); // Extra tick for async resolution
107
+
108
+ expect(value).toEqual(result(3, 'success', 'resolved'));
109
+ expect(w).toHaveSignalCounts({ compute: 2, effect: 2 });
110
+
111
+ a.set(2);
112
+
113
+ await sleep(); // First tick to start async computation
114
+
115
+ expect(value).toEqual(result(3, 'pending', 'resolved'));
116
+ expect(w).toHaveSignalCounts({ compute: 3, effect: 3 });
117
+
118
+ await nextTick(); // Extra tick for async resolution
119
+
120
+ expect(value).toEqual(result(4, 'success', 'resolved'));
121
+ expect(w).toHaveSignalCounts({ compute: 4, effect: 4 });
122
+
123
+ unsub();
124
+ });
125
+
126
+ test('immediate option works', async () => {
127
+ const a = createStateSignal(1);
128
+ const b = createStateSignal(2);
129
+
130
+ const c = createAsyncComputedSignal(async () => {
131
+ return a.get() + b.get();
132
+ });
133
+
134
+ const w = createWatcherSignal(() => {
135
+ return c.get();
136
+ });
137
+
138
+ let value;
139
+ w.addListener(
140
+ v => {
141
+ value = { ...v };
142
+ // do something
143
+ },
144
+ {
145
+ immediate: true,
146
+ },
147
+ );
148
+
149
+ expect(value).toEqual(result(undefined, 'pending', 'initial'));
150
+ expect(w).toHaveSignalCounts({ compute: 1, effect: 1 });
151
+
152
+ await nextTick(); // Extra tick for async resolution
153
+
154
+ expect(value).toEqual(result(3, 'success', 'resolved'));
155
+ expect(w).toHaveSignalCounts({ compute: 2, effect: 2 });
156
+
157
+ a.set(2);
158
+
159
+ await sleep(); // First tick to start async computation
160
+
161
+ expect(value).toEqual(result(3, 'pending', 'resolved'));
162
+ expect(w).toHaveSignalCounts({ compute: 3, effect: 3 });
163
+
164
+ await nextTick(); // Extra tick for async resolution
165
+
166
+ expect(value).toEqual(result(4, 'success', 'resolved'));
167
+ expect(w).toHaveSignalCounts({ compute: 4, effect: 4 });
168
+ });
169
+ });
170
+
171
+ describe('with subscriptions', () => {
172
+ test('watches subscription values', async () => {
173
+ let value = createStateSignal(1);
174
+
175
+ const s = createSubscriptionSignal((get, set) => {
176
+ set(value.get());
177
+
178
+ return {
179
+ update() {
180
+ set(value.get());
181
+ },
182
+ };
183
+ });
184
+
185
+ let watchedValue;
186
+ const w = createWatcherSignal(() => {
187
+ watchedValue = s.get();
188
+ });
189
+
190
+ w.addListener(() => {
191
+ // do something
192
+ });
193
+
194
+ expect(watchedValue).toBe(undefined);
195
+ expect(w).toHaveSignalCounts({ compute: 0, effect: 0 });
196
+ expect(s).toHaveSignalValueAndCounts(undefined, { subscribe: 0 });
197
+
198
+ await nextTick();
199
+
200
+ expect(watchedValue).toBe(1);
201
+ expect(w).toHaveSignalCounts({ compute: 1, effect: 1 });
202
+ expect(s).toHaveSignalValueAndCounts(1, { subscribe: 1 });
203
+
204
+ value.set(2);
205
+ s.get();
206
+
207
+ await nextTick();
208
+
209
+ expect(watchedValue).toBe(2);
210
+ expect(w).toHaveSignalCounts({ compute: 2, effect: 2 });
211
+ expect(s).toHaveSignalValueAndCounts(2, { subscribe: 1 });
212
+ });
213
+
214
+ test('immediate option works', async () => {
215
+ let value = createStateSignal(1);
216
+
217
+ const s = createSubscriptionSignal((get, set) => {
218
+ set(value.get());
219
+
220
+ return {
221
+ update() {
222
+ set(value.get());
223
+ },
224
+ };
225
+ });
226
+
227
+ let watchedValue;
228
+ const w = createWatcherSignal(() => {
229
+ watchedValue = s.get();
230
+ });
231
+
232
+ w.addListener(
233
+ () => {
234
+ // do something
235
+ },
236
+ { immediate: true },
237
+ );
238
+
239
+ expect(watchedValue).toBe(1);
240
+ expect(w).toHaveSignalCounts({ compute: 1, effect: 1 });
241
+ expect(s).toHaveSignalValueAndCounts(1, { subscribe: 1 });
242
+
243
+ value.set(2);
244
+ s.get();
245
+
246
+ await nextTick();
247
+
248
+ expect(watchedValue).toBe(2);
249
+ expect(w).toHaveSignalCounts({ compute: 2, effect: 2 });
250
+ expect(s).toHaveSignalValueAndCounts(2, { subscribe: 1 });
251
+ });
252
+ });
253
+ });
@@ -0,0 +1,6 @@
1
+ import { settled } from '../../scheduling.js';
2
+
3
+ export const sleep = (ms = 0) => new Promise(r => setTimeout(r, ms));
4
+ export const nextTick = async () => {
5
+ await settled();
6
+ };
@@ -0,0 +1,22 @@
1
+ import { expect } from 'vitest';
2
+ import { AsyncResult } from '../../types.js';
3
+
4
+ export const result = <T>(
5
+ value: T | undefined,
6
+ promiseState: 'pending' | 'error' | 'success',
7
+ readyState: 'initial' | 'ready' | 'resolved',
8
+ error?: any,
9
+ ): AsyncResult<T> =>
10
+ ({
11
+ result: value,
12
+ error,
13
+ isPending: promiseState === 'pending',
14
+ isError: promiseState === 'error',
15
+ isSuccess: promiseState === 'success',
16
+
17
+ isReady: readyState === 'ready' || readyState === 'resolved',
18
+ didResolve: readyState === 'resolved',
19
+
20
+ await: expect.any(Function),
21
+ invalidate: expect.any(Function),
22
+ }) as AsyncResult<T>;
@@ -0,0 +1,309 @@
1
+ import { afterEach, Assertion, beforeEach, expect } from 'vitest';
2
+ import {
3
+ asyncComputed as _asyncComputed,
4
+ asyncTask as _asyncTask,
5
+ computed as _computed,
6
+ subscription as _subscription,
7
+ watcher,
8
+ SignalSubscribe,
9
+ withContext,
10
+ clearRootScope,
11
+ } from '../../hooks.js';
12
+ import { SignalOptionsWithInit, SignalSubscription, Watcher } from '../../types.js';
13
+
14
+ class SignalHookCounts {
15
+ name: string;
16
+
17
+ get = 0;
18
+ set = 0;
19
+ compute = 0;
20
+
21
+ resolve = 0;
22
+
23
+ subscribe = 0;
24
+ update = 0;
25
+ unsubscribe = 0;
26
+ internalGet = 0;
27
+ internalSet = 0;
28
+
29
+ effect = 0;
30
+
31
+ constructor(name: string) {
32
+ this.name = name;
33
+ }
34
+ }
35
+
36
+ const countsKeys = Object.keys(new SignalHookCounts('')).filter(k => k !== 'name') as (keyof SignalHookCounts)[];
37
+
38
+ let currentOrder: string[] | undefined = [];
39
+ const COUNTS = new WeakMap<object, SignalHookCounts>();
40
+
41
+ interface CustomMatchers<R = unknown> {
42
+ toHaveHookValue: (v: any) => Assertion<R>;
43
+ toHaveCounts: (counts: Partial<SignalHookCounts>) => Assertion<R>;
44
+ toHaveValueAndCounts: (v: any, counts: Partial<SignalHookCounts>) => Assertion<R>;
45
+ toHaveComputedOrder: (order: string[]) => Assertion<R>;
46
+ withParams: R extends (...args: infer P) => any ? (...args: P) => Assertion<R> : (...args: any[]) => Assertion<R>;
47
+ withContexts: (contexts: Record<symbol, any>) => Assertion<R>;
48
+ }
49
+
50
+ declare module 'vitest' {
51
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
52
+ interface Assertion<T = any> extends CustomMatchers<T> {}
53
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
54
+ interface AsymmetricMatchersContaining extends CustomMatchers {}
55
+ }
56
+
57
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
58
+ const NEXT_ARGS = new WeakMap<Function, any[]>();
59
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
60
+ const NEXT_CONTEXTS = new WeakMap<Function, Record<symbol, any>>();
61
+
62
+ let w: Watcher<unknown> | undefined;
63
+
64
+ let unsubs: (() => void)[] = [];
65
+
66
+ afterEach(() => {
67
+ unsubs.forEach(unsub => unsub());
68
+ });
69
+
70
+ function toHaveHookValue(
71
+ this: { equals(a: unknown, b: unknown): boolean },
72
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
73
+ hook: Function,
74
+ value: any,
75
+ ) {
76
+ const args = NEXT_ARGS.get(hook) ?? [];
77
+ const contexts = NEXT_CONTEXTS.get(hook);
78
+
79
+ NEXT_ARGS.delete(hook);
80
+ NEXT_CONTEXTS.delete(hook);
81
+
82
+ let w = watcher(() => {
83
+ if (contexts) {
84
+ return withContext(contexts, () => {
85
+ return hook(...args);
86
+ });
87
+ } else {
88
+ return hook(...args);
89
+ }
90
+ });
91
+
92
+ let signalValue: any;
93
+ unsubs.push(
94
+ w.addListener(
95
+ (v: any) => {
96
+ signalValue = v;
97
+ },
98
+ {
99
+ immediate: true,
100
+ },
101
+ ),
102
+ );
103
+
104
+ if (signalValue && typeof signalValue === 'object' && 'result' in signalValue) {
105
+ signalValue = signalValue.result;
106
+ }
107
+
108
+ return {
109
+ pass: this.equals(signalValue, value),
110
+ message: () => `Expected signal value to be ${JSON.stringify(value)}, but got ${JSON.stringify(signalValue)}`,
111
+ };
112
+ }
113
+
114
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
115
+ function toHaveCounts(hook: Function, counts: SignalHookCounts) {
116
+ const signalCounts = COUNTS.get(hook);
117
+
118
+ if (!signalCounts) {
119
+ return {
120
+ pass: false,
121
+ message: () => 'Signal not found in counts map',
122
+ };
123
+ }
124
+
125
+ for (const key of countsKeys) {
126
+ const count = counts[key];
127
+
128
+ if (count !== undefined && signalCounts[key] !== count) {
129
+ return {
130
+ pass: false,
131
+ message: () => `Expected ${key} count to be ${count} but got ${signalCounts[key]}`,
132
+ };
133
+ }
134
+ }
135
+
136
+ return {
137
+ pass: true,
138
+ message: () => 'Counts match',
139
+ };
140
+ }
141
+
142
+ expect.extend({
143
+ toHaveHookValue,
144
+ toHaveCounts,
145
+
146
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
147
+ withParams(fn: Function, ...args: any[]) {
148
+ NEXT_ARGS.set(fn, args);
149
+
150
+ return {
151
+ pass: true,
152
+ message: () => 'Params match',
153
+ };
154
+ },
155
+
156
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
157
+ withContexts(fn: Function, contexts: Record<symbol, any>) {
158
+ NEXT_CONTEXTS.set(fn, contexts);
159
+
160
+ return {
161
+ pass: true,
162
+ message: () => 'Contexts match',
163
+ };
164
+ },
165
+
166
+ toHaveValueAndCounts(signal, value, counts) {
167
+ const valueResult = toHaveHookValue.call(this, signal, value);
168
+ const countsResult = toHaveCounts.call(this, signal, counts);
169
+
170
+ return {
171
+ pass: valueResult.pass && countsResult.pass,
172
+ message: () => {
173
+ const messages = [
174
+ !valueResult.pass && valueResult.message(),
175
+ !countsResult.pass && countsResult.message(),
176
+ ].filter(m => m);
177
+
178
+ return messages.join('\n');
179
+ },
180
+ };
181
+ },
182
+
183
+ toHaveComputedOrder(fn: () => void, expectedOrder: string[]) {
184
+ const order = (currentOrder = []);
185
+
186
+ fn();
187
+
188
+ currentOrder = undefined;
189
+
190
+ return {
191
+ pass: this.equals(order, expectedOrder),
192
+ message: () => `Expected compute count to be ${expectedOrder.toString()} but got ${order.toString()}`,
193
+ };
194
+ },
195
+ });
196
+
197
+ export const wrapHook = <T, Args extends unknown[]>(original: object, fn: (...args: Args) => T) => {
198
+ const counts = COUNTS.get(original);
199
+
200
+ if (!counts) {
201
+ throw new Error('Signal not found in counts map');
202
+ }
203
+
204
+ COUNTS.set(fn, counts);
205
+
206
+ return fn;
207
+ };
208
+
209
+ export const computed: typeof _computed = (fn, opts) => {
210
+ const counts = new SignalHookCounts(opts?.desc ?? 'unknown');
211
+
212
+ const wrapper = _computed((...args) => {
213
+ counts.compute++;
214
+
215
+ return fn(...(args as any));
216
+ }, opts);
217
+
218
+ COUNTS.set(wrapper, counts);
219
+
220
+ return wrapper;
221
+ };
222
+
223
+ export const asyncComputed: typeof _asyncComputed = (fn, opts) => {
224
+ const counts = new SignalHookCounts(opts?.desc ?? 'unknown');
225
+
226
+ const wrapper = _asyncComputed((...args) => {
227
+ counts.compute++;
228
+
229
+ return fn(...(args as any));
230
+ }, opts) as any;
231
+
232
+ COUNTS.set(wrapper, counts);
233
+
234
+ return wrapper;
235
+ };
236
+
237
+ export const asyncTask: typeof _asyncTask = (fn, opts) => {
238
+ const counts = new SignalHookCounts(opts?.desc ?? 'unknown');
239
+
240
+ const wrapper = _asyncTask((...args) => {
241
+ counts.compute++;
242
+
243
+ return fn(...(args as any));
244
+ }, opts) as any;
245
+
246
+ COUNTS.set(wrapper, counts);
247
+
248
+ return wrapper;
249
+ };
250
+
251
+ export const subscription = <T, Args extends unknown[]>(
252
+ fn: SignalSubscribe<T, Args>,
253
+ opts?: Partial<SignalOptionsWithInit<T, Args>>,
254
+ ): ReturnType<typeof _subscription<T, Args>> => {
255
+ const counts = new SignalHookCounts(opts?.desc ?? 'unknown');
256
+
257
+ let wrapper = _subscription<T, Args>(({ get, set }, ...args) => {
258
+ counts.subscribe++;
259
+ counts.compute++;
260
+
261
+ const result = fn(
262
+ {
263
+ get: () => {
264
+ counts.internalGet++;
265
+ return get();
266
+ },
267
+ set: v => {
268
+ counts.internalSet++;
269
+ set(v);
270
+ },
271
+ },
272
+ ...args,
273
+ );
274
+
275
+ let subscriptionWrapper: SignalSubscription | (() => unknown) | undefined;
276
+
277
+ if (result) {
278
+ if (typeof result === 'function') {
279
+ subscriptionWrapper = () => {
280
+ counts.unsubscribe++;
281
+ result();
282
+ };
283
+ } else {
284
+ subscriptionWrapper = {};
285
+
286
+ if (result.unsubscribe) {
287
+ subscriptionWrapper.unsubscribe = () => {
288
+ counts.unsubscribe++;
289
+ result.unsubscribe!();
290
+ };
291
+ }
292
+
293
+ if (result.update) {
294
+ subscriptionWrapper.update = () => {
295
+ counts.compute++;
296
+ counts.update++;
297
+ result.update!();
298
+ };
299
+ }
300
+ }
301
+ }
302
+
303
+ return subscriptionWrapper;
304
+ }, opts);
305
+
306
+ COUNTS.set(wrapper, counts);
307
+
308
+ return wrapper;
309
+ };