@pithyjs/signals 0.1.0-beta.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 +162 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/signal.d.ts +58 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +230 -0
- package/dist/signal.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PithyJS
|
|
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,162 @@
|
|
|
1
|
+
# @pithyjs/signals
|
|
2
|
+
|
|
3
|
+
Fine-grained reactive signal system for the PithyJS framework. Signals provide O(1) reactive state with automatic dependency tracking — no virtual DOM, no re-renders, only the exact DOM nodes that depend on a signal update.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
<!-- @codex:auto install="@pithyjs/signals" -->
|
|
8
|
+
```bash
|
|
9
|
+
npm install @pithyjs/signals
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add @pithyjs/signals
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
yarn add @pithyjs/signals
|
|
18
|
+
```
|
|
19
|
+
<!-- @codex:end -->
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { signal, computed, effect, batch, onCleanup, isSignal } from '@pithyjs/signals';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
<!-- @codex:auto examples="pithy.feature.signals" limit="1" -->
|
|
28
|
+
**creates a reactive signal with initial value**
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
const count = signal(0);
|
|
32
|
+
count(); // → 0
|
|
33
|
+
count.set(1);
|
|
34
|
+
count(); // → 1
|
|
35
|
+
```
|
|
36
|
+
<!-- @codex:end -->
|
|
37
|
+
|
|
38
|
+
## API Reference
|
|
39
|
+
|
|
40
|
+
<!-- @codex:auto api="pithy.feature.signals" format="table" -->
|
|
41
|
+
| API | Signature | Stability | Description |
|
|
42
|
+
| --- | --- | --- | --- |
|
|
43
|
+
| isSignal | `(value: unknown) => value is Signal<any>` | stable | - |
|
|
44
|
+
| onCleanup | `(fn: () => void) => void` | stable | - |
|
|
45
|
+
| effect | `(fn: () => void) => () => void` | stable | - |
|
|
46
|
+
| batch | `(fn: () => void) => void` | stable | - |
|
|
47
|
+
| signal | `<T>(initial: T) => Signal<T>` | stable | - |
|
|
48
|
+
| computed | `<T>(compute: () => T) => () => T` | stable | - |
|
|
49
|
+
<!-- @codex:end -->
|
|
50
|
+
|
|
51
|
+
## Examples
|
|
52
|
+
|
|
53
|
+
<!-- @codex:auto examples="pithy.feature.signals" -->
|
|
54
|
+
**creates a reactive signal with initial value**
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const count = signal(0);
|
|
58
|
+
count(); // → 0
|
|
59
|
+
count.set(1);
|
|
60
|
+
count(); // → 1
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**supports functional updates via .set()**
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const count = signal(0);
|
|
67
|
+
count.set(n => n + 1);
|
|
68
|
+
count(); // → 1
|
|
69
|
+
count.set(n => n * 10);
|
|
70
|
+
count(); // → 10
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**tracks signal dependencies and re-runs**
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const count = signal(0);
|
|
77
|
+
let observed = -1;
|
|
78
|
+
effect(() => {
|
|
79
|
+
observed = count();
|
|
80
|
+
});
|
|
81
|
+
observed; // → 0
|
|
82
|
+
count.set(5);
|
|
83
|
+
observed; // → 5
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**derives a value from other signals**
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const a = signal(2);
|
|
90
|
+
const b = signal(3);
|
|
91
|
+
const sum = computed(() => a() + b());
|
|
92
|
+
sum(); // → 5
|
|
93
|
+
a.set(10);
|
|
94
|
+
sum(); // → 13
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**defers effect execution until batch completes**
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const a = signal(1);
|
|
101
|
+
const b = signal(2);
|
|
102
|
+
let runCount = 0;
|
|
103
|
+
effect(() => {
|
|
104
|
+
a();
|
|
105
|
+
b();
|
|
106
|
+
runCount++;
|
|
107
|
+
});
|
|
108
|
+
runCount; // → 1
|
|
109
|
+
|
|
110
|
+
batch(() => {
|
|
111
|
+
a.set(10);
|
|
112
|
+
b.set(20);
|
|
113
|
+
});
|
|
114
|
+
// Effect should have run only once after batch, not twice
|
|
115
|
+
runCount; // → 2
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**runs cleanup on disposal**
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const count = signal(0);
|
|
122
|
+
let cleaned = false;
|
|
123
|
+
|
|
124
|
+
const dispose = effect(() => {
|
|
125
|
+
count();
|
|
126
|
+
onCleanup(() => {
|
|
127
|
+
cleaned = true;
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
cleaned; // → false
|
|
132
|
+
dispose();
|
|
133
|
+
cleaned; // → true
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**detects signal values at runtime**
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const count = signal(0);
|
|
140
|
+
isSignal(count); // → true
|
|
141
|
+
isSignal(42); // → false
|
|
142
|
+
isSignal(() => {}); // → false
|
|
143
|
+
isSignal(null); // → false
|
|
144
|
+
```
|
|
145
|
+
<!-- @codex:end -->
|
|
146
|
+
|
|
147
|
+
## Testing
|
|
148
|
+
|
|
149
|
+
<!-- @codex:auto testing="pithy.feature.signals" format="table" -->
|
|
150
|
+
| Feature | Unit | Integration | E2E | Status |
|
|
151
|
+
| --- | --- | --- | --- | --- |
|
|
152
|
+
| isSignal | ✓ | - | - | Missing integration |
|
|
153
|
+
| onCleanup | ✓ | - | - | Missing integration |
|
|
154
|
+
| effect | ✓ | - | - | Missing integration |
|
|
155
|
+
| batch | ✓ | - | - | Missing integration |
|
|
156
|
+
| signal | ✓ | - | - | Missing integration |
|
|
157
|
+
| computed | ✓ | - | - | Missing integration |
|
|
158
|
+
<!-- @codex:end -->
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC"}
|
package/dist/signal.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** @codex
|
|
2
|
+
{
|
|
3
|
+
"id": "pithy.feature.signals",
|
|
4
|
+
"title": "Reactive Signal System",
|
|
5
|
+
"category": "feature"
|
|
6
|
+
}
|
|
7
|
+
*/
|
|
8
|
+
import { Signal } from './types';
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a value is a Signal at runtime.
|
|
11
|
+
* @param value - The value to check.
|
|
12
|
+
* @returns True if the value is a Signal.
|
|
13
|
+
*/
|
|
14
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"isSignal","stability":"stable","signature":"(value: unknown) => value is Signal<any>"} */
|
|
15
|
+
export declare function isSignal(value: unknown): value is Signal<unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* Registers a cleanup callback that will be executed
|
|
18
|
+
* before the current effect is re-run.
|
|
19
|
+
*
|
|
20
|
+
* @param fn - The cleanup function.
|
|
21
|
+
*/
|
|
22
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"onCleanup","stability":"stable","signature":"(fn: () => void) => void"} */
|
|
23
|
+
export declare function onCleanup(fn: () => void): void;
|
|
24
|
+
/**
|
|
25
|
+
* Creates a reactive effect that automatically tracks
|
|
26
|
+
* signals it reads and re-runs when any of them change.
|
|
27
|
+
*
|
|
28
|
+
* @param fn - The effect function to run reactively.
|
|
29
|
+
* @returns A disposer function to stop tracking this effect.
|
|
30
|
+
*/
|
|
31
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"effect","stability":"stable","signature":"(fn: () => void) => () => void"} */
|
|
32
|
+
export declare function effect(fn: () => void): () => void;
|
|
33
|
+
/**
|
|
34
|
+
* Batches multiple signal updates together.
|
|
35
|
+
* Effects triggered during the batch are deferred and executed once after the batch.
|
|
36
|
+
*
|
|
37
|
+
* @param fn - The function performing multiple signal updates.
|
|
38
|
+
*/
|
|
39
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"batch","stability":"stable","signature":"(fn: () => void) => void"} */
|
|
40
|
+
export declare function batch(fn: () => void): void;
|
|
41
|
+
/**
|
|
42
|
+
* Creates a signal: a reactive function holding a value and supporting subscriptions.
|
|
43
|
+
*
|
|
44
|
+
* @param initial - Initial value of the signal.
|
|
45
|
+
* @returns A getter function with `.set()` and `.subscribe()` attached.
|
|
46
|
+
*/
|
|
47
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"signal","stability":"stable","signature":"<T>(initial: T) => Signal<T>"} */
|
|
48
|
+
export declare function signal<T>(initial: T): Signal<T>;
|
|
49
|
+
/**
|
|
50
|
+
* Creates a derived signal (computed value) that automatically
|
|
51
|
+
* recalculates when its dependencies change.
|
|
52
|
+
*
|
|
53
|
+
* @param compute - A function returning the computed value.
|
|
54
|
+
* @returns A readonly function returning the latest computed value.
|
|
55
|
+
*/
|
|
56
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"computed","stability":"stable","signature":"<T>(compute: () => T) => () => T"} */
|
|
57
|
+
export declare function computed<T>(compute: () => T): () => T;
|
|
58
|
+
//# sourceMappingURL=signal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../src/signal.ts"],"names":[],"mappings":"AAAA;;;;;;EAME;AAEF,OAAO,EAAE,MAAM,EAAiB,MAAM,SAAS,CAAC;AAmChD;;;;GAIG;AACH,iJAAiJ;AACjJ,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,OAAO,CAAC,CAKjE;AAiBD;;;;;GAKG;AACH,kIAAkI;AAClI,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAK9C;AAED;;;;;;GAMG;AACH,qIAAqI;AACrI,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAajD;AAED;;;;;GAKG;AACH,8HAA8H;AAC9H,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAa1C;AAED;;;;;GAKG;AACH,mIAAmI;AACnI,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAoE/C;AAED;;;;;;GAMG;AACH,yIAAyI;AACzI,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,CAmBrD"}
|
package/dist/signal.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/** @codex
|
|
2
|
+
{
|
|
3
|
+
"id": "pithy.feature.signals",
|
|
4
|
+
"title": "Reactive Signal System",
|
|
5
|
+
"category": "feature"
|
|
6
|
+
}
|
|
7
|
+
*/
|
|
8
|
+
import { SIGNAL_SYMBOL } from './types';
|
|
9
|
+
/**
|
|
10
|
+
* Tracks the current reactive effect being registered.
|
|
11
|
+
* Used to record dependencies when a signal is read.
|
|
12
|
+
*/
|
|
13
|
+
let currentEffect = null;
|
|
14
|
+
/**
|
|
15
|
+
* Internal cleanup registry to track cleanup functions for each registered effect.
|
|
16
|
+
* Each effect can have multiple cleanups that are called before re-execution.
|
|
17
|
+
*/
|
|
18
|
+
const cleanupMap = new WeakMap();
|
|
19
|
+
/**
|
|
20
|
+
* Flag to indicate if we are currently inside a batched update.
|
|
21
|
+
*/
|
|
22
|
+
let isBatching = false;
|
|
23
|
+
/**
|
|
24
|
+
* Set of effect wrappers scheduled to run after a batch ends.
|
|
25
|
+
*/
|
|
26
|
+
const pendingEffects = new Set();
|
|
27
|
+
/**
|
|
28
|
+
* Indicates whether we are currently inside a tracking context (effect or computed).
|
|
29
|
+
* Used to suppress signal read warnings in appropriate scopes.
|
|
30
|
+
*/
|
|
31
|
+
let isTrackingContext = false;
|
|
32
|
+
/**
|
|
33
|
+
* Tracks suppression depth for temporarily suppressing signal read warnings.
|
|
34
|
+
*/
|
|
35
|
+
let suppressionDepth = 0;
|
|
36
|
+
/**
|
|
37
|
+
* Checks if a value is a Signal at runtime.
|
|
38
|
+
* @param value - The value to check.
|
|
39
|
+
* @returns True if the value is a Signal.
|
|
40
|
+
*/
|
|
41
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"isSignal","stability":"stable","signature":"(value: unknown) => value is Signal<any>"} */
|
|
42
|
+
export function isSignal(value) {
|
|
43
|
+
return (typeof value === 'function' &&
|
|
44
|
+
value[SIGNAL_SYMBOL] === true);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Temporarily suppresses signal read warnings inside the given function.
|
|
48
|
+
* Handles nested suppression correctly.
|
|
49
|
+
* @param fn - Function during which warnings should be suppressed.
|
|
50
|
+
* @returns The return value of the function.
|
|
51
|
+
*/
|
|
52
|
+
function suppressWarnings(fn) {
|
|
53
|
+
suppressionDepth++;
|
|
54
|
+
try {
|
|
55
|
+
return fn();
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
suppressionDepth--;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Registers a cleanup callback that will be executed
|
|
63
|
+
* before the current effect is re-run.
|
|
64
|
+
*
|
|
65
|
+
* @param fn - The cleanup function.
|
|
66
|
+
*/
|
|
67
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"onCleanup","stability":"stable","signature":"(fn: () => void) => void"} */
|
|
68
|
+
export function onCleanup(fn) {
|
|
69
|
+
if (currentEffect) {
|
|
70
|
+
const stack = cleanupMap.get(currentEffect);
|
|
71
|
+
if (stack)
|
|
72
|
+
stack.push(fn);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Creates a reactive effect that automatically tracks
|
|
77
|
+
* signals it reads and re-runs when any of them change.
|
|
78
|
+
*
|
|
79
|
+
* @param fn - The effect function to run reactively.
|
|
80
|
+
* @returns A disposer function to stop tracking this effect.
|
|
81
|
+
*/
|
|
82
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"effect","stability":"stable","signature":"(fn: () => void) => () => void"} */
|
|
83
|
+
export function effect(fn) {
|
|
84
|
+
const wrapped = () => runEffect(wrapped, fn);
|
|
85
|
+
// Run immediately to establish dependencies
|
|
86
|
+
wrapped();
|
|
87
|
+
// Return disposer to manually stop the effect
|
|
88
|
+
const disposer = () => {
|
|
89
|
+
cleanupMap.get(wrapped)?.forEach(fn => fn());
|
|
90
|
+
cleanupMap.delete(wrapped);
|
|
91
|
+
};
|
|
92
|
+
return disposer;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Batches multiple signal updates together.
|
|
96
|
+
* Effects triggered during the batch are deferred and executed once after the batch.
|
|
97
|
+
*
|
|
98
|
+
* @param fn - The function performing multiple signal updates.
|
|
99
|
+
*/
|
|
100
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"batch","stability":"stable","signature":"(fn: () => void) => void"} */
|
|
101
|
+
export function batch(fn) {
|
|
102
|
+
isBatching = true;
|
|
103
|
+
try {
|
|
104
|
+
fn();
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
isBatching = false;
|
|
108
|
+
// Execute all pending effects once
|
|
109
|
+
const effects = Array.from(pendingEffects);
|
|
110
|
+
pendingEffects.clear();
|
|
111
|
+
effects.forEach(run => run());
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Creates a signal: a reactive function holding a value and supporting subscriptions.
|
|
116
|
+
*
|
|
117
|
+
* @param initial - Initial value of the signal.
|
|
118
|
+
* @returns A getter function with `.set()` and `.subscribe()` attached.
|
|
119
|
+
*/
|
|
120
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"signal","stability":"stable","signature":"<T>(initial: T) => Signal<T>"} */
|
|
121
|
+
export function signal(initial) {
|
|
122
|
+
let value = initial;
|
|
123
|
+
const subscribers = new Set();
|
|
124
|
+
const listeners = new Set();
|
|
125
|
+
const read = () => suppressWarnings(() => {
|
|
126
|
+
if (currentEffect) {
|
|
127
|
+
// Register currentEffect as a subscriber
|
|
128
|
+
subscribers.add(currentEffect);
|
|
129
|
+
// IMPORTANT: Capture currentEffect NOW, not when cleanup runs!
|
|
130
|
+
// Otherwise, if cleanup runs while a different effect is active,
|
|
131
|
+
// we'd delete the wrong effect from subscribers.
|
|
132
|
+
const effectToRemove = currentEffect;
|
|
133
|
+
onCleanup(() => {
|
|
134
|
+
subscribers.delete(effectToRemove);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Warn if a signal is read outside of reactive context (in dev mode)
|
|
139
|
+
if (!isTrackingContext &&
|
|
140
|
+
suppressionDepth === 0 &&
|
|
141
|
+
import.meta.env?.DEV &&
|
|
142
|
+
!import.meta.env?.TEST) {
|
|
143
|
+
const error = new Error();
|
|
144
|
+
const stack = error.stack ?? '';
|
|
145
|
+
console.warn(`[PithyJS] ❗ Signal (value: ${value}) was read outside of a reactive context.\n` +
|
|
146
|
+
` • This might lead to missed updates in the UI.\n\n` +
|
|
147
|
+
`🔎 Where it happened:\n${stack.split('\n').slice(2).join('\n')}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
});
|
|
152
|
+
read.set = (next) => {
|
|
153
|
+
const newValue = suppressWarnings(() => typeof next === 'function' ? next(value) : next);
|
|
154
|
+
if (value !== newValue) {
|
|
155
|
+
value = newValue;
|
|
156
|
+
// Notify all reactive subscribers
|
|
157
|
+
subscribers.forEach(fn => {
|
|
158
|
+
if (isBatching) {
|
|
159
|
+
pendingEffects.add(fn);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
fn();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
// Notify all external listeners (subscriptions)
|
|
166
|
+
listeners.forEach(fn => fn(value));
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
read.subscribe = (fn) => {
|
|
170
|
+
listeners.add(fn);
|
|
171
|
+
fn(value); // Immediately call with current value
|
|
172
|
+
return () => listeners.delete(fn);
|
|
173
|
+
};
|
|
174
|
+
read[SIGNAL_SYMBOL] =
|
|
175
|
+
true;
|
|
176
|
+
return read;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Creates a derived signal (computed value) that automatically
|
|
180
|
+
* recalculates when its dependencies change.
|
|
181
|
+
*
|
|
182
|
+
* @param compute - A function returning the computed value.
|
|
183
|
+
* @returns A readonly function returning the latest computed value.
|
|
184
|
+
*/
|
|
185
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"computed","stability":"stable","signature":"<T>(compute: () => T) => () => T"} */
|
|
186
|
+
export function computed(compute) {
|
|
187
|
+
isTrackingContext = true; // Allow first compute() call to access signals without warnings
|
|
188
|
+
let initial;
|
|
189
|
+
try {
|
|
190
|
+
initial = compute();
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
isTrackingContext = false;
|
|
194
|
+
}
|
|
195
|
+
const result = signal(initial);
|
|
196
|
+
const owner = currentEffect;
|
|
197
|
+
const dispose = effect(() => {
|
|
198
|
+
result.set(compute());
|
|
199
|
+
});
|
|
200
|
+
if (owner) {
|
|
201
|
+
cleanupMap.get(owner)?.push(dispose);
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Runs the provided effect function, setting up the current tracking context,
|
|
207
|
+
* managing cleanups, and allowing signal dependencies to be re-collected.
|
|
208
|
+
*
|
|
209
|
+
* @param wrapped - The wrapper function associated with the effect.
|
|
210
|
+
* @param fn - The effect body function.
|
|
211
|
+
*/
|
|
212
|
+
function runEffect(wrapped, fn) {
|
|
213
|
+
isTrackingContext = true;
|
|
214
|
+
if (isBatching) {
|
|
215
|
+
// Defer running effect until after batch completes
|
|
216
|
+
pendingEffects.add(() => runEffect(wrapped, fn));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Prepare new cleanup stack
|
|
220
|
+
cleanupMap.set(wrapped, []);
|
|
221
|
+
currentEffect = wrapped;
|
|
222
|
+
try {
|
|
223
|
+
fn(); // Run the effect
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
currentEffect = null;
|
|
227
|
+
isTrackingContext = false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=signal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal.js","sourceRoot":"","sources":["../src/signal.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type EffectFn = () => void;
|
|
2
|
+
export type Signal<T> = (() => T) & {
|
|
3
|
+
set: (value: T | ((prev: T) => T)) => void;
|
|
4
|
+
subscribe: (fn: (value: T) => void) => () => void;
|
|
5
|
+
};
|
|
6
|
+
/** Read-only accessor for a signal's value. Compatible with effects for dependency tracking. */
|
|
7
|
+
export type ReadonlySignal<T> = () => T;
|
|
8
|
+
/** The signature of the `effect()` function from @pithyjs/signals. */
|
|
9
|
+
export type EffectRunner = (fn: EffectFn) => () => void;
|
|
10
|
+
export declare const SIGNAL_SYMBOL: unique symbol;
|
|
11
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC;AAElC,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG;IAClC,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC;IAC3C,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;CACnD,CAAC;AAEF,gGAAgG;AAChG,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC;AAExC,sEAAsE;AACtE,MAAM,MAAM,YAAY,GAAG,CAAC,EAAE,EAAE,QAAQ,KAAK,MAAM,IAAI,CAAC;AAExD,eAAO,MAAM,aAAa,eAAyB,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAaA,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pithyjs/signals",
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
|
+
"description": "Reactive signal system for PithyJS framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "^5.0.0",
|
|
16
|
+
"vitest": "^1.0.0"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"tag": "beta"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"dev": "tsc --watch",
|
|
29
|
+
"test": "vitest"
|
|
30
|
+
}
|
|
31
|
+
}
|