signalium 0.2.8 → 0.3.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 (161) 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 +263 -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/setup.d.ts +2 -0
  32. package/dist/cjs/react/setup.d.ts.map +1 -0
  33. package/dist/cjs/react/setup.js +13 -0
  34. package/dist/cjs/react/setup.js.map +1 -0
  35. package/dist/cjs/react/signal-value.d.ts +2 -0
  36. package/dist/cjs/react/signal-value.d.ts.map +1 -0
  37. package/dist/cjs/react/signal-value.js +35 -0
  38. package/dist/cjs/react/signal-value.js.map +1 -0
  39. package/dist/cjs/react/state.d.ts +3 -0
  40. package/dist/cjs/react/state.d.ts.map +1 -0
  41. package/dist/cjs/react/state.js +13 -0
  42. package/dist/cjs/react/state.js.map +1 -0
  43. package/dist/cjs/scheduling.d.ts +5 -0
  44. package/dist/cjs/scheduling.d.ts.map +1 -1
  45. package/dist/cjs/scheduling.js +59 -5
  46. package/dist/cjs/scheduling.js.map +1 -1
  47. package/dist/cjs/signals.d.ts +28 -68
  48. package/dist/cjs/signals.d.ts.map +1 -1
  49. package/dist/cjs/signals.js +223 -76
  50. package/dist/cjs/signals.js.map +1 -1
  51. package/dist/cjs/trace.d.ts +127 -0
  52. package/dist/cjs/trace.d.ts.map +1 -0
  53. package/dist/cjs/trace.js +319 -0
  54. package/dist/cjs/trace.js.map +1 -0
  55. package/dist/cjs/types.d.ts +66 -0
  56. package/dist/cjs/types.d.ts.map +1 -0
  57. package/dist/cjs/types.js +3 -0
  58. package/dist/cjs/types.js.map +1 -0
  59. package/dist/cjs/utils.d.ts +4 -0
  60. package/dist/cjs/utils.d.ts.map +1 -0
  61. package/dist/cjs/utils.js +80 -0
  62. package/dist/cjs/utils.js.map +1 -0
  63. package/dist/esm/config.d.ts +14 -5
  64. package/dist/esm/config.d.ts.map +1 -1
  65. package/dist/esm/config.js +19 -11
  66. package/dist/esm/config.js.map +1 -1
  67. package/dist/esm/debug.d.ts +3 -0
  68. package/dist/esm/debug.d.ts.map +1 -0
  69. package/dist/esm/debug.js +3 -0
  70. package/dist/esm/debug.js.map +1 -0
  71. package/dist/esm/hooks.d.ts +45 -0
  72. package/dist/esm/hooks.d.ts.map +1 -0
  73. package/dist/esm/hooks.js +246 -0
  74. package/dist/esm/hooks.js.map +1 -0
  75. package/dist/esm/index.d.ts +5 -3
  76. package/dist/esm/index.d.ts.map +1 -1
  77. package/dist/esm/index.js +4 -2
  78. package/dist/esm/index.js.map +1 -1
  79. package/dist/esm/react/context.d.ts +4 -0
  80. package/dist/esm/react/context.d.ts.map +1 -0
  81. package/dist/esm/react/context.js +6 -0
  82. package/dist/esm/react/context.js.map +1 -0
  83. package/dist/esm/react/index.d.ts +5 -0
  84. package/dist/esm/react/index.d.ts.map +1 -0
  85. package/dist/esm/react/index.js +5 -0
  86. package/dist/esm/react/index.js.map +1 -0
  87. package/dist/esm/react/provider.d.ts +7 -0
  88. package/dist/esm/react/provider.d.ts.map +1 -0
  89. package/dist/esm/react/provider.js +10 -0
  90. package/dist/esm/react/provider.js.map +1 -0
  91. package/dist/esm/react/setup.d.ts +2 -0
  92. package/dist/esm/react/setup.d.ts.map +1 -0
  93. package/dist/esm/react/setup.js +10 -0
  94. package/dist/esm/react/setup.js.map +1 -0
  95. package/dist/esm/react/signal-value.d.ts +2 -0
  96. package/dist/esm/react/signal-value.d.ts.map +1 -0
  97. package/dist/esm/react/signal-value.js +32 -0
  98. package/dist/esm/react/signal-value.js.map +1 -0
  99. package/dist/esm/react/state.d.ts +3 -0
  100. package/dist/esm/react/state.d.ts.map +1 -0
  101. package/dist/esm/react/state.js +10 -0
  102. package/dist/esm/react/state.js.map +1 -0
  103. package/dist/esm/scheduling.d.ts +5 -0
  104. package/dist/esm/scheduling.d.ts.map +1 -1
  105. package/dist/esm/scheduling.js +51 -1
  106. package/dist/esm/scheduling.js.map +1 -1
  107. package/dist/esm/signals.d.ts +28 -68
  108. package/dist/esm/signals.d.ts.map +1 -1
  109. package/dist/esm/signals.js +215 -72
  110. package/dist/esm/signals.js.map +1 -1
  111. package/dist/esm/trace.d.ts +127 -0
  112. package/dist/esm/trace.d.ts.map +1 -0
  113. package/dist/esm/trace.js +311 -0
  114. package/dist/esm/trace.js.map +1 -0
  115. package/dist/esm/types.d.ts +66 -0
  116. package/dist/esm/types.d.ts.map +1 -0
  117. package/dist/esm/types.js +2 -0
  118. package/dist/esm/types.js.map +1 -0
  119. package/dist/esm/utils.d.ts +4 -0
  120. package/dist/esm/utils.d.ts.map +1 -0
  121. package/dist/esm/utils.js +75 -0
  122. package/dist/esm/utils.js.map +1 -0
  123. package/package.json +43 -2
  124. package/src/__tests__/hooks/async-computed.test.ts +190 -0
  125. package/src/__tests__/hooks/async-task.test.ts +227 -0
  126. package/src/__tests__/hooks/computed.test.ts +126 -0
  127. package/src/__tests__/hooks/context.test.ts +527 -0
  128. package/src/__tests__/hooks/nesting.test.ts +303 -0
  129. package/src/__tests__/hooks/params-and-state.test.ts +168 -0
  130. package/src/__tests__/hooks/subscription.test.ts +97 -0
  131. package/src/__tests__/signals/async.test.ts +416 -0
  132. package/src/__tests__/signals/basic.test.ts +399 -0
  133. package/src/__tests__/signals/subscription.test.ts +632 -0
  134. package/src/__tests__/signals/watcher.test.ts +253 -0
  135. package/src/__tests__/utils/async.ts +6 -0
  136. package/src/__tests__/utils/builders.ts +22 -0
  137. package/src/__tests__/utils/instrumented-hooks.ts +309 -0
  138. package/src/__tests__/utils/instrumented-signals.ts +281 -0
  139. package/src/__tests__/utils/permute.ts +74 -0
  140. package/src/config.ts +32 -17
  141. package/src/debug.ts +14 -0
  142. package/src/hooks.ts +433 -0
  143. package/src/index.ts +28 -3
  144. package/src/react/__tests__/react.test.tsx +227 -0
  145. package/src/react/context.ts +8 -0
  146. package/src/react/index.ts +4 -0
  147. package/src/react/provider.tsx +18 -0
  148. package/src/react/setup.ts +10 -0
  149. package/src/react/signal-value.ts +49 -0
  150. package/src/react/state.ts +13 -0
  151. package/src/scheduling.ts +69 -1
  152. package/src/signals.ts +328 -169
  153. package/src/trace.ts +449 -0
  154. package/src/types.ts +86 -0
  155. package/src/utils.ts +83 -0
  156. package/tsconfig.json +2 -1
  157. package/vitest.workspace.ts +24 -0
  158. package/src/__tests__/async.test.ts +0 -426
  159. package/src/__tests__/basic.test.ts +0 -378
  160. package/src/__tests__/subscription.test.ts +0 -645
  161. package/src/__tests__/utils/instrumented.ts +0 -326
