@hoosk/minisignals 1.0.6 → 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/README.md +32 -0
- package/dist/computed.d.ts +2 -2
- package/dist/computed.js +37 -24
- package/dist/effect.d.ts +33 -9
- package/dist/effect.js +177 -61
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/signal.js +26 -19
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -31,4 +31,36 @@ batch(() => {
|
|
|
31
31
|
}); // Logs once: Count: 20, Double: 40
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
## Performance
|
|
35
|
+
|
|
36
|
+
The core is built on a linked-list dependency graph with O(1) dependency lookup (`currentLink` pointer), lazy computed evaluation (dirty flag, no eager recomputation), and class-based signal instances for V8 monomorphization.
|
|
37
|
+
|
|
38
|
+
Benchmark results on Node.js (warmup 150 ms, sample 350 ms):
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
── Signal ────────────────────────────────────────────────────
|
|
42
|
+
scenario ops/s time/op
|
|
43
|
+
─────────────────────────────────────────────────────────────
|
|
44
|
+
signal-read 420.38M ops/s 2.38 ns/op
|
|
45
|
+
signal-write-no-subscribers 210.86M ops/s 4.74 ns/op
|
|
46
|
+
signal-write-same-value 760.84M ops/s 1.31 ns/op
|
|
47
|
+
|
|
48
|
+
── Effect ────────────────────────────────────────────────────
|
|
49
|
+
scenario ops/s time/op
|
|
50
|
+
─────────────────────────────────────────────────────────────
|
|
51
|
+
effect-fanout-write 17.77K ops/s 56.27 µs/op
|
|
52
|
+
effect-create-dispose 5.86M ops/s 170.66 ns/op
|
|
53
|
+
effect-dynamic-deps 7.08M ops/s 141.15 ns/op
|
|
54
|
+
|
|
55
|
+
── Computed ──────────────────────────────────────────────────
|
|
56
|
+
scenario ops/s time/op
|
|
57
|
+
─────────────────────────────────────────────────────────────
|
|
58
|
+
computed-chain 51.07K ops/s 19.58 µs/op
|
|
59
|
+
computed-cached-read 44.59M ops/s 22.42 ns/op
|
|
60
|
+
computed-batched-writes-no-read 742.71K ops/s 1.35 µs/op
|
|
61
|
+
computed-batched-writes-then-read 706.54K ops/s 1.42 µs/op
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Run `npm run bench` to reproduce locally.
|
|
65
|
+
|
|
34
66
|
For full documentation and advanced features, please refer to the [main repository README](../../README.md).
|
package/dist/computed.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ReadonlySignal } from './signal';
|
|
1
|
+
import { ReadonlySignal } from './signal.js';
|
|
2
2
|
export interface Computed<T> extends ReadonlySignal<T> {
|
|
3
3
|
/**
|
|
4
|
-
* Stops the
|
|
4
|
+
* Stops the computed signal from observing dependencies.
|
|
5
5
|
* Call this when the computed signal is no longer needed to prevent memory leaks.
|
|
6
6
|
*/
|
|
7
7
|
dispose: () => void;
|
package/dist/computed.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { effect } from './effect';
|
|
1
|
+
import { cleanupSubscriber, createDependencySource, getActiveSubscriber, scheduleSource, trackDependency, trackSubscriber, } from './effect.js';
|
|
3
2
|
/**
|
|
4
3
|
* Creates a computed signal that derives its value from other signals.
|
|
5
4
|
* It only calculates and subscribes when its value is read for the first time.
|
|
@@ -8,39 +7,53 @@ import { effect } from './effect';
|
|
|
8
7
|
* @returns A read-only signal with a dispose method.
|
|
9
8
|
*/
|
|
10
9
|
export function computed(fn) {
|
|
10
|
+
const source = createDependencySource();
|
|
11
|
+
const observer = {
|
|
12
|
+
notify() {
|
|
13
|
+
if (isDisposed || dirty)
|
|
14
|
+
return;
|
|
15
|
+
dirty = true;
|
|
16
|
+
scheduleSource(source);
|
|
17
|
+
},
|
|
18
|
+
depsHead: null,
|
|
19
|
+
depsTail: null,
|
|
20
|
+
trackId: 0,
|
|
21
|
+
pending: false,
|
|
22
|
+
};
|
|
11
23
|
let initialized = false;
|
|
12
|
-
let internalSignal = null;
|
|
13
|
-
let unsubscribe = null;
|
|
14
24
|
let isDisposed = false;
|
|
25
|
+
let dirty = true;
|
|
26
|
+
let cachedValue;
|
|
27
|
+
function refreshValue() {
|
|
28
|
+
// Clear pending so a batch flush after an eager read inside the same batch
|
|
29
|
+
// does not re-trigger notify() on an already-fresh computed.
|
|
30
|
+
// We do NOT call cleanupSubscriber here: trackSubscriber increments trackId
|
|
31
|
+
// and pruneStaleDependencies removes only stale links, reusing stable ones.
|
|
32
|
+
observer.pending = false;
|
|
33
|
+
cachedValue = trackSubscriber(observer, fn);
|
|
34
|
+
initialized = true;
|
|
35
|
+
dirty = false;
|
|
36
|
+
}
|
|
15
37
|
return {
|
|
16
38
|
get value() {
|
|
39
|
+
if (getActiveSubscriber())
|
|
40
|
+
trackDependency(source);
|
|
17
41
|
if (isDisposed) {
|
|
18
42
|
if (!initialized) {
|
|
19
|
-
|
|
43
|
+
cachedValue = fn();
|
|
20
44
|
initialized = true;
|
|
45
|
+
dirty = false;
|
|
21
46
|
}
|
|
22
|
-
return
|
|
23
|
-
}
|
|
24
|
-
if (!initialized) {
|
|
25
|
-
unsubscribe = effect(() => {
|
|
26
|
-
const newValue = fn();
|
|
27
|
-
if (!initialized) {
|
|
28
|
-
internalSignal = signal(newValue);
|
|
29
|
-
initialized = true;
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
internalSignal.value = newValue;
|
|
33
|
-
}
|
|
34
|
-
});
|
|
47
|
+
return cachedValue;
|
|
35
48
|
}
|
|
36
|
-
|
|
49
|
+
if (!initialized || dirty)
|
|
50
|
+
refreshValue();
|
|
51
|
+
return cachedValue;
|
|
37
52
|
},
|
|
38
53
|
dispose() {
|
|
39
54
|
isDisposed = true;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
}
|
|
55
|
+
dirty = false;
|
|
56
|
+
cleanupSubscriber(observer);
|
|
57
|
+
},
|
|
45
58
|
};
|
|
46
59
|
}
|
package/dist/effect.d.ts
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
deps: Set<SubscriberSet>;
|
|
5
|
-
isRunning: boolean;
|
|
6
|
-
needsRerun: boolean;
|
|
1
|
+
export interface DependencySource {
|
|
2
|
+
subsHead: DependencyLink | null;
|
|
3
|
+
currentLink: DependencyLink | null;
|
|
7
4
|
}
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
interface DependencyLink {
|
|
6
|
+
source: DependencySource;
|
|
7
|
+
subscriber: Subscriber;
|
|
8
|
+
trackId: number;
|
|
9
|
+
rollbackCurrentLink: DependencyLink | null;
|
|
10
|
+
nextInSource: DependencyLink | null;
|
|
11
|
+
prevInSource: DependencyLink | null;
|
|
12
|
+
nextInSubscriber: DependencyLink | null;
|
|
13
|
+
prevInSubscriber: DependencyLink | null;
|
|
14
|
+
}
|
|
15
|
+
export interface Subscriber {
|
|
16
|
+
notify: () => void;
|
|
17
|
+
depsHead: DependencyLink | null;
|
|
18
|
+
depsTail: DependencyLink | null;
|
|
19
|
+
trackId: number;
|
|
20
|
+
pending: boolean;
|
|
21
|
+
}
|
|
22
|
+
declare let _activeSubscriber: Subscriber | null;
|
|
23
|
+
export { _activeSubscriber };
|
|
24
|
+
export declare function createDependencySource(): DependencySource;
|
|
25
|
+
export declare function getActiveSubscriber(): Subscriber | null;
|
|
26
|
+
export declare function trackDependency(source: DependencySource): void;
|
|
27
|
+
export declare function trackSubscriber<T>(subscriber: Subscriber, fn: () => T): T;
|
|
28
|
+
export declare function cleanupSubscriber(subscriber: Subscriber): void;
|
|
29
|
+
export declare function scheduleSource(source: DependencySource): void;
|
|
10
30
|
export declare function batch<T>(fn: () => T): T;
|
|
11
31
|
export declare function untracked<T>(fn: () => T): T;
|
|
32
|
+
/**
|
|
33
|
+
* Creates an effect that runs immediately and tracks dependencies.
|
|
34
|
+
* @param fn The function to execute.
|
|
35
|
+
* @returns An unsubscribe function.
|
|
36
|
+
*/
|
|
12
37
|
export declare function effect(fn: () => void): () => void;
|
|
13
|
-
export {};
|
package/dist/effect.js
CHANGED
|
@@ -1,20 +1,147 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// ─── Dependency graph types ───────────────────────────────────────────────────
|
|
2
|
+
let _activeSubscriber = null;
|
|
3
|
+
// Exported so signal.ts can do an inline null-check in the getter hot path,
|
|
4
|
+
// avoiding a function call when there is no active subscriber.
|
|
5
|
+
export { _activeSubscriber };
|
|
6
|
+
export function createDependencySource() {
|
|
7
|
+
return { subsHead: null, currentLink: null };
|
|
4
8
|
}
|
|
5
|
-
|
|
9
|
+
export function getActiveSubscriber() {
|
|
10
|
+
return _activeSubscriber;
|
|
11
|
+
}
|
|
12
|
+
// ─── Link management ─────────────────────────────────────────────────────────
|
|
13
|
+
// Before running a subscriber, stamp each of its existing dep links onto
|
|
14
|
+
// source.currentLink (saving whatever was there as rollbackCurrentLink).
|
|
15
|
+
// This lets linkDependency find the reusable link with a single pointer read.
|
|
16
|
+
function prepareSources(subscriber) {
|
|
17
|
+
let link = subscriber.depsHead;
|
|
18
|
+
while (link) {
|
|
19
|
+
link.rollbackCurrentLink = link.source.currentLink;
|
|
20
|
+
link.source.currentLink = link;
|
|
21
|
+
link = link.nextInSubscriber;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function linkDependency(source, subscriber) {
|
|
25
|
+
const existing = source.currentLink;
|
|
26
|
+
if (existing !== null && existing.subscriber === subscriber) {
|
|
27
|
+
// Stable dep: just mark as seen in this run.
|
|
28
|
+
existing.trackId = subscriber.trackId;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// New dep: create a link and install it. Save whatever was in currentLink
|
|
32
|
+
// so it can be restored when the tracking pass finishes (rollback chain).
|
|
33
|
+
const link = {
|
|
34
|
+
source,
|
|
35
|
+
subscriber,
|
|
36
|
+
trackId: subscriber.trackId,
|
|
37
|
+
rollbackCurrentLink: existing,
|
|
38
|
+
nextInSource: source.subsHead,
|
|
39
|
+
prevInSource: null,
|
|
40
|
+
nextInSubscriber: null,
|
|
41
|
+
prevInSubscriber: subscriber.depsTail,
|
|
42
|
+
};
|
|
43
|
+
if (source.subsHead)
|
|
44
|
+
source.subsHead.prevInSource = link;
|
|
45
|
+
source.subsHead = link;
|
|
46
|
+
if (subscriber.depsTail) {
|
|
47
|
+
subscriber.depsTail.nextInSubscriber = link;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
subscriber.depsHead = link;
|
|
51
|
+
}
|
|
52
|
+
subscriber.depsTail = link;
|
|
53
|
+
source.currentLink = link;
|
|
54
|
+
}
|
|
55
|
+
function unlinkDependency(link) {
|
|
56
|
+
const { source, subscriber } = link;
|
|
57
|
+
if (link.prevInSource) {
|
|
58
|
+
link.prevInSource.nextInSource = link.nextInSource;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
source.subsHead = link.nextInSource;
|
|
62
|
+
}
|
|
63
|
+
if (link.nextInSource)
|
|
64
|
+
link.nextInSource.prevInSource = link.prevInSource;
|
|
65
|
+
if (link.prevInSubscriber) {
|
|
66
|
+
link.prevInSubscriber.nextInSubscriber = link.nextInSubscriber;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
subscriber.depsHead = link.nextInSubscriber;
|
|
70
|
+
}
|
|
71
|
+
if (link.nextInSubscriber) {
|
|
72
|
+
link.nextInSubscriber.prevInSubscriber = link.prevInSubscriber;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
subscriber.depsTail = link.prevInSubscriber;
|
|
76
|
+
}
|
|
77
|
+
link.nextInSource = link.prevInSource = null;
|
|
78
|
+
link.nextInSubscriber = link.prevInSubscriber = null;
|
|
79
|
+
}
|
|
80
|
+
// After the fn runs: prune stale links and restore source.currentLink for all
|
|
81
|
+
// links we touched (both kept and removed) so outer contexts are unaffected.
|
|
82
|
+
function pruneStaleDependencies(subscriber) {
|
|
83
|
+
let link = subscriber.depsHead;
|
|
84
|
+
while (link) {
|
|
85
|
+
const next = link.nextInSubscriber;
|
|
86
|
+
// Restore regardless of whether we keep or prune the link.
|
|
87
|
+
link.source.currentLink = link.rollbackCurrentLink;
|
|
88
|
+
link.rollbackCurrentLink = null;
|
|
89
|
+
if (link.trackId !== subscriber.trackId)
|
|
90
|
+
unlinkDependency(link);
|
|
91
|
+
link = next;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function trackDependency(source) {
|
|
95
|
+
if (_activeSubscriber)
|
|
96
|
+
linkDependency(source, _activeSubscriber);
|
|
97
|
+
}
|
|
98
|
+
export function trackSubscriber(subscriber, fn) {
|
|
99
|
+
const previous = _activeSubscriber;
|
|
100
|
+
subscriber.trackId++;
|
|
101
|
+
prepareSources(subscriber);
|
|
102
|
+
_activeSubscriber = subscriber;
|
|
103
|
+
try {
|
|
104
|
+
return fn();
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
_activeSubscriber = previous;
|
|
108
|
+
pruneStaleDependencies(subscriber);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function cleanupSubscriber(subscriber) {
|
|
112
|
+
let link = subscriber.depsHead;
|
|
113
|
+
while (link) {
|
|
114
|
+
const next = link.nextInSubscriber;
|
|
115
|
+
// Restore currentLink if it still points to this link.
|
|
116
|
+
if (link.source.currentLink === link) {
|
|
117
|
+
link.source.currentLink = link.rollbackCurrentLink;
|
|
118
|
+
}
|
|
119
|
+
link.rollbackCurrentLink = null;
|
|
120
|
+
unlinkDependency(link);
|
|
121
|
+
link = next;
|
|
122
|
+
}
|
|
123
|
+
subscriber.pending = false;
|
|
124
|
+
}
|
|
125
|
+
// ─── Batch scheduling ────────────────────────────────────────────────────────
|
|
6
126
|
let _batchDepth = 0;
|
|
7
|
-
const _pendingNotifications =
|
|
8
|
-
|
|
127
|
+
const _pendingNotifications = [];
|
|
128
|
+
function queueSubscriber(subscriber) {
|
|
9
129
|
if (_batchDepth > 0) {
|
|
10
|
-
|
|
11
|
-
|
|
130
|
+
if (!subscriber.pending) {
|
|
131
|
+
subscriber.pending = true;
|
|
132
|
+
_pendingNotifications.push(subscriber);
|
|
12
133
|
}
|
|
13
134
|
}
|
|
14
135
|
else {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
136
|
+
subscriber.notify();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function scheduleSource(source) {
|
|
140
|
+
let link = source.subsHead;
|
|
141
|
+
while (link) {
|
|
142
|
+
const next = link.nextInSource;
|
|
143
|
+
queueSubscriber(link.subscriber);
|
|
144
|
+
link = next;
|
|
18
145
|
}
|
|
19
146
|
}
|
|
20
147
|
export function batch(fn) {
|
|
@@ -25,73 +152,62 @@ export function batch(fn) {
|
|
|
25
152
|
finally {
|
|
26
153
|
_batchDepth--;
|
|
27
154
|
if (_batchDepth === 0) {
|
|
28
|
-
const pending =
|
|
29
|
-
_pendingNotifications.clear();
|
|
155
|
+
const pending = _pendingNotifications.splice(0);
|
|
30
156
|
for (const sub of pending) {
|
|
31
|
-
sub
|
|
157
|
+
if (!sub.pending)
|
|
158
|
+
continue;
|
|
159
|
+
sub.pending = false;
|
|
160
|
+
sub.notify();
|
|
32
161
|
}
|
|
33
162
|
}
|
|
34
163
|
}
|
|
35
164
|
}
|
|
36
|
-
//
|
|
165
|
+
// ─── Untracked read ───────────────────────────────────────────────────────────
|
|
37
166
|
export function untracked(fn) {
|
|
38
|
-
const previous =
|
|
39
|
-
|
|
167
|
+
const previous = _activeSubscriber;
|
|
168
|
+
_activeSubscriber = null;
|
|
40
169
|
try {
|
|
41
170
|
return fn();
|
|
42
171
|
}
|
|
43
172
|
finally {
|
|
44
|
-
|
|
173
|
+
_activeSubscriber = previous;
|
|
45
174
|
}
|
|
46
175
|
}
|
|
47
|
-
//
|
|
176
|
+
// ─── Effect ──────────────────────────────────────────────────────────────────
|
|
177
|
+
const MAX_EFFECT_ITERATIONS = 100;
|
|
48
178
|
/**
|
|
49
179
|
* Creates an effect that runs immediately and tracks dependencies.
|
|
50
180
|
* @param fn The function to execute.
|
|
51
181
|
* @returns An unsubscribe function.
|
|
52
182
|
*/
|
|
53
|
-
const MAX_EFFECT_ITERATIONS = 100;
|
|
54
183
|
export function effect(fn) {
|
|
55
|
-
const effectObj = {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
effectObj.isRunning = true;
|
|
62
|
-
let iterations = 0;
|
|
63
|
-
try {
|
|
64
|
-
do {
|
|
65
|
-
if (++iterations > MAX_EFFECT_ITERATIONS) {
|
|
66
|
-
throw new Error('Circular dependency detected: effect exceeded maximum re-run limit');
|
|
67
|
-
}
|
|
68
|
-
effectObj.needsRerun = false;
|
|
69
|
-
cleanup(effectObj);
|
|
70
|
-
const previousEffect = _activeEffect;
|
|
71
|
-
_activeEffect = effectObj;
|
|
72
|
-
try {
|
|
73
|
-
fn();
|
|
74
|
-
}
|
|
75
|
-
finally {
|
|
76
|
-
_activeEffect = previousEffect;
|
|
77
|
-
}
|
|
78
|
-
} while (effectObj.needsRerun);
|
|
79
|
-
}
|
|
80
|
-
finally {
|
|
81
|
-
effectObj.isRunning = false;
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
deps: new Set(),
|
|
85
|
-
isRunning: false,
|
|
86
|
-
needsRerun: false,
|
|
87
|
-
};
|
|
88
|
-
function cleanup(eff) {
|
|
89
|
-
for (const depSet of eff.deps) {
|
|
90
|
-
depSet.delete(eff.run);
|
|
184
|
+
const effectObj = {};
|
|
185
|
+
effectObj.notify = () => {
|
|
186
|
+
if (effectObj.isRunning) {
|
|
187
|
+
effectObj.needsRerun = true;
|
|
188
|
+
return;
|
|
91
189
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
190
|
+
effectObj.isRunning = true;
|
|
191
|
+
let iterations = 0;
|
|
192
|
+
try {
|
|
193
|
+
do {
|
|
194
|
+
if (++iterations > MAX_EFFECT_ITERATIONS) {
|
|
195
|
+
throw new Error('Circular dependency detected: effect exceeded maximum re-run limit');
|
|
196
|
+
}
|
|
197
|
+
effectObj.needsRerun = false;
|
|
198
|
+
trackSubscriber(effectObj, fn);
|
|
199
|
+
} while (effectObj.needsRerun);
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
effectObj.isRunning = false;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
effectObj.depsHead = null;
|
|
206
|
+
effectObj.depsTail = null;
|
|
207
|
+
effectObj.trackId = 0;
|
|
208
|
+
effectObj.pending = false;
|
|
209
|
+
effectObj.isRunning = false;
|
|
210
|
+
effectObj.needsRerun = false;
|
|
211
|
+
effectObj.notify();
|
|
212
|
+
return () => cleanupSubscriber(effectObj);
|
|
97
213
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from './signal';
|
|
2
|
-
export * from './effect';
|
|
3
|
-
export * from './computed';
|
|
1
|
+
export * from './signal.js';
|
|
2
|
+
export * from './effect.js';
|
|
3
|
+
export * from './computed.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from './signal';
|
|
2
|
-
export * from './effect';
|
|
3
|
-
export * from './computed';
|
|
1
|
+
export * from './signal.js';
|
|
2
|
+
export * from './effect.js';
|
|
3
|
+
export * from './computed.js';
|
package/dist/signal.js
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _activeSubscriber, createDependencySource, scheduleSource, trackDependency } from './effect.js';
|
|
2
|
+
// Class-based implementation so all signal instances share the same prototype
|
|
3
|
+
// getter/setter. V8 can monomorphize and inline the accessors instead of
|
|
4
|
+
// treating every signal as a unique object with closure-based functions.
|
|
5
|
+
class SignalImpl {
|
|
6
|
+
/** @internal */ _source;
|
|
7
|
+
/** @internal */ _value;
|
|
8
|
+
constructor(initialValue) {
|
|
9
|
+
this._source = createDependencySource();
|
|
10
|
+
this._value = initialValue;
|
|
11
|
+
}
|
|
12
|
+
get value() {
|
|
13
|
+
// Inline null-check to avoid the trackDependency() function call entirely
|
|
14
|
+
// when there is no active subscriber — the common case outside effects.
|
|
15
|
+
if (_activeSubscriber !== null)
|
|
16
|
+
trackDependency(this._source);
|
|
17
|
+
return this._value;
|
|
18
|
+
}
|
|
19
|
+
set value(newValue) {
|
|
20
|
+
if (this._value !== newValue) {
|
|
21
|
+
this._value = newValue;
|
|
22
|
+
scheduleSource(this._source);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
2
26
|
/**
|
|
3
27
|
* Creates a reactive signal.
|
|
4
28
|
* @param initialValue The starting value.
|
|
5
29
|
* @returns An object with a reactive .value property.
|
|
6
30
|
*/
|
|
7
31
|
export function signal(initialValue) {
|
|
8
|
-
|
|
9
|
-
let _value = initialValue;
|
|
10
|
-
return {
|
|
11
|
-
get value() {
|
|
12
|
-
const active = getActiveEffect();
|
|
13
|
-
if (active) {
|
|
14
|
-
subscribers.add(active.run);
|
|
15
|
-
active.deps.add(subscribers);
|
|
16
|
-
}
|
|
17
|
-
return _value;
|
|
18
|
-
},
|
|
19
|
-
set value(newValue) {
|
|
20
|
-
if (_value !== newValue) {
|
|
21
|
-
_value = newValue;
|
|
22
|
-
scheduleSubs(subscribers);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
};
|
|
32
|
+
return new SignalImpl(initialValue);
|
|
26
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hoosk/minisignals",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Minimal agnostic signals library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc",
|
|
21
21
|
"prepare": "npm run build",
|
|
22
|
-
"test": "vitest run"
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"bench": "npm run build && node ./bench/core.bench.mjs"
|
|
23
24
|
},
|
|
24
25
|
"keywords": [],
|
|
25
26
|
"author": "Hoosk",
|