@webreflection/signals 0.1.9 → 0.2.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/README.md +8 -45
- package/dist/disposable.js +1 -1
- package/dist/signals.js +1 -1
- package/package.json +21 -14
- package/src/disposable.js +1 -1
- package/src/index.js +171 -4
- package/types/index.d.ts +19 -4
- package/dist/branded.js +0 -1
- package/src/branded.js +0 -25
- package/src/computed.js +0 -31
- package/src/effect.js +0 -65
- package/src/signal.js +0 -26
- package/src/stack.js +0 -18
- package/src/untracked.js +0 -9
- package/types/branded.d.ts +0 -15
- package/types/computed.d.ts +0 -5
- package/types/effect.d.ts +0 -4
- package/types/signal.d.ts +0 -5
- package/types/stack.d.ts +0 -5
- package/types/untracked.d.ts +0 -2
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ Once minified and compressed, this module is actually [0.5KB](https://cdn.jsdeli
|
|
|
12
12
|
```js
|
|
13
13
|
// basic core features
|
|
14
14
|
import {
|
|
15
|
+
Signal, // class for brand check
|
|
16
|
+
Computed, // extends Signal: brand check
|
|
15
17
|
batch, // Preact-like API
|
|
16
18
|
computed, // Preact-like API
|
|
17
19
|
effect, // Preact-like API
|
|
@@ -29,38 +31,15 @@ Exposes a Preact-like [createModel](https://github.com/preactjs/signals/blob/mai
|
|
|
29
31
|
import {
|
|
30
32
|
// extra:
|
|
31
33
|
disposable, // equivalent of createModel(fn)
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
computed,
|
|
35
|
-
effect,
|
|
36
|
-
signal,
|
|
37
|
-
untracked,
|
|
34
|
+
// all other exports from core
|
|
35
|
+
...core
|
|
38
36
|
} from '@webreflection/signals/disposable';
|
|
39
37
|
```
|
|
40
38
|
|
|
41
39
|
|
|
42
|
-
### branded
|
|
43
|
-
|
|
44
|
-
This variant offers an `isSignal` utility that returns `true` or `false` if the passed argument is either a `signal` or a `computed` reference.
|
|
45
|
-
|
|
46
|
-
```js
|
|
47
|
-
// extra core features
|
|
48
|
-
import {
|
|
49
|
-
// extra:
|
|
50
|
-
disposable, // equivalent of createModel(fn)
|
|
51
|
-
isSignal, // true if `isSignal(ref)` is signal or computed
|
|
52
|
-
// ... same as core features ...
|
|
53
|
-
batch,
|
|
54
|
-
computed,
|
|
55
|
-
effect,
|
|
56
|
-
signal,
|
|
57
|
-
untracked,
|
|
58
|
-
} from '@webreflection/signals/branded';
|
|
59
|
-
```
|
|
60
|
-
|
|
61
40
|
### In Depth
|
|
62
41
|
|
|
63
|
-
* simply stack-based, maybe not the best approach, but one that can guarantee reasonable performance with minimal code size.
|
|
42
|
+
* simply (swapped) stack-based, maybe not the best approach, but one that can guarantee reasonable performance with minimal code size.
|
|
64
43
|
* only `signal` and `computed` subscribe while reading values, unless `sig_or_comp.peek()` is used.
|
|
65
44
|
* any `effect` updates synchronously but then runs only in isolation. Every effect is disposed of if the outer effect is running, meaning stacked effects work out of the box and always™ do the right thing.
|
|
66
45
|
* `disposable` uses the very same `effect` logic to dispose itself when not needed anymore.
|
|
@@ -73,26 +52,10 @@ import {
|
|
|
73
52
|
You know, nowadays it's hard to find libraries that are still 100% under control, minimalistic, not bloated, yet correct, and this one would like to be one of those 😇
|
|
74
53
|
|
|
75
54
|
|
|
76
|
-
#### The Beauty
|
|
77
|
-
|
|
78
|
-
* [signal](https://github.com/WebReflection/signals/blob/main/src/signal.js) is 26 LOC.
|
|
79
|
-
* [computed](https://github.com/WebReflection/signals/blob/main/src/computed.js) is 31 LOC.
|
|
80
|
-
* the shared [stack](https://github.com/WebReflection/signals/blob/main/src/stack.js) is 18 LOC.
|
|
81
|
-
* [effect](https://github.com/WebReflection/signals/blob/main/src/effect.js) is where business happens, 65 LOC.
|
|
82
|
-
* [disposable](https://github.com/WebReflection/signals/blob/main/src/disposable.js) is 10 LOC, based on the core library mentioned in the previous points.
|
|
83
|
-
* [branded](https://github.com/WebReflection/signals/blob/main/src/branded.js) is 25 LOC extra needed only for libraries building on top.
|
|
84
|
-
|
|
85
|
-
I mean ... that's coding, isn't it ... today I really needed something that would remind me why I love what I do ❤️
|
|
86
|
-
|
|
87
|
-
|
|
88
55
|
#### Benchmark
|
|
89
56
|
|
|
90
57
|

|
|
91
58
|
|
|
92
|
-
There is a *huge* difference between *NodeJS* and *Bun* but that's likely because *JSC* handles *Set* or *Map* in a better way, meaning all *WebKit* based browsers and mobile devices will have similar *Preact* performance, while *Chromium* based browsers will have half Preact size, but 1.5X slowdown.
|
|
93
|
-
|
|
94
|
-
However, in common scenarios with no more than 10 to 100 signals per *effect*, the performance are consistently better or really close to Preact.
|
|
95
|
-
|
|
96
59
|
## Architecture
|
|
97
60
|
|
|
98
61
|
Fine-tuned signals are a piece of art:
|
|
@@ -179,8 +142,8 @@ No *effect*? No reactivity! This is the *signals* contract, but there is a *catc
|
|
|
179
142
|
|
|
180
143
|
Great questions. Here are the details about why that's never a concern:
|
|
181
144
|
|
|
182
|
-
* *effect* never
|
|
183
|
-
* *effect* never runs if it knows outer *effects* are queued to resolve the latest change
|
|
145
|
+
* *effect* never add subscribers to itself, like signals or computeds do, it just registers itself as an *observer* (*subscriber*)
|
|
146
|
+
* *effect* never runs if it knows outer *effects* are queued to resolve the latest change or changes are happening while it's running
|
|
184
147
|
* the previous point means if `signal.value` is registered both at the *inner* effect level and at the *outer* one, the *outer* one will dictate the execution because ...
|
|
185
148
|
* only the top-most subscribed effects will eventually execute, and ...
|
|
186
149
|
* any *effect* previously registered for its outer *effect* will be **disposed** and never react to anything again!
|
|
@@ -190,7 +153,7 @@ I am not sure you are still following, but because *effect* is a bottom-up probl
|
|
|
190
153
|
|
|
191
154
|
### Batch
|
|
192
155
|
|
|
193
|
-
If you followed everything else I've explained around this architecture, `batch(callback)` simply represents a running *callback* with no
|
|
156
|
+
If you followed everything else I've explained around this architecture, `batch(callback)` simply represents a running *callback* with no instant reactivity, it simply accumulates changes and trigger after all changes happend for whatever effect was involved.
|
|
194
157
|
|
|
195
158
|
### Untracked
|
|
196
159
|
|
package/dist/disposable.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var r,p,u,f,i,n=!0,o=class{static{p=s=>s.#t,u=s=>s.#s,f=(s,e)=>{s.#s=e}}#s=new Set;#t;constructor(s){this.#t=s}get value(){return y(this.#s),this.#t}set value(s){if(this.#t=s,n){let e=this.#s;this.#s=new Set,k(e)}}peek(){return this.#t}},a=Symbol(),h=class extends o{#s=!0;#t;[a](){if(this.#s)return;this.#s=!0;let s=u(this);f(this,new Set),k(s)}get value(){return y(u(this)),this.peek()}peek(){for(;this.#s;)this.#s=!1,this.#t=x(this,p(this));return this.#t}},l=class{disposed=!1;invalid=!0;sub=[];cleanup;fn;constructor(s){this.fn=s}[a](){this.invalid||this.disposed||(this.invalid=!0,i||(r?r.push(this):this.peek()))}peek(){for(;this.invalid&&!this.disposed;)this.invalid=!1,b(this),this.cleanup=x(this,this.fn)}},g=t=>{let s=r;s||(r=[]);try{return t()}finally{if(!s){[s,r]=[r,s];for(let e of s)e.peek()}}},b=t=>{t.cleanup?.(),t.sub.length&&t.sub.splice(0).forEach(d)},m=t=>new h(t),d=t=>{t.disposed=!0,b(t)},v=t=>{let s=new l(t);return i&&i.sub.push(s),s.peek(),()=>{s.disposed||d(s)}},k=t=>{for(let s of t)s[a]()},y=t=>{n&&i&&t.add(i)},x=(t,s)=>{let e=i;i=t;try{return s()}finally{i=e}},S=t=>new o(t),V=t=>{let s=n;n=!1;try{return t()}finally{n=s}};var z=t=>function(...e){let c,w=v(()=>{c??=t.apply(this,e)??this});return c[Symbol.dispose]=w,c};export{h as Computed,o as Signal,g as batch,m as computed,z as disposable,v as effect,S as signal,V as untracked};
|
package/dist/signals.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var f=!0,a=t=>
|
|
1
|
+
var r,a,c,f,e,n=!0,o=class{static{a=s=>s.#t,c=s=>s.#s,f=(s,i)=>{s.#s=i}}#s=new Set;#t;constructor(s){this.#t=s}get value(){return k(this.#s),this.#t}set value(s){if(this.#t=s,n){let i=this.#s;this.#s=new Set,d(i)}}peek(){return this.#t}},l=Symbol(),u=class extends o{#s=!0;#t;[l](){if(this.#s)return;this.#s=!0;let s=c(this);f(this,new Set),d(s)}get value(){return k(c(this)),this.peek()}peek(){for(;this.#s;)this.#s=!1,this.#t=v(this,a(this));return this.#t}},h=class{disposed=!1;invalid=!0;sub=[];cleanup;fn;constructor(s){this.fn=s}[l](){this.invalid||this.disposed||(this.invalid=!0,e||(r?r.push(this):this.peek()))}peek(){for(;this.invalid&&!this.disposed;)this.invalid=!1,p(this),this.cleanup=v(this,this.fn)}},w=t=>{let s=r;s||(r=[]);try{return t()}finally{if(!s){[s,r]=[r,s];for(let i of s)i.peek()}}},p=t=>{t.cleanup?.(),t.sub.length&&t.sub.splice(0).forEach(b)},y=t=>new u(t),b=t=>{t.disposed=!0,p(t)},x=t=>{let s=new h(t);return e&&e.sub.push(s),s.peek(),()=>{s.disposed||b(s)}},d=t=>{for(let s of t)s[l]()},k=t=>{n&&e&&t.add(e)},v=(t,s)=>{let i=e;e=t;try{return s()}finally{e=i}},g=t=>new o(t),S=t=>{let s=n;n=!1;try{return t()}finally{n=s}};export{u as Computed,o as Signal,w as batch,y as computed,x as effect,g as signal,S as untracked};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webreflection/signals",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A minimalistic Preact-like signals implementation",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -13,19 +13,27 @@
|
|
|
13
13
|
],
|
|
14
14
|
"types": {
|
|
15
15
|
".": "./types/index.d.ts",
|
|
16
|
-
"./branded": "./types/branded.d.ts",
|
|
17
16
|
"./disposable": "./types/disposable.d.ts",
|
|
18
17
|
"./disp": "./types/disposable.d.ts",
|
|
19
|
-
"./dist": "./types/index.d.ts"
|
|
20
|
-
"./package.json": "./types/package.d.json"
|
|
18
|
+
"./dist": "./types/index.d.ts"
|
|
21
19
|
},
|
|
22
20
|
"exports": {
|
|
23
|
-
".":
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"./
|
|
28
|
-
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./types/index.d.ts",
|
|
23
|
+
"import": "./src/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./disposable": {
|
|
26
|
+
"types": "./types/disposable.d.ts",
|
|
27
|
+
"import": "./src/disposable.js"
|
|
28
|
+
},
|
|
29
|
+
"./disp": {
|
|
30
|
+
"types": "./types/disposable.d.ts",
|
|
31
|
+
"import": "./dist/disposable.js"
|
|
32
|
+
},
|
|
33
|
+
"./dist": {
|
|
34
|
+
"types": "./types/index.d.ts",
|
|
35
|
+
"import": "./dist/signals.js"
|
|
36
|
+
},
|
|
29
37
|
"./package.json": "./package.json"
|
|
30
38
|
},
|
|
31
39
|
"overrides": {
|
|
@@ -34,13 +42,12 @@
|
|
|
34
42
|
}
|
|
35
43
|
},
|
|
36
44
|
"scripts": {
|
|
37
|
-
"build": "npm run build:dist && npm run build:disp && npm run build:
|
|
38
|
-
"build:branded": "esbuild src/branded.js --bundle --minify --platform=neutral --format=esm --outfile=dist/branded.js",
|
|
45
|
+
"build": "npm run build:dist && npm run build:disp && npm run build:types && npm run test && npm run size",
|
|
39
46
|
"build:dist": "esbuild src/index.js --bundle --minify --platform=neutral --format=esm --outfile=dist/signals.js",
|
|
40
47
|
"build:disp": "esbuild src/disposable.js --bundle --minify --platform=neutral --format=esm --outfile=dist/disposable.js",
|
|
41
|
-
"build:types": "tsc --allowJs --declaration --emitDeclarationOnly --stripInternal --outDir types --target es2022 --lib es2024 --module nodenext --moduleResolution nodenext src/*.js",
|
|
48
|
+
"build:types": "tsc --allowJs --declaration --emitDeclarationOnly --stripInternal --removeComments --outDir types --target es2022 --lib es2024 --module nodenext --moduleResolution nodenext src/*.js",
|
|
42
49
|
"coverage": "mkdir -p ./coverage; c8 report --reporter=text-lcov > ./coverage/lcov.info",
|
|
43
|
-
"size": "echo 'signals'; cat dist/signals.js | brotli | wc -c; echo '\ndisposable'; cat dist/disposable.js | brotli | wc -c
|
|
50
|
+
"size": "echo 'signals'; cat dist/signals.js | brotli | wc -c; echo '\ndisposable'; cat dist/disposable.js | brotli | wc -c",
|
|
44
51
|
"test": "c8 node --expose-gc test/coverage.js"
|
|
45
52
|
},
|
|
46
53
|
"keywords": [
|
package/src/disposable.js
CHANGED
package/src/index.js
CHANGED
|
@@ -1,4 +1,171 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export
|
|
1
|
+
let batches, getValue, getSubscribers, setSubscribers, stack, tracking = true;
|
|
2
|
+
|
|
3
|
+
/** @template T */
|
|
4
|
+
export class Signal {
|
|
5
|
+
static {
|
|
6
|
+
getValue = self => self.#value;
|
|
7
|
+
getSubscribers = self => self.#subscribers;
|
|
8
|
+
setSubscribers = (self, subscribers) => {
|
|
9
|
+
self.#subscribers = subscribers;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#subscribers = new Set;
|
|
14
|
+
#value;
|
|
15
|
+
|
|
16
|
+
/** @param {T} init */
|
|
17
|
+
constructor(init) { this.#value = init }
|
|
18
|
+
|
|
19
|
+
get value() {
|
|
20
|
+
push(this.#subscribers);
|
|
21
|
+
return this.#value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
set value(value) {
|
|
25
|
+
this.#value = value;
|
|
26
|
+
if (tracking) {
|
|
27
|
+
const subscribers = this.#subscribers;
|
|
28
|
+
this.#subscribers = new Set;
|
|
29
|
+
notify(subscribers);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
peek() {
|
|
34
|
+
return this.#value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @type {never} */
|
|
39
|
+
const compute = Symbol();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @template T
|
|
43
|
+
* @extends {Signal<() => T>}
|
|
44
|
+
*/
|
|
45
|
+
export class Computed extends Signal {
|
|
46
|
+
#invalid = true;
|
|
47
|
+
#result;
|
|
48
|
+
|
|
49
|
+
[compute]() {
|
|
50
|
+
if (this.#invalid) return;
|
|
51
|
+
this.#invalid = true;
|
|
52
|
+
const subscribers = getSubscribers(this);
|
|
53
|
+
setSubscribers(this, new Set);
|
|
54
|
+
notify(subscribers);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @readonly @returns {T} */
|
|
58
|
+
get value() {
|
|
59
|
+
push(getSubscribers(this));
|
|
60
|
+
return this.peek();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @returns {T} */
|
|
64
|
+
peek() {
|
|
65
|
+
while (this.#invalid) {
|
|
66
|
+
this.#invalid = false;
|
|
67
|
+
this.#result = run(this, getValue(this));
|
|
68
|
+
}
|
|
69
|
+
return this.#result;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class Effect {
|
|
74
|
+
disposed = false;
|
|
75
|
+
invalid = true;
|
|
76
|
+
sub = [];
|
|
77
|
+
cleanup;
|
|
78
|
+
fn;
|
|
79
|
+
|
|
80
|
+
constructor(fn) { this.fn = fn }
|
|
81
|
+
|
|
82
|
+
[compute]() {
|
|
83
|
+
if (this.invalid || this.disposed) return;
|
|
84
|
+
this.invalid = true;
|
|
85
|
+
if (!stack) {
|
|
86
|
+
if (batches) batches.push(this);
|
|
87
|
+
else this.peek();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
peek() {
|
|
92
|
+
while (this.invalid && !this.disposed) {
|
|
93
|
+
this.invalid = false;
|
|
94
|
+
cleanup(this);
|
|
95
|
+
this.cleanup = run(this, this.fn);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @type {<T>(fn: () => T) => T} */
|
|
101
|
+
export const batch = fn => {
|
|
102
|
+
let before = batches;
|
|
103
|
+
if (!before) batches = [];
|
|
104
|
+
try { return fn() }
|
|
105
|
+
finally {
|
|
106
|
+
if (!before) {
|
|
107
|
+
[before, batches] = [batches, before];
|
|
108
|
+
for (const fx of before) fx.peek();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const cleanup = fx => {
|
|
114
|
+
fx.cleanup?.();
|
|
115
|
+
if (fx.sub.length) fx.sub.splice(0).forEach(dispose);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @template T
|
|
120
|
+
* @param {() => T} fn
|
|
121
|
+
* @returns {Computed<T>}
|
|
122
|
+
*/
|
|
123
|
+
export const computed = fn => new Computed(fn);
|
|
124
|
+
|
|
125
|
+
const dispose = fx => {
|
|
126
|
+
fx.disposed = true;
|
|
127
|
+
cleanup(fx);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {() => (void | (() => void))} fn
|
|
132
|
+
* @returns {() => void}
|
|
133
|
+
*/
|
|
134
|
+
export const effect = fn => {
|
|
135
|
+
const fx = new Effect(fn);
|
|
136
|
+
if (stack) stack.sub.push(fx);
|
|
137
|
+
fx.peek();
|
|
138
|
+
return () => {
|
|
139
|
+
if (!fx.disposed) dispose(fx);
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const notify = subscribers => {
|
|
144
|
+
for (const subscriber of subscribers) subscriber[compute]();
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const push = subscribers => {
|
|
148
|
+
if (tracking && stack) subscribers.add(stack);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const run = (state, callback) => {
|
|
152
|
+
const before = stack;
|
|
153
|
+
stack = state;
|
|
154
|
+
try { return callback() }
|
|
155
|
+
finally { stack = before }
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @template T
|
|
160
|
+
* @param {T} init
|
|
161
|
+
* @returns
|
|
162
|
+
*/
|
|
163
|
+
export const signal = init => new Signal(init);
|
|
164
|
+
|
|
165
|
+
/** @type {<T>(fn: () => T) => T} */
|
|
166
|
+
export const untracked = fn => {
|
|
167
|
+
const before = tracking;
|
|
168
|
+
tracking = false;
|
|
169
|
+
try { return fn() }
|
|
170
|
+
finally { tracking = before }
|
|
171
|
+
};
|
package/types/index.d.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export class Signal<T> {
|
|
2
|
+
constructor(init: T);
|
|
3
|
+
set value(value: T);
|
|
4
|
+
get value(): T;
|
|
5
|
+
peek(): T;
|
|
6
|
+
#private;
|
|
7
|
+
}
|
|
8
|
+
export class Computed<T> extends Signal<() => T> {
|
|
9
|
+
[x: number]: () => void;
|
|
10
|
+
constructor(init: () => T);
|
|
11
|
+
readonly get value(): T;
|
|
12
|
+
peek(): T;
|
|
13
|
+
#private;
|
|
14
|
+
}
|
|
15
|
+
export const batch: <T>(fn: () => T) => T;
|
|
16
|
+
export function computed<T>(fn: () => T): Computed<T>;
|
|
17
|
+
export function effect(fn: () => (void | (() => void))): () => void;
|
|
18
|
+
export function signal<T>(init: T): Signal<T>;
|
|
19
|
+
export const untracked: <T>(fn: () => T) => T;
|
package/dist/branded.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var f=!0,l=t=>{f=t},s,u=t=>{f&&s&&t.add(s)},a=(t,e)=>{let r=s;s=t;try{return e()}finally{s=r}};var i=new WeakMap,p,S=t=>{let e=p;e||(p=[]);try{return t()}finally{if(!e){[e,p]=[p,e];for(let[r,o]of e)i.has(r)&&o()}}},d=t=>{t.c?.(),t.s.length&&t.s.splice(0).forEach(x)},x=t=>{let e=i.get(t);e.d=!0,d(e),i.delete(t)},b=t=>{let e=()=>{o||c.d||(o=!0,s||(p?p.push([e,r]):r()))},r=()=>{for(;o;)if(o=!1,d(c),c.c=a(e,t),c.d)return},o=!0,n,c={s:[],d:!o,c:n};return s&&i.get(s).s.push(e),i.set(e,c),r(),()=>{c.d||x(e)}};var g=t=>{let e=new Set,r=!0,o,n=()=>{for(;r;)r=!1,o=a(c,t);return o},c=()=>{if(r)return;r=!0;let k=e;e=new Set;for(let w of k)w()};return{get value(){return u(e),n()},peek:n}};var h=t=>{let e=new Set;return{get value(){return u(e),t},set value(r){if(t=r,f){let o=e;e=new Set;for(let n of o)n()}},peek(){return t}}};var j=t=>{let e=f;l(!1);try{return t()}finally{l(e)}};var G=t=>function(...r){let o,n=b(()=>{o??=t.apply(this,r)??this});return o[Symbol.dispose]=n,o};var m=new WeakSet,L=t=>{let e=g(t);return m.add(e),e},N=t=>m.has(t),O=t=>{let e=h(t);return m.add(e),e};export{S as batch,L as computed,G as disposable,b as effect,N as isSignal,O as signal,j as untracked};
|
package/src/branded.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export * from './disposable.js';
|
|
2
|
-
export * from './effect.js';
|
|
3
|
-
export * from './untracked.js';
|
|
4
|
-
|
|
5
|
-
import { computed as $computed } from './computed.js';
|
|
6
|
-
import { signal as $signal } from './signal.js';
|
|
7
|
-
|
|
8
|
-
const branded = new WeakSet;
|
|
9
|
-
|
|
10
|
-
/** @type {<T>(fn: () => T) => { readonly value: T, peek: () => T }} */
|
|
11
|
-
export const computed = fn => {
|
|
12
|
-
const computed = $computed(fn);
|
|
13
|
-
branded.add(computed);
|
|
14
|
-
return computed;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/** @type {(value: unknown) => boolean} */
|
|
18
|
-
export const isSignal = value => branded.has(value);
|
|
19
|
-
|
|
20
|
-
/** @type {<T>(init: T) => { value: T, peek: () => T }} */
|
|
21
|
-
export const signal = init => {
|
|
22
|
-
const signal = $signal(init);
|
|
23
|
-
branded.add(signal);
|
|
24
|
-
return signal;
|
|
25
|
-
};
|
package/src/computed.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { push, run } from './stack.js';
|
|
2
|
-
|
|
3
|
-
/** @type {<T>(fn: () => T) => { readonly value: T, peek: () => T }} */
|
|
4
|
-
export const computed = fn => {
|
|
5
|
-
let subscribers = new Set, invalid = true, value;
|
|
6
|
-
|
|
7
|
-
const peek = () => {
|
|
8
|
-
while (invalid) {
|
|
9
|
-
invalid = false;
|
|
10
|
-
value = run(subscriber, fn);
|
|
11
|
-
}
|
|
12
|
-
return value;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const subscriber = () => {
|
|
16
|
-
if (invalid) return;
|
|
17
|
-
invalid = true;
|
|
18
|
-
const before = subscribers;
|
|
19
|
-
subscribers = new Set;
|
|
20
|
-
for (const sub of before) sub();
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
get value() {
|
|
25
|
-
push(subscribers);
|
|
26
|
-
return peek();
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
peek,
|
|
30
|
-
};
|
|
31
|
-
};
|
package/src/effect.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { run, stack } from './stack.js';
|
|
2
|
-
|
|
3
|
-
const effects = new WeakMap;
|
|
4
|
-
|
|
5
|
-
let batches;
|
|
6
|
-
|
|
7
|
-
/** @type {<T>(fn: () => T) => T} */
|
|
8
|
-
export const batch = fn => {
|
|
9
|
-
let before = batches;
|
|
10
|
-
if (!before) batches = [];
|
|
11
|
-
try { return fn() }
|
|
12
|
-
finally {
|
|
13
|
-
if (!before) {
|
|
14
|
-
[before, batches] = [batches, before];
|
|
15
|
-
for (const [sub, loop] of before) {
|
|
16
|
-
if (effects.has(sub)) loop();
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const cleanUp = state => {
|
|
23
|
-
state.c?.();
|
|
24
|
-
if (state.s.length) state.s.splice(0).forEach(dispose);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const dispose = subscriber => {
|
|
28
|
-
const state = effects.get(subscriber);
|
|
29
|
-
state.d = true;
|
|
30
|
-
cleanUp(state);
|
|
31
|
-
effects.delete(subscriber);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/** @type {(fn: (() => void | (() => void))) => (() => void)} */
|
|
35
|
-
export const effect = fn => {
|
|
36
|
-
const subscriber = () => {
|
|
37
|
-
if (invalid || state.d) return;
|
|
38
|
-
invalid = true;
|
|
39
|
-
if (!stack) {
|
|
40
|
-
if (batches) batches.push([subscriber, loop]);
|
|
41
|
-
else loop();
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const loop = () => {
|
|
46
|
-
while (invalid) {
|
|
47
|
-
invalid = false;
|
|
48
|
-
cleanUp(state);
|
|
49
|
-
state.c = run(subscriber, fn);
|
|
50
|
-
if (state.d) return;
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
let invalid = true, c, state = { s: [], d: !invalid, c };
|
|
55
|
-
|
|
56
|
-
if (stack) effects.get(stack).s.push(subscriber);
|
|
57
|
-
|
|
58
|
-
effects.set(subscriber, state);
|
|
59
|
-
|
|
60
|
-
loop();
|
|
61
|
-
|
|
62
|
-
return () => {
|
|
63
|
-
if (!state.d) dispose(subscriber);
|
|
64
|
-
};
|
|
65
|
-
};
|
package/src/signal.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { push, tracking } from './stack.js';
|
|
2
|
-
|
|
3
|
-
/** @type {<T>(init: T) => { value: T, peek: () => T }} */
|
|
4
|
-
export const signal = init => {
|
|
5
|
-
let subscribers = new Set;
|
|
6
|
-
|
|
7
|
-
return {
|
|
8
|
-
get value() {
|
|
9
|
-
push(subscribers);
|
|
10
|
-
return init;
|
|
11
|
-
},
|
|
12
|
-
|
|
13
|
-
set value(value) {
|
|
14
|
-
init = value;
|
|
15
|
-
if (tracking) {
|
|
16
|
-
const before = subscribers;
|
|
17
|
-
subscribers = new Set;
|
|
18
|
-
for (const sub of before) sub();
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
peek() {
|
|
23
|
-
return init;
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
};
|
package/src/stack.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export let tracking = true;
|
|
2
|
-
|
|
3
|
-
export const forceTracking = value => {
|
|
4
|
-
tracking = value;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export let stack;
|
|
8
|
-
|
|
9
|
-
export const push = subscribers => {
|
|
10
|
-
if (tracking && stack) subscribers.add(stack);
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const run = (subscriber, callback) => {
|
|
14
|
-
const before = stack;
|
|
15
|
-
stack = subscriber;
|
|
16
|
-
try { return callback() }
|
|
17
|
-
finally { stack = before }
|
|
18
|
-
};
|
package/src/untracked.js
DELETED
package/types/branded.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export * from "./disposable.js";
|
|
2
|
-
export * from "./effect.js";
|
|
3
|
-
export * from "./untracked.js";
|
|
4
|
-
/** @type {<T>(fn: () => T) => { readonly value: T, peek: () => T }} */
|
|
5
|
-
export const computed: <T>(fn: () => T) => {
|
|
6
|
-
readonly value: T;
|
|
7
|
-
peek: () => T;
|
|
8
|
-
};
|
|
9
|
-
/** @type {(value: unknown) => boolean} */
|
|
10
|
-
export const isSignal: (value: unknown) => boolean;
|
|
11
|
-
/** @type {<T>(init: T) => { value: T, peek: () => T }} */
|
|
12
|
-
export const signal: <T>(init: T) => {
|
|
13
|
-
value: T;
|
|
14
|
-
peek: () => T;
|
|
15
|
-
};
|
package/types/computed.d.ts
DELETED
package/types/effect.d.ts
DELETED
package/types/signal.d.ts
DELETED
package/types/stack.d.ts
DELETED
package/types/untracked.d.ts
DELETED