flatsignals 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 kevintakeda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # FlatSignals
2
+
3
+ FlatSignals is an extremely fast reactivity library (~0.7kb).
4
+
5
+ - Lightning-fast batch updates.
6
+ - No graph traversals.
7
+ - Dynamic dependencies managed in O(1) time.
8
+ - Automatic disposals.
9
+ - Computeds are lazy by default.
10
+
11
+ ## Benchmarks
12
+
13
+ You can execute the [benchmarks](https://github.com/kevintakeda/flatsignals/tree/main/benchmarks) by running `pnpm bench`.
14
+
15
+ ```console
16
+ flatsignals - benchmarks/signals.bench.ts > one to many sparse (32x)
17
+ 1.60x faster than @preact/signals
18
+ 1.70x faster than @reactively/core
19
+ 1.95x faster than alien-signals
20
+ 2.27x faster than @maverick-js/signals
21
+
22
+ flatsignals - benchmarks/signals.bench.ts > wide propagation (32x)
23
+ 1.72x faster than @preact/signals
24
+ 1.74x faster than alien-signals
25
+ 2.22x faster than @reactively/core
26
+ 2.94x faster than @maverick-js/signals
27
+
28
+ flatsignals - benchmarks/signals.bench.ts > deep propagation (32x)
29
+ 1.02x faster than alien-signals
30
+ 1.29x faster than @preact/signals
31
+ 1.55x faster than @reactively/core
32
+ 1.97x faster than @maverick-js/signals
33
+
34
+ flatsignals - benchmarks/signals.bench.ts > dynamic
35
+ 2.82x faster than @preact/signals
36
+ 2.98x faster than @reactively/core
37
+ 3.54x faster than @maverick-js/signals
38
+ 4.56x faster than alien-signals
39
+
40
+ flatsignals - benchmarks/signals.bench.ts > batch 25%
41
+ 1.26x faster than @preact/signals
42
+ 1.56x faster than alien-signals
43
+ 1.66x faster than @reactively/core
44
+ 1.97x faster than @maverick-js/signals
45
+
46
+ flatsignals - benchmarks/signals.bench.ts > batch 50%
47
+ 1.48x faster than @preact/signals
48
+ 1.62x faster than alien-signals
49
+ 1.72x faster than @reactively/core
50
+ 2.09x faster than @maverick-js/signals
51
+
52
+ flatsignals - benchmarks/signals.bench.ts > packed 30%
53
+ 1.49x faster than @reactively/core
54
+ 1.77x faster than alien-signals
55
+ 1.77x faster than @preact/signals
56
+ 1.99x faster than @maverick-js/signals
57
+
58
+ flatsignals - benchmarks/signals.bench.ts > dense batch ~1/3 (2x layers)
59
+ 1.72x faster than @preact/signals
60
+ 2.17x faster than @reactively/core
61
+ 2.25x faster than alien-signals
62
+ 2.86x faster than @maverick-js/signals
63
+
64
+ flatsignals - benchmarks/signals.bench.ts > dense batch ~1/3 (4x layers)
65
+ 1.98x faster than @preact/signals
66
+ 2.11x faster than @reactively/core
67
+ 2.75x faster than alien-signals
68
+ 3.05x faster than @maverick-js/signals
69
+
70
+ alien-signals - benchmarks/signals.bench.ts > one to one to one (32x)
71
+ 1.00x faster than @preact/signals
72
+ 1.05x faster than flatsignals
73
+ 1.16x faster than @maverick-js/signals
74
+ 1.94x faster than @reactively/core
75
+ ```
76
+
77
+ ## Tradeoffs
78
+
79
+ 1. Supports only 32 signals per root.
80
+ 2. Set operations have 𝑂(𝑁) complexity, where 𝑁 is the number of computations. However, multiple updates can be batched into a single operation.
81
+ 3. When data sources change, all dependent nodes are marked dirty, even if intermediate values stay the same.
package/dist/index.cjs ADDED
@@ -0,0 +1,222 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var ROOT = null;
5
+ var COMPUTED = null;
6
+ var BATCHING = false;
7
+ var ROOT_QUEUE = [];
8
+ var Scope = class {
9
+ /* @internal */
10
+ _disposing = false;
11
+ /* @internal */
12
+ _disposals = null;
13
+ constructor() {
14
+ getScope()?._onDispose(this._dispose.bind(this));
15
+ }
16
+ /* @internal */
17
+ _dispose() {
18
+ if (this._disposing) return;
19
+ this._disposing = true;
20
+ this._disposals?.forEach((fn) => fn());
21
+ this._disposing = false;
22
+ }
23
+ /* @internal */
24
+ _onDispose(fn) {
25
+ if (!this._disposals) this._disposals = [];
26
+ this._disposals.push(fn);
27
+ }
28
+ };
29
+ var Root = class extends Scope {
30
+ /* @internal computeds */
31
+ _c = [];
32
+ /* @internal disposed computeds */
33
+ _x = [];
34
+ /* @internal id generator */
35
+ _i = 0;
36
+ /* @internal batch mask */
37
+ #batch = 0;
38
+ /* @internal */
39
+ _dispose() {
40
+ super._dispose();
41
+ this._c = [];
42
+ this._x = [];
43
+ this._i = 0;
44
+ }
45
+ /* @internal Add source */
46
+ _as() {
47
+ return this._i++ % 32;
48
+ }
49
+ /* @internal Add computed */
50
+ _ac(c) {
51
+ if (this._x.length) {
52
+ const u = this._x.pop();
53
+ this._c[u] = c;
54
+ return u;
55
+ } else {
56
+ return this._c.push(c) - 1;
57
+ }
58
+ }
59
+ /* @internal Destroy computed */
60
+ _dc(idx) {
61
+ this._x.push(idx);
62
+ }
63
+ /* @internal */
64
+ _queue(mask) {
65
+ if (!this.#batch) {
66
+ ROOT_QUEUE.push(this);
67
+ }
68
+ this.#batch |= mask;
69
+ if (!BATCHING) this._flush(true);
70
+ }
71
+ /* @internal */
72
+ _flush(force = false) {
73
+ if (!this.#batch || !force) return;
74
+ for (const item of this._c) {
75
+ if (!item._dirty && (item._sources & this.#batch) !== 0) {
76
+ item._dirty = true;
77
+ if (item._effect) item.val;
78
+ }
79
+ }
80
+ this.#batch = 0;
81
+ }
82
+ };
83
+ var DataSignal = class {
84
+ #root;
85
+ #val;
86
+ #id = 0;
87
+ equals = defaultEquality;
88
+ constructor(val) {
89
+ if (!ROOT) ROOT = new Root();
90
+ this.#root = ROOT;
91
+ this.#val = val;
92
+ this.#id |= 1 << this.#root._as();
93
+ }
94
+ get val() {
95
+ if (COMPUTED) {
96
+ COMPUTED._sources |= this.#id;
97
+ }
98
+ return this.#val;
99
+ }
100
+ set val(val) {
101
+ if (this.equals(val, this.#val)) return;
102
+ this.#val = val;
103
+ this.#root._queue(this.#id);
104
+ }
105
+ };
106
+ var Computation = class extends Scope {
107
+ #root;
108
+ #id;
109
+ #val;
110
+ #fn;
111
+ /* @internal */
112
+ _effect = false;
113
+ /* @internal */
114
+ _sources = 0;
115
+ /* @internal */
116
+ _dirty = true;
117
+ /* @internal destroyed */
118
+ _d = false;
119
+ constructor(compute, val, effect2) {
120
+ if (!ROOT) ROOT = new Root();
121
+ super();
122
+ this.#root = ROOT;
123
+ this.#fn = compute;
124
+ this.#val = val;
125
+ this.#id = this.#root._ac(this);
126
+ if (effect2) {
127
+ this._effect = effect2;
128
+ this.val;
129
+ }
130
+ }
131
+ get val() {
132
+ this.#root._flush();
133
+ const prevCurrent = COMPUTED;
134
+ if (this._dirty) {
135
+ super._dispose();
136
+ COMPUTED = this;
137
+ this._sources = 0;
138
+ this.#val = this.#fn();
139
+ this._dirty = false;
140
+ COMPUTED = prevCurrent;
141
+ }
142
+ if (prevCurrent) {
143
+ prevCurrent._sources |= this._sources;
144
+ }
145
+ return this.#val;
146
+ }
147
+ get peek() {
148
+ return this.#val;
149
+ }
150
+ getRoot() {
151
+ return this.#root;
152
+ }
153
+ /* @internal */
154
+ _dispose() {
155
+ if (this._d) return;
156
+ super._dispose();
157
+ this._sources = 0;
158
+ this._dirty = false;
159
+ this._d = true;
160
+ this.#root._dc(this.#id);
161
+ }
162
+ };
163
+ function defaultEquality(a, b) {
164
+ return a === b;
165
+ }
166
+ function getScope() {
167
+ return COMPUTED || ROOT;
168
+ }
169
+ function withScope(scope, fn) {
170
+ const prevComputed = COMPUTED, prevRoot = ROOT;
171
+ if (scope instanceof Computation) COMPUTED = scope;
172
+ ROOT = COMPUTED?.getRoot() ?? ROOT;
173
+ const result = fn();
174
+ COMPUTED = prevComputed;
175
+ ROOT = prevRoot;
176
+ return result;
177
+ }
178
+ function onDispose(fn) {
179
+ getScope()?._onDispose(fn);
180
+ }
181
+ function batch(fn) {
182
+ if (BATCHING) return fn();
183
+ BATCHING = true;
184
+ fn();
185
+ if (ROOT_QUEUE.length) {
186
+ for (const el of ROOT_QUEUE) el._flush(true);
187
+ ROOT_QUEUE = [];
188
+ }
189
+ BATCHING = false;
190
+ }
191
+ function root(fn, existingRoot) {
192
+ const prevRoot = ROOT;
193
+ const prevScope = getScope();
194
+ ROOT = existingRoot ?? new Root();
195
+ prevScope?._onDispose(ROOT._dispose.bind(ROOT));
196
+ const result = fn(ROOT._dispose.bind(ROOT));
197
+ ROOT = prevRoot;
198
+ return result;
199
+ }
200
+ function signal(value) {
201
+ return new DataSignal(value);
202
+ }
203
+ function computed(val) {
204
+ return new Computation(val);
205
+ }
206
+ function effect(fn) {
207
+ const sig = new Computation(fn, void 0, true);
208
+ return sig._dispose.bind(sig);
209
+ }
210
+
211
+ exports.Computation = Computation;
212
+ exports.DataSignal = DataSignal;
213
+ exports.Root = Root;
214
+ exports.Scope = Scope;
215
+ exports.batch = batch;
216
+ exports.computed = computed;
217
+ exports.effect = effect;
218
+ exports.getScope = getScope;
219
+ exports.onDispose = onDispose;
220
+ exports.root = root;
221
+ exports.signal = signal;
222
+ exports.withScope = withScope;
@@ -0,0 +1,32 @@
1
+ declare class Scope {
2
+ constructor();
3
+ }
4
+ declare class Root extends Scope {
5
+ #private;
6
+ }
7
+ declare class DataSignal<T = any> {
8
+ #private;
9
+ equals: typeof defaultEquality;
10
+ constructor(val?: T);
11
+ get val(): T;
12
+ set val(val: T);
13
+ }
14
+ declare class Computation<T = unknown> extends Scope {
15
+ #private;
16
+ constructor(compute?: () => T, val?: T, effect?: boolean);
17
+ get val(): T;
18
+ get peek(): T;
19
+ getRoot(): Root;
20
+ }
21
+ declare function defaultEquality(a: unknown, b: unknown): boolean;
22
+ declare function getScope(): Scope | null;
23
+ declare function withScope(scope: Scope, fn: () => void): void;
24
+ declare function onDispose(fn: () => void): void;
25
+ declare function batch(fn: () => void): void;
26
+ declare function root<T>(fn: (dispose: () => void) => T, existingRoot?: Root): T;
27
+ declare function signal<T>(value: T): DataSignal<T>;
28
+ declare function signal<T = undefined>(): DataSignal<T | undefined>;
29
+ declare function computed<T>(val: () => T): Computation<T>;
30
+ declare function effect<T = unknown>(fn: () => T): () => void;
31
+
32
+ export { Computation, DataSignal, Root, Scope, batch, computed, effect, getScope, onDispose, root, signal, withScope };
@@ -0,0 +1,32 @@
1
+ declare class Scope {
2
+ constructor();
3
+ }
4
+ declare class Root extends Scope {
5
+ #private;
6
+ }
7
+ declare class DataSignal<T = any> {
8
+ #private;
9
+ equals: typeof defaultEquality;
10
+ constructor(val?: T);
11
+ get val(): T;
12
+ set val(val: T);
13
+ }
14
+ declare class Computation<T = unknown> extends Scope {
15
+ #private;
16
+ constructor(compute?: () => T, val?: T, effect?: boolean);
17
+ get val(): T;
18
+ get peek(): T;
19
+ getRoot(): Root;
20
+ }
21
+ declare function defaultEquality(a: unknown, b: unknown): boolean;
22
+ declare function getScope(): Scope | null;
23
+ declare function withScope(scope: Scope, fn: () => void): void;
24
+ declare function onDispose(fn: () => void): void;
25
+ declare function batch(fn: () => void): void;
26
+ declare function root<T>(fn: (dispose: () => void) => T, existingRoot?: Root): T;
27
+ declare function signal<T>(value: T): DataSignal<T>;
28
+ declare function signal<T = undefined>(): DataSignal<T | undefined>;
29
+ declare function computed<T>(val: () => T): Computation<T>;
30
+ declare function effect<T = unknown>(fn: () => T): () => void;
31
+
32
+ export { Computation, DataSignal, Root, Scope, batch, computed, effect, getScope, onDispose, root, signal, withScope };
package/dist/index.js ADDED
@@ -0,0 +1,209 @@
1
+ // src/index.ts
2
+ var ROOT = null;
3
+ var COMPUTED = null;
4
+ var BATCHING = false;
5
+ var ROOT_QUEUE = [];
6
+ var Scope = class {
7
+ /* @internal */
8
+ _disposing = false;
9
+ /* @internal */
10
+ _disposals = null;
11
+ constructor() {
12
+ getScope()?._onDispose(this._dispose.bind(this));
13
+ }
14
+ /* @internal */
15
+ _dispose() {
16
+ if (this._disposing) return;
17
+ this._disposing = true;
18
+ this._disposals?.forEach((fn) => fn());
19
+ this._disposing = false;
20
+ }
21
+ /* @internal */
22
+ _onDispose(fn) {
23
+ if (!this._disposals) this._disposals = [];
24
+ this._disposals.push(fn);
25
+ }
26
+ };
27
+ var Root = class extends Scope {
28
+ /* @internal computeds */
29
+ _c = [];
30
+ /* @internal disposed computeds */
31
+ _x = [];
32
+ /* @internal id generator */
33
+ _i = 0;
34
+ /* @internal batch mask */
35
+ #batch = 0;
36
+ /* @internal */
37
+ _dispose() {
38
+ super._dispose();
39
+ this._c = [];
40
+ this._x = [];
41
+ this._i = 0;
42
+ }
43
+ /* @internal Add source */
44
+ _as() {
45
+ return this._i++ % 32;
46
+ }
47
+ /* @internal Add computed */
48
+ _ac(c) {
49
+ if (this._x.length) {
50
+ const u = this._x.pop();
51
+ this._c[u] = c;
52
+ return u;
53
+ } else {
54
+ return this._c.push(c) - 1;
55
+ }
56
+ }
57
+ /* @internal Destroy computed */
58
+ _dc(idx) {
59
+ this._x.push(idx);
60
+ }
61
+ /* @internal */
62
+ _queue(mask) {
63
+ if (!this.#batch) {
64
+ ROOT_QUEUE.push(this);
65
+ }
66
+ this.#batch |= mask;
67
+ if (!BATCHING) this._flush(true);
68
+ }
69
+ /* @internal */
70
+ _flush(force = false) {
71
+ if (!this.#batch || !force) return;
72
+ for (const item of this._c) {
73
+ if (!item._dirty && (item._sources & this.#batch) !== 0) {
74
+ item._dirty = true;
75
+ if (item._effect) item.val;
76
+ }
77
+ }
78
+ this.#batch = 0;
79
+ }
80
+ };
81
+ var DataSignal = class {
82
+ #root;
83
+ #val;
84
+ #id = 0;
85
+ equals = defaultEquality;
86
+ constructor(val) {
87
+ if (!ROOT) ROOT = new Root();
88
+ this.#root = ROOT;
89
+ this.#val = val;
90
+ this.#id |= 1 << this.#root._as();
91
+ }
92
+ get val() {
93
+ if (COMPUTED) {
94
+ COMPUTED._sources |= this.#id;
95
+ }
96
+ return this.#val;
97
+ }
98
+ set val(val) {
99
+ if (this.equals(val, this.#val)) return;
100
+ this.#val = val;
101
+ this.#root._queue(this.#id);
102
+ }
103
+ };
104
+ var Computation = class extends Scope {
105
+ #root;
106
+ #id;
107
+ #val;
108
+ #fn;
109
+ /* @internal */
110
+ _effect = false;
111
+ /* @internal */
112
+ _sources = 0;
113
+ /* @internal */
114
+ _dirty = true;
115
+ /* @internal destroyed */
116
+ _d = false;
117
+ constructor(compute, val, effect2) {
118
+ if (!ROOT) ROOT = new Root();
119
+ super();
120
+ this.#root = ROOT;
121
+ this.#fn = compute;
122
+ this.#val = val;
123
+ this.#id = this.#root._ac(this);
124
+ if (effect2) {
125
+ this._effect = effect2;
126
+ this.val;
127
+ }
128
+ }
129
+ get val() {
130
+ this.#root._flush();
131
+ const prevCurrent = COMPUTED;
132
+ if (this._dirty) {
133
+ super._dispose();
134
+ COMPUTED = this;
135
+ this._sources = 0;
136
+ this.#val = this.#fn();
137
+ this._dirty = false;
138
+ COMPUTED = prevCurrent;
139
+ }
140
+ if (prevCurrent) {
141
+ prevCurrent._sources |= this._sources;
142
+ }
143
+ return this.#val;
144
+ }
145
+ get peek() {
146
+ return this.#val;
147
+ }
148
+ getRoot() {
149
+ return this.#root;
150
+ }
151
+ /* @internal */
152
+ _dispose() {
153
+ if (this._d) return;
154
+ super._dispose();
155
+ this._sources = 0;
156
+ this._dirty = false;
157
+ this._d = true;
158
+ this.#root._dc(this.#id);
159
+ }
160
+ };
161
+ function defaultEquality(a, b) {
162
+ return a === b;
163
+ }
164
+ function getScope() {
165
+ return COMPUTED || ROOT;
166
+ }
167
+ function withScope(scope, fn) {
168
+ const prevComputed = COMPUTED, prevRoot = ROOT;
169
+ if (scope instanceof Computation) COMPUTED = scope;
170
+ ROOT = COMPUTED?.getRoot() ?? ROOT;
171
+ const result = fn();
172
+ COMPUTED = prevComputed;
173
+ ROOT = prevRoot;
174
+ return result;
175
+ }
176
+ function onDispose(fn) {
177
+ getScope()?._onDispose(fn);
178
+ }
179
+ function batch(fn) {
180
+ if (BATCHING) return fn();
181
+ BATCHING = true;
182
+ fn();
183
+ if (ROOT_QUEUE.length) {
184
+ for (const el of ROOT_QUEUE) el._flush(true);
185
+ ROOT_QUEUE = [];
186
+ }
187
+ BATCHING = false;
188
+ }
189
+ function root(fn, existingRoot) {
190
+ const prevRoot = ROOT;
191
+ const prevScope = getScope();
192
+ ROOT = existingRoot ?? new Root();
193
+ prevScope?._onDispose(ROOT._dispose.bind(ROOT));
194
+ const result = fn(ROOT._dispose.bind(ROOT));
195
+ ROOT = prevRoot;
196
+ return result;
197
+ }
198
+ function signal(value) {
199
+ return new DataSignal(value);
200
+ }
201
+ function computed(val) {
202
+ return new Computation(val);
203
+ }
204
+ function effect(fn) {
205
+ const sig = new Computation(fn, void 0, true);
206
+ return sig._dispose.bind(sig);
207
+ }
208
+
209
+ export { Computation, DataSignal, Root, Scope, batch, computed, effect, getScope, onDispose, root, signal, withScope };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "flatsignals",
3
+ "version": "0.1.0",
4
+ "description": "FlatSignals is an extremely fast reactivity library (~0.7kb)",
5
+ "main": "dist/index.cjs",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "type": "module",
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "dev": "tsup --watch",
15
+ "test": "vitest run",
16
+ "bench": "vitest bench",
17
+ "test:watch": "vitest",
18
+ "size": "size-limit"
19
+ },
20
+ "license": "MIT",
21
+ "devDependencies": {
22
+ "@maverick-js/signals": "^6.0.0",
23
+ "@preact/signals-core": "^1.12.1",
24
+ "@reactively/core": "^0.0.8",
25
+ "@size-limit/preset-small-lib": "^11.2.0",
26
+ "@types/node": "^22.18.11",
27
+ "alien-signals": "^3.0.1",
28
+ "size-limit": "^11.2.0",
29
+ "tsup": "^8.5.0",
30
+ "typescript": "^5.9.3",
31
+ "vitest": "^2.1.9"
32
+ },
33
+ "size-limit": [
34
+ {
35
+ "path": "dist/index.js",
36
+ "limit": "1 kB"
37
+ }
38
+ ]
39
+ }