@pulse-js/core 0.2.2 → 0.3.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/dist/index.d.cts CHANGED
@@ -302,6 +302,14 @@ declare function guardOk<T>(value: T): T;
302
302
  declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>): Guard<T>;
303
303
  declare namespace guard {
304
304
  var map: <T, U>(source: Source<T>, mapper: (value: T) => U | Promise<U>, name?: string) => Guard<U>;
305
+ var select: <T extends object, U>(pulseObj: T, selector: (obj: T) => U | Promise<U>, name?: string) => Guard<U>;
306
+ var from: <T>(getValue: () => {
307
+ value?: T;
308
+ isLoading?: boolean;
309
+ error?: any;
310
+ } | T, options?: {
311
+ name?: string;
312
+ }) => Guard<T | undefined>;
305
313
  }
306
314
 
307
315
  /**
@@ -367,6 +375,139 @@ declare function guardAny(nameOrGuards: string | Guard<any>[], maybeGuards?: Gua
367
375
  */
368
376
  declare function guardNot(nameOrTarget: string | Guard<any> | (() => any), maybeTarget?: Guard<any> | (() => any)): Guard<boolean>;
369
377
 
378
+ /**
379
+ * Pulse Signal - Low-level reactive primitive
380
+ *
381
+ * Internal building block for fine-grained reactivity.
382
+ * Signals are the foundation for both `source()` and `pulse()` APIs.
383
+ *
384
+ * @internal
385
+ */
386
+
387
+ /**
388
+ * A reactive signal holding a value.
389
+ * @template T The type of value held by the signal.
390
+ */
391
+ interface Signal<T> {
392
+ /** Get the current value, auto-tracking if inside a Guard. */
393
+ get(): T;
394
+ /** Set a new value, notifying all dependents. */
395
+ set(value: T): void;
396
+ /** Update value using a transformer function. */
397
+ update(fn: (current: T) => T): void;
398
+ /** Subscribe to value changes. */
399
+ subscribe(listener: Subscriber<T>): () => void;
400
+ /** Get value without tracking (break dependency). */
401
+ peek(): T;
402
+ }
403
+ /**
404
+ * Batch multiple reactive updates into a single notification cycle.
405
+ * Reduces unnecessary re-evaluations when updating multiple signals.
406
+ *
407
+ * @param fn Function containing multiple signal updates.
408
+ *
409
+ * @example
410
+ * ```ts
411
+ * batch(() => {
412
+ * count.set(1);
413
+ * name.set('Alice');
414
+ * // Dependents notified only once after both updates
415
+ * });
416
+ * ```
417
+ */
418
+ declare function batch(fn: () => void): void;
419
+ /**
420
+ * Creates a low-level reactive signal.
421
+ *
422
+ * Signals automatically track dependencies when read inside a Guard context.
423
+ * When the signal value changes, all dependent Guards are re-evaluated.
424
+ *
425
+ * @template T The type of value to store.
426
+ * @param initialValue The initial value.
427
+ * @param equals Optional equality function (defaults to ===).
428
+ * @returns A reactive Signal.
429
+ *
430
+ * @example
431
+ * ```ts
432
+ * const count = createSignal(0);
433
+ * count.get(); // 0
434
+ * count.set(5);
435
+ * count.get(); // 5
436
+ * ```
437
+ */
438
+ declare function createSignal<T>(initialValue: T, equals?: (a: T, b: T) => boolean): Signal<T>;
439
+ /**
440
+ * Effect tracking - run a function when its dependencies change.
441
+ * Similar to Solid's createEffect or Vue's watchEffect.
442
+ *
443
+ * @param fn The effect function to run.
444
+ * @returns Cleanup function to stop the effect.
445
+ *
446
+ * @example
447
+ * ```ts
448
+ * const count = createSignal(0);
449
+ * const cleanup = effect(() => {
450
+ * console.log('Count changed:', count.get());
451
+ * });
452
+ * // Later: cleanup() to stop
453
+ * ```
454
+ */
455
+ declare function effect(fn: () => void | (() => void)): () => void;
456
+
457
+ /**
458
+ * Metadata attached to pulse objects for internal tracking.
459
+ */
460
+ interface PulseMeta<T> {
461
+ signals: Map<string | symbol, Signal<any>>;
462
+ subscribers: Set<Subscriber<T>>;
463
+ dependents: Set<Trackable>;
464
+ name?: string;
465
+ target: T;
466
+ }
467
+ declare const PULSE_META: unique symbol;
468
+ /**
469
+ * Represents a reactive Pulse Object.
470
+ */
471
+ type PulseObject<T extends object> = T & {
472
+ /** @internal */
473
+ [PULSE_META]: PulseMeta<T>;
474
+ /** Subscribe to any property change on this object. */
475
+ $subscribe(listener: Subscriber<T>): () => void;
476
+ /** Take a non-reactive snapshot of the current state. */
477
+ $snapshot(): T;
478
+ /** Access the raw target object. */
479
+ $raw: T;
480
+ };
481
+ /**
482
+ * Configuration options for pulse objects.
483
+ */
484
+ interface PulseOptions {
485
+ /** Optional name for registry and debugging. */
486
+ name?: string;
487
+ /** Whether to recursively wrap nested objects (default: true). */
488
+ deep?: boolean;
489
+ }
490
+ /**
491
+ * Creates a reactive Pulse Object from a plain JavaScript object.
492
+ *
493
+ * Pulse Objects use Proxies to automatically track property access and mutations.
494
+ * They are ideal for managing complex state structures without manual source calls.
495
+ */
496
+ declare function pulse<T extends object>(target: T, options?: PulseOptions): PulseObject<T>;
497
+ /**
498
+ * Check if a value is a Pulse Object.
499
+ */
500
+ declare function isPulseObject(value: any): value is PulseObject<any>;
501
+ /**
502
+ * Get the raw (non-reactive) object from a Pulse Object.
503
+ */
504
+ declare function toRaw<T extends object>(pulseObj: PulseObject<T>): T;
505
+ /**
506
+ * Create a readonly view of a Pulse Object.
507
+ * Attempts to write will throw in development.
508
+ */
509
+ declare function readonly<T extends object>(pulseObj: PulseObject<T>): Readonly<T>;
510
+
370
511
  /**
371
512
  * Serialized state of guards for transfer from server to client.
372
513
  */
