pulse-js-framework 1.4.4 → 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/runtime/native.js CHANGED
@@ -5,6 +5,9 @@
5
5
  */
6
6
 
7
7
  import { pulse, effect, batch } from './pulse.js';
8
+ import { loggers } from './logger.js';
9
+
10
+ const log = loggers.native;
8
11
 
9
12
  /**
10
13
  * Check if PulseMobile bridge is available
@@ -223,8 +226,8 @@ export const NativeUI = {
223
226
  if (isNativeAvailable()) {
224
227
  return getNative().UI.showToast(message, isLong);
225
228
  }
226
- // Fallback: simple console log
227
- console.log('[Toast]', message);
229
+ // Fallback: log toast message
230
+ log.info('Toast:', message);
228
231
  return Promise.resolve();
229
232
  },
230
233
 
@@ -335,7 +338,7 @@ export function exitApp() {
335
338
  if (isNativeAvailable() && getNative().isAndroid) {
336
339
  return getNative().App.exit();
337
340
  }
338
- console.warn('exitApp is only available on Android');
341
+ log.warn('exitApp is only available on Android');
339
342
  return Promise.resolve();
340
343
  }
341
344
 
@@ -346,7 +349,7 @@ export function minimizeApp() {
346
349
  if (isNativeAvailable()) {
347
350
  return getNative().App.minimize();
348
351
  }
349
- console.warn('minimizeApp is only available in native apps');
352
+ log.warn('minimizeApp is only available in native apps');
350
353
  return Promise.resolve();
351
354
  }
352
355
 
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
- * Register a cleanup function for the current effect
17
- * Called when the effect re-runs or is disposed
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
- console.error('Effect error:', error);
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
- console.warn('Pulse: Maximum effect iterations reached. Possible infinite loop.');
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 simple pulse with an initial value
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
- * when its dependencies change
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
- console.error('Cleanup error:', e);
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
- console.error('Effect execution error:', error);
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
- * Effects only run after all updates complete
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 reactive dependencies
458
- * Only recomputes when dependencies change
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 a pulse from a promise
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
- * Untrack - read pulses without creating dependencies
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;