package/src/hooks.ts ADDED
@@ -0,0 +1,433 @@
1
+ import {
2
+ createComputedSignal,
3
+ createAsyncComputedSignal,
4
+ createSubscriptionSignal,
5
+ getCurrentConsumer,
6
+ createWatcherSignal,
7
+ ComputedSignal,
8
+ createStateSignal,
9
+ createAsyncTaskSignal,
10
+ StateSignal,
11
+ } from './signals.js';
12
+ import {
13
+ type AsyncTask,
14
+ type AsyncReady,
15
+ type AsyncResult,
16
+ type Signal,
17
+ type SignalOptions,
18
+ type SignalOptionsWithInit,
19
+ SignalSubscription,
20
+ Watcher,
21
+ WriteableSignal,
22
+ } from './types.js';
23
+ import { getObjectId, getUnknownSignalFnName, hashValue } from './utils.js';
24
+ import { getFrameworkScope, useSignalValue } from './config.js';
25
+ import WeakRef from './weakref.js';
26
+
27
+ declare const CONTEXT_KEY: unique symbol;
28
+
29
+ export type Context<T> = symbol & {
30
+ [CONTEXT_KEY]: T;
31
+ };
32
+
33
+ const CONTEXT_DEFAULT_VALUES = new Map<Context<unknown>, unknown>();
34
+ const CONTEXT_MASKS = new Map<Context<unknown>, bigint>();
35
+
36
+ let CONTEXT_MASKS_COUNT = 0;
37
+
38
+ const COMPUTED_CONTEXT_MASKS = new Map<object, bigint>();
39
+ const COMPUTED_OWNERS = new WeakMap<ComputedSignal<unknown>, SignalScope>();
40
+
41
+ let CURRENT_MASK: bigint | null = null;
42
+
43
+ export const state = <T>(value: T, opts?: Partial<SignalOptions<T, unknown[]>>): WriteableSignal<T> => {
44
+ const signal = createStateSignal(value, opts) as StateSignal<T>;
45
+
46
+ return {
47
+ set(v: T) {
48
+ signal.set(v);
49
+ },
50
+
51
+ get() {
52
+ // eslint-disable-next-line react-hooks/rules-of-hooks
53
+ return useSignalValue(signal._desc, () => signal.get());
54
+ },
55
+ };
56
+ };
57
+
58
+ export const createContext = <T>(initialValue: T, description?: string) => {
59
+ const count = CONTEXT_MASKS_COUNT++;
60
+ const key = Symbol(description ?? `context:${count}`) as Context<T>;
61
+
62
+ CONTEXT_DEFAULT_VALUES.set(key, initialValue);
63
+ CONTEXT_MASKS.set(key, BigInt(1) << BigInt(count));
64
+
65
+ return key;
66
+ };
67
+
68
+ export type SignalStoreMap = {
69
+ [K in Context<unknown>]: K extends Context<infer T> ? T : never;
70
+ };
71
+
72
+ export class SignalScope {
73
+ constructor(
74
+ contexts: SignalStoreMap,
75
+ private parent?: SignalScope,
76
+ ) {
77
+ this.contexts = Object.create(parent?.contexts || null);
78
+
79
+ for (const key of Object.getOwnPropertySymbols(contexts)) {
80
+ this.contexts[key as Context<unknown>] = contexts[key as Context<unknown>];
81
+ this.contextMask |= CONTEXT_MASKS.get(key as Context<unknown>)!;
82
+ }
83
+ }
84
+
85
+ private contexts: SignalStoreMap;
86
+ private children = new Map<string, SignalScope>();
87
+ private contextMask = 0n;
88
+ private signals = new Map<string, WeakRef<Signal<unknown>>>();
89
+
90
+ getChild(contexts: SignalStoreMap) {
91
+ const key = hashValue(contexts);
92
+
93
+ let child = this.children.get(key);
94
+
95
+ if (child === undefined) {
96
+ child = new SignalScope(contexts, this);
97
+ this.children.set(key, child);
98
+ }
99
+
100
+ return child;
101
+ }
102
+
103
+ getContext<T>(context: Context<T>): T | undefined {
104
+ const value = this.contexts[context];
105
+
106
+ if (CURRENT_MASK !== null) {
107
+ CURRENT_MASK |= CONTEXT_MASKS.get(context)!;
108
+ }
109
+
110
+ return value as T | undefined;
111
+ }
112
+
113
+ private getSignal(key: string, computedMask: bigint): Signal<unknown> | undefined {
114
+ return (this.contextMask & computedMask) === 0n && this.parent
115
+ ? this.parent.getSignal(key, computedMask)
116
+ : this.signals.get(key)?.deref();
117
+ }
118
+
119
+ private setSignal(key: string, signal: Signal<unknown>, mask: bigint, isPromoting: boolean) {
120
+ if ((this.contextMask & mask) === 0n && this.parent) {
121
+ this.parent.setSignal(key, signal, mask, isPromoting);
122
+ } else {
123
+ this.signals.set(key, new WeakRef(signal));
124
+
125
+ if (isPromoting) {
126
+ this.parent?.deleteSignal(key);
127
+ }
128
+ }
129
+ }
130
+
131
+ private deleteSignal(key: string) {
132
+ this.signals.delete(key);
133
+ this.parent?.deleteSignal(key);
134
+ }
135
+
136
+ run(fn: (...args: any[]) => any, args: any[], key: string, signal: Signal<unknown>, initialized: boolean) {
137
+ const prevMask = CURRENT_MASK;
138
+ const fnMask = COMPUTED_CONTEXT_MASKS.get(fn) ?? 0n;
139
+ const signalMask = COMPUTED_CONTEXT_MASKS.get(signal) ?? 0n;
140
+
141
+ try {
142
+ CURRENT_MASK = signalMask | fnMask;
143
+
144
+ return fn(...args);
145
+ } finally {
146
+ if (!initialized || signalMask !== CURRENT_MASK) {
147
+ COMPUTED_CONTEXT_MASKS.set(fn, CURRENT_MASK!);
148
+ COMPUTED_CONTEXT_MASKS.set(signal, CURRENT_MASK!);
149
+ getCurrentScope().setSignal(key, signal!, CURRENT_MASK!, initialized);
150
+ initialized = true;
151
+ }
152
+
153
+ CURRENT_MASK = prevMask;
154
+ }
155
+ }
156
+
157
+ get(
158
+ makeSignal: (
159
+ fn: (...args: any[]) => any,
160
+ opts?: Partial<SignalOptionsWithInit<unknown, unknown[]>>,
161
+ ) => Signal<unknown>,
162
+ fn: (...args: any[]) => any,
163
+ key: string,
164
+ params: string,
165
+ args: unknown[],
166
+ opts?: Partial<SignalOptions<unknown, unknown[]>>,
167
+ ): unknown {
168
+ const computedMask = COMPUTED_CONTEXT_MASKS.get(fn) ?? 0n;
169
+
170
+ const fnName = opts?.desc ?? fn.name ?? getUnknownSignalFnName(fn, makeSignal);
171
+
172
+ let signal = this.getSignal(key, computedMask);
173
+
174
+ if (signal === undefined) {
175
+ let initialized = false;
176
+
177
+ if (makeSignal === createSubscriptionSignal) {
178
+ signal = makeSignal(
179
+ (get, set) => {
180
+ const sub = this.run(fn, [{ get, set }, ...args], key, signal!, initialized) as
181
+ | SignalSubscription
182
+ | undefined;
183
+
184
+ if (sub?.update) {
185
+ const originalUpdate = sub.update;
186
+
187
+ sub.update = (...args) => {
188
+ return this.run(originalUpdate, [], key, signal!, initialized);
189
+ };
190
+ }
191
+
192
+ initialized = true;
193
+
194
+ return sub;
195
+ },
196
+ { ...opts, id: key, desc: fnName, params },
197
+ );
198
+ } else {
199
+ signal = makeSignal(
200
+ () => {
201
+ const result = this.run(fn, args, key, signal!, initialized);
202
+
203
+ initialized = true;
204
+
205
+ return result;
206
+ },
207
+ { ...opts, id: key, desc: fnName, params },
208
+ );
209
+ }
210
+ }
211
+
212
+ COMPUTED_OWNERS.set(signal as ComputedSignal<unknown>, this);
213
+
214
+ const value = signal.get();
215
+
216
+ if (CURRENT_MASK !== null) {
217
+ CURRENT_MASK |= COMPUTED_CONTEXT_MASKS.get(fn) ?? 0n;
218
+ }
219
+
220
+ return value;
221
+ }
222
+ }
223
+
224
+ export let ROOT_SCOPE = new SignalScope({});
225
+
226
+ export const clearRootScope = () => {
227
+ ROOT_SCOPE = new SignalScope({});
228
+ };
229
+
230
+ let OVERRIDE_SCOPE: SignalScope | undefined;
231
+
232
+ const getCurrentScope = (): SignalScope => {
233
+ if (OVERRIDE_SCOPE !== undefined) {
234
+ return OVERRIDE_SCOPE;
235
+ }
236
+
237
+ const currentConsumer = getCurrentConsumer();
238
+
239
+ if (currentConsumer) {
240
+ const scope = COMPUTED_OWNERS.get(currentConsumer);
241
+
242
+ return scope ?? ROOT_SCOPE;
243
+ }
244
+
245
+ return getFrameworkScope() ?? ROOT_SCOPE;
246
+ };
247
+
248
+ export const withContext = <T>(contexts: SignalStoreMap, fn: () => T): T => {
249
+ const prevScope = OVERRIDE_SCOPE;
250
+ const currentScope = getCurrentScope();
251
+
252
+ try {
253
+ OVERRIDE_SCOPE = currentScope.getChild(contexts);
254
+ return fn();
255
+ } finally {
256
+ OVERRIDE_SCOPE = prevScope;
257
+ }
258
+ };
259
+
260
+ export const useContext = <T>(context: Context<T>): T => {
261
+ let scope = OVERRIDE_SCOPE;
262
+
263
+ if (scope === undefined) {
264
+ const currentConsumer = getCurrentConsumer();
265
+ scope = currentConsumer ? COMPUTED_OWNERS.get(currentConsumer) : undefined;
266
+ }
267
+
268
+ if (scope === undefined) {
269
+ scope = getFrameworkScope();
270
+ }
271
+
272
+ if (scope === undefined) {
273
+ throw new Error(
274
+ 'useContext must be used within a signal hook, a withContext, or within a framework-specific context provider.',
275
+ );
276
+ }
277
+
278
+ return scope.getContext(context) ?? (CONTEXT_DEFAULT_VALUES.get(context) as T);
279
+ };
280
+
281
+ const getParamsKey = (args: unknown[], opts?: Partial<SignalOptions<any, any[]>>) => {
282
+ return opts?.paramKey ? opts.paramKey(...args) : args.map(arg => hashValue(arg)).join(', ');
283
+ };
284
+
285
+ const getComputedKey = (fn: (...args: any[]) => any, params: string) => {
286
+ const fnId = getObjectId(fn);
287
+ return `${fnId}(${params})`;
288
+ };
289
+
290
+ export function computed<T, Args extends unknown[]>(
291
+ fn: (...args: Args) => T,
292
+ opts?: Partial<SignalOptions<T, Args>>,
293
+ ): (...args: Args) => T {
294
+ return (...args) => {
295
+ const params = getParamsKey(args, opts);
296
+ const key = getComputedKey(fn, params);
297
+
298
+ return useSignalValue(key, () => {
299
+ const scope = getCurrentScope();
300
+ return scope.get(
301
+ createComputedSignal,
302
+ fn,
303
+ key,
304
+ params,
305
+ args,
306
+ opts as Partial<SignalOptionsWithInit<unknown, unknown[]>>,
307
+ ) as T;
308
+ });
309
+ };
310
+ }
311
+
312
+ export type AsyncAwaitableResult<T> = T | Promise<T>;
313
+
314
+ export function asyncComputed<T, Args extends unknown[]>(
315
+ fn: (...args: Args) => T | Promise<T>,
316
+ opts?: SignalOptions<T, Args>,
317
+ ): (...args: Args) => AsyncResult<T>;
318
+ export function asyncComputed<T, Args extends unknown[]>(
319
+ fn: (...args: Args) => T | Promise<T>,
320
+ opts: SignalOptionsWithInit<T, Args>,
321
+ ): (...args: Args) => AsyncReady<T>;
322
+ export function asyncComputed<T, Args extends unknown[]>(
323
+ fn: (...args: Args) => T | Promise<T>,
324
+ opts?: Partial<SignalOptionsWithInit<T, Args>>,
325
+ ): (...args: Args) => AsyncResult<T> | AsyncReady<T> {
326
+ return (...args) => {
327
+ const params = getParamsKey(args, opts);
328
+ const key = getComputedKey(fn, params);
329
+
330
+ return useSignalValue(key, () => {
331
+ const scope = getCurrentScope();
332
+ return scope.get(
333
+ createAsyncComputedSignal,
334
+ fn,
335
+ key,
336
+ params,
337
+ args,
338
+ opts as Partial<SignalOptionsWithInit<unknown, unknown[]>>,
339
+ ) as AsyncResult<T>;
340
+ });
341
+ };
342
+ }
343
+
344
+ export interface SubscriptionState<T> {
345
+ get: () => T;
346
+ set: (value: T) => void;
347
+ }
348
+
349
+ export type SignalSubscribe<T, Args extends unknown[]> = (
350
+ state: SubscriptionState<T>,
351
+ ...args: Args
352
+ ) => SignalSubscription | (() => unknown) | undefined | void;
353
+
354
+ export function subscription<T, Args extends unknown[]>(
355
+ fn: SignalSubscribe<T, Args>,
356
+ opts?: Partial<SignalOptionsWithInit<T, Args>>,
357
+ ): (...args: Args) => T {
358
+ const wrapper = (state: SubscriptionState<T>, ...args: Args) => {
359
+ let result = fn(state, ...args);
360
+
361
+ if (typeof result === 'function') {
362
+ return {
363
+ update() {
364
+ (result as () => void)();
365
+ result = fn(state, ...args);
366
+ },
367
+
368
+ unsubscribe() {
369
+ (result as () => void)();
370
+ },
371
+ };
372
+ }
373
+
374
+ return result;
375
+ };
376
+
377
+ Object.defineProperty(wrapper, 'name', {
378
+ value: fn.name,
379
+ writable: false,
380
+ });
381
+
382
+ return (...args) => {
383
+ const params = getParamsKey(args, opts);
384
+ const key = getComputedKey(fn, params);
385
+
386
+ return useSignalValue(key, () => {
387
+ const scope = getCurrentScope();
388
+ return scope.get(
389
+ createSubscriptionSignal,
390
+ wrapper,
391
+ key,
392
+ params,
393
+ args,
394
+ opts as Partial<SignalOptionsWithInit<unknown, unknown[]>>,
395
+ ) as T;
396
+ });
397
+ };
398
+ }
399
+
400
+ export const asyncTask = <T, Args extends unknown[]>(
401
+ fn: (...args: Args) => Promise<T>,
402
+ opts?: Partial<SignalOptionsWithInit<T, Args>>,
403
+ ): ((...args: Args) => AsyncTask<T>) => {
404
+ return (...args) => {
405
+ const params = getParamsKey(args, opts);
406
+ const key = getComputedKey(fn, params);
407
+
408
+ return useSignalValue(key, () => {
409
+ const scope = getCurrentScope();
410
+ return scope.get(
411
+ createAsyncTaskSignal,
412
+ fn,
413
+ key,
414
+ params,
415
+ args,
416
+ opts as Partial<SignalOptionsWithInit<unknown, unknown[]>>,
417
+ ) as AsyncTask<T>;
418
+ });
419
+ };
420
+ };
421
+
422
+ export function watcher<T>(
423
+ fn: (prev: T | undefined) => T,
424
+ opts?: SignalOptions<T, unknown[]> & { scope?: SignalScope },
425
+ ): Watcher<T> {
426
+ const scope = opts?.scope ?? ROOT_SCOPE;
427
+
428
+ const w = createWatcherSignal(fn, opts);
429
+
430
+ COMPUTED_OWNERS.set(w as ComputedSignal<unknown>, scope);
431
+
432
+ return w;
433
+ }
package/src/index.ts CHANGED
@@ -9,11 +9,36 @@ export type {
9
9
  SignalOptions,
10
10
  SignalOptionsWithInit,
11
11
  SignalSubscription,
12
- SignalWatcherEffect,
13
12
  AsyncPending,
14
13
  AsyncReady,
15
14
  AsyncResult,
15
+ Watcher,
16
+ } from './types.js';
17
+
18
+ export {
19
+ createStateSignal,
20
+ createComputedSignal,
21
+ createAsyncComputedSignal,
22
+ createSubscriptionSignal,
23
+ createWatcherSignal,
24
+ createAsyncTaskSignal,
25
+ getCurrentConsumer,
16
26
  } from './signals.js';