@@ -421,40 +562,35 @@ type PulseUnit = Source<any> | Guard<any>;
421
562
  * Root Registry for Pulse.
422
563
  *
423
564
  * Tracks all registered Units (Sources and Guards) globally for DevTools.
424
- *
425
- * **IMPORTANT**: Only units with explicit names are registered and visible in DevTools.
426
- * Unnamed units work perfectly but are not tracked to avoid HMR instability.
427
- *
428
- * @example
429
- * ```ts
430
- * // ✅ Visible in DevTools
431
- * const count = source(0, { name: 'count' });
432
- *
433
- * // ❌ Not visible in DevTools (but works fine)
434
- * const temp = source(0);
435
- * ```
565
+ * Uses the Proxy of Identity pattern to maintain stable references during HMR.
436
566
  */
437
567
  declare class Registry {
438
- private units;
568
+ private targets;
569
+ private proxies;
439
570
  private listeners;
440
571
  private currentGeneration;
441
572
  private cleanupScheduled;
573
+ private hmrDebounce;
442
574
  /**
443
- * Schedules cleanup of units that weren't re-registered (deleted from code).
575
+ * Registers a unit and returns a stable Identity Proxy.
576
+ *
577
+ * If a unit with the same UID already exists, it updates the internal
578
+ * target of the existing proxy and returns that same proxy.
444
579
  */
445
- private scheduleCleanup;
580
+ register<T extends PulseUnit>(unit: T): T;
446
581
  /**
447
- * Removes units that weren't re-registered in the current generation.
448
- * Uses mark-and-sweep: units that were re-registered have current generation,
449
- * units that weren't are from old generation and should be removed.
582
+ * Schedules cleanup of units that weren't re-registered.
450
583
  */
584
+ private scheduleCleanup;
451
585
  private cleanupDeadUnits;
452
- /**
453
- * Registers a unit (only if it has an explicit name).
454
- */
455
- register(unit: PulseUnit): void;
586
+ private notifyListeners;
587
+ get(nameOrUid: string): PulseUnit | undefined;
456
588
  getAll(): PulseUnit[];
457
- onRegister(listener: (unit: PulseUnit) => void): () => void;
589
+ getAllWithMeta(): Array<{
590
+ unit: PulseUnit;
591
+ uid: string;
592
+ }>;
593
+ onRegister(listener: (unit: PulseUnit, event?: 'add' | 'update' | 'remove') => void): () => void;
458
594
  reset(): void;
459
595
  }
