signalium 1.0.2 → 1.1.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.
- package/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +18 -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 +2 -3
- package/dist/cjs/react/provider.d.ts.map +1 -1
- package/dist/cjs/react/provider.js +2 -6
- 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 +2 -3
- package/dist/esm/react/provider.d.ts.map +1 -1
- package/dist/esm/react/provider.js +3 -7
- 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 +103 -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 +84 -2
- package/src/react/context.ts +6 -0
- package/src/react/provider.tsx +4 -12
- 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
@@ -0,0 +1,256 @@
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
2
|
+
import { reactive, createContext, withContexts, watcher, state } from '../index.js';
|
3
|
+
import { SignalScope, ROOT_SCOPE, forceGc, clearRootScope } from '../internals/contexts.js';
|
4
|
+
import { nextTick, sleep } from './utils/async.js';
|
5
|
+
|
6
|
+
// Helper to access private properties for testing
|
7
|
+
const getSignalsMap = (scope: SignalScope) => {
|
8
|
+
return (scope as any).signals as Map<number, any>;
|
9
|
+
};
|
10
|
+
|
11
|
+
const getGCCandidates = (scope: SignalScope) => {
|
12
|
+
return (scope as any).gcCandidates as Set<any>;
|
13
|
+
};
|
14
|
+
|
15
|
+
describe('Garbage Collection', () => {
|
16
|
+
beforeEach(() => {
|
17
|
+
clearRootScope();
|
18
|
+
});
|
19
|
+
|
20
|
+
it('should automatically garbage collect unwatched signals', async () => {
|
21
|
+
const testSignal = reactive(() => 42);
|
22
|
+
|
23
|
+
const w = watcher(() => testSignal());
|
24
|
+
|
25
|
+
// Watch the signal
|
26
|
+
const unwatch = w.addListener(() => {
|
27
|
+
testSignal();
|
28
|
+
});
|
29
|
+
|
30
|
+
await nextTick();
|
31
|
+
|
32
|
+
// Signal should be in the scope
|
33
|
+
expect(getSignalsMap(ROOT_SCOPE).size).toBe(1);
|
34
|
+
|
35
|
+
// Unwatch the signal
|
36
|
+
unwatch();
|
37
|
+
|
38
|
+
await sleep(50);
|
39
|
+
|
40
|
+
// Signal should be garbage collected
|
41
|
+
expect(getSignalsMap(ROOT_SCOPE).size).toBe(0);
|
42
|
+
});
|
43
|
+
|
44
|
+
it('should not garbage collect signals with shouldGC returning false', async () => {
|
45
|
+
// Create a signal that should not be garbage collected
|
46
|
+
const persistentSignal = reactive(() => 'persist', {
|
47
|
+
shouldGC: () => false,
|
48
|
+
});
|
49
|
+
|
50
|
+
const w = watcher(() => persistentSignal());
|
51
|
+
|
52
|
+
// Watch the signal
|
53
|
+
const unwatch = w.addListener(() => {
|
54
|
+
persistentSignal();
|
55
|
+
});
|
56
|
+
|
57
|
+
await nextTick();
|
58
|
+
|
59
|
+
// Signal should be in the scope
|
60
|
+
expect(getSignalsMap(ROOT_SCOPE).size).toBe(1);
|
61
|
+
|
62
|
+
// Unwatch the signal
|
63
|
+
unwatch();
|
64
|
+
|
65
|
+
await sleep(50);
|
66
|
+
|
67
|
+
// Signal should still be in the scope
|
68
|
+
expect(getSignalsMap(ROOT_SCOPE).size).toBe(1);
|
69
|
+
});
|
70
|
+
|
71
|
+
it('should support conditional GC based on signal state', async () => {
|
72
|
+
// Create a signal with conditional GC
|
73
|
+
const shouldAllowGC = state(false);
|
74
|
+
|
75
|
+
const conditionalSignal = reactive(() => shouldAllowGC.get(), {
|
76
|
+
shouldGC: (signal, value) => {
|
77
|
+
// console.log('shouldGC', signal, value);
|
78
|
+
return value;
|
79
|
+
},
|
80
|
+
});
|
81
|
+
|
82
|
+
const w = watcher(() => conditionalSignal());
|
83
|
+
|
84
|
+
// Watch the signal
|
85
|
+
const unwatch = w.addListener(() => {
|
86
|
+
conditionalSignal();
|
87
|
+
});
|
88
|
+
|
89
|
+
await nextTick();
|
90
|
+
|
91
|
+
// Signal should be in the scope
|
92
|
+
expect(getSignalsMap(ROOT_SCOPE).size).toBe(1);
|
93
|
+
|
94
|
+
// Unwatch the signal
|
95
|
+
unwatch();
|
96
|
+
|
97
|
+
await sleep(50);
|
98
|
+
|
99
|
+
// Signal should still be in the scope because shouldGC returns false
|
100
|
+
expect(getSignalsMap(ROOT_SCOPE).size).toBe(1);
|
101
|
+
|
102
|
+
// Now allow GC
|
103
|
+
shouldAllowGC.set(true);
|
104
|
+
|
105
|
+
// Watch the signal
|
106
|
+
const unwatch2 = w.addListener(() => {
|
107
|
+
conditionalSignal();
|
108
|
+
});
|
109
|
+
|
110
|
+
await nextTick();
|
111
|
+
|
112
|
+
// Signal should be in GC candidates
|
113
|
+
expect(getSignalsMap(ROOT_SCOPE).size).toBe(1);
|
114
|
+
|
115
|
+
unwatch2();
|
116
|
+
|
117
|
+
await sleep(50);
|
118
|
+
|
119
|
+
// Signal should be garbage collected
|
120
|
+
expect(getSignalsMap(ROOT_SCOPE).size).toBe(0);
|
121
|
+
});
|
122
|
+
|
123
|
+
it('should support manual garbage collection', async () => {
|
124
|
+
let signalObj: object;
|
125
|
+
|
126
|
+
// Create multiple signals
|
127
|
+
const signal1 = reactive(() => 'signal1');
|
128
|
+
const signal2 = reactive(() => 'signal2', {
|
129
|
+
shouldGC: signal => {
|
130
|
+
signalObj = signal;
|
131
|
+
return false;
|
132
|
+
},
|
133
|
+
});
|
134
|
+
|
135
|
+
const w1 = watcher(() => signal1());
|
136
|
+
const w2 = watcher(() => signal2());
|
137
|
+
|
138
|
+
// Watch both signals
|
139
|
+
const unwatch1 = w1.addListener(() => {
|
140
|
+
signal1();
|
141
|
+
});
|
142
|
+
const unwatch2 = w2.addListener(() => {
|
143
|
+
signal2();
|
144
|
+
});
|
145
|
+
|
146
|
+
await nextTick();
|
147
|
+
|
148
|
+
// Both signals should be in the scope
|
149
|
+
expect(getSignalsMap(ROOT_SCOPE).size).toBe(2);
|
150
|
+
|
151
|
+
// Unwatch both signals
|
152
|
+
unwatch1();
|
153
|
+
unwatch2();
|
154
|
+
|
155
|
+
await sleep(50);
|
156
|
+
|
157
|
+
// Only signal1 should be garbage collected
|
158
|
+
expect(getSignalsMap(ROOT_SCOPE).size).toBe(1);
|
159
|
+
|
160
|
+
// The remaining signal should be signal2
|
161
|
+
const remainingSignal = Array.from(getSignalsMap(ROOT_SCOPE).values())[0];
|
162
|
+
expect(remainingSignal.get()).toBe('signal2');
|
163
|
+
|
164
|
+
forceGc(signalObj!);
|
165
|
+
|
166
|
+
await sleep(50);
|
167
|
+
|
168
|
+
expect(getSignalsMap(ROOT_SCOPE).size).toBe(0);
|
169
|
+
});
|
170
|
+
|
171
|
+
it('should not garbage collect signals that are still being watched', async () => {
|
172
|
+
// Create a signal
|
173
|
+
const watchedSignal = reactive(() => 'watched');
|
174
|
+
|
175
|
+
const w = watcher(() => watchedSignal());
|
176
|
+
|
177
|
+
// Watch the signal but don't unwatch
|
178
|
+
w.addListener(() => {
|
179
|
+
watchedSignal();
|
180
|
+
});
|
181
|
+
|
182
|
+
await nextTick();
|
183
|
+
|
184
|
+
// Signal should be in the scope
|
185
|
+
expect(getSignalsMap(ROOT_SCOPE).size).toBe(1);
|
186
|
+
|
187
|
+
await sleep(50);
|
188
|
+
|
189
|
+
// Signal should still be in the scope because it's being watched
|
190
|
+
expect(getSignalsMap(ROOT_SCOPE).size).toBe(1);
|
191
|
+
});
|
192
|
+
|
193
|
+
it('should handle context-scoped signals correctly', async () => {
|
194
|
+
// Create a context
|
195
|
+
const TestContext = createContext('test');
|
196
|
+
|
197
|
+
// Create signals in context
|
198
|
+
let contextSignal: any;
|
199
|
+
|
200
|
+
withContexts([[TestContext, 'value']], () => {
|
201
|
+
contextSignal = reactive(() => 'context-scoped');
|
202
|
+
|
203
|
+
const w = watcher(() => contextSignal());
|
204
|
+
|
205
|
+
// Watch and unwatch
|
206
|
+
const unwatch = w.addListener(() => {
|
207
|
+
contextSignal();
|
208
|
+
});
|
209
|
+
|
210
|
+
unwatch();
|
211
|
+
});
|
212
|
+
|
213
|
+
await nextTick();
|
214
|
+
|
215
|
+
// Get the context scope (this is a bit hacky for testing)
|
216
|
+
const contextScope = (ROOT_SCOPE as any).children.values().next().value;
|
217
|
+
|
218
|
+
await sleep(50);
|
219
|
+
|
220
|
+
// Signal should be garbage collected from the context scope
|
221
|
+
expect(getSignalsMap(contextScope).size).toBe(0);
|
222
|
+
});
|
223
|
+
|
224
|
+
it('should remove signal from GC candidates if watched again', async () => {
|
225
|
+
// Create a signal
|
226
|
+
const signal = reactive(() => 'rewatch');
|
227
|
+
|
228
|
+
const w = watcher(() => signal());
|
229
|
+
|
230
|
+
// Watch and unwatch
|
231
|
+
const unwatch = w.addListener(() => {
|
232
|
+
signal();
|
233
|
+
});
|
234
|
+
|
235
|
+
await nextTick();
|
236
|
+
|
237
|
+
// Signal should be in the scope
|
238
|
+
expect(getSignalsMap(ROOT_SCOPE).size).toBe(1);
|
239
|
+
|
240
|
+
unwatch();
|
241
|
+
await nextTick();
|
242
|
+
|
243
|
+
// Signal should be in GC candidates
|
244
|
+
expect(getGCCandidates(ROOT_SCOPE).size).toBe(1);
|
245
|
+
|
246
|
+
// Watch again
|
247
|
+
w.addListener(() => {
|
248
|
+
signal();
|
249
|
+
});
|
250
|
+
|
251
|
+
await nextTick();
|
252
|
+
|
253
|
+
// Signal should be removed from GC candidates
|
254
|
+
expect(getGCCandidates(ROOT_SCOPE).size).toBe(0);
|
255
|
+
});
|
256
|
+
});
|
@@ -9,7 +9,7 @@ import {
|
|
9
9
|
} from '../../index.js';
|
10
10
|
import { ReactiveTask, ReactiveValue, SignalOptionsWithInit, SignalSubscription } from '../../types.js';
|
11
11
|
import { Context, ContextImpl, getCurrentScope, ROOT_SCOPE, SignalScope } from '../../internals/contexts.js';
|
12
|
-
import {
|
12
|
+
import { DerivedSignal } from '../../internals/derived.js';
|
13
13
|
import { ReactivePromise } from '../../internals/async.js';
|
14
14
|
import { hashValue } from '../../internals/utils/hash.js';
|
15
15
|
|
@@ -71,10 +71,10 @@ const WATCHERS = new WeakMap<Function, DerivedSignal<unknown, unknown[]>>();
|
|
71
71
|
|
72
72
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
73
73
|
function getWatcherForHook(hook: Function) {
|
74
|
-
let
|
74
|
+
let w = WATCHERS.get(hook);
|
75
75
|
|
76
|
-
if (!
|
77
|
-
|
76
|
+
if (!w) {
|
77
|
+
w = watcher(
|
78
78
|
() => {
|
79
79
|
let result = hook();
|
80
80
|
|
@@ -84,18 +84,15 @@ function getWatcherForHook(hook: Function) {
|
|
84
84
|
|
85
85
|
return result;
|
86
86
|
},
|
87
|
-
undefined,
|
88
|
-
undefined,
|
89
|
-
undefined,
|
90
87
|
{ desc: 'test' + TEST_ID++ },
|
91
|
-
)
|
88
|
+
) as DerivedSignal<unknown, unknown[]>;
|
92
89
|
|
93
|
-
unsubs.push(
|
90
|
+
unsubs.push(w.addListener(() => {}));
|
94
91
|
|
95
|
-
WATCHERS.set(hook,
|
92
|
+
WATCHERS.set(hook, w);
|
96
93
|
}
|
97
94
|
|
98
|
-
return
|
95
|
+
return w;
|
99
96
|
}
|
100
97
|
|
101
98
|
function toHaveSignalValue(
|
package/src/config.ts
CHANGED
@@ -14,7 +14,7 @@ interface SignalHooksConfig {
|
|
14
14
|
runBatch: BatchFn;
|
15
15
|
getFrameworkScope: () => SignalScope | undefined;
|
16
16
|
useStateSignal: <T>(signal: StateSignal<T>) => T;
|
17
|
-
useDerivedSignal: <T>(signal: DerivedSignal<T,
|
17
|
+
useDerivedSignal: <T, Args extends unknown[]>(signal: DerivedSignal<T, Args>) => ReactiveValue<T>;
|
18
18
|
}
|
19
19
|
|
20
20
|
export let scheduleFlush: FlushFn = flushWatchers => {
|
@@ -28,9 +28,11 @@ export let runBatch: BatchFn = fn => fn();
|
|
28
28
|
export let getFrameworkScope: () => SignalScope | undefined = () => undefined;
|
29
29
|
|
30
30
|
let useFrameworkStateSignal: <T>(signal: StateSignal<T>) => T = signal => signal.get();
|
31
|
-
let useFrameworkDerivedSignal: <T
|
31
|
+
let useFrameworkDerivedSignal: <T, Args extends unknown[]>(
|
32
|
+
signal: DerivedSignal<T, Args>,
|
33
|
+
) => ReactiveValue<T> = signal => signal.get();
|
32
34
|
|
33
|
-
export function useDerivedSignal<T>(signal: DerivedSignal<T,
|
35
|
+
export function useDerivedSignal<T, Args extends unknown[]>(signal: DerivedSignal<T, Args>): ReactiveValue<T> {
|
34
36
|
if (CURRENT_CONSUMER !== undefined) {
|
35
37
|
return signal.get();
|
36
38
|
} else {
|
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
|
}
|