17
27
 
18
- export { state, computed, asyncComputed, subscription, watcher } from './signals.js';
19
- export { setRunBatch, setScheduleFlush } from './config.js';
28
+ export {
29
+ state,
30
+ createContext,
31
+ useContext,
32
+ withContext,
33
+ computed,
34
+ asyncComputed,
35
+ subscription,
36
+ watcher,
37
+ SignalScope,
38
+ type Context,
39
+ type SignalStoreMap,
40
+ } from './hooks.js';
41
+
42
+ export { setConfig } from './config.js';
43
+
44
+ export { hashValue as stringifyArgs } from './utils.js';
@@ -0,0 +1,227 @@
1
+ import { beforeEach, describe, expect, test } from 'vitest';
2
+ import { render } from 'vitest-browser-react';
3
+ import { state, asyncComputed, computed, createContext, useContext } from '../../index.js';
4
+ import { ContextProvider, setupReact, useStateSignal } from '../index.js';
5
+ import React, { useState } from 'react';
6
+ import { userEvent } from '@vitest/browser/context';
7
+ import { sleep } from '../../__tests__/utils/async.js';
8
+
9
+ describe('React', () => {
10
+ beforeEach(() => {
11
+ setupReact();
12
+ });
13
+
14
+ test('basic state usage works', async () => {
15
+ const value = state('Hello');
16
+
17
+ function Component(): React.ReactNode {
18
+ return <div>{value.get()}</div>;
19
+ }
20
+
21
+ const { getByText } = render(<Component />);
22
+
23
+ await expect.element(getByText('Hello')).toBeInTheDocument();
24
+
25
+ value.set('World');
26
+
27
+ await expect.element(getByText('World')).toBeInTheDocument();
28
+ });
29
+
30
+ test('useStateSignal works', async () => {
31
+ function Component(): React.ReactNode {
32
+ const value = useStateSignal('Hello');
33
+
34
+ return (
35
+ <div>
36
+ {value.get()}
37
+ <button onClick={() => value.set('World')}>Toggle</button>
38
+ </div>
39
+ );
40
+ }
41
+
42
+ const { getByText } = render(<Component />);
43
+
44
+ await expect.element(getByText('Hello')).toBeInTheDocument();
45
+
46
+ await userEvent.click(getByText('Toggle'));
47
+
48
+ await expect.element(getByText('World')).toBeInTheDocument();
49
+ });
50
+
51
+ test('basic computed usage works', async () => {
52
+ const value = state('Hello');
53
+
54
+ const derived = computed(() => `${value.get()}, World`);
55
+
56
+ function Component(): React.ReactNode {
57
+ return <div>{derived()}</div>;
58
+ }
59
+
60
+ const { getByText } = render(<Component />);
61
+
62
+ await expect.element(getByText('Hello, World')).toBeInTheDocument();
63
+
64
+ value.set('Hey');
65
+
66
+ await expect.element(getByText('Hey, World')).toBeInTheDocument();
67
+ });
68
+
69
+ test('computed updates when params change', async () => {
70
+ const value = state('Hello');
71
+
72
+ const derived = computed((universe: boolean) => `${value.get()}, ${universe ? 'Universe' : 'World'}`);
73
+
74
+ function Component(): React.ReactNode {
75
+ const [universe, setUniverse] = useState(true);
76
+
77
+ return (
78
+ <div>
79
+ {derived(universe)}
80
+ <button onClick={() => setUniverse(!universe)}>Toggle Universe</button>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ const { getByText } = render(<Component />);
86
+
87
+ await expect.element(getByText('Hello, Universe')).toBeInTheDocument();
88
+
89
+ value.set('Hey');
90
+
91
+ await expect.element(getByText('Hey, Universe')).toBeInTheDocument();
92
+
93
+ await userEvent.click(getByText('Toggle Universe'));
94
+
95
+ await expect.element(getByText('Hey, World')).toBeInTheDocument();
96
+ });
97
+
98
+ test('works with async computed', async () => {
99
+ const value = state('Hello');
100
+
101
+ const derived = asyncComputed(async (universe: boolean) => {
102
+ const v = value.get();
103
+ await sleep(100);
104
+ return `${v}, ${universe ? 'Universe' : 'World'}`;
105
+ });
106
+
107
+ function Component(): React.ReactNode {
108
+ const [universe, setUniverse] = useState(true);
109
+
110
+ const d = derived(universe);
111
+
112
+ return (
113
+ <div>
114
+ {d.isSuccess ? d.result : 'Loading...'}
115
+ <button onClick={() => setUniverse(!universe)}>Toggle Universe</button>
116
+ </div>
117
+ );
118
+ }
119
+
120
+ const { getByText } = render(<Component />);
121
+
122
+ await expect.element(getByText('Loading...')).toBeInTheDocument();
123
+ await expect.element(getByText('Hello, Universe')).toBeInTheDocument();
124
+
125
+ value.set('Hey');
126
+
127
+ await expect.element(getByText('Loading...')).toBeInTheDocument();
128
+ await expect.element(getByText('Hey, Universe')).toBeInTheDocument();
129
+
130
+ await userEvent.click(getByText('Toggle Universe'));
131
+
132
+ await expect.element(getByText('Loading...')).toBeInTheDocument();
133
+ await expect.element(getByText('Hey, World')).toBeInTheDocument();
134
+ });
135
+
136
+ describe('contexts', () => {
137
+ test('useContext works inside computed with default value', async () => {
138
+ const value = state('Hello');
139
+ const context = createContext(value);
140
+
141
+ const derived = computed(() => `${useContext(context).get()}, World`);
142
+
143
+ function Component(): React.ReactNode {
144
+ return <div>{derived()}</div>;
145
+ }
146
+
147
+ const { getByText } = render(<Component />);
148
+
149
+ await expect.element(getByText('Hello, World')).toBeInTheDocument();
150
+
151
+ value.set('Hey');
152
+
153
+ await expect.element(getByText('Hey, World')).toBeInTheDocument();
154
+ });
155
+
156
+ test('useContext works at root level with default value', async () => {
157
+ const value = state('Hello');
158
+ const context = createContext(value);
159
+
160
+ function Component(): React.ReactNode {
161
+ const v = useContext(context);
162
+
163
+ return <div>{v.get()}, World</div>;
164
+ }
165
+
166
+ const { getByText } = render(
167
+ <ContextProvider contexts={{}}>
168
+ <Component />
169
+ </ContextProvider>,
170
+ );
171
+
172
+ await expect.element(getByText('Hello, World')).toBeInTheDocument();
173
+
174
+ value.set('Hey');
175
+
176
+ await expect.element(getByText('Hey, World')).toBeInTheDocument();
177
+ });
178
+
179
+ test('useContext works inside computed value passed via context provider', async () => {
180
+ const value = state('Hello');
181
+ const override = state('Hey');
182
+ const context = createContext(value);
183
+
184
+ const derived = computed(() => `${useContext(context).get()}, World`);
185
+
186
+ function Component(): React.ReactNode {
187
+ return <div>{derived()}</div>;
188
+ }
189
+
190
+ const { getByText } = render(
191
+ <ContextProvider contexts={{ [context]: override }}>
192
+ <Component />
193
+ </ContextProvider>,
194
+ );
195
+
196
+ await expect.element(getByText('Hey, World')).toBeInTheDocument();
197
+
198
+ override.set('Hi');
199
+
200
+ await expect.element(getByText('Hi, World')).toBeInTheDocument();
201
+ });
202
+
203
+ test('useContext works at root level with default value', async () => {
204
+ const value = state('Hello');
205
+ const override = state('Hey');
206
+ const context = createContext(value);
207
+
208
+ function Component(): React.ReactNode {
209
+ const v = useContext(context);
210
+
211
+ return <div>{v.get()}, World</div>;
212
+ }
213
+
214
+ const { getByText } = render(
215
+ <ContextProvider contexts={{ [context]: override }}>
216
+ <Component />
217
+ </ContextProvider>,
218
+ );
219
+
220
+ await expect.element(getByText('Hey, World')).toBeInTheDocument();
221
+
222
+ override.set('Hi');
223
+
224
+ await expect.element(getByText('Hi, World')).toBeInTheDocument();
225
+ });
226
+ });
227
+ });
@@ -0,0 +1,8 @@
1
+ import { createContext, useContext } from 'react';
2
+ import { SignalScope } from '../hooks.js';
3
+
4
+ export const ScopeContext = createContext<SignalScope | undefined>(undefined);
5
+
6
+ export function useScope() {
7
+ return useContext(ScopeContext);
8
+ }
@@ -0,0 +1,4 @@
1
+ export { ContextProvider } from './provider.js';
2
+ export { useScope } from './context.js';
3
+ export { setupReact } from './setup.js';
4
+ export { useStateSignal } from './state.js';