460
596
  declare const PulseRegistry: Registry;
@@ -489,4 +625,4 @@ declare const extendedGuard: typeof guard & {
489
625
  compute: typeof compute;
490
626
  };
491
627
 
492
- export { type Guard, type GuardExplanation, type GuardNode, type GuardReason, type GuardState, type GuardStatus, type HydrationState, type InferGuardType, PulseRegistry, type PulseUnit, type Source, type SourceOptions, type Subscriber, type Trackable, compute, evaluate, getCurrentGuard, extendedGuard as guard, guardFail, guardOk, hydrate, registerGuardForHydration, runInContext, source };
628
+ export { type Guard, type GuardExplanation, type GuardNode, type GuardReason, type GuardState, type GuardStatus, type HydrationState, type InferGuardType, type PulseObject, type PulseOptions, PulseRegistry, type PulseUnit, type Signal, type Source, type SourceOptions, type Subscriber, type Trackable, batch, compute, createSignal, effect, evaluate, getCurrentGuard, extendedGuard as guard, guardFail, guardOk, hydrate, isPulseObject, pulse, readonly, registerGuardForHydration, runInContext, source, toRaw };
package/dist/index.d.ts CHANGED
@@ -302,6 +302,14 @@ declare function guardOk<T>(value: T): T;
302
302
  declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>): Guard<T>;
303
303
  declare namespace guard {
304
304
  var map: <T, U>(source: Source<T>, mapper: (value: T) => U | Promise<U>, name?: string) => Guard<U>;
305
+ var select: <T extends object, U>(pulseObj: T, selector: (obj: T) => U | Promise<U>, name?: string) => Guard<U>;
306
+ var from: <T>(getValue: () => {
307
+ value?: T;
308
+ isLoading?: boolean;
309
+ error?: any;
310
+ } | T, options?: {
311
+ name?: string;
312
+ }) => Guard<T | undefined>;
305
313
  }
306
314
 
307
315
  /**
@@ -367,6 +375,139 @@ declare function guardAny(nameOrGuards: string | Guard<any>[], maybeGuards?: Gua
367
375
  */
368
376
  declare function guardNot(nameOrTarget: string | Guard<any> | (() => any), maybeTarget?: Guard<any> | (() => any)): Guard<boolean>;
369
377
 
