signalium 1.0.2 → 1.1.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.
- package/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +12 -0
- package/dist/cjs/config.d.ts +2 -2
- package/dist/cjs/config.d.ts.map +1 -1
- package/dist/cjs/config.js.map +1 -1
- package/dist/cjs/hooks.d.ts.map +1 -1
- package/dist/cjs/hooks.js +14 -2
- package/dist/cjs/hooks.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/internals/async.d.ts +1 -1
- package/dist/cjs/internals/async.d.ts.map +1 -1
- package/dist/cjs/internals/async.js +20 -15
- package/dist/cjs/internals/async.js.map +1 -1
- package/dist/cjs/internals/connect.d.ts.map +1 -1
- package/dist/cjs/internals/connect.js +6 -0
- package/dist/cjs/internals/connect.js.map +1 -1
- package/dist/cjs/internals/contexts.d.ts +9 -2
- package/dist/cjs/internals/contexts.d.ts.map +1 -1
- package/dist/cjs/internals/contexts.js +37 -3
- package/dist/cjs/internals/contexts.js.map +1 -1
- package/dist/cjs/internals/derived.d.ts +15 -5
- package/dist/cjs/internals/derived.d.ts.map +1 -1
- package/dist/cjs/internals/derived.js +11 -11
- package/dist/cjs/internals/derived.js.map +1 -1
- package/dist/cjs/internals/get.js +2 -2
- package/dist/cjs/internals/get.js.map +1 -1
- package/dist/cjs/internals/scheduling.d.ts +2 -0
- package/dist/cjs/internals/scheduling.d.ts.map +1 -1
- package/dist/cjs/internals/scheduling.js +16 -1
- package/dist/cjs/internals/scheduling.js.map +1 -1
- package/dist/cjs/internals/state.d.ts +2 -2
- package/dist/cjs/internals/state.d.ts.map +1 -1
- package/dist/cjs/internals/utils/equals.d.ts +2 -0
- package/dist/cjs/internals/utils/equals.d.ts.map +1 -1
- package/dist/cjs/internals/utils/equals.js +5 -3
- package/dist/cjs/internals/utils/equals.js.map +1 -1
- package/dist/cjs/react/context.d.ts.map +1 -1
- package/dist/cjs/react/context.js +5 -0
- package/dist/cjs/react/context.js.map +1 -1
- package/dist/cjs/react/provider.d.ts.map +1 -1
- package/dist/cjs/react/provider.js +0 -4
- package/dist/cjs/react/provider.js.map +1 -1
- package/dist/cjs/react/rendering.d.ts +2 -0
- package/dist/cjs/react/rendering.d.ts.map +1 -0
- package/dist/cjs/react/rendering.js +25 -0
- package/dist/cjs/react/rendering.js.map +1 -0
- package/dist/cjs/react/signal-value.d.ts +1 -1
- package/dist/cjs/react/signal-value.d.ts.map +1 -1
- package/dist/cjs/react/signal-value.js +4 -53
- package/dist/cjs/react/signal-value.js.map +1 -1
- package/dist/cjs/react/state.d.ts +2 -2
- package/dist/cjs/react/state.d.ts.map +1 -1
- package/dist/cjs/react/state.js.map +1 -1
- package/dist/cjs/types.d.ts +13 -1
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/esm/config.d.ts +2 -2
- package/dist/esm/config.d.ts.map +1 -1
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/hooks.d.ts.map +1 -1
- package/dist/esm/hooks.js +14 -2
- package/dist/esm/hooks.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internals/async.d.ts +1 -1
- package/dist/esm/internals/async.d.ts.map +1 -1
- package/dist/esm/internals/async.js +21 -16
- package/dist/esm/internals/async.js.map +1 -1
- package/dist/esm/internals/connect.d.ts.map +1 -1
- package/dist/esm/internals/connect.js +6 -0
- package/dist/esm/internals/connect.js.map +1 -1
- package/dist/esm/internals/contexts.d.ts +9 -2
- package/dist/esm/internals/contexts.d.ts.map +1 -1
- package/dist/esm/internals/contexts.js +35 -3
- package/dist/esm/internals/contexts.js.map +1 -1
- package/dist/esm/internals/derived.d.ts +15 -5
- package/dist/esm/internals/derived.d.ts.map +1 -1
- package/dist/esm/internals/derived.js +11 -11
- package/dist/esm/internals/derived.js.map +1 -1
- package/dist/esm/internals/get.js +2 -2
- package/dist/esm/internals/get.js.map +1 -1
- package/dist/esm/internals/scheduling.d.ts +2 -0
- package/dist/esm/internals/scheduling.d.ts.map +1 -1
- package/dist/esm/internals/scheduling.js +14 -0
- package/dist/esm/internals/scheduling.js.map +1 -1
- package/dist/esm/internals/state.d.ts +2 -2
- package/dist/esm/internals/state.d.ts.map +1 -1
- package/dist/esm/internals/utils/equals.d.ts +2 -0
- package/dist/esm/internals/utils/equals.d.ts.map +1 -1
- package/dist/esm/internals/utils/equals.js +2 -2
- package/dist/esm/internals/utils/equals.js.map +1 -1
- package/dist/esm/react/context.d.ts.map +1 -1
- package/dist/esm/react/context.js +5 -0
- package/dist/esm/react/context.js.map +1 -1
- package/dist/esm/react/provider.d.ts.map +1 -1
- package/dist/esm/react/provider.js +0 -4
- package/dist/esm/react/provider.js.map +1 -1
- package/dist/esm/react/rendering.d.ts +2 -0
- package/dist/esm/react/rendering.d.ts.map +1 -0
- package/dist/esm/react/rendering.js +19 -0
- package/dist/esm/react/rendering.js.map +1 -0
- package/dist/esm/react/signal-value.d.ts +1 -1
- package/dist/esm/react/signal-value.d.ts.map +1 -1
- package/dist/esm/react/signal-value.js +2 -18
- package/dist/esm/react/signal-value.js.map +1 -1
- package/dist/esm/react/state.d.ts +2 -2
- package/dist/esm/react/state.d.ts.map +1 -1
- package/dist/esm/react/state.js.map +1 -1
- package/dist/esm/types.d.ts +13 -1
- package/dist/esm/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/context.test.ts +55 -1
- package/src/__tests__/gc.test.ts +256 -0
- package/src/__tests__/utils/instrumented-hooks.ts +8 -11
- package/src/config.ts +5 -3
- package/src/hooks.ts +17 -3
- package/src/index.ts +8 -1
- package/src/internals/async.ts +13 -8
- package/src/internals/connect.ts +8 -0
- package/src/internals/contexts.ts +45 -5
- package/src/internals/derived.ts +25 -16
- package/src/internals/get.ts +2 -2
- package/src/internals/scheduling.ts +20 -0
- package/src/internals/state.ts +3 -3
- package/src/internals/utils/equals.ts +2 -2
- package/src/react/__tests__/contexts.test.tsx +20 -1
- package/src/react/context.ts +6 -0
- package/src/react/provider.tsx +0 -6
- package/src/react/rendering.ts +25 -0
- package/src/react/signal-value.ts +3 -26
- package/src/react/state.ts +3 -3
- package/src/types.ts +15 -1
package/src/hooks.ts
CHANGED
@@ -11,9 +11,10 @@ import {
|
|
11
11
|
import { useDerivedSignal } from './config.js';
|
12
12
|
import { getCurrentScope, SignalScope } from './internals/contexts.js';
|
13
13
|
import { createStateSignal } from './internals/state.js';
|
14
|
-
import { createDerivedSignal } from './internals/derived.js';
|
14
|
+
import { createDerivedSignal, DerivedSignalDefinition } from './internals/derived.js';
|
15
15
|
import { ReactivePromise } from './internals/async.js';
|
16
16
|
import { Tracer } from './trace.js';
|
17
|
+
import { equalsFrom } from './internals/utils/equals.js';
|
17
18
|
|
18
19
|
export const state = createStateSignal;
|
19
20
|
|
@@ -29,9 +30,16 @@ export function reactive<T, Args extends unknown[]>(
|
|
29
30
|
fn: (...args: Args) => T,
|
30
31
|
opts?: Partial<SignalOptionsWithInit<T, Args>>,
|
31
32
|
): (...args: Args) => ReactiveValue<T> {
|
33
|
+
const def: DerivedSignalDefinition<T, Args> = {
|
34
|
+
compute: fn,
|
35
|
+
equals: equalsFrom(opts?.equals),
|
36
|
+
shouldGC: opts?.shouldGC,
|
37
|
+
isSubscription: false,
|
38
|
+
};
|
39
|
+
|
32
40
|
return (...args) => {
|
33
41
|
const scope = getCurrentScope();
|
34
|
-
const signal = scope.get(
|
42
|
+
const signal = scope.get(def, args, opts);
|
35
43
|
|
36
44
|
return useDerivedSignal(signal)!;
|
37
45
|
};
|
@@ -64,5 +72,11 @@ export function watcher<T>(
|
|
64
72
|
fn: () => T,
|
65
73
|
opts?: SignalOptions<T, unknown[]> & { scope?: SignalScope; tracer?: Tracer },
|
66
74
|
): Signal<ReactiveValue<T>> {
|
67
|
-
|
75
|
+
const def: DerivedSignalDefinition<T, unknown[]> = {
|
76
|
+
compute: fn,
|
77
|
+
equals: equalsFrom(opts?.equals),
|
78
|
+
isSubscription: false,
|
79
|
+
};
|
80
|
+
|
81
|
+
return createDerivedSignal(def, undefined, undefined, opts?.scope, opts);
|
68
82
|
}
|
package/src/index.ts
CHANGED
@@ -6,7 +6,14 @@ export { isReactivePromise, isReactiveTask, isReactiveSubscription } from './int
|
|
6
6
|
|
7
7
|
export { callback } from './internals/get.js';
|
8
8
|
|
9
|
-
export {
|
9
|
+
export {
|
10
|
+
createContext,
|
11
|
+
useContext,
|
12
|
+
withContexts,
|
13
|
+
setRootContexts,
|
14
|
+
SignalScope,
|
15
|
+
CONTEXT_KEY,
|
16
|
+
} from './internals/contexts.js';
|
10
17
|
|
11
18
|
export { setConfig } from './config.js';
|
12
19
|
|
package/src/internals/async.ts
CHANGED
@@ -8,7 +8,7 @@ import {
|
|
8
8
|
SignalSubscribe,
|
9
9
|
SignalSubscription,
|
10
10
|
} from '../types.js';
|
11
|
-
import { createDerivedSignal, DerivedSignal, SignalState } from './derived.js';
|
11
|
+
import { createDerivedSignal, DerivedSignal, DerivedSignalDefinition, SignalState } from './derived.js';
|
12
12
|
import { CURRENT_CONSUMER, generatorResultToPromise, getSignal } from './get.js';
|
13
13
|
import { dirtySignal, dirtySignalConsumers } from './dirty.js';
|
14
14
|
import { scheduleAsyncPull, schedulePull, setResolved } from './scheduling.js';
|
@@ -17,7 +17,7 @@ import { getCurrentScope, ROOT_SCOPE, SignalScope, withScope } from './contexts.
|
|
17
17
|
import { createStateSignal } from './state.js';
|
18
18
|
import { useStateSignal } from '../config.js';
|
19
19
|
import { isGeneratorResult, isPromise } from './utils/type-utils.js';
|
20
|
-
import { equalsFrom } from './utils/equals.js';
|
20
|
+
import { DEFAULT_EQUALS, equalsFrom, FALSE_EQUALS } from './utils/equals.js';
|
21
21
|
|
22
22
|
const enum AsyncFlags {
|
23
23
|
// ======= Notifiers ========
|
@@ -72,10 +72,10 @@ export class ReactivePromise<T, Args extends unknown[] = unknown[]> implements B
|
|
72
72
|
private _boundRun: ((...args: Args) => ReactivePromise<T, Args>) | undefined;
|
73
73
|
|
74
74
|
static createPromise<T>(promise: Promise<T>, signal?: DerivedSignal<T, unknown[]>, initValue?: T | undefined) {
|
75
|
-
const p = new ReactivePromise();
|
75
|
+
const p = new ReactivePromise<T>();
|
76
76
|
|
77
77
|
p._signal = signal;
|
78
|
-
p._equals = signal?.equals ??
|
78
|
+
p._equals = signal?.def.equals ?? DEFAULT_EQUALS;
|
79
79
|
|
80
80
|
p._initFlags(AsyncFlags.Pending, initValue);
|
81
81
|
|
@@ -130,8 +130,8 @@ export class ReactivePromise<T, Args extends unknown[] = unknown[]> implements B
|
|
130
130
|
},
|
131
131
|
};
|
132
132
|
|
133
|
-
|
134
|
-
() => {
|
133
|
+
const def: DerivedSignalDefinition<() => void, unknown[]> = {
|
134
|
+
compute: () => {
|
135
135
|
if (active === false) {
|
136
136
|
currentSub = subscribe(state);
|
137
137
|
active = true;
|
@@ -144,11 +144,16 @@ export class ReactivePromise<T, Args extends unknown[] = unknown[]> implements B
|
|
144
144
|
|
145
145
|
return unsubscribe;
|
146
146
|
},
|
147
|
+
equals: DEFAULT_EQUALS,
|
148
|
+
isSubscription: true,
|
149
|
+
};
|
150
|
+
|
151
|
+
p._signal = createDerivedSignal<() => void, unknown[]>(
|
152
|
+
def,
|
147
153
|
[],
|
148
154
|
undefined,
|
149
155
|
scope,
|
150
|
-
opts as Omit<SignalOptionsWithInit<T, unknown[]>, 'equals' | 'initValue' | 'paramKey'>,
|
151
|
-
true,
|
156
|
+
opts as Omit<SignalOptionsWithInit<T, unknown[]>, 'equals' | 'initValue' | 'paramKey' | 'shouldGC'>,
|
152
157
|
);
|
153
158
|
|
154
159
|
p._equals = equalsFrom(opts?.equals);
|
package/src/internals/connect.ts
CHANGED
@@ -10,6 +10,9 @@ export function watchSignal(signal: DerivedSignal<any, any>): void {
|
|
10
10
|
// If > 0, already watching, return
|
11
11
|
if (watchCount > 0) return;
|
12
12
|
|
13
|
+
// If signal is being watched again, remove from GC candidates
|
14
|
+
signal.scope?.removeFromGc(signal);
|
15
|
+
|
13
16
|
for (const dep of signal.deps.keys()) {
|
14
17
|
watchSignal(dep);
|
15
18
|
}
|
@@ -38,4 +41,9 @@ export function unwatchSignal(signal: DerivedSignal<any, any>, count = 1) {
|
|
38
41
|
// teardown the subscription
|
39
42
|
signal.value?.();
|
40
43
|
}
|
44
|
+
|
45
|
+
// If watchCount is now zero, mark the signal for GC
|
46
|
+
if (newWatchCount === 0 && signal.scope) {
|
47
|
+
signal.scope.markForGc(signal);
|
48
|
+
}
|
41
49
|
}
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import { getFrameworkScope } from '../config.js';
|
2
2
|
import { SignalOptionsWithInit } from '../types.js';
|
3
|
-
import { DerivedSignal, createDerivedSignal } from './derived.js';
|
3
|
+
import { DerivedSignal, DerivedSignalDefinition, createDerivedSignal } from './derived.js';
|
4
4
|
import { CURRENT_CONSUMER } from './get.js';
|
5
5
|
import { hashReactiveFn, hashValue } from './utils/hash.js';
|
6
|
+
import { scheduleGcSweep } from './scheduling.js';
|
6
7
|
|
7
8
|
export const CONTEXT_KEY = Symbol('signalium:context');
|
8
9
|
|
@@ -46,7 +47,8 @@ export class SignalScope {
|
|
46
47
|
private parentScope?: SignalScope = undefined;
|
47
48
|
private contexts: Record<symbol, unknown>;
|
48
49
|
private children = new Map<number, SignalScope>();
|
49
|
-
private signals = new Map<number, DerivedSignal<any, any
|
50
|
+
private signals = new Map<number, DerivedSignal<any, any>>();
|
51
|
+
private gcCandidates = new Set<DerivedSignal<any, any>>();
|
50
52
|
|
51
53
|
getChild(contexts: [ContextImpl<unknown>, unknown][]) {
|
52
54
|
const key = hashValue(contexts);
|
@@ -68,25 +70,58 @@ export class SignalScope {
|
|
68
70
|
}
|
69
71
|
|
70
72
|
get<T, Args extends unknown[]>(
|
71
|
-
|
73
|
+
def: DerivedSignalDefinition<T, Args>,
|
72
74
|
args: Args,
|
73
75
|
opts?: Partial<SignalOptionsWithInit<T, Args>>,
|
74
76
|
): DerivedSignal<T, Args> {
|
75
77
|
const paramKey = opts?.paramKey?.(...args);
|
76
|
-
const key = hashReactiveFn(
|
78
|
+
const key = hashReactiveFn(def.compute, paramKey ? [paramKey] : args);
|
77
79
|
let signal = this.signals.get(key) as DerivedSignal<T, Args> | undefined;
|
78
80
|
|
79
81
|
if (signal === undefined) {
|
80
|
-
signal = createDerivedSignal(
|
82
|
+
signal = createDerivedSignal(def, args, key, this, opts);
|
81
83
|
this.signals.set(key, signal);
|
82
84
|
}
|
83
85
|
|
84
86
|
return signal;
|
85
87
|
}
|
88
|
+
|
89
|
+
markForGc(signal: DerivedSignal<any, any>) {
|
90
|
+
if (!this.gcCandidates.has(signal)) {
|
91
|
+
this.gcCandidates.add(signal);
|
92
|
+
scheduleGcSweep(this);
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
removeFromGc(signal: DerivedSignal<any, any>) {
|
97
|
+
this.gcCandidates.delete(signal);
|
98
|
+
}
|
99
|
+
|
100
|
+
forceGc(signal: DerivedSignal<any, any>) {
|
101
|
+
this.signals.delete(signal.key!);
|
102
|
+
}
|
103
|
+
|
104
|
+
sweepGc() {
|
105
|
+
for (const signal of this.gcCandidates) {
|
106
|
+
if (signal.watchCount === 0) {
|
107
|
+
const { shouldGC } = signal.def;
|
108
|
+
|
109
|
+
if (!shouldGC || shouldGC(signal, signal.value, signal.args)) {
|
110
|
+
this.signals.delete(signal.key!);
|
111
|
+
}
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
this.gcCandidates = new Set();
|
116
|
+
}
|
86
117
|
}
|
87
118
|
|
88
119
|
export let ROOT_SCOPE = new SignalScope([]);
|
89
120
|
|
121
|
+
export function setRootContexts<C extends unknown[], U>(contexts: [...ContextPair<C>]): void {
|
122
|
+
ROOT_SCOPE = new SignalScope(contexts as [ContextImpl<unknown>, unknown][], ROOT_SCOPE);
|
123
|
+
}
|
124
|
+
|
90
125
|
export const clearRootScope = () => {
|
91
126
|
ROOT_SCOPE = new SignalScope([]);
|
92
127
|
};
|
@@ -131,3 +166,8 @@ export const useContext = <T>(context: Context<T>): T => {
|
|
131
166
|
|
132
167
|
return scope.getContext(context) ?? (context as unknown as ContextImpl<T>).defaultValue;
|
133
168
|
};
|
169
|
+
|
170
|
+
export function forceGc(_signal: object) {
|
171
|
+
const signal = _signal as DerivedSignal<any, any>;
|
172
|
+
signal.scope?.forceGc(signal);
|
173
|
+
}
|
package/src/internals/derived.ts
CHANGED
@@ -3,12 +3,11 @@ import { Tracer, TRACER, TracerMeta } from '../trace.js';
|
|
3
3
|
import { ReactiveValue, Signal, SignalEquals, SignalListener, SignalOptionsWithInit } from '../types.js';
|
4
4
|
import { getUnknownSignalFnName } from './utils/debug-name.js';
|
5
5
|
import { SignalScope } from './contexts.js';
|
6
|
-
import {
|
7
|
-
import { Edge
|
6
|
+
import { getSignal } from './get.js';
|
7
|
+
import { Edge } from './edge.js';
|
8
8
|
import { schedulePull, scheduleUnwatch } from './scheduling.js';
|
9
9
|
import { hashValue } from './utils/hash.js';
|
10
10
|
import { stringifyValue } from './utils/stringify.js';
|
11
|
-
import { equalsFrom } from './utils/equals.js';
|
12
11
|
|
13
12
|
/**
|
14
13
|
* This file contains computed signal base types and struct definitions.
|
@@ -52,6 +51,17 @@ interface ListenerMeta {
|
|
52
51
|
cachedBoundAdd: (listener: SignalListener) => () => void;
|
53
52
|
}
|
54
53
|
|
54
|
+
/**
|
55
|
+
* Shared definition for derived signals to reduce memory usage.
|
56
|
+
* Contains configuration that's common across all instances of a reactive function.
|
57
|
+
*/
|
58
|
+
export interface DerivedSignalDefinition<T, Args extends unknown[]> {
|
59
|
+
compute: (...args: Args) => T;
|
60
|
+
equals: SignalEquals<T>;
|
61
|
+
shouldGC?: (signal: object, value: T, args: Args) => boolean;
|
62
|
+
isSubscription: boolean;
|
63
|
+
}
|
64
|
+
|
55
65
|
export class DerivedSignal<T, Args extends unknown[]> implements Signal<ReactiveValue<T>> {
|
56
66
|
// Bitmask containing state in the first 2 bits and boolean properties in the remaining bits
|
57
67
|
private flags: number;
|
@@ -62,7 +72,6 @@ export class DerivedSignal<T, Args extends unknown[]> implements Signal<Reactive
|
|
62
72
|
|
63
73
|
ref: WeakRef<DerivedSignal<T, Args>> = new WeakRef(this);
|
64
74
|
|
65
|
-
equals: SignalEquals<any>;
|
66
75
|
dirtyHead: Edge | undefined = undefined;
|
67
76
|
|
68
77
|
updatedCount: number = 0;
|
@@ -72,32 +81,33 @@ export class DerivedSignal<T, Args extends unknown[]> implements Signal<Reactive
|
|
72
81
|
|
73
82
|
_listeners: ListenerMeta | null = null;
|
74
83
|
|
75
|
-
|
84
|
+
key: SignalId | undefined;
|
76
85
|
args: Args;
|
77
86
|
value: ReactiveValue<T> | undefined;
|
78
87
|
|
79
88
|
tracerMeta?: TracerMeta;
|
80
89
|
|
90
|
+
// Reference to the shared definition
|
91
|
+
def: DerivedSignalDefinition<T, Args>;
|
92
|
+
|
81
93
|
constructor(
|
82
|
-
|
83
|
-
compute: (...args: Args) => T,
|
94
|
+
definition: DerivedSignalDefinition<T, Args>,
|
84
95
|
args: Args,
|
85
96
|
key?: SignalId,
|
86
97
|
scope?: SignalScope,
|
87
98
|
opts?: Partial<SignalOptionsWithInit<T, Args>> & { tracer?: Tracer },
|
88
99
|
) {
|
89
|
-
this.flags = (isSubscription ? SignalFlags.isSubscription : 0) | SignalState.Dirty;
|
100
|
+
this.flags = (definition.isSubscription ? SignalFlags.isSubscription : 0) | SignalState.Dirty;
|
90
101
|
this.scope = scope;
|
91
|
-
this.
|
102
|
+
this.key = key;
|
92
103
|
this.args = args;
|
93
|
-
|
94
|
-
this.equals = equalsFrom(opts?.equals);
|
104
|
+
this.def = definition;
|
95
105
|
this.value = opts?.initValue as ReactiveValue<T>;
|
96
106
|
|
97
107
|
if (TRACER) {
|
98
108
|
this.tracerMeta = {
|
99
|
-
id: opts?.id ?? key ?? hashValue([compute, ID++]),
|
100
|
-
desc: opts?.desc ?? compute.name ?? getUnknownSignalFnName(compute),
|
109
|
+
id: opts?.id ?? key ?? hashValue([definition.compute, ID++]),
|
110
|
+
desc: opts?.desc ?? definition.compute.name ?? getUnknownSignalFnName(definition.compute),
|
101
111
|
params: args.map(arg => stringifyValue(arg)).join(', '),
|
102
112
|
tracer: opts?.tracer,
|
103
113
|
};
|
@@ -197,12 +207,11 @@ export const isSubscription = (signal: unknown): boolean => {
|
|
197
207
|
};
|
198
208
|
|
199
209
|
export function createDerivedSignal<T, Args extends unknown[]>(
|
200
|
-
|
210
|
+
def: DerivedSignalDefinition<T, Args>,
|
201
211
|
args: Args = [] as any,
|
202
212
|
key?: SignalId,
|
203
213
|
scope?: SignalScope,
|
204
214
|
opts?: Partial<SignalOptionsWithInit<T, Args>> & { tracer?: Tracer },
|
205
|
-
isSubscription: boolean = false,
|
206
215
|
): DerivedSignal<T, Args> {
|
207
|
-
return new DerivedSignal(
|
216
|
+
return new DerivedSignal(def, args, key, scope, opts);
|
208
217
|
}
|
package/src/internals/get.ts
CHANGED
@@ -128,7 +128,7 @@ export function runSignal(signal: DerivedSignal<any, any[]>) {
|
|
128
128
|
|
129
129
|
const initialized = updatedCount !== 0;
|
130
130
|
const prevValue = signal.value;
|
131
|
-
let nextValue = signal.compute(...signal.args);
|
131
|
+
let nextValue = signal.def.compute(...signal.args);
|
132
132
|
let valueIsPromise = false;
|
133
133
|
|
134
134
|
if (nextValue !== null && typeof nextValue === 'object') {
|
@@ -177,7 +177,7 @@ export function runSignal(signal: DerivedSignal<any, any[]>) {
|
|
177
177
|
signal.value = ReactivePromise.createPromise(nextValue, signal, initValue);
|
178
178
|
signal.updatedCount = updatedCount + 1;
|
179
179
|
}
|
180
|
-
} else if (!initialized || !signal.equals(prevValue!, nextValue)) {
|
180
|
+
} else if (!initialized || !signal.def.equals(prevValue!, nextValue)) {
|
181
181
|
signal.value = nextValue;
|
182
182
|
signal.updatedCount = updatedCount + 1;
|
183
183
|
}
|
@@ -6,6 +6,11 @@ import { runListeners as runStateListeners } from './state.js';
|
|
6
6
|
import { Tracer } from '../trace.js';
|
7
7
|
import { unwatchSignal } from './connect.js';
|
8
8
|
import { StateSignal } from './state.js';
|
9
|
+
import { ROOT_SCOPE, SignalScope } from './contexts.js';
|
10
|
+
|
11
|
+
// Determine once at startup which scheduling function to use for GC
|
12
|
+
const scheduleIdleCallback =
|
13
|
+
typeof requestIdleCallback === 'function' ? requestIdleCallback : (cb: () => void) => _scheduleFlush(cb);
|
9
14
|
|
10
15
|
let PROMISE_WAS_RESOLVED = false;
|
11
16
|
|
@@ -14,6 +19,7 @@ let PENDING_ASYNC_PULLS: DerivedSignal<any, any>[] = [];
|
|
14
19
|
let PENDING_UNWATCH = new Map<DerivedSignal<any, any>, number>();
|
15
20
|
let PENDING_LISTENERS: (DerivedSignal<any, any> | StateSignal<any>)[] = [];
|
16
21
|
let PENDING_TRACERS: Tracer[] = [];
|
22
|
+
let PENDING_GC = new Set<SignalScope>();
|
17
23
|
|
18
24
|
const microtask = () => Promise.resolve();
|
19
25
|
|
@@ -62,6 +68,20 @@ export const scheduleTracer = (tracer: Tracer) => {
|
|
62
68
|
scheduleFlush(flushWatchers);
|
63
69
|
};
|
64
70
|
|
71
|
+
export const scheduleGcSweep = (scope: SignalScope) => {
|
72
|
+
PENDING_GC.add(scope);
|
73
|
+
|
74
|
+
if (PENDING_GC.size > 1) return;
|
75
|
+
|
76
|
+
scheduleIdleCallback(() => {
|
77
|
+
for (const scope of PENDING_GC) {
|
78
|
+
scope.sweepGc();
|
79
|
+
}
|
80
|
+
|
81
|
+
PENDING_GC.clear();
|
82
|
+
});
|
83
|
+
};
|
84
|
+
|
65
85
|
const flushWatchers = async () => {
|
66
86
|
const flush = currentFlush!;
|
67
87
|
|
package/src/internals/state.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import { TRACER as TRACER, TracerEventType } from '../trace.js';
|
2
|
-
import {
|
3
|
-
import { DerivedSignal
|
2
|
+
import { SignalEquals, SignalListener, SignalOptions, StateSignal as IStateSignal } from '../types.js';
|
3
|
+
import { DerivedSignal } from './derived.js';
|
4
4
|
import { dirtySignal } from './dirty.js';
|
5
5
|
import { CURRENT_CONSUMER } from './get.js';
|
6
6
|
import { useStateSignal } from '../config.js';
|
@@ -8,7 +8,7 @@ import { scheduleListeners } from './scheduling.js';
|
|
8
8
|
|
9
9
|
let STATE_ID = 0;
|
10
10
|
|
11
|
-
export class StateSignal<T> implements
|
11
|
+
export class StateSignal<T> implements IStateSignal<T> {
|
12
12
|
private _value: T;
|
13
13
|
private _equals: SignalEquals<T>;
|
14
14
|
private _subs = new Map<WeakRef<DerivedSignal<unknown, unknown[]>>, number>();
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { SignalEquals } from '../../types.js';
|
2
2
|
|
3
|
-
const DEFAULT_EQUALS: SignalEquals<unknown> = (a, b) => a === b;
|
4
|
-
const FALSE_EQUALS: SignalEquals<unknown> = () => false;
|
3
|
+
export const DEFAULT_EQUALS: SignalEquals<unknown> = (a, b) => a === b;
|
4
|
+
export const FALSE_EQUALS: SignalEquals<unknown> = () => false;
|
5
5
|
|
6
6
|
export const equalsFrom = <T>(equals: SignalEquals<T> | false | undefined): SignalEquals<T> => {
|
7
7
|
if (equals === false) {
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
2
2
|
import { render } from 'vitest-browser-react';
|
3
3
|
import { state, reactive, createContext, useContext } from 'signalium';
|
4
|
-
import { ContextProvider, setupReact } from '../index.js';
|
4
|
+
import { ContextProvider, setupReact, useScope } from '../index.js';
|
5
5
|
import React, { useState } from 'react';
|
6
6
|
|
7
7
|
setupReact();
|
@@ -96,4 +96,23 @@ describe('React > contexts', () => {
|
|
96
96
|
|
97
97
|
await expect.element(getByText('Hi, World')).toBeInTheDocument();
|
98
98
|
});
|
99
|
+
|
100
|
+
test('useScope returns undefined outside of rendering context', async () => {
|
101
|
+
// Direct call outside of rendering should return undefined
|
102
|
+
expect(useScope()).toBeUndefined();
|
103
|
+
|
104
|
+
// Inside a component during rendering, it should return the scope
|
105
|
+
function TestComponent() {
|
106
|
+
const scope = useScope();
|
107
|
+
return <div data-testid="scope">{scope ? 'has-scope' : 'no-scope'}</div>;
|
108
|
+
}
|
109
|
+
|
110
|
+
const { getByTestId } = render(
|
111
|
+
<ContextProvider contexts={[]}>
|
112
|
+
<TestComponent />
|
113
|
+
</ContextProvider>,
|
114
|
+
);
|
115
|
+
|
116
|
+
await expect.element(getByTestId('scope')).toHaveTextContent('has-scope');
|
117
|
+
});
|
99
118
|
});
|
package/src/react/context.ts
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
import { createContext, useContext } from 'react';
|
2
2
|
import { SignalScope } from '../internals/contexts.js';
|
3
|
+
import { isRendering } from './rendering.js';
|
3
4
|
|
4
5
|
export const ScopeContext = createContext<SignalScope | undefined>(undefined);
|
5
6
|
|
6
7
|
export function useScope() {
|
8
|
+
if (!isRendering()) {
|
9
|
+
return undefined;
|
10
|
+
}
|
11
|
+
|
12
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
7
13
|
return useContext(ScopeContext);
|
8
14
|
}
|
package/src/react/provider.tsx
CHANGED
@@ -13,12 +13,6 @@ export function ContextProvider<C extends unknown[]>({
|
|
13
13
|
inherit?: boolean;
|
14
14
|
root?: boolean;
|
15
15
|
}) {
|
16
|
-
// if (root) {
|
17
|
-
// useEffect(() => )
|
18
|
-
|
19
|
-
// return <ScopeContext.Provider value={scope}>{children}</ScopeContext.Provider>;
|
20
|
-
// }
|
21
|
-
|
22
16
|
const parentScope = useContext(ScopeContext);
|
23
17
|
const scope = new SignalScope(contexts as [ContextImpl<unknown>, unknown][], inherit ? parentScope : undefined);
|
24
18
|
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
// This is a private React internal that we need to access to check if we are rendering.
|
4
|
+
// There is no other consistent way to check if we are rendering in both development
|
5
|
+
// and production, and it doesn't appear that the React team wants to add one. This
|
6
|
+
// should be checked on every major React version upgrade.
|
7
|
+
const REACT_INTERNALS =
|
8
|
+
(React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED ||
|
9
|
+
(React as any).__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ||
|
10
|
+
(React as any).__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
11
|
+
|
12
|
+
const IS_REACT_18 = !!REACT_INTERNALS.ReactCurrentDispatcher;
|
13
|
+
const ReactCurrentDispatcher = REACT_INTERNALS.ReactCurrentDispatcher || REACT_INTERNALS;
|
14
|
+
|
15
|
+
export function isRendering() {
|
16
|
+
const dispatcher = IS_REACT_18 ? ReactCurrentDispatcher.current : ReactCurrentDispatcher.H;
|
17
|
+
|
18
|
+
return (
|
19
|
+
!!dispatcher &&
|
20
|
+
// dispatcher can be in a state where it's defined, but all hooks are invalid to call.
|
21
|
+
// Only way we can tell is that if they are invalid, they will all be equal to each other
|
22
|
+
// (e.g. because it's the function that throws an error)
|
23
|
+
dispatcher.useState !== dispatcher.useEffect
|
24
|
+
);
|
25
|
+
}
|
@@ -1,34 +1,11 @@
|
|
1
1
|
/* eslint-disable react-hooks/rules-of-hooks */
|
2
|
-
import
|
2
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
3
3
|
import type { DerivedSignal } from '../internals/derived.js';
|
4
4
|
import type { StateSignal } from '../internals/state.js';
|
5
5
|
import type { ReactiveValue } from '../types.js';
|
6
6
|
import { isReactivePromise } from '../internals/utils/type-utils.js';
|
7
7
|
import { isReactiveSubscription } from '../internals/async.js';
|
8
|
-
|
9
|
-
// This is a private React internal that we need to access to check if we are rendering.
|
10
|
-
// There is no other consistent way to check if we are rendering in both development
|
11
|
-
// and production, and it doesn't appear that the React team wants to add one. This
|
12
|
-
// should be checked on every major React version upgrade.
|
13
|
-
const REACT_INTERNALS =
|
14
|
-
(React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED ||
|
15
|
-
(React as any).__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ||
|
16
|
-
(React as any).__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
17
|
-
|
18
|
-
const IS_REACT_18 = !!REACT_INTERNALS.ReactCurrentDispatcher;
|
19
|
-
const ReactCurrentDispatcher = REACT_INTERNALS.ReactCurrentDispatcher || REACT_INTERNALS;
|
20
|
-
|
21
|
-
function isRendering() {
|
22
|
-
const dispatcher = IS_REACT_18 ? ReactCurrentDispatcher.current : ReactCurrentDispatcher.H;
|
23
|
-
|
24
|
-
return (
|
25
|
-
!!dispatcher &&
|
26
|
-
// dispatcher can be in a state where it's defined, but all hooks are invalid to call.
|
27
|
-
// Only way we can tell is that if they are invalid, they will all be equal to each other
|
28
|
-
// (e.g. because it's the function that throws an error)
|
29
|
-
dispatcher.useState !== dispatcher.useEffect
|
30
|
-
);
|
31
|
-
}
|
8
|
+
import { isRendering } from './rendering.js';
|
32
9
|
|
33
10
|
export function useStateSignal<T>(signal: StateSignal<T>): T {
|
34
11
|
if (!isRendering()) {
|
@@ -42,7 +19,7 @@ export function useStateSignal<T>(signal: StateSignal<T>): T {
|
|
42
19
|
);
|
43
20
|
}
|
44
21
|
|
45
|
-
export function useDerivedSignal<T>(signal: DerivedSignal<T,
|
22
|
+
export function useDerivedSignal<T, Args extends unknown[]>(signal: DerivedSignal<T, Args>): ReactiveValue<T> {
|
46
23
|
if (!isRendering()) {
|
47
24
|
return signal.get();
|
48
25
|
}
|
package/src/react/state.ts
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
import { useRef } from 'react';
|
2
2
|
import { state } from '../hooks.js';
|
3
|
-
import { SignalOptions,
|
3
|
+
import { SignalOptions, StateSignal } from '../types.js';
|
4
4
|
|
5
|
-
export function useStateSignal<T>(value: T, opts?: SignalOptions<T, unknown[]>):
|
6
|
-
const ref = useRef<
|
5
|
+
export function useStateSignal<T>(value: T, opts?: SignalOptions<T, unknown[]>): StateSignal<T> {
|
6
|
+
const ref = useRef<StateSignal<T> | undefined>(undefined);
|
7
7
|
|
8
8
|
if (!ref.current) {
|
9
9
|
ref.current = state(value, opts);
|
package/src/types.ts
CHANGED
@@ -5,10 +5,17 @@ export interface Signal<T = unknown> {
|
|
5
5
|
addListener(listener: SignalListener): () => void;
|
6
6
|
}
|
7
7
|
|
8
|
-
export interface
|
8
|
+
export interface StateSignal<T> extends Signal<T> {
|
9
9
|
set(value: T): void;
|
10
|
+
peek(): T;
|
11
|
+
update(updater: (value: T) => T): void;
|
10
12
|
}
|
11
13
|
|
14
|
+
/**
|
15
|
+
* @deprecated Use `StateSignal` instead.
|
16
|
+
*/
|
17
|
+
export type WriteableSignal<T> = StateSignal<T>;
|
18
|
+
|
12
19
|
export type SignalEquals<T> = (prev: T, next: T) => boolean;
|
13
20
|
|
14
21
|
export type SignalListener = () => void;
|
@@ -34,6 +41,13 @@ export interface SignalOptions<T, Args extends unknown[]> {
|
|
34
41
|
desc?: string;
|
35
42
|
scope?: SignalScope;
|
36
43
|
paramKey?: (...args: Args) => string;
|
44
|
+
|
45
|
+
/**
|
46
|
+
* Called when signal's watchCount reaches 0.
|
47
|
+
* Return `true` to allow GC, `false` to prevent it.
|
48
|
+
* If not provided, defaults to always allowing GC.
|
49
|
+
*/
|
50
|
+
shouldGC?: (signal: object, value: T, args: Args) => boolean;
|
37
51
|
}
|
38
52
|
|
39
53
|
export interface SignalOptionsWithInit<T, Args extends unknown[]> extends SignalOptions<T, Args> {
|