pulse-js-framework 1.4.3 → 1.4.5
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 +22 -0
- package/cli/index.js +23 -20
- package/cli/logger.js +122 -0
- package/cli/mobile.js +48 -71
- package/compiler/parser.js +57 -136
- package/compiler/transformer.js +75 -197
- package/index.js +8 -2
- package/package.json +53 -8
- package/runtime/dom.js +6 -3
- package/runtime/index.js +2 -0
- package/runtime/logger.js +304 -0
- package/runtime/native.js +7 -4
- package/runtime/pulse.js +308 -25
- package/runtime/store.js +227 -19
- package/types/dom.d.ts +288 -0
- package/types/index.d.ts +135 -0
- package/types/logger.d.ts +122 -0
- package/types/pulse.d.ts +149 -0
- package/types/router.d.ts +197 -0
- package/types/store.d.ts +170 -0
package/runtime/pulse.js
CHANGED
|
@@ -1,20 +1,91 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pulse - Reactive Primitives
|
|
3
|
+
* @module pulse-js-framework/runtime/pulse
|
|
3
4
|
*
|
|
4
5
|
* The core reactivity system based on "pulsations" -
|
|
5
6
|
* reactive values that propagate changes through the system.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { pulse, effect, computed } from './pulse.js';
|
|
10
|
+
*
|
|
11
|
+
* const count = pulse(0);
|
|
12
|
+
* const doubled = computed(() => count.get() * 2);
|
|
13
|
+
*
|
|
14
|
+
* effect(() => {
|
|
15
|
+
* console.log('Count:', count.get(), 'Doubled:', doubled.get());
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* count.set(5); // Logs: Count: 5 Doubled: 10
|
|
6
19
|
*/
|
|
7
20
|
|
|
21
|
+
import { loggers } from './logger.js';
|
|
22
|
+
|
|
23
|
+
const log = loggers.pulse;
|
|
24
|
+
|
|
8
25
|
// Current tracking context for automatic dependency collection
|
|
26
|
+
/** @type {EffectFn|null} */
|
|
9
27
|
let currentEffect = null;
|
|
28
|
+
/** @type {number} */
|
|
10
29
|
let batchDepth = 0;
|
|
30
|
+
/** @type {Set<EffectFn>} */
|
|
11
31
|
let pendingEffects = new Set();
|
|
32
|
+
/** @type {boolean} */
|
|
12
33
|
let isRunningEffects = false;
|
|
34
|
+
/** @type {Array<Function>} */
|
|
13
35
|
let cleanupQueue = [];
|
|
14
36
|
|
|
15
37
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
38
|
+
* @typedef {Object} EffectFn
|
|
39
|
+
* @property {Function} run - The effect function to execute
|
|
40
|
+
* @property {Set<Pulse>} dependencies - Set of pulse dependencies
|
|
41
|
+
* @property {Array<Function>} cleanups - Cleanup functions to run
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Object} PulseOptions
|
|
46
|
+
* @property {function(*, *): boolean} [equals] - Custom equality function (default: Object.is)
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} ComputedOptions
|
|
51
|
+
* @property {boolean} [lazy=false] - If true, only compute when value is read
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {Object} MemoOptions
|
|
56
|
+
* @property {function(*, *): boolean} [equals] - Custom equality function for args comparison
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {Object} MemoComputedOptions
|
|
61
|
+
* @property {Array<Pulse|Function>} [deps] - Dependencies to watch for changes
|
|
62
|
+
* @property {function(*, *): boolean} [equals] - Custom equality function
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @typedef {Object} ReactiveState
|
|
67
|
+
* @property {Object.<string, Pulse>} $pulses - Access to underlying pulse objects
|
|
68
|
+
* @property {function(string): Pulse} $pulse - Get pulse by key
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {Object} PromiseState
|
|
73
|
+
* @property {Pulse<T>} value - The resolved value
|
|
74
|
+
* @property {Pulse<boolean>} loading - Loading state
|
|
75
|
+
* @property {Pulse<Error|null>} error - Error state
|
|
76
|
+
* @template T
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Register a cleanup function for the current effect.
|
|
81
|
+
* Called when the effect re-runs or is disposed.
|
|
82
|
+
* @param {Function} fn - Cleanup function to register
|
|
83
|
+
* @returns {void}
|
|
84
|
+
* @example
|
|
85
|
+
* effect(() => {
|
|
86
|
+
* const timer = setInterval(() => console.log('tick'), 1000);
|
|
87
|
+
* onCleanup(() => clearInterval(timer));
|
|
88
|
+
* });
|
|
18
89
|
*/
|
|
19
90
|
export function onCleanup(fn) {
|
|
20
91
|
if (currentEffect) {
|
|
@@ -23,14 +94,23 @@ export function onCleanup(fn) {
|
|
|
23
94
|
}
|
|
24
95
|
|
|
25
96
|
/**
|
|
26
|
-
* Pulse - A reactive value container
|
|
27
|
-
* When the value changes, it "pulses" to all its dependents
|
|
97
|
+
* Pulse - A reactive value container.
|
|
98
|
+
* When the value changes, it "pulses" to all its dependents.
|
|
99
|
+
* @template T
|
|
28
100
|
*/
|
|
29
101
|
export class Pulse {
|
|
102
|
+
/** @type {T} */
|
|
30
103
|
#value;
|
|
104
|
+
/** @type {Set<EffectFn>} */
|
|
31
105
|
#subscribers = new Set();
|
|
106
|
+
/** @type {function(T, T): boolean} */
|
|
32
107
|
#equals;
|
|
33
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Create a new Pulse with an initial value
|
|
111
|
+
* @param {T} value - The initial value
|
|
112
|
+
* @param {PulseOptions} [options={}] - Configuration options
|
|
113
|
+
*/
|
|
34
114
|
constructor(value, options = {}) {
|
|
35
115
|
this.#value = value;
|
|
36
116
|
this.#equals = options.equals ?? Object.is;
|
|
@@ -38,6 +118,12 @@ export class Pulse {
|
|
|
38
118
|
|
|
39
119
|
/**
|
|
40
120
|
* Get the current value and track dependency if in an effect context
|
|
121
|
+
* @returns {T} The current value
|
|
122
|
+
* @example
|
|
123
|
+
* const name = pulse('Alice');
|
|
124
|
+
* effect(() => {
|
|
125
|
+
* console.log(name.get()); // Tracks dependency automatically
|
|
126
|
+
* });
|
|
41
127
|
*/
|
|
42
128
|
get() {
|
|
43
129
|
if (currentEffect) {
|
|
@@ -49,6 +135,11 @@ export class Pulse {
|
|
|
49
135
|
|
|
50
136
|
/**
|
|
51
137
|
* Set a new value and notify all subscribers
|
|
138
|
+
* @param {T} newValue - The new value to set
|
|
139
|
+
* @returns {void}
|
|
140
|
+
* @example
|
|
141
|
+
* const count = pulse(0);
|
|
142
|
+
* count.set(5);
|
|
52
143
|
*/
|
|
53
144
|
set(newValue) {
|
|
54
145
|
if (this.#equals(this.#value, newValue)) return;
|
|
@@ -58,13 +149,25 @@ export class Pulse {
|
|
|
58
149
|
|
|
59
150
|
/**
|
|
60
151
|
* Update value using a function
|
|
152
|
+
* @param {function(T): T} fn - Update function receiving current value
|
|
153
|
+
* @returns {void}
|
|
154
|
+
* @example
|
|
155
|
+
* const count = pulse(0);
|
|
156
|
+
* count.update(n => n + 1); // Increment
|
|
61
157
|
*/
|
|
62
158
|
update(fn) {
|
|
63
159
|
this.set(fn(this.#value));
|
|
64
160
|
}
|
|
65
161
|
|
|
66
162
|
/**
|
|
67
|
-
* Subscribe to changes
|
|
163
|
+
* Subscribe to value changes
|
|
164
|
+
* @param {function(T): void} fn - Callback invoked on each change
|
|
165
|
+
* @returns {function(): void} Unsubscribe function
|
|
166
|
+
* @example
|
|
167
|
+
* const count = pulse(0);
|
|
168
|
+
* const unsub = count.subscribe(value => console.log(value));
|
|
169
|
+
* count.set(1); // Logs: 1
|
|
170
|
+
* unsub(); // Stop listening
|
|
68
171
|
*/
|
|
69
172
|
subscribe(fn) {
|
|
70
173
|
const subscriber = { run: fn, dependencies: new Set() };
|
|
@@ -74,6 +177,13 @@ export class Pulse {
|
|
|
74
177
|
|
|
75
178
|
/**
|
|
76
179
|
* Create a derived pulse that recomputes when this changes
|
|
180
|
+
* @template U
|
|
181
|
+
* @param {function(T): U} fn - Derivation function
|
|
182
|
+
* @returns {Pulse<U>} A new computed pulse
|
|
183
|
+
* @example
|
|
184
|
+
* const count = pulse(5);
|
|
185
|
+
* const doubled = count.derive(n => n * 2);
|
|
186
|
+
* doubled.get(); // 10
|
|
77
187
|
*/
|
|
78
188
|
derive(fn) {
|
|
79
189
|
return computed(() => fn(this.get()));
|
|
@@ -81,6 +191,8 @@ export class Pulse {
|
|
|
81
191
|
|
|
82
192
|
/**
|
|
83
193
|
* Notify all subscribers of a change
|
|
194
|
+
* @private
|
|
195
|
+
* @returns {void}
|
|
84
196
|
*/
|
|
85
197
|
#notify() {
|
|
86
198
|
// Copy subscribers to avoid mutation during iteration
|
|
@@ -96,7 +208,10 @@ export class Pulse {
|
|
|
96
208
|
}
|
|
97
209
|
|
|
98
210
|
/**
|
|
99
|
-
* Unsubscribe a specific subscriber
|
|
211
|
+
* Unsubscribe a specific subscriber (internal use)
|
|
212
|
+
* @param {EffectFn} subscriber - The subscriber to remove
|
|
213
|
+
* @returns {void}
|
|
214
|
+
* @internal
|
|
100
215
|
*/
|
|
101
216
|
_unsubscribe(subscriber) {
|
|
102
217
|
this.#subscribers.delete(subscriber);
|
|
@@ -104,6 +219,10 @@ export class Pulse {
|
|
|
104
219
|
|
|
105
220
|
/**
|
|
106
221
|
* Get value without tracking (for debugging/inspection)
|
|
222
|
+
* @returns {T} The current value without tracking dependency
|
|
223
|
+
* @example
|
|
224
|
+
* const count = pulse(5);
|
|
225
|
+
* count.peek(); // 5, no dependency tracked
|
|
107
226
|
*/
|
|
108
227
|
peek() {
|
|
109
228
|
return this.#value;
|
|
@@ -111,6 +230,9 @@ export class Pulse {
|
|
|
111
230
|
|
|
112
231
|
/**
|
|
113
232
|
* Initialize value without triggering notifications (internal use)
|
|
233
|
+
* @param {T} value - The value to set
|
|
234
|
+
* @returns {void}
|
|
235
|
+
* @internal
|
|
114
236
|
*/
|
|
115
237
|
_init(value) {
|
|
116
238
|
this.#value = value;
|
|
@@ -118,6 +240,9 @@ export class Pulse {
|
|
|
118
240
|
|
|
119
241
|
/**
|
|
120
242
|
* Set from computed - propagates to subscribers (internal use)
|
|
243
|
+
* @param {T} newValue - The new value
|
|
244
|
+
* @returns {void}
|
|
245
|
+
* @internal
|
|
121
246
|
*/
|
|
122
247
|
_setFromComputed(newValue) {
|
|
123
248
|
if (this.#equals(this.#value, newValue)) return;
|
|
@@ -127,6 +252,9 @@ export class Pulse {
|
|
|
127
252
|
|
|
128
253
|
/**
|
|
129
254
|
* Add a subscriber directly (internal use)
|
|
255
|
+
* @param {EffectFn} subscriber - The subscriber to add
|
|
256
|
+
* @returns {void}
|
|
257
|
+
* @internal
|
|
130
258
|
*/
|
|
131
259
|
_addSubscriber(subscriber) {
|
|
132
260
|
this.#subscribers.add(subscriber);
|
|
@@ -134,6 +262,8 @@ export class Pulse {
|
|
|
134
262
|
|
|
135
263
|
/**
|
|
136
264
|
* Trigger notification to all subscribers (internal use)
|
|
265
|
+
* @returns {void}
|
|
266
|
+
* @internal
|
|
137
267
|
*/
|
|
138
268
|
_triggerNotify() {
|
|
139
269
|
this.#notify();
|
|
@@ -142,6 +272,9 @@ export class Pulse {
|
|
|
142
272
|
|
|
143
273
|
/**
|
|
144
274
|
* Run a single effect safely
|
|
275
|
+
* @private
|
|
276
|
+
* @param {EffectFn} effectFn - The effect to run
|
|
277
|
+
* @returns {void}
|
|
145
278
|
*/
|
|
146
279
|
function runEffect(effectFn) {
|
|
147
280
|
if (!effectFn || !effectFn.run) return;
|
|
@@ -149,12 +282,14 @@ function runEffect(effectFn) {
|
|
|
149
282
|
try {
|
|
150
283
|
effectFn.run();
|
|
151
284
|
} catch (error) {
|
|
152
|
-
|
|
285
|
+
log.error('Effect error:', error);
|
|
153
286
|
}
|
|
154
287
|
}
|
|
155
288
|
|
|
156
289
|
/**
|
|
157
290
|
* Flush all pending effects
|
|
291
|
+
* @private
|
|
292
|
+
* @returns {void}
|
|
158
293
|
*/
|
|
159
294
|
function flushEffects() {
|
|
160
295
|
if (isRunningEffects) return;
|
|
@@ -175,7 +310,7 @@ function flushEffects() {
|
|
|
175
310
|
}
|
|
176
311
|
|
|
177
312
|
if (iterations >= maxIterations) {
|
|
178
|
-
|
|
313
|
+
log.warn('Maximum effect iterations reached. Possible infinite loop.');
|
|
179
314
|
pendingEffects.clear();
|
|
180
315
|
}
|
|
181
316
|
} finally {
|
|
@@ -184,15 +319,39 @@ function flushEffects() {
|
|
|
184
319
|
}
|
|
185
320
|
|
|
186
321
|
/**
|
|
187
|
-
* Create a
|
|
322
|
+
* Create a reactive pulse with an initial value
|
|
323
|
+
* @template T
|
|
324
|
+
* @param {T} value - The initial value
|
|
325
|
+
* @param {PulseOptions} [options] - Configuration options
|
|
326
|
+
* @returns {Pulse<T>} A new Pulse instance
|
|
327
|
+
* @example
|
|
328
|
+
* const count = pulse(0);
|
|
329
|
+
* const user = pulse({ name: 'Alice' });
|
|
330
|
+
*
|
|
331
|
+
* // With custom equality
|
|
332
|
+
* const data = pulse(obj, { equals: (a, b) => a.id === b.id });
|
|
188
333
|
*/
|
|
189
334
|
export function pulse(value, options) {
|
|
190
335
|
return new Pulse(value, options);
|
|
191
336
|
}
|
|
192
337
|
|
|
193
338
|
/**
|
|
194
|
-
* Create a computed pulse that automatically updates
|
|
195
|
-
*
|
|
339
|
+
* Create a computed pulse that automatically updates when its dependencies change
|
|
340
|
+
* @template T
|
|
341
|
+
* @param {function(): T} fn - Computation function
|
|
342
|
+
* @param {ComputedOptions} [options={}] - Configuration options
|
|
343
|
+
* @returns {Pulse<T>} A read-only Pulse that updates automatically
|
|
344
|
+
* @example
|
|
345
|
+
* const firstName = pulse('John');
|
|
346
|
+
* const lastName = pulse('Doe');
|
|
347
|
+
* const fullName = computed(() => `${firstName.get()} ${lastName.get()}`);
|
|
348
|
+
*
|
|
349
|
+
* fullName.get(); // 'John Doe'
|
|
350
|
+
* firstName.set('Jane');
|
|
351
|
+
* fullName.get(); // 'Jane Doe'
|
|
352
|
+
*
|
|
353
|
+
* // Lazy computation (only runs when read)
|
|
354
|
+
* const expensive = computed(() => heavyCalculation(), { lazy: true });
|
|
196
355
|
*/
|
|
197
356
|
export function computed(fn, options = {}) {
|
|
198
357
|
const { lazy = false } = options;
|
|
@@ -288,6 +447,24 @@ export function computed(fn, options = {}) {
|
|
|
288
447
|
|
|
289
448
|
/**
|
|
290
449
|
* Create an effect that runs when its dependencies change
|
|
450
|
+
* @param {function(): void|function(): void} fn - Effect function, may return a cleanup function
|
|
451
|
+
* @returns {function(): void} Dispose function to stop the effect
|
|
452
|
+
* @example
|
|
453
|
+
* const count = pulse(0);
|
|
454
|
+
*
|
|
455
|
+
* // Basic effect
|
|
456
|
+
* const dispose = effect(() => {
|
|
457
|
+
* console.log('Count is:', count.get());
|
|
458
|
+
* });
|
|
459
|
+
*
|
|
460
|
+
* count.set(1); // Logs: Count is: 1
|
|
461
|
+
* dispose(); // Stop listening
|
|
462
|
+
*
|
|
463
|
+
* // With cleanup
|
|
464
|
+
* effect(() => {
|
|
465
|
+
* const timer = setInterval(() => tick(), 1000);
|
|
466
|
+
* return () => clearInterval(timer); // Cleanup on re-run or dispose
|
|
467
|
+
* });
|
|
291
468
|
*/
|
|
292
469
|
export function effect(fn) {
|
|
293
470
|
const effectFn = {
|
|
@@ -297,7 +474,7 @@ export function effect(fn) {
|
|
|
297
474
|
try {
|
|
298
475
|
cleanup();
|
|
299
476
|
} catch (e) {
|
|
300
|
-
|
|
477
|
+
log.error('Cleanup error:', e);
|
|
301
478
|
}
|
|
302
479
|
}
|
|
303
480
|
effectFn.cleanups = [];
|
|
@@ -315,7 +492,7 @@ export function effect(fn) {
|
|
|
315
492
|
try {
|
|
316
493
|
fn();
|
|
317
494
|
} catch (error) {
|
|
318
|
-
|
|
495
|
+
log.error('Effect execution error:', error);
|
|
319
496
|
} finally {
|
|
320
497
|
currentEffect = prevEffect;
|
|
321
498
|
}
|
|
@@ -347,13 +524,30 @@ export function effect(fn) {
|
|
|
347
524
|
}
|
|
348
525
|
|
|
349
526
|
/**
|
|
350
|
-
* Batch multiple updates into one
|
|
351
|
-
*
|
|
527
|
+
* Batch multiple updates into one. Effects only run after all updates complete.
|
|
528
|
+
* @template T
|
|
529
|
+
* @param {function(): T} fn - Function containing multiple updates
|
|
530
|
+
* @returns {T} The return value of fn
|
|
531
|
+
* @example
|
|
532
|
+
* const x = pulse(0);
|
|
533
|
+
* const y = pulse(0);
|
|
534
|
+
*
|
|
535
|
+
* effect(() => console.log(x.get(), y.get()));
|
|
536
|
+
*
|
|
537
|
+
* // Without batch: logs twice
|
|
538
|
+
* x.set(1);
|
|
539
|
+
* y.set(1);
|
|
540
|
+
*
|
|
541
|
+
* // With batch: logs once
|
|
542
|
+
* batch(() => {
|
|
543
|
+
* x.set(2);
|
|
544
|
+
* y.set(2);
|
|
545
|
+
* });
|
|
352
546
|
*/
|
|
353
547
|
export function batch(fn) {
|
|
354
548
|
batchDepth++;
|
|
355
549
|
try {
|
|
356
|
-
fn();
|
|
550
|
+
return fn();
|
|
357
551
|
} finally {
|
|
358
552
|
batchDepth--;
|
|
359
553
|
if (batchDepth === 0) {
|
|
@@ -363,8 +557,27 @@ export function batch(fn) {
|
|
|
363
557
|
}
|
|
364
558
|
|
|
365
559
|
/**
|
|
366
|
-
* Create a reactive state object from a plain object
|
|
367
|
-
* Each property becomes a pulse
|
|
560
|
+
* Create a reactive state object from a plain object.
|
|
561
|
+
* Each property becomes a pulse with getters/setters.
|
|
562
|
+
* @template T
|
|
563
|
+
* @param {T} obj - Plain object with initial values
|
|
564
|
+
* @returns {T & ReactiveState} Reactive state object
|
|
565
|
+
* @example
|
|
566
|
+
* const state = createState({
|
|
567
|
+
* count: 0,
|
|
568
|
+
* items: ['a', 'b']
|
|
569
|
+
* });
|
|
570
|
+
*
|
|
571
|
+
* // Use as regular properties
|
|
572
|
+
* state.count = 5;
|
|
573
|
+
* console.log(state.count); // 5
|
|
574
|
+
*
|
|
575
|
+
* // Array helpers
|
|
576
|
+
* state.items$push('c');
|
|
577
|
+
* state.items$filter(item => item !== 'a');
|
|
578
|
+
*
|
|
579
|
+
* // Access underlying pulses
|
|
580
|
+
* state.$pulse('count').subscribe(v => console.log(v));
|
|
368
581
|
*/
|
|
369
582
|
export function createState(obj) {
|
|
370
583
|
const state = {};
|
|
@@ -454,8 +667,21 @@ export function createState(obj) {
|
|
|
454
667
|
}
|
|
455
668
|
|
|
456
669
|
/**
|
|
457
|
-
* Memoize a function based on
|
|
458
|
-
* Only recomputes when
|
|
670
|
+
* Memoize a function based on its arguments.
|
|
671
|
+
* Only recomputes when arguments change.
|
|
672
|
+
* @template {function} T
|
|
673
|
+
* @param {T} fn - Function to memoize
|
|
674
|
+
* @param {MemoOptions} [options={}] - Configuration options
|
|
675
|
+
* @returns {T} Memoized function
|
|
676
|
+
* @example
|
|
677
|
+
* const expensiveCalc = memo((x, y) => {
|
|
678
|
+
* console.log('Computing...');
|
|
679
|
+
* return x * y;
|
|
680
|
+
* });
|
|
681
|
+
*
|
|
682
|
+
* expensiveCalc(2, 3); // Logs: Computing... Returns: 6
|
|
683
|
+
* expensiveCalc(2, 3); // Returns: 6 (cached, no log)
|
|
684
|
+
* expensiveCalc(3, 4); // Logs: Computing... Returns: 12
|
|
459
685
|
*/
|
|
460
686
|
export function memo(fn, options = {}) {
|
|
461
687
|
const { equals = Object.is } = options;
|
|
@@ -480,8 +706,20 @@ export function memo(fn, options = {}) {
|
|
|
480
706
|
}
|
|
481
707
|
|
|
482
708
|
/**
|
|
483
|
-
* Create a memoized computed value
|
|
484
|
-
* Combines memo with computed for expensive derivations
|
|
709
|
+
* Create a memoized computed value.
|
|
710
|
+
* Combines memo with computed for expensive derivations.
|
|
711
|
+
* @template T
|
|
712
|
+
* @param {function(): T} fn - Computation function
|
|
713
|
+
* @param {MemoComputedOptions} [options={}] - Configuration options
|
|
714
|
+
* @returns {Pulse<T>} A computed pulse that only recalculates when deps change
|
|
715
|
+
* @example
|
|
716
|
+
* const items = pulse([1, 2, 3, 4, 5]);
|
|
717
|
+
* const filter = pulse('');
|
|
718
|
+
*
|
|
719
|
+
* const filtered = memoComputed(
|
|
720
|
+
* () => items.get().filter(i => String(i).includes(filter.get())),
|
|
721
|
+
* { deps: [items, filter] }
|
|
722
|
+
* );
|
|
485
723
|
*/
|
|
486
724
|
export function memoComputed(fn, options = {}) {
|
|
487
725
|
const { deps = [], equals = Object.is } = options;
|
|
@@ -505,7 +743,26 @@ export function memoComputed(fn, options = {}) {
|
|
|
505
743
|
}
|
|
506
744
|
|
|
507
745
|
/**
|
|
508
|
-
* Watch specific pulses and run a callback when they change
|
|
746
|
+
* Watch specific pulses and run a callback when they change.
|
|
747
|
+
* Unlike effect, provides both new and old values.
|
|
748
|
+
* @template T
|
|
749
|
+
* @param {Pulse<T>|Array<Pulse<T>>} sources - Pulse(s) to watch
|
|
750
|
+
* @param {function(Array<T>, Array<T>): void} callback - Called with (newValues, oldValues)
|
|
751
|
+
* @returns {function(): void} Dispose function to stop watching
|
|
752
|
+
* @example
|
|
753
|
+
* const count = pulse(0);
|
|
754
|
+
*
|
|
755
|
+
* const stop = watch(count, ([newVal], [oldVal]) => {
|
|
756
|
+
* console.log(`Changed from ${oldVal} to ${newVal}`);
|
|
757
|
+
* });
|
|
758
|
+
*
|
|
759
|
+
* count.set(1); // Logs: Changed from 0 to 1
|
|
760
|
+
* stop();
|
|
761
|
+
*
|
|
762
|
+
* // Watch multiple
|
|
763
|
+
* watch([a, b], ([newA, newB], [oldA, oldB]) => {
|
|
764
|
+
* console.log('Values changed');
|
|
765
|
+
* });
|
|
509
766
|
*/
|
|
510
767
|
export function watch(sources, callback) {
|
|
511
768
|
const sourcesArray = Array.isArray(sources) ? sources : [sources];
|
|
@@ -520,7 +777,22 @@ export function watch(sources, callback) {
|
|
|
520
777
|
}
|
|
521
778
|
|
|
522
779
|
/**
|
|
523
|
-
* Create
|
|
780
|
+
* Create pulses from a promise, tracking loading and error states.
|
|
781
|
+
* @template T
|
|
782
|
+
* @param {Promise<T>} promise - The promise to track
|
|
783
|
+
* @param {T} [initialValue=undefined] - Initial value while loading
|
|
784
|
+
* @returns {PromiseState<T>} Object with value, loading, and error pulses
|
|
785
|
+
* @example
|
|
786
|
+
* const { value, loading, error } = fromPromise(
|
|
787
|
+
* fetch('/api/data').then(r => r.json()),
|
|
788
|
+
* [] // Initial value
|
|
789
|
+
* );
|
|
790
|
+
*
|
|
791
|
+
* effect(() => {
|
|
792
|
+
* if (loading.get()) return console.log('Loading...');
|
|
793
|
+
* if (error.get()) return console.log('Error:', error.get());
|
|
794
|
+
* console.log('Data:', value.get());
|
|
795
|
+
* });
|
|
524
796
|
*/
|
|
525
797
|
export function fromPromise(promise, initialValue = undefined) {
|
|
526
798
|
const p = pulse(initialValue);
|
|
@@ -545,7 +817,18 @@ export function fromPromise(promise, initialValue = undefined) {
|
|
|
545
817
|
}
|
|
546
818
|
|
|
547
819
|
/**
|
|
548
|
-
*
|
|
820
|
+
* Execute a function without tracking any pulse dependencies.
|
|
821
|
+
* Useful for reading values without creating subscriptions.
|
|
822
|
+
* @template T
|
|
823
|
+
* @param {function(): T} fn - Function to execute untracked
|
|
824
|
+
* @returns {T} The return value of fn
|
|
825
|
+
* @example
|
|
826
|
+
* effect(() => {
|
|
827
|
+
* const a = aSignal.get(); // Tracked
|
|
828
|
+
* const b = untrack(() => bSignal.get()); // Not tracked
|
|
829
|
+
* console.log(a, b);
|
|
830
|
+
* });
|
|
831
|
+
* // Effect only re-runs when aSignal changes, not bSignal
|
|
549
832
|
*/
|
|
550
833
|
export function untrack(fn) {
|
|
551
834
|
const prevEffect = currentEffect;
|