378
+ /**
379
+ * Pulse Signal - Low-level reactive primitive
380
+ *
381
+ * Internal building block for fine-grained reactivity.
382
+ * Signals are the foundation for both `source()` and `pulse()` APIs.
383
+ *
384
+ * @internal
385
+ */
386
+
387
+ /**
388
+ * A reactive signal holding a value.
389
+ * @template T The type of value held by the signal.
390
+ */
391
+ interface Signal<T> {
392
+ /** Get the current value, auto-tracking if inside a Guard. */
393
+ get(): T;
394
+ /** Set a new value, notifying all dependents. */
395
+ set(value: T): void;
396
+ /** Update value using a transformer function. */
397
+ update(fn: (current: T) => T): void;
398
+ /** Subscribe to value changes. */
399
+ subscribe(listener: Subscriber<T>): () => void;
400
+ /** Get value without tracking (break dependency). */
401
+ peek(): T;
402
+ }
403
+ /**
404
+ * Batch multiple reactive updates into a single notification cycle.
405
+ * Reduces unnecessary re-evaluations when updating multiple signals.
406
+ *
407
+ * @param fn Function containing multiple signal updates.
408
+ *
409
+ * @example
410
+ * ```ts
411
+ * batch(() => {
412
+ * count.set(1);
413
+ * name.set('Alice');
414
+ * // Dependents notified only once after both updates
415
+ * });
416
+ * ```
417
+ */
418
+ declare function batch(fn: () => void): void;
419
+ /**
420
+ * Creates a low-level reactive signal.
421
+ *
422
+ * Signals automatically track dependencies when read inside a Guard context.
423
+ * When the signal value changes, all dependent Guards are re-evaluated.
424
+ *
425
+ * @template T The type of value to store.
426
+ * @param initialValue The initial value.
427
+ * @param equals Optional equality function (defaults to ===).
428
+ * @returns A reactive Signal.
429
+ *
430
+ * @example
431
+ * ```ts
432
+ * const count = createSignal(0);
433
+ * count.get(); // 0
434
+ * count.set(5);
435
+ * count.get(); // 5
436
+ * ```
437
+ */
438
+ declare function createSignal<T>(initialValue: T, equals?: (a: T, b: T) => boolean): Signal<T>;
439
+ /**
440
+ * Effect tracking - run a function when its dependencies change.
441
+ * Similar to Solid's createEffect or Vue's watchEffect.
442
+ *
443
+ * @param fn The effect function to run.
444
+ * @returns Cleanup function to stop the effect.
445
+ *
446
+ * @example
447
+ * ```ts
448
+ * const count = createSignal(0);
449
+ * const cleanup = effect(() => {
450
+ * console.log('Count changed:', count.get());
451
+ * });
452
+ * // Later: cleanup() to stop
453
+ * ```
454
+ */
455
+ declare function effect(fn: () => void | (() => void)): () => void;
456
+
457
+ /**
458
+ * Metadata attached to pulse objects for internal tracking.
459
+ */
460
+ interface PulseMeta<T> {
461
+ signals: Map<string | symbol, Signal<any>>;
462
+ subscribers: Set<Subscriber<T>>;
463
+ dependents: Set<Trackable>;
464
+ name?: string;
465
+ target: T;
466
+ }
467
+ declare const PULSE_META: unique symbol;
468
+ /**
469
+ * Represents a reactive Pulse Object.
470
+ */
471
+ type PulseObject<T extends object> = T & {
472
+ /** @internal */
473
+ [PULSE_META]: PulseMeta<T>;
474
+ /** Subscribe to any property change on this object. */
475
+ $subscribe(listener: Subscriber<T>): () => void;
476
+ /** Take a non-reactive snapshot of the current state. */
477
+ $snapshot(): T;
478
+ /** Access the raw target object. */
479
+ $raw: T;
480
+ };
481
+ /**
482
+ * Configuration options for pulse objects.
483
+ */
484
+ interface PulseOptions {
485
+ /** Optional name for registry and debugging. */
486
+ name?: string;
487
+ /** Whether to recursively wrap nested objects (default: true). */
488
+ deep?: boolean;
489
+ }
490
+ /**
491
+ * Creates a reactive Pulse Object from a plain JavaScript object.
492
+ *
493
+ * Pulse Objects use Proxies to automatically track property access and mutations.
494
+ * They are ideal for managing complex state structures without manual source calls.
495
+ */
496
+ declare function pulse<T extends object>(target: T, options?: PulseOptions): PulseObject<T>;
497
+ /**
498
+ * Check if a value is a Pulse Object.
499
+ */
500
+ declare function isPulseObject(value: any): value is PulseObject<any>;
501
+ /**
502
+ * Get the raw (non-reactive) object from a Pulse Object.
503
+ */
504
+ declare function toRaw<T extends object>(pulseObj: PulseObject<T>): T;
505
+ /**
506
+ * Create a readonly view of a Pulse Object.
507
+ * Attempts to write will throw in development.
508
+ */
509
+ declare function readonly<T extends object>(pulseObj: PulseObject<T>): Readonly<T>;
510
+
370
511
  /**
371
512
  * Serialized state of guards for transfer from server to client.
372
513
  */
