@liteforge/core 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 +21 -0
- package/README.md +173 -0
- package/dist/batch.d.ts +32 -0
- package/dist/batch.d.ts.map +1 -0
- package/dist/batch.js +41 -0
- package/dist/batch.js.map +1 -0
- package/dist/cleanup.d.ts +30 -0
- package/dist/cleanup.d.ts.map +1 -0
- package/dist/cleanup.js +33 -0
- package/dist/cleanup.js.map +1 -0
- package/dist/computed.d.ts +46 -0
- package/dist/computed.d.ts.map +1 -0
- package/dist/computed.js +144 -0
- package/dist/computed.js.map +1 -0
- package/dist/debug.d.ts +264 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +275 -0
- package/dist/debug.js.map +1 -0
- package/dist/effect.d.ts +46 -0
- package/dist/effect.d.ts.map +1 -0
- package/dist/effect.js +131 -0
- package/dist/effect.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/internals.d.ts +83 -0
- package/dist/internals.d.ts.map +1 -0
- package/dist/internals.js +141 -0
- package/dist/internals.js.map +1 -0
- package/dist/signal.d.ts +70 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +104 -0
- package/dist/signal.js.map +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SchildW3rk
|
|
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,173 @@
|
|
|
1
|
+
# @liteforge/core
|
|
2
|
+
|
|
3
|
+
Fine-grained reactive primitives for LiteForge.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @liteforge/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
`@liteforge/core` provides the reactive foundation for LiteForge applications. It includes signals for reactive state, computed values for derived state, effects for side effects, and batching for performance optimization.
|
|
14
|
+
|
|
15
|
+
## API
|
|
16
|
+
|
|
17
|
+
### signal
|
|
18
|
+
|
|
19
|
+
Creates a reactive value that notifies subscribers when it changes.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { signal } from '@liteforge/core'
|
|
23
|
+
|
|
24
|
+
const count = signal(0)
|
|
25
|
+
|
|
26
|
+
// Read the value
|
|
27
|
+
count() // 0
|
|
28
|
+
|
|
29
|
+
// Set a new value
|
|
30
|
+
count.set(5)
|
|
31
|
+
|
|
32
|
+
// Update based on previous value
|
|
33
|
+
count.update(n => n + 1) // 6
|
|
34
|
+
|
|
35
|
+
// Peek without subscribing
|
|
36
|
+
count.peek() // 6
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Options:**
|
|
40
|
+
|
|
41
|
+
| Option | Type | Description |
|
|
42
|
+
|--------|------|-------------|
|
|
43
|
+
| `name` | `string` | Debug name for devtools |
|
|
44
|
+
| `equals` | `(a, b) => boolean` | Custom equality function |
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const user = signal({ name: 'Alice' }, {
|
|
48
|
+
name: 'currentUser',
|
|
49
|
+
equals: (a, b) => a.name === b.name
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### computed
|
|
54
|
+
|
|
55
|
+
Creates a derived value that automatically tracks dependencies.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { signal, computed } from '@liteforge/core'
|
|
59
|
+
|
|
60
|
+
const firstName = signal('John')
|
|
61
|
+
const lastName = signal('Doe')
|
|
62
|
+
|
|
63
|
+
const fullName = computed(() => `${firstName()} ${lastName()}`)
|
|
64
|
+
|
|
65
|
+
fullName() // "John Doe"
|
|
66
|
+
|
|
67
|
+
firstName.set('Jane')
|
|
68
|
+
fullName() // "Jane Doe"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Computed values are lazy and cached — they only recalculate when dependencies change and only when read.
|
|
72
|
+
|
|
73
|
+
### effect
|
|
74
|
+
|
|
75
|
+
Runs a function whenever its dependencies change.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { signal, effect } from '@liteforge/core'
|
|
79
|
+
|
|
80
|
+
const count = signal(0)
|
|
81
|
+
|
|
82
|
+
const dispose = effect(() => {
|
|
83
|
+
console.log(`Count is now: ${count()}`)
|
|
84
|
+
})
|
|
85
|
+
// Logs: "Count is now: 0"
|
|
86
|
+
|
|
87
|
+
count.set(1)
|
|
88
|
+
// Logs: "Count is now: 1"
|
|
89
|
+
|
|
90
|
+
// Stop the effect
|
|
91
|
+
dispose()
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### batch
|
|
95
|
+
|
|
96
|
+
Groups multiple signal updates into a single notification cycle.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { signal, effect, batch } from '@liteforge/core'
|
|
100
|
+
|
|
101
|
+
const a = signal(1)
|
|
102
|
+
const b = signal(2)
|
|
103
|
+
|
|
104
|
+
effect(() => {
|
|
105
|
+
console.log(`a=${a()}, b=${b()}`)
|
|
106
|
+
})
|
|
107
|
+
// Logs once: "a=1, b=2"
|
|
108
|
+
|
|
109
|
+
batch(() => {
|
|
110
|
+
a.set(10)
|
|
111
|
+
b.set(20)
|
|
112
|
+
})
|
|
113
|
+
// Logs once: "a=10, b=20"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Without batch, the effect would run twice (once for each signal update).
|
|
117
|
+
|
|
118
|
+
### onCleanup
|
|
119
|
+
|
|
120
|
+
Registers a cleanup function to run before an effect re-executes or is disposed.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { signal, effect, onCleanup } from '@liteforge/core'
|
|
124
|
+
|
|
125
|
+
const userId = signal(1)
|
|
126
|
+
|
|
127
|
+
effect(() => {
|
|
128
|
+
const id = userId()
|
|
129
|
+
const controller = new AbortController()
|
|
130
|
+
|
|
131
|
+
fetch(`/api/users/${id}`, { signal: controller.signal })
|
|
132
|
+
.then(r => r.json())
|
|
133
|
+
.then(console.log)
|
|
134
|
+
|
|
135
|
+
onCleanup(() => {
|
|
136
|
+
controller.abort()
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Types
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import type {
|
|
145
|
+
Signal,
|
|
146
|
+
ReadonlySignal,
|
|
147
|
+
SignalOptions,
|
|
148
|
+
EffectFn,
|
|
149
|
+
DisposeFn,
|
|
150
|
+
EffectOptions,
|
|
151
|
+
ComputeFn,
|
|
152
|
+
ComputedOptions
|
|
153
|
+
} from '@liteforge/core'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Debug Utilities
|
|
157
|
+
|
|
158
|
+
For integration with devtools:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { enableDebug, disableDebug, createDebugBus } from '@liteforge/core'
|
|
162
|
+
|
|
163
|
+
enableDebug()
|
|
164
|
+
|
|
165
|
+
const bus = createDebugBus()
|
|
166
|
+
bus.on('signal:update', (payload) => {
|
|
167
|
+
console.log(`Signal ${payload.name} changed to ${payload.value}`)
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
package/dist/batch.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiteForge Batch
|
|
3
|
+
*
|
|
4
|
+
* Batch multiple signal updates together to avoid redundant effect executions.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Batch multiple signal updates together.
|
|
8
|
+
*
|
|
9
|
+
* Effects will only be notified once after the batch completes,
|
|
10
|
+
* even if multiple signals are updated. Supports nested batches.
|
|
11
|
+
*
|
|
12
|
+
* @param fn - Function containing signal updates to batch
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const firstName = signal('John');
|
|
17
|
+
* const lastName = signal('Doe');
|
|
18
|
+
*
|
|
19
|
+
* effect(() => {
|
|
20
|
+
* console.log(firstName(), lastName());
|
|
21
|
+
* });
|
|
22
|
+
* // logs: "John Doe"
|
|
23
|
+
*
|
|
24
|
+
* batch(() => {
|
|
25
|
+
* firstName.set('Jane');
|
|
26
|
+
* lastName.set('Smith');
|
|
27
|
+
* });
|
|
28
|
+
* // logs: "Jane Smith" (only once, not twice)
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function batch(fn: () => void): void;
|
|
32
|
+
//# sourceMappingURL=batch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAO1C"}
|
package/dist/batch.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiteForge Batch
|
|
3
|
+
*
|
|
4
|
+
* Batch multiple signal updates together to avoid redundant effect executions.
|
|
5
|
+
*/
|
|
6
|
+
import { startBatch, endBatch } from './internals.js';
|
|
7
|
+
/**
|
|
8
|
+
* Batch multiple signal updates together.
|
|
9
|
+
*
|
|
10
|
+
* Effects will only be notified once after the batch completes,
|
|
11
|
+
* even if multiple signals are updated. Supports nested batches.
|
|
12
|
+
*
|
|
13
|
+
* @param fn - Function containing signal updates to batch
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const firstName = signal('John');
|
|
18
|
+
* const lastName = signal('Doe');
|
|
19
|
+
*
|
|
20
|
+
* effect(() => {
|
|
21
|
+
* console.log(firstName(), lastName());
|
|
22
|
+
* });
|
|
23
|
+
* // logs: "John Doe"
|
|
24
|
+
*
|
|
25
|
+
* batch(() => {
|
|
26
|
+
* firstName.set('Jane');
|
|
27
|
+
* lastName.set('Smith');
|
|
28
|
+
* });
|
|
29
|
+
* // logs: "Jane Smith" (only once, not twice)
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function batch(fn) {
|
|
33
|
+
startBatch();
|
|
34
|
+
try {
|
|
35
|
+
fn();
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
endBatch();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=batch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.js","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,KAAK,CAAC,EAAc;IAClC,UAAU,EAAE,CAAC;IACb,IAAI,CAAC;QACH,EAAE,EAAE,CAAC;IACP,CAAC;YAAS,CAAC;QACT,QAAQ,EAAE,CAAC;IACb,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiteForge Cleanup
|
|
3
|
+
*
|
|
4
|
+
* Register cleanup functions to run before an effect re-executes
|
|
5
|
+
* or when the effect is disposed.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Register a cleanup function for the current effect.
|
|
9
|
+
*
|
|
10
|
+
* The cleanup function will be called:
|
|
11
|
+
* - Before the effect re-runs (when dependencies change)
|
|
12
|
+
* - When the effect is disposed
|
|
13
|
+
*
|
|
14
|
+
* @param fn - The cleanup function to register
|
|
15
|
+
* @throws Error if called outside of an effect
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* effect(() => {
|
|
20
|
+
* const handler = () => console.log(count());
|
|
21
|
+
* window.addEventListener('resize', handler);
|
|
22
|
+
*
|
|
23
|
+
* onCleanup(() => {
|
|
24
|
+
* window.removeEventListener('resize', handler);
|
|
25
|
+
* });
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function onCleanup(fn: () => void): void;
|
|
30
|
+
//# sourceMappingURL=cleanup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../src/cleanup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAE9C"}
|
package/dist/cleanup.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiteForge Cleanup
|
|
3
|
+
*
|
|
4
|
+
* Register cleanup functions to run before an effect re-executes
|
|
5
|
+
* or when the effect is disposed.
|
|
6
|
+
*/
|
|
7
|
+
import { registerCleanup } from './internals.js';
|
|
8
|
+
/**
|
|
9
|
+
* Register a cleanup function for the current effect.
|
|
10
|
+
*
|
|
11
|
+
* The cleanup function will be called:
|
|
12
|
+
* - Before the effect re-runs (when dependencies change)
|
|
13
|
+
* - When the effect is disposed
|
|
14
|
+
*
|
|
15
|
+
* @param fn - The cleanup function to register
|
|
16
|
+
* @throws Error if called outside of an effect
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* effect(() => {
|
|
21
|
+
* const handler = () => console.log(count());
|
|
22
|
+
* window.addEventListener('resize', handler);
|
|
23
|
+
*
|
|
24
|
+
* onCleanup(() => {
|
|
25
|
+
* window.removeEventListener('resize', handler);
|
|
26
|
+
* });
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function onCleanup(fn) {
|
|
31
|
+
registerCleanup(fn);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=cleanup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../src/cleanup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,SAAS,CAAC,EAAc;IACtC,eAAe,CAAC,EAAE,CAAC,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiteForge Computed
|
|
3
|
+
*
|
|
4
|
+
* A Computed is a read-only signal derived from other signals.
|
|
5
|
+
* It lazily evaluates and caches its value until dependencies change.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReadonlySignal } from './signal.js';
|
|
8
|
+
/** Function that computes the derived value */
|
|
9
|
+
export type ComputeFn<T> = () => T;
|
|
10
|
+
/** Options for creating a computed value. */
|
|
11
|
+
export interface ComputedOptions {
|
|
12
|
+
/** Debug label for DevTools. If not provided, an auto-generated ID is used. */
|
|
13
|
+
label?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Mark computed as internal (DevTools-owned).
|
|
16
|
+
* Internal computeds don't emit debug events to avoid infinite loops.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
__internal?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create a computed (derived) signal.
|
|
23
|
+
*
|
|
24
|
+
* The computation only runs when the value is read AND dependencies have changed.
|
|
25
|
+
* The result is cached until a dependency changes.
|
|
26
|
+
*
|
|
27
|
+
* @param fn - Function that computes the derived value
|
|
28
|
+
* @param options - Optional configuration including debug label
|
|
29
|
+
* @returns A read-only signal
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const count = signal(0);
|
|
34
|
+
* const doubled = computed(() => count() * 2);
|
|
35
|
+
*
|
|
36
|
+
* doubled(); // → 0 (computed on first read)
|
|
37
|
+
* count.set(5);
|
|
38
|
+
* doubled(); // → 10 (recomputed because count changed)
|
|
39
|
+
* doubled(); // → 10 (cached, not recomputed)
|
|
40
|
+
*
|
|
41
|
+
* // With debug label
|
|
42
|
+
* const tripled = computed(() => count() * 3, { label: 'tripled' });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function computed<T>(fn: ComputeFn<T>, options?: ComputedOptions): ReadonlySignal<T>;
|
|
46
|
+
//# sourceMappingURL=computed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"computed.d.ts","sourceRoot":"","sources":["../src/computed.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAUlD,+CAA+C;AAC/C,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC;AAEnC,6CAA6C;AAC7C,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAMD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,EAChB,OAAO,CAAC,EAAE,eAAe,GACxB,cAAc,CAAC,CAAC,CAAC,CA0HnB"}
|
package/dist/computed.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiteForge Computed
|
|
3
|
+
*
|
|
4
|
+
* A Computed is a read-only signal derived from other signals.
|
|
5
|
+
* It lazily evaluates and caches its value until dependencies change.
|
|
6
|
+
*/
|
|
7
|
+
import { observerStack, getCurrentObserver, notifySubscribers, } from './internals.js';
|
|
8
|
+
import { generateDebugId, emitComputedRecalc, } from './debug.js';
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Implementation
|
|
11
|
+
// ============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Create a computed (derived) signal.
|
|
14
|
+
*
|
|
15
|
+
* The computation only runs when the value is read AND dependencies have changed.
|
|
16
|
+
* The result is cached until a dependency changes.
|
|
17
|
+
*
|
|
18
|
+
* @param fn - Function that computes the derived value
|
|
19
|
+
* @param options - Optional configuration including debug label
|
|
20
|
+
* @returns A read-only signal
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const count = signal(0);
|
|
25
|
+
* const doubled = computed(() => count() * 2);
|
|
26
|
+
*
|
|
27
|
+
* doubled(); // → 0 (computed on first read)
|
|
28
|
+
* count.set(5);
|
|
29
|
+
* doubled(); // → 10 (recomputed because count changed)
|
|
30
|
+
* doubled(); // → 10 (cached, not recomputed)
|
|
31
|
+
*
|
|
32
|
+
* // With debug label
|
|
33
|
+
* const tripled = computed(() => count() * 3, { label: 'tripled' });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function computed(fn, options) {
|
|
37
|
+
let value;
|
|
38
|
+
let dirty = true; // Start dirty so first read computes
|
|
39
|
+
// Debug info
|
|
40
|
+
const debugLabel = options?.label;
|
|
41
|
+
const debugId = debugLabel ?? generateDebugId('computed');
|
|
42
|
+
const isInternal = options?.__internal === true;
|
|
43
|
+
// Subscribers to this computed (other effects/computeds that read us)
|
|
44
|
+
const subscribers = new Set();
|
|
45
|
+
// Map from execute function to tagged subscriber (for efficient removal)
|
|
46
|
+
const subscriberMap = new Map();
|
|
47
|
+
// Track our own dependencies
|
|
48
|
+
const observer = {
|
|
49
|
+
execute: markDirty,
|
|
50
|
+
cleanup: () => { }, // Computeds don't need cleanup
|
|
51
|
+
dependencies: new Set(),
|
|
52
|
+
isEffect: false, // Mark as computed for synchronous dirty propagation
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Mark as dirty and notify downstream subscribers.
|
|
56
|
+
* Called when any upstream dependency changes.
|
|
57
|
+
*/
|
|
58
|
+
function markDirty() {
|
|
59
|
+
if (!dirty) {
|
|
60
|
+
dirty = true;
|
|
61
|
+
// Notify our subscribers that we might have changed
|
|
62
|
+
// Computeds are notified synchronously, effects are scheduled
|
|
63
|
+
notifySubscribers(subscribers);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Recompute the value if dirty.
|
|
68
|
+
*/
|
|
69
|
+
function recompute() {
|
|
70
|
+
// Clear old dependencies
|
|
71
|
+
for (const depSet of observer.dependencies) {
|
|
72
|
+
for (const sub of depSet) {
|
|
73
|
+
if (sub.fn === observer.execute) {
|
|
74
|
+
depSet.delete(sub);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
observer.dependencies.clear();
|
|
80
|
+
// Track new dependencies during computation
|
|
81
|
+
observerStack.push(observer);
|
|
82
|
+
const startTime = performance.now();
|
|
83
|
+
try {
|
|
84
|
+
value = fn();
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
observerStack.pop();
|
|
88
|
+
}
|
|
89
|
+
const duration = performance.now() - startTime;
|
|
90
|
+
// Emit recalc event (zero cost if debug not enabled, skip for internal)
|
|
91
|
+
if (!isInternal) {
|
|
92
|
+
emitComputedRecalc(debugId, debugLabel, value, duration);
|
|
93
|
+
}
|
|
94
|
+
dirty = false;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read the computed value.
|
|
98
|
+
*/
|
|
99
|
+
function read() {
|
|
100
|
+
// Subscribe the current observer to this computed
|
|
101
|
+
const currentObserver = getCurrentObserver();
|
|
102
|
+
if (currentObserver) {
|
|
103
|
+
let taggedSub = subscriberMap.get(currentObserver.execute);
|
|
104
|
+
if (!taggedSub) {
|
|
105
|
+
taggedSub = {
|
|
106
|
+
fn: currentObserver.execute,
|
|
107
|
+
isEffect: currentObserver.isEffect,
|
|
108
|
+
};
|
|
109
|
+
subscriberMap.set(currentObserver.execute, taggedSub);
|
|
110
|
+
}
|
|
111
|
+
subscribers.add(taggedSub);
|
|
112
|
+
currentObserver.dependencies.add(subscribers);
|
|
113
|
+
}
|
|
114
|
+
// Recompute if necessary
|
|
115
|
+
if (dirty) {
|
|
116
|
+
recompute();
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Peek at the value without subscribing.
|
|
122
|
+
*/
|
|
123
|
+
function peek() {
|
|
124
|
+
if (dirty) {
|
|
125
|
+
recompute();
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
// Attach peek method
|
|
130
|
+
read.peek = peek;
|
|
131
|
+
// Attach debug info (read-only)
|
|
132
|
+
Object.defineProperty(read, '__debugId', {
|
|
133
|
+
value: debugId,
|
|
134
|
+
writable: false,
|
|
135
|
+
enumerable: false,
|
|
136
|
+
});
|
|
137
|
+
Object.defineProperty(read, '__debugLabel', {
|
|
138
|
+
value: debugLabel,
|
|
139
|
+
writable: false,
|
|
140
|
+
enumerable: false,
|
|
141
|
+
});
|
|
142
|
+
return read;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=computed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"computed.js","sourceRoot":"","sources":["../src/computed.ts"],"names":[],"mappings":"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"}
|