@webreflection/signals 0.1.8 → 0.2.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 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
- // ... same as core features ...
33
- batch,
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 33 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, 74 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
  ![benchmark](https://raw.githubusercontent.com/WebReflection/usignal/main/test/benchmark.png)
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 subscribes to changes, it just registers itself as an *observer*
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 tracking whatsoever, except the implementation keeps tracing what should run fresh and what doesn't exist anymore. The latest *effect* that got it right will trigger and eventually access, or *re-subscribe* to, anything in it, but just once, after the *batch* callback has finished.
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
 
@@ -1 +1 @@
1
- var c=!0,a=t=>{c=t},n,i=t=>{c&&n&&t.add(n)},p=(t,e)=>{let r=n;n=t;try{return e()}finally{n=r}};var l=new WeakSet,u=new WeakMap,f,y=t=>{let e=f;e||(f=[]);try{return t()}finally{if(!e){[e,f]=[f,e];for(let[r,o]of e)l.has(r)||o()}}},b=t=>{let e=u.get(t);if(e.length)for(let o of e.splice(0))h(o)},h=t=>{l.add(t),b(t),u.delete(t)},m=t=>{let e=()=>{o||l.has(e)||(o=!0,n||(f?f.push([e,r]):r()))},r=()=>{for(;o;)if(o=!1,b(e),s?.(),s=p(e,t),l.has(e))return},o=!0,s;return n&&u.get(n).push(e),u.set(e,[]),r(),()=>{u.has(e)&&(s?.(),h(e))}};var T=t=>{let e=new Set,r=!0,o,s=()=>{for(;r;)r=!1,o=p(d,t);return o},d=()=>{if(r)return;r=!0;let x=e;e=new Set;for(let g of x)g()};return{get value(){return i(e),s()},peek:s}};var U=t=>{let e=new Set;return{get value(){return i(e),t},set value(r){if(t=r,c){let o=e;e=new Set;for(let s of o)s()}},peek(){return t}}};var z=t=>{let e=c;a(!1);try{return t()}finally{a(e)}};var H=t=>function(...r){let o,s=m(()=>{o??=t.apply(this,r)??this});return o[Symbol.dispose]=s,o};export{y as batch,T as computed,H as disposable,m as effect,U as signal,z as untracked};
1
+ var r,f,u,b,i,o=!0,n=class{static{f=s=>s.#t,u=s=>s.#s,b=(s,e)=>{s.#s=e}}#s=new Set;#t;constructor(s){this.#t=s}get value(){return x(this.#s),this.#t}set value(s){if(this.#t=s,o){let e=this.#s;this.#s=new Set,y(e)}}peek(){return this.#t}},a=Symbol(),h=class extends n{#s=!0;#t;[a](){if(this.#s)return;this.#s=!0;let s=u(this);b(this,new Set),y(s)}get value(){return x(u(this)),this.peek()}peek(){for(;this.#s;)this.#s=!1,this.#t=w(this,f(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,d(this),this.cleanup=w(this,this.fn)}},m=t=>{let s=r;s||(r=[]);try{return t()}finally{if(!s){[s,r]=[r,s];for(let e of s)e.peek()}}},d=t=>{t.cleanup?.(),t.sub.length&&t.sub.splice(0).forEach(k)},S=t=>new h(t),k=t=>{t.disposed=!0,d(t)},v=t=>{let s=new l(t);return i&&i.sub.push(s),s.peek(),()=>{s.disposed||k(s)}},p=t=>{o=t},y=t=>{for(let s of t)s[a]()},x=t=>{o&&i&&t.add(i)},w=(t,s)=>{let e=i;i=t;try{return s()}finally{i=e}},T=t=>new n(t),V=t=>{let s=o;p(!1);try{return t()}finally{p(s)}};var z=t=>function(...e){let c,g=v(()=>{c??=t.apply(this,e)??this});return c[Symbol.dispose]=g,c};export{h as Computed,n as Signal,m as batch,S as computed,z as disposable,v as effect,T as signal,V as untracked};
package/dist/signals.js CHANGED
@@ -1 +1 @@
1
- var c=!0,p=t=>{c=t},s,a=t=>{c&&s&&t.add(s)},i=(t,e)=>{let r=s;s=t;try{return e()}finally{s=r}};var l=new WeakSet,u=new WeakMap,f,w=t=>{let e=f;e||(f=[]);try{return t()}finally{if(!e){[e,f]=[f,e];for(let[r,o]of e)l.has(r)||o()}}},b=t=>{let e=u.get(t);if(e.length)for(let o of e.splice(0))h(o)},h=t=>{l.add(t),b(t),u.delete(t)},v=t=>{let e=()=>{o||l.has(e)||(o=!0,s||(f?f.push([e,r]):r()))},r=()=>{for(;o;)if(o=!1,b(e),n?.(),n=i(e,t),l.has(e))return},o=!0,n;return s&&u.get(s).push(e),u.set(e,[]),r(),()=>{u.has(e)&&(n?.(),h(e))}};var T=t=>{let e=new Set,r=!0,o,n=()=>{for(;r;)r=!1,o=i(x,t);return o},x=()=>{if(r)return;r=!0;let d=e;e=new Set;for(let g of d)g()};return{get value(){return a(e),n()},peek:n}};var U=t=>{let e=new Set;return{get value(){return a(e),t},set value(r){if(t=r,c){let o=e;e=new Set;for(let n of o)n()}},peek(){return t}}};var z=t=>{let e=c;p(!1);try{return t()}finally{p(e)}};export{w as batch,T as computed,v as effect,U as signal,z as untracked};
1
+ var r,f,o,p,e,c=!0,n=class{static{f=s=>s.#t,o=s=>s.#s,p=(s,i)=>{s.#s=i}}#s=new Set;#t;constructor(s){this.#t=s}get value(){return v(this.#s),this.#t}set value(s){if(this.#t=s,c){let i=this.#s;this.#s=new Set,k(i)}}peek(){return this.#t}},l=Symbol(),u=class extends n{#s=!0;#t;[l](){if(this.#s)return;this.#s=!0;let s=o(this);p(this,new Set),k(s)}get value(){return v(o(this)),this.peek()}peek(){for(;this.#s;)this.#s=!1,this.#t=w(this,f(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,b(this),this.cleanup=w(this,this.fn)}},y=t=>{let s=r;s||(r=[]);try{return t()}finally{if(!s){[s,r]=[r,s];for(let i of s)i.peek()}}},b=t=>{t.cleanup?.(),t.sub.length&&t.sub.splice(0).forEach(d)},g=t=>new u(t),d=t=>{t.disposed=!0,b(t)},x=t=>{let s=new h(t);return e&&e.sub.push(s),s.peek(),()=>{s.disposed||d(s)}},a=t=>{c=t},k=t=>{for(let s of t)s[l]()},v=t=>{c&&e&&t.add(e)},w=(t,s)=>{let i=e;e=t;try{return s()}finally{e=i}},S=t=>new n(t),m=t=>{let s=c;a(!1);try{return t()}finally{a(s)}};export{u as Computed,n as Signal,y as batch,g as computed,x as effect,S as signal,m as untracked};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webreflection/signals",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
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
- ".": "./src/index.js",
24
- "./brand": "./dist/branded.js",
25
- "./branded": "./src/branded.js",
26
- "./disposable": "./src/disposable.js",
27
- "./disp": "./dist/disposable.js",
28
- "./dist": "./dist/signals.js",
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:branded && npm run build:types && npm run test && npm run size",
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; echo '\nbranded'; cat dist/branded.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
@@ -1,5 +1,5 @@
1
1
  export * from './index.js';
2
- import { effect } from './effect.js';
2
+ import { effect } from './index.js';
3
3
 
4
4
  export const disposable = fn => function disposable(...args) {
5
5
  let ref, value = effect(() => {
package/src/index.js CHANGED
@@ -1,4 +1,175 @@
1
- export * from './effect.js';
2
- export * from './computed.js';
3
- export * from './signal.js';
4
- export * from './untracked.js';
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 forceTracking = value => {
144
+ tracking = value;
145
+ };
146
+
147
+ const notify = subscribers => {
148
+ for (const subscriber of subscribers) subscriber[compute]();
149
+ };
150
+
151
+ const push = subscribers => {
152
+ if (tracking && stack) subscribers.add(stack);
153
+ };
154
+
155
+ const run = (state, callback) => {
156
+ const before = stack;
157
+ stack = state;
158
+ try { return callback() }
159
+ finally { stack = before }
160
+ };
161
+
162
+ /**
163
+ * @template T
164
+ * @param {T} init
165
+ * @returns
166
+ */
167
+ export const signal = init => new Signal(init);
168
+
169
+ /** @type {<T>(fn: () => T) => T} */
170
+ export const untracked = fn => {
171
+ const before = tracking;
172
+ forceTracking(false);
173
+ try { return fn() }
174
+ finally { forceTracking(before) }
175
+ };
package/types/index.d.ts CHANGED
@@ -1,4 +1,19 @@
1
- export * from "./effect.js";
2
- export * from "./computed.js";
3
- export * from "./signal.js";
4
- export * from "./untracked.js";
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 c=!0,l=t=>{c=t},n,a=t=>{c&&n&&t.add(n)},i=(t,e)=>{let r=n;n=t;try{return e()}finally{n=r}};var u=new WeakSet,p=new WeakMap,f,W=t=>{let e=f;e||(f=[]);try{return t()}finally{if(!e){[e,f]=[f,e];for(let[r,o]of e)u.has(r)||o()}}},d=t=>{let e=p.get(t);if(e.length)for(let o of e.splice(0))b(o)},b=t=>{u.add(t),d(t),p.delete(t)},x=t=>{let e=()=>{o||u.has(e)||(o=!0,n||(f?f.push([e,r]):r()))},r=()=>{for(;o;)if(o=!1,d(e),s?.(),s=i(e,t),u.has(e))return},o=!0,s;return n&&p.get(n).push(e),p.set(e,[]),r(),()=>{p.has(e)&&(s?.(),b(e))}};var h=t=>{let e=new Set,r=!0,o,s=()=>{for(;r;)r=!1,o=i(k,t);return o},k=()=>{if(r)return;r=!0;let w=e;e=new Set;for(let y of w)y()};return{get value(){return a(e),s()},peek:s}};var g=t=>{let e=new Set;return{get value(){return a(e),t},set value(r){if(t=r,c){let o=e;e=new Set;for(let s of o)s()}},peek(){return t}}};var z=t=>{let e=c;l(!1);try{return t()}finally{l(e)}};var H=t=>function(...r){let o,s=x(()=>{o??=t.apply(this,r)??this});return o[Symbol.dispose]=s,o};var m=new WeakSet,N=t=>{let e=h(t);return m.add(e),e},O=t=>m.has(t),P=t=>{let e=g(t);return m.add(e),e};export{W as batch,N as computed,H as disposable,x as effect,O as isSignal,P as signal,z 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,74 +0,0 @@
1
- import { run, stack } from './stack.js';
2
-
3
- const disposed = new WeakSet;
4
- const effects = new WeakMap;
5
-
6
- let batches;
7
-
8
- /** @type {<T>(fn: () => T) => T} */
9
- export const batch = fn => {
10
- let updates = batches;
11
- if (!updates) batches = [];
12
- try { return fn() }
13
- finally {
14
- if (!updates) {
15
- [updates, batches] = [batches, updates];
16
- for (const [subscriber, run] of updates) {
17
- if (!disposed.has(subscriber)) run();
18
- }
19
- }
20
- }
21
- };
22
-
23
- const cleanUp = subscriber => {
24
- const subscribers = effects.get(subscriber);
25
- const length = subscribers.length;
26
- if (length) {
27
- for (const subscriber of subscribers.splice(0)) {
28
- drop(subscriber);
29
- }
30
- }
31
- };
32
-
33
- const drop = subscriber => {
34
- disposed.add(subscriber);
35
- cleanUp(subscriber);
36
- effects.delete(subscriber);
37
- };
38
-
39
- /** @type {(fn: (() => void | (() => void))) => (() => void)} */
40
- export const effect = fn => {
41
- const subscriber = () => {
42
- if (invalid || disposed.has(subscriber)) return;
43
- invalid = true;
44
- if (!stack) {
45
- if (batches) batches.push([subscriber, loop]);
46
- else loop();
47
- }
48
- };
49
-
50
- const loop = () => {
51
- while (invalid) {
52
- invalid = false;
53
- cleanUp(subscriber);
54
- clean?.();
55
- clean = run(subscriber, fn);
56
- if (disposed.has(subscriber)) return;
57
- }
58
- };
59
-
60
- let invalid = true, clean;
61
-
62
- if (stack) effects.get(stack).push(subscriber);
63
-
64
- effects.set(subscriber, []);
65
-
66
- loop();
67
-
68
- return () => {
69
- if (effects.has(subscriber)) {
70
- clean?.();
71
- drop(subscriber);
72
- }
73
- };
74
- };
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
@@ -1,9 +0,0 @@
1
- import { forceTracking, tracking } from './stack.js';
2
-
3
- /** @type {<T>(fn: () => T) => T} */
4
- export const untracked = fn => {
5
- const before = tracking;
6
- forceTracking(false);
7
- try { return fn() }
8
- finally { forceTracking(before) }
9
- };
@@ -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
- };
@@ -1,5 +0,0 @@
1
- /** @type {<T>(fn: () => T) => { readonly value: T, peek: () => T }} */
2
- export const computed: <T>(fn: () => T) => {
3
- readonly value: T;
4
- peek: () => T;
5
- };
package/types/effect.d.ts DELETED
@@ -1,4 +0,0 @@
1
- /** @type {<T>(fn: () => T) => T} */
2
- export const batch: <T>(fn: () => T) => T;
3
- /** @type {(fn: (() => void | (() => void))) => (() => void)} */
4
- export const effect: (fn: (() => void | (() => void))) => (() => void);
package/types/signal.d.ts DELETED
@@ -1,5 +0,0 @@
1
- /** @type {<T>(init: T) => { value: T, peek: () => T }} */
2
- export const signal: <T>(init: T) => {
3
- value: T;
4
- peek: () => T;
5
- };
package/types/stack.d.ts DELETED
@@ -1,5 +0,0 @@
1
- export let tracking: boolean;
2
- export function forceTracking(value: any): void;
3
- export let stack: any;
4
- export function push(subscribers: any): void;
5
- export function run(subscriber: any, callback: any): any;
@@ -1,2 +0,0 @@
1
- /** @type {<T>(fn: () => T) => T} */
2
- export const untracked: <T>(fn: () => T) => T;