@@ -421,40 +562,35 @@ type PulseUnit = Source<any> | Guard<any>;
421
562
  * Root Registry for Pulse.
422
563
  *
423
564
  * Tracks all registered Units (Sources and Guards) globally for DevTools.
424
- *
425
- * **IMPORTANT**: Only units with explicit names are registered and visible in DevTools.
426
- * Unnamed units work perfectly but are not tracked to avoid HMR instability.
427
- *
428
- * @example
429
- * ```ts
430
- * // ✅ Visible in DevTools
431
- * const count = source(0, { name: 'count' });
432
- *
433
- * // ❌ Not visible in DevTools (but works fine)
434
- * const temp = source(0);
435
- * ```
565
+ * Uses the Proxy of Identity pattern to maintain stable references during HMR.
436
566
  */
437
567
  declare class Registry {
438
- private units;
568
+ private targets;
569
+ private proxies;
439
570
  private listeners;
440
571
  private currentGeneration;
441
572
  private cleanupScheduled;
573
+ private hmrDebounce;
442
574
  /**
443
- * Schedules cleanup of units that weren't re-registered (deleted from code).
575
+ * Registers a unit and returns a stable Identity Proxy.
576
+ *
577
+ * If a unit with the same UID already exists, it updates the internal
578
+ * target of the existing proxy and returns that same proxy.
444
579
  */
445
- private scheduleCleanup;
580
+ register<T extends PulseUnit>(unit: T): T;
446
581
  /**
447
- * Removes units that weren't re-registered in the current generation.
448
- * Uses mark-and-sweep: units that were re-registered have current generation,
449
- * units that weren't are from old generation and should be removed.
582
+ * Schedules cleanup of units that weren't re-registered.
450
583
  */
584
+ private scheduleCleanup;
451
585
  private cleanupDeadUnits;
452
- /**
453
- * Registers a unit (only if it has an explicit name).
454
- */
455
- register(unit: PulseUnit): void;
586
+ private notifyListeners;
587
+ get(nameOrUid: string): PulseUnit | undefined;
456
588
  getAll(): PulseUnit[];
457
- onRegister(listener: (unit: PulseUnit) => void): () => void;
589
+ getAllWithMeta(): Array<{
590
+ unit: PulseUnit;
591
+ uid: string;
592
+ }>;
593
+ onRegister(listener: (unit: PulseUnit, event?: 'add' | 'update' | 'remove') => void): () => void;
458
594
  reset(): void;
459
595
  }
460
596
  declare const PulseRegistry: Registry;
@@ -489,4 +625,4 @@ declare const extendedGuard: typeof guard & {
489
625
  compute: typeof compute;
490
626
  };
491
627
 
492
- export { type Guard, type GuardExplanation, type GuardNode, type GuardReason, type GuardState, type GuardStatus, type HydrationState, type InferGuardType, PulseRegistry, type PulseUnit, type Source, type SourceOptions, type Subscriber, type Trackable, compute, evaluate, getCurrentGuard, extendedGuard as guard, guardFail, guardOk, hydrate, registerGuardForHydration, runInContext, source };
628
+ export { type Guard, type GuardExplanation, type GuardNode, type GuardReason, type GuardState, type GuardStatus, type HydrationState, type InferGuardType, type PulseObject, type PulseOptions, PulseRegistry, type PulseUnit, type Signal, type Source, type SourceOptions, type Subscriber, type Trackable, batch, compute, createSignal, effect, evaluate, getCurrentGuard, extendedGuard as guard, guardFail, guardOk, hydrate, isPulseObject, pulse, readonly, registerGuardForHydration, runInContext, source, toRaw };