@pulse-js/core 0.1.9 → 0.2.1

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.cjs CHANGED
@@ -95,7 +95,7 @@ function guardFail(reason) {
95
95
  function guardOk(value) {
96
96
  return value;
97
97
  }
98
- function guard(nameOrFn, fn, _internalOffset = 3) {
98
+ function guard(nameOrFn, fn) {
99
99
  const name = typeof nameOrFn === "string" ? nameOrFn : void 0;
100
100
  const evaluator = typeof nameOrFn === "function" ? nameOrFn : fn;
101
101
  if (!evaluator) {
@@ -259,11 +259,20 @@ function guard(nameOrFn, fn, _internalOffset = 3) {
259
259
  lastDeps.forEach((dep) => {
260
260
  const depName = dep._name || "unnamed";
261
261
  const isG = "state" in dep;
262
- deps.push({
263
- name: depName,
264
- type: isG ? "guard" : "source",
265
- status: isG ? dep.state().status : void 0
266
- });
262
+ if (isG) {
263
+ const depState = dep.state();
264
+ deps.push({
265
+ name: depName,
266
+ type: "guard",
267
+ status: depState.status,
268
+ reason: depState.status === "fail" ? depState.reason : void 0
269
+ });
270
+ } else {
271
+ deps.push({
272
+ name: depName,
273
+ type: "source"
274
+ });
275
+ }
267
276
  });
268
277
  return {
269
278
  name: name || "guard",
@@ -288,6 +297,13 @@ function guard(nameOrFn, fn, _internalOffset = 3) {
288
297
  evaluate2();
289
298
  return g;
290
299
  }
300
+ guard.map = function(source2, mapper, name) {
301
+ const guardName = name || `map-${source2._name || "source"}`;
302
+ return guard(guardName, () => {
303
+ const value = source2();
304
+ return mapper(value);
305
+ });
306
+ };
291
307
 
292
308
  // src/registry.ts
293
309
  var Registry = class {
@@ -295,53 +311,23 @@ var Registry = class {
295
311
  listeners = /* @__PURE__ */ new Set();
296
312
  currentGeneration = 0;
297
313
  cleanupScheduled = false;
298
- autoNameCache = /* @__PURE__ */ new Map();
299
314
  /**
300
- * Generates a stable auto-name based on source code location.
301
- * Uses file path and line number to ensure the same location always gets the same name.
302
- * Cached to avoid repeated stack trace parsing.
303
- */
304
- generateAutoName(type, offset = 3) {
305
- const err = new Error();
306
- const stack = err.stack?.split("\n") || [];
307
- let callSite = stack[offset]?.trim() || "";
308
- const cacheKey = `${type}:${callSite}`;
309
- if (this.autoNameCache.has(cacheKey)) {
310
- return this.autoNameCache.get(cacheKey);
311
- }
312
- const match = callSite.match(/([^/\\]+)\.(?:ts|tsx|js|jsx):(\d+):\d+/);
313
- let name;
314
- if (match) {
315
- const filename = match[1];
316
- const line = match[2];
317
- if (filename && line) {
318
- name = `${type}@${filename}:${line}`;
319
- } else {
320
- name = `${type}#${Math.random().toString(36).substring(2, 7)}`;
321
- }
322
- } else {
323
- name = `${type}#${Math.random().toString(36).substring(2, 7)}`;
324
- }
325
- this.autoNameCache.set(cacheKey, name);
326
- return name;
327
- }
328
- /**
329
- * Increments generation and schedules cleanup of old units.
330
- * Called automatically when HMR is detected.
315
+ * Schedules cleanup of units that weren't re-registered (deleted from code).
331
316
  */
332
317
  scheduleCleanup() {
333
318
  if (this.cleanupScheduled) return;
334
319
  this.cleanupScheduled = true;
335
- this.currentGeneration++;
336
320
  setTimeout(() => {
337
- this.cleanupOldGenerations();
321
+ this.cleanupDeadUnits();
338
322
  this.cleanupScheduled = false;
339
323
  }, 100);
340
324
  }
341
325
  /**
342
- * Removes units from old generations (likely orphaned by HMR).
326
+ * Removes units that weren't re-registered in the current generation.
327
+ * Uses mark-and-sweep: units that were re-registered have current generation,
328
+ * units that weren't are from old generation and should be removed.
343
329
  */
344
- cleanupOldGenerations() {
330
+ cleanupDeadUnits() {
345
331
  const toDelete = [];
346
332
  this.units.forEach((unit, key) => {
347
333
  const gen = unit._generation;
@@ -351,53 +337,46 @@ var Registry = class {
351
337
  });
352
338
  toDelete.forEach((key) => this.units.delete(key));
353
339
  if (toDelete.length > 0) {
354
- console.log(`[Pulse] Cleaned up ${toDelete.length} stale units after HMR`);
340
+ console.log(`[Pulse] Cleaned up ${toDelete.length} deleted units after HMR`);
355
341
  }
356
342
  }
357
343
  /**
358
- * Registers a new unit (Source or Guard).
359
- * Auto-assigns stable names to unnamed units for HMR stability.
344
+ * Registers a unit (only if it has an explicit name).
360
345
  */
361
- register(unit, offset = 3) {
346
+ register(unit) {
362
347
  const unitWithMetadata = unit;
363
- let name = unitWithMetadata._name;
348
+ const name = unitWithMetadata._name;
364
349
  if (!name) {
365
- const isGuard2 = "state" in unit;
366
- name = this.generateAutoName(isGuard2 ? "guard" : "source", offset);
367
- unitWithMetadata._name = name;
350
+ return;
368
351
  }
369
- unitWithMetadata._generation = this.currentGeneration;
370
- if (this.units.has(name)) {
352
+ const existingUnit = this.units.get(name);
353
+ if (existingUnit) {
354
+ const existingGen = existingUnit?._generation;
355
+ if (existingGen === this.currentGeneration) {
356
+ unitWithMetadata._generation = this.currentGeneration;
357
+ this.units.set(name, unit);
358
+ this.listeners.forEach((l) => l(unit));
359
+ return;
360
+ }
361
+ this.currentGeneration++;
371
362
  this.scheduleCleanup();
372
363
  }
364
+ unitWithMetadata._generation = this.currentGeneration;
373
365
  this.units.set(name, unit);
374
366
  this.listeners.forEach((l) => l(unit));
375
367
  }
376
- /**
377
- * Retrieves all registered units.
378
- */
379
368
  getAll() {
380
369
  return Array.from(this.units.values());
381
370
  }
382
- /**
383
- * Subscribes to new unit registrations.
384
- *
385
- * @param listener - Callback receiving the newly registered unit.
386
- * @returns Unsubscribe function.
387
- */
388
371
  onRegister(listener) {
389
372
  this.listeners.add(listener);
390
373
  return () => {
391
374
  this.listeners.delete(listener);
392
375
  };
393
376
  }
394
- /**
395
- * Clears all registered units.
396
- */
397
377
  reset() {
398
378
  this.units.clear();
399
379
  this.currentGeneration = 0;
400
- this.autoNameCache.clear();
401
380
  }
402
381
  };
403
382
  var GLOBAL_KEY = "__PULSE_REGISTRY__";
@@ -408,7 +387,7 @@ if (!globalSymbols[GLOBAL_KEY]) {
408
387
  var PulseRegistry = globalSymbols[GLOBAL_KEY];
409
388
 
410
389
  // src/source.ts
411
- function source(initialValue, options = {}, _internalOffset = 3) {
390
+ function source(initialValue, options = {}) {
412
391
  let value = initialValue;
413
392
  const subscribers = /* @__PURE__ */ new Set();
414
393
  const dependents = /* @__PURE__ */ new Set();
@@ -447,7 +426,7 @@ function compute(name, dependencies, processor) {
447
426
  return guard(name, () => {
448
427
  const values = dependencies.map((dep) => typeof dep === "function" ? dep() : dep);
449
428
  return processor(...values);
450
- }, 4);
429
+ });
451
430
  }
452
431
 
453
432
  // src/composition.ts
@@ -470,7 +449,7 @@ function guardAll(nameOrGuards, maybeGuards) {
470
449
  throw new Error(message);
471
450
  }
472
451
  return true;
473
- }, 4);
452
+ });
474
453
  }
475
454
  function guardAny(nameOrGuards, maybeGuards) {
476
455
  const name = typeof nameOrGuards === "string" ? nameOrGuards : void 0;
@@ -484,7 +463,7 @@ function guardAny(nameOrGuards, maybeGuards) {
484
463
  allFails.push(message);
485
464
  }
486
465
  throw new Error(allFails.length > 0 ? allFails.join(" and ") : "no conditions met");
487
- }, 4);
466
+ });
488
467
  }
489
468
  function guardNot(nameOrTarget, maybeTarget) {
490
469
  const name = typeof nameOrTarget === "string" ? nameOrTarget : void 0;
@@ -494,7 +473,7 @@ function guardNot(nameOrTarget, maybeTarget) {
494
473
  return !target.ok();
495
474
  }
496
475
  return !target();
497
- }, 4);
476
+ });
498
477
  }
499
478
  var guardExtensions = {
500
479
  all: guardAll,
package/dist/index.d.cts CHANGED
@@ -41,6 +41,118 @@ declare function runInContext<T>(guard: GuardNode, fn: () => T): T;
41
41
  */
42
42
  declare function getCurrentGuard(): GuardNode | null;
43
43
 
44
+ /**
45
+ * Options for configuring a Pulse Source.
46
+ *
47
+ * @template T - The type of value stored in the source.
48
+ */
49
+ interface SourceOptions<T> {
50
+ /**
51
+ * A descriptive name for the source.
52
+ * Required for SSR hydration and highly recommended for debugging in DevTools.
53
+ */
54
+ name?: string;
55
+ /**
56
+ * Custom equality function to determine if a value has changed.
57
+ * By default, Pulse uses strict equality (`===`).
58
+ *
59
+ * @param a - The current value.
60
+ * @param b - The new value.
61
+ * @returns `true` if the values are considered equal, `false` otherwise.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const list = source([1], {
66
+ * equals: (a, b) => a.length === b.length
67
+ * });
68
+ * ```
69
+ */
70
+ equals?: (a: T, b: T) => boolean;
71
+ }
72
+ /**
73
+ * A Pulse Source is a reactive container for a value.
74
+ * It tracks which Guards read its value and notifies them when it changes.
75
+ *
76
+ * @template T The type of the value held by the source.
77
+ */
78
+ interface Source<T> {
79
+ /**
80
+ * Returns the current value of the source.
81
+ * If called within a Guard evaluation, it automatically registers that Guard as a dependent.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const count = source(0);
86
+ * console.log(count()); // 0
87
+ * ```
88
+ */
89
+ (): T;
90
+ /**
91
+ * Updates the source with a new value.
92
+ * If the value is different (based on strict equality or `options.equals`),
93
+ * all dependent Guards and subscribers will be notified.
94
+ *
95
+ * @param value The new value to set.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const count = source(0);
100
+ * count.set(1); // Triggers re-evaluation of dependents
101
+ * ```
102
+ *
103
+ * @error
104
+ * Common error: Mutating an object property without setting a new object reference.
105
+ * Pulse uses reference equality by default. If you mutate a property, Pulse won't know it changed.
106
+ * Solution: Always provide a new object or implement a custom `equals`.
107
+ */
108
+ set(value: T): void;
109
+ /**
110
+ * Updates the source value using a transformer function based on the current value.
111
+ * Useful for increments or toggles.
112
+ *
113
+ * @param updater A function that receives the current value and returns the new value.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * const count = source(0);
118
+ * count.update(n => n + 1);
119
+ * ```
120
+ */
121
+ update(updater: (current: T) => T): void;
122
+ /**
123
+ * Manually subscribes to changes in the source value.
124
+ *
125
+ * @param listener A callback that receives the new value.
126
+ * @returns An unsubscription function.
127
+ *
128
+ * @note Most users should use `guard()` or `usePulse()` instead of manual subscriptions.
129
+ */
130
+ subscribe(listener: Subscriber<T>): () => void;
131
+ }
132
+ /**
133
+ * Creates a new Pulse Source.
134
+ *
135
+ * Sources are the fundamental building blocks of state in Pulse. They hold a value
136
+ * and track which Guards depend on them.
137
+ *
138
+ * @template T - The type of value to store.
139
+ * @param initialValue - The initial state.
140
+ * @param options - Configuration options (name, equality).
141
+ * @returns A reactive Pulse Source.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * const user = source({ name: 'Alice' }, { name: 'user_state' });
146
+ *
147
+ * // Read value (auto-tracks if inside a guard)
148
+ * console.log(user());
149
+ *
150
+ * // Update value
151
+ * user.set({ name: 'Bob' });
152
+ * ```
153
+ */
154
+ declare function source<T>(initialValue: T, options?: SourceOptions<T>): Source<T>;
155
+
44
156
  /**
45
157
  * Status of a Pulse Guard evaluation.
46
158
  * - 'pending': Async evaluation is in progress.
@@ -86,6 +198,7 @@ interface GuardExplanation {
86
198
  name: string;
87
199
  type: 'source' | 'guard';
88
200
  status?: GuardStatus;
201
+ reason?: string | GuardReason;
89
202
  }>;
90
203
  }
91
204
  /**
@@ -186,7 +299,10 @@ declare function guardFail(reason: string | GuardReason): never;
186
299
  * Returns the value passed to it.
187
300
  */
188
301
  declare function guardOk<T>(value: T): T;
189
- declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>, _internalOffset?: number): Guard<T>;
302
+ declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>): Guard<T>;
303
+ declare namespace guard {
304
+ var map: <T, U>(source: Source<T>, mapper: (value: T) => U | Promise<U>, name?: string) => Guard<U>;
305
+ }
190
306
 
191
307
  /**
192
308
  * Utility to transform reactive dependencies into a new derived value.
@@ -251,118 +367,6 @@ declare function guardAny(nameOrGuards: string | Guard<any>[], maybeGuards?: Gua
251
367
  */
252
368
  declare function guardNot(nameOrTarget: string | Guard<any> | (() => any), maybeTarget?: Guard<any> | (() => any)): Guard<boolean>;
253
369
 
254
- /**
255
- * Options for configuring a Pulse Source.
256
- *
257
- * @template T - The type of value stored in the source.
258
- */
259
- interface SourceOptions<T> {
260
- /**
261
- * A descriptive name for the source.
262
- * Required for SSR hydration and highly recommended for debugging in DevTools.
263
- */
264
- name?: string;
265
- /**
266
- * Custom equality function to determine if a value has changed.
267
- * By default, Pulse uses strict equality (`===`).
268
- *
269
- * @param a - The current value.
270
- * @param b - The new value.
271
- * @returns `true` if the values are considered equal, `false` otherwise.
272
- *
273
- * @example
274
- * ```ts
275
- * const list = source([1], {
276
- * equals: (a, b) => a.length === b.length
277
- * });
278
- * ```
279
- */
280
- equals?: (a: T, b: T) => boolean;
281
- }
282
- /**
283
- * A Pulse Source is a reactive container for a value.
284
- * It tracks which Guards read its value and notifies them when it changes.
285
- *
286
- * @template T The type of the value held by the source.
287
- */
288
- interface Source<T> {
289
- /**
290
- * Returns the current value of the source.
291
- * If called within a Guard evaluation, it automatically registers that Guard as a dependent.
292
- *
293
- * @example
294
- * ```ts
295
- * const count = source(0);
296
- * console.log(count()); // 0
297
- * ```
298
- */
299
- (): T;
300
- /**
301
- * Updates the source with a new value.
302
- * If the value is different (based on strict equality or `options.equals`),
303
- * all dependent Guards and subscribers will be notified.
304
- *
305
- * @param value The new value to set.
306
- *
307
- * @example
308
- * ```ts
309
- * const count = source(0);
310
- * count.set(1); // Triggers re-evaluation of dependents
311
- * ```
312
- *
313
- * @error
314
- * Common error: Mutating an object property without setting a new object reference.
315
- * Pulse uses reference equality by default. If you mutate a property, Pulse won't know it changed.
316
- * Solution: Always provide a new object or implement a custom `equals`.
317
- */
318
- set(value: T): void;
319
- /**
320
- * Updates the source value using a transformer function based on the current value.
321
- * Useful for increments or toggles.
322
- *
323
- * @param updater A function that receives the current value and returns the new value.
324
- *
325
- * @example
326
- * ```ts
327
- * const count = source(0);
328
- * count.update(n => n + 1);
329
- * ```
330
- */
331
- update(updater: (current: T) => T): void;
332
- /**
333
- * Manually subscribes to changes in the source value.
334
- *
335
- * @param listener A callback that receives the new value.
336
- * @returns An unsubscription function.
337
- *
338
- * @note Most users should use `guard()` or `usePulse()` instead of manual subscriptions.
339
- */
340
- subscribe(listener: Subscriber<T>): () => void;
341
- }
342
- /**
343
- * Creates a new Pulse Source.
344
- *
345
- * Sources are the fundamental building blocks of state in Pulse. They hold a value
346
- * and track which Guards depend on them.
347
- *
348
- * @template T - The type of value to store.
349
- * @param initialValue - The initial state.
350
- * @param options - Configuration options (name, equality).
351
- * @returns A reactive Pulse Source.
352
- *
353
- * @example
354
- * ```ts
355
- * const user = source({ name: 'Alice' }, { name: 'user_state' });
356
- *
357
- * // Read value (auto-tracks if inside a guard)
358
- * console.log(user());
359
- *
360
- * // Update value
361
- * user.set({ name: 'Bob' });
362
- * ```
363
- */
364
- declare function source<T>(initialValue: T, options?: SourceOptions<T>, _internalOffset?: number): Source<T>;
365
-
366
370
  /**
367
371
  * Serialized state of guards for transfer from server to client.
368
372
  */
@@ -416,53 +420,56 @@ type PulseUnit = Source<any> | Guard<any>;
416
420
  /**
417
421
  * Root Registry for Pulse.
418
422
  *
419
- * It tracks all registered Units (Sources and Guards) globally, providing
420
- * the data source for DevTools and HMR stability.
423
+ * 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
+ * ```
421
436
  */
422
437
  declare class Registry {
423
438
  private units;
424
439
  private listeners;
425
440
  private currentGeneration;
426
441
  private cleanupScheduled;
427
- private autoNameCache;
428
- /**
429
- * Generates a stable auto-name based on source code location.
430
- * Uses file path and line number to ensure the same location always gets the same name.
431
- * Cached to avoid repeated stack trace parsing.
432
- */
433
- generateAutoName(type: 'source' | 'guard', offset?: number): string;
434
442
  /**
435
- * Increments generation and schedules cleanup of old units.
436
- * Called automatically when HMR is detected.
443
+ * Schedules cleanup of units that weren't re-registered (deleted from code).
437
444
  */
438
445
  private scheduleCleanup;
439
446
  /**
440
- * Removes units from old generations (likely orphaned by HMR).
441
- */
442
- private cleanupOldGenerations;
443
- /**
444
- * Registers a new unit (Source or Guard).
445
- * Auto-assigns stable names to unnamed units for HMR stability.
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.
446
450
  */
447
- register(unit: PulseUnit, offset?: number): void;
451
+ private cleanupDeadUnits;
448
452
  /**
449
- * Retrieves all registered units.
453
+ * Registers a unit (only if it has an explicit name).
450
454
  */
455
+ register(unit: PulseUnit): void;
451
456
  getAll(): PulseUnit[];
452
- /**
453
- * Subscribes to new unit registrations.
454
- *
455
- * @param listener - Callback receiving the newly registered unit.
456
- * @returns Unsubscribe function.
457
- */
458
457
  onRegister(listener: (unit: PulseUnit) => void): () => void;
459
- /**
460
- * Clears all registered units.
461
- */
462
458
  reset(): void;
463
459
  }
464
460
  declare const PulseRegistry: Registry;
465
461
 
462
+ /**
463
+ * Extracts the result type T from a Guard<T>.
464
+ *
465
+ * @example
466
+ * ```ts
467
+ * const authGuard = guard('auth', async () => fetchUser());
468
+ * type AuthUser = InferGuardType<typeof authGuard>; // User
469
+ * ```
470
+ */
471
+ type InferGuardType<T> = T extends Guard<infer U> ? U : never;
472
+
466
473
  /**
467
474
  * Pulse Guard with integrated Composition Helpers.
468
475
  *
@@ -482,4 +489,4 @@ declare const extendedGuard: typeof guard & {
482
489
  compute: typeof compute;
483
490
  };
484
491
 
485
- export { type Guard, type GuardExplanation, type GuardNode, type GuardReason, type GuardState, type GuardStatus, type HydrationState, PulseRegistry, type PulseUnit, type Source, type SourceOptions, type Subscriber, type Trackable, compute, evaluate, getCurrentGuard, extendedGuard as guard, guardFail, guardOk, hydrate, registerGuardForHydration, runInContext, source };
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 };
package/dist/index.d.ts CHANGED
@@ -41,6 +41,118 @@ declare function runInContext<T>(guard: GuardNode, fn: () => T): T;
41
41
  */
42
42
  declare function getCurrentGuard(): GuardNode | null;
43
43
 
44
+ /**
45
+ * Options for configuring a Pulse Source.
46
+ *
47
+ * @template T - The type of value stored in the source.
48
+ */
49
+ interface SourceOptions<T> {
50
+ /**
51
+ * A descriptive name for the source.
52
+ * Required for SSR hydration and highly recommended for debugging in DevTools.
53
+ */
54
+ name?: string;
55
+ /**
56
+ * Custom equality function to determine if a value has changed.
57
+ * By default, Pulse uses strict equality (`===`).
58
+ *
59
+ * @param a - The current value.
60
+ * @param b - The new value.
61
+ * @returns `true` if the values are considered equal, `false` otherwise.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const list = source([1], {
66
+ * equals: (a, b) => a.length === b.length
67
+ * });
68
+ * ```
69
+ */
70
+ equals?: (a: T, b: T) => boolean;
71
+ }
72
+ /**
73
+ * A Pulse Source is a reactive container for a value.
74
+ * It tracks which Guards read its value and notifies them when it changes.
75
+ *
76
+ * @template T The type of the value held by the source.
77
+ */
78
+ interface Source<T> {
79
+ /**
80
+ * Returns the current value of the source.
81
+ * If called within a Guard evaluation, it automatically registers that Guard as a dependent.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const count = source(0);
86
+ * console.log(count()); // 0
87
+ * ```
88
+ */
89
+ (): T;
90
+ /**
91
+ * Updates the source with a new value.
92
+ * If the value is different (based on strict equality or `options.equals`),
93
+ * all dependent Guards and subscribers will be notified.
94
+ *
95
+ * @param value The new value to set.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const count = source(0);
100
+ * count.set(1); // Triggers re-evaluation of dependents
101
+ * ```
102
+ *
103
+ * @error
104
+ * Common error: Mutating an object property without setting a new object reference.
105
+ * Pulse uses reference equality by default. If you mutate a property, Pulse won't know it changed.
106
+ * Solution: Always provide a new object or implement a custom `equals`.
107
+ */
108
+ set(value: T): void;
109
+ /**
110
+ * Updates the source value using a transformer function based on the current value.
111
+ * Useful for increments or toggles.
112
+ *
113
+ * @param updater A function that receives the current value and returns the new value.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * const count = source(0);
118
+ * count.update(n => n + 1);
119
+ * ```
120
+ */
121
+ update(updater: (current: T) => T): void;
122
+ /**
123
+ * Manually subscribes to changes in the source value.
124
+ *
125
+ * @param listener A callback that receives the new value.
126
+ * @returns An unsubscription function.
127
+ *
128
+ * @note Most users should use `guard()` or `usePulse()` instead of manual subscriptions.
129
+ */
130
+ subscribe(listener: Subscriber<T>): () => void;
131
+ }
132
+ /**
133
+ * Creates a new Pulse Source.
134
+ *
135
+ * Sources are the fundamental building blocks of state in Pulse. They hold a value
136
+ * and track which Guards depend on them.
137
+ *
138
+ * @template T - The type of value to store.
139
+ * @param initialValue - The initial state.
140
+ * @param options - Configuration options (name, equality).
141
+ * @returns A reactive Pulse Source.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * const user = source({ name: 'Alice' }, { name: 'user_state' });
146
+ *
147
+ * // Read value (auto-tracks if inside a guard)
148
+ * console.log(user());
149
+ *
150
+ * // Update value
151
+ * user.set({ name: 'Bob' });
152
+ * ```
153
+ */
154
+ declare function source<T>(initialValue: T, options?: SourceOptions<T>): Source<T>;
155
+
44
156
  /**
45
157
  * Status of a Pulse Guard evaluation.
46
158
  * - 'pending': Async evaluation is in progress.
@@ -86,6 +198,7 @@ interface GuardExplanation {
86
198
  name: string;
87
199
  type: 'source' | 'guard';
88
200
  status?: GuardStatus;
201
+ reason?: string | GuardReason;
89
202
  }>;
90
203
  }
91
204
  /**
@@ -186,7 +299,10 @@ declare function guardFail(reason: string | GuardReason): never;
186
299
  * Returns the value passed to it.
187
300
  */
188
301
  declare function guardOk<T>(value: T): T;
189
- declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>, _internalOffset?: number): Guard<T>;
302
+ declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>): Guard<T>;
303
+ declare namespace guard {
304
+ var map: <T, U>(source: Source<T>, mapper: (value: T) => U | Promise<U>, name?: string) => Guard<U>;
305
+ }
190
306
 
191
307
  /**
192
308
  * Utility to transform reactive dependencies into a new derived value.
@@ -251,118 +367,6 @@ declare function guardAny(nameOrGuards: string | Guard<any>[], maybeGuards?: Gua
251
367
  */
252
368
  declare function guardNot(nameOrTarget: string | Guard<any> | (() => any), maybeTarget?: Guard<any> | (() => any)): Guard<boolean>;
253
369
 
254
- /**
255
- * Options for configuring a Pulse Source.
256
- *
257
- * @template T - The type of value stored in the source.
258
- */
259
- interface SourceOptions<T> {
260
- /**
261
- * A descriptive name for the source.
262
- * Required for SSR hydration and highly recommended for debugging in DevTools.
263
- */
264
- name?: string;
265
- /**
266
- * Custom equality function to determine if a value has changed.
267
- * By default, Pulse uses strict equality (`===`).
268
- *
269
- * @param a - The current value.
270
- * @param b - The new value.
271
- * @returns `true` if the values are considered equal, `false` otherwise.
272
- *
273
- * @example
274
- * ```ts
275
- * const list = source([1], {
276
- * equals: (a, b) => a.length === b.length
277
- * });
278
- * ```
279
- */
280
- equals?: (a: T, b: T) => boolean;
281
- }
282
- /**
283
- * A Pulse Source is a reactive container for a value.
284
- * It tracks which Guards read its value and notifies them when it changes.
285
- *
286
- * @template T The type of the value held by the source.
287
- */
288
- interface Source<T> {
289
- /**
290
- * Returns the current value of the source.
291
- * If called within a Guard evaluation, it automatically registers that Guard as a dependent.
292
- *
293
- * @example
294
- * ```ts
295
- * const count = source(0);
296
- * console.log(count()); // 0
297
- * ```
298
- */
299
- (): T;
300
- /**
301
- * Updates the source with a new value.
302
- * If the value is different (based on strict equality or `options.equals`),
303
- * all dependent Guards and subscribers will be notified.
304
- *
305
- * @param value The new value to set.
306
- *
307
- * @example
308
- * ```ts
309
- * const count = source(0);
310
- * count.set(1); // Triggers re-evaluation of dependents
311
- * ```
312
- *
313
- * @error
314
- * Common error: Mutating an object property without setting a new object reference.
315
- * Pulse uses reference equality by default. If you mutate a property, Pulse won't know it changed.
316
- * Solution: Always provide a new object or implement a custom `equals`.
317
- */
318
- set(value: T): void;
319
- /**
320
- * Updates the source value using a transformer function based on the current value.
321
- * Useful for increments or toggles.
322
- *
323
- * @param updater A function that receives the current value and returns the new value.
324
- *
325
- * @example
326
- * ```ts
327
- * const count = source(0);
328
- * count.update(n => n + 1);
329
- * ```
330
- */
331
- update(updater: (current: T) => T): void;
332
- /**
333
- * Manually subscribes to changes in the source value.
334
- *
335
- * @param listener A callback that receives the new value.
336
- * @returns An unsubscription function.
337
- *
338
- * @note Most users should use `guard()` or `usePulse()` instead of manual subscriptions.
339
- */
340
- subscribe(listener: Subscriber<T>): () => void;
341
- }
342
- /**
343
- * Creates a new Pulse Source.
344
- *
345
- * Sources are the fundamental building blocks of state in Pulse. They hold a value
346
- * and track which Guards depend on them.
347
- *
348
- * @template T - The type of value to store.
349
- * @param initialValue - The initial state.
350
- * @param options - Configuration options (name, equality).
351
- * @returns A reactive Pulse Source.
352
- *
353
- * @example
354
- * ```ts
355
- * const user = source({ name: 'Alice' }, { name: 'user_state' });
356
- *
357
- * // Read value (auto-tracks if inside a guard)
358
- * console.log(user());
359
- *
360
- * // Update value
361
- * user.set({ name: 'Bob' });
362
- * ```
363
- */
364
- declare function source<T>(initialValue: T, options?: SourceOptions<T>, _internalOffset?: number): Source<T>;
365
-
366
370
  /**
367
371
  * Serialized state of guards for transfer from server to client.
368
372
  */
@@ -416,53 +420,56 @@ type PulseUnit = Source<any> | Guard<any>;
416
420
  /**
417
421
  * Root Registry for Pulse.
418
422
  *
419
- * It tracks all registered Units (Sources and Guards) globally, providing
420
- * the data source for DevTools and HMR stability.
423
+ * 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
+ * ```
421
436
  */
422
437
  declare class Registry {
423
438
  private units;
424
439
  private listeners;
425
440
  private currentGeneration;
426
441
  private cleanupScheduled;
427
- private autoNameCache;
428
- /**
429
- * Generates a stable auto-name based on source code location.
430
- * Uses file path and line number to ensure the same location always gets the same name.
431
- * Cached to avoid repeated stack trace parsing.
432
- */
433
- generateAutoName(type: 'source' | 'guard', offset?: number): string;
434
442
  /**
435
- * Increments generation and schedules cleanup of old units.
436
- * Called automatically when HMR is detected.
443
+ * Schedules cleanup of units that weren't re-registered (deleted from code).
437
444
  */
438
445
  private scheduleCleanup;
439
446
  /**
440
- * Removes units from old generations (likely orphaned by HMR).
441
- */
442
- private cleanupOldGenerations;
443
- /**
444
- * Registers a new unit (Source or Guard).
445
- * Auto-assigns stable names to unnamed units for HMR stability.
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.
446
450
  */
447
- register(unit: PulseUnit, offset?: number): void;
451
+ private cleanupDeadUnits;
448
452
  /**
449
- * Retrieves all registered units.
453
+ * Registers a unit (only if it has an explicit name).
450
454
  */
455
+ register(unit: PulseUnit): void;
451
456
  getAll(): PulseUnit[];
452
- /**
453
- * Subscribes to new unit registrations.
454
- *
455
- * @param listener - Callback receiving the newly registered unit.
456
- * @returns Unsubscribe function.
457
- */
458
457
  onRegister(listener: (unit: PulseUnit) => void): () => void;
459
- /**
460
- * Clears all registered units.
461
- */
462
458
  reset(): void;
463
459
  }
464
460
  declare const PulseRegistry: Registry;
465
461
 
462
+ /**
463
+ * Extracts the result type T from a Guard<T>.
464
+ *
465
+ * @example
466
+ * ```ts
467
+ * const authGuard = guard('auth', async () => fetchUser());
468
+ * type AuthUser = InferGuardType<typeof authGuard>; // User
469
+ * ```
470
+ */
471
+ type InferGuardType<T> = T extends Guard<infer U> ? U : never;
472
+
466
473
  /**
467
474
  * Pulse Guard with integrated Composition Helpers.
468
475
  *
@@ -482,4 +489,4 @@ declare const extendedGuard: typeof guard & {
482
489
  compute: typeof compute;
483
490
  };
484
491
 
485
- export { type Guard, type GuardExplanation, type GuardNode, type GuardReason, type GuardState, type GuardStatus, type HydrationState, PulseRegistry, type PulseUnit, type Source, type SourceOptions, type Subscriber, type Trackable, compute, evaluate, getCurrentGuard, extendedGuard as guard, guardFail, guardOk, hydrate, registerGuardForHydration, runInContext, source };
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 };
package/dist/index.js CHANGED
@@ -59,7 +59,7 @@ function guardFail(reason) {
59
59
  function guardOk(value) {
60
60
  return value;
61
61
  }
62
- function guard(nameOrFn, fn, _internalOffset = 3) {
62
+ function guard(nameOrFn, fn) {
63
63
  const name = typeof nameOrFn === "string" ? nameOrFn : void 0;
64
64
  const evaluator = typeof nameOrFn === "function" ? nameOrFn : fn;
65
65
  if (!evaluator) {
@@ -223,11 +223,20 @@ function guard(nameOrFn, fn, _internalOffset = 3) {
223
223
  lastDeps.forEach((dep) => {
224
224
  const depName = dep._name || "unnamed";
225
225
  const isG = "state" in dep;
226
- deps.push({
227
- name: depName,
228
- type: isG ? "guard" : "source",
229
- status: isG ? dep.state().status : void 0
230
- });
226
+ if (isG) {
227
+ const depState = dep.state();
228
+ deps.push({
229
+ name: depName,
230
+ type: "guard",
231
+ status: depState.status,
232
+ reason: depState.status === "fail" ? depState.reason : void 0
233
+ });
234
+ } else {
235
+ deps.push({
236
+ name: depName,
237
+ type: "source"
238
+ });
239
+ }
231
240
  });
232
241
  return {
233
242
  name: name || "guard",
@@ -252,6 +261,13 @@ function guard(nameOrFn, fn, _internalOffset = 3) {
252
261
  evaluate2();
253
262
  return g;
254
263
  }
264
+ guard.map = function(source2, mapper, name) {
265
+ const guardName = name || `map-${source2._name || "source"}`;
266
+ return guard(guardName, () => {
267
+ const value = source2();
268
+ return mapper(value);
269
+ });
270
+ };
255
271
 
256
272
  // src/registry.ts
257
273
  var Registry = class {
@@ -259,53 +275,23 @@ var Registry = class {
259
275
  listeners = /* @__PURE__ */ new Set();
260
276
  currentGeneration = 0;
261
277
  cleanupScheduled = false;
262
- autoNameCache = /* @__PURE__ */ new Map();
263
278
  /**
264
- * Generates a stable auto-name based on source code location.
265
- * Uses file path and line number to ensure the same location always gets the same name.
266
- * Cached to avoid repeated stack trace parsing.
267
- */
268
- generateAutoName(type, offset = 3) {
269
- const err = new Error();
270
- const stack = err.stack?.split("\n") || [];
271
- let callSite = stack[offset]?.trim() || "";
272
- const cacheKey = `${type}:${callSite}`;
273
- if (this.autoNameCache.has(cacheKey)) {
274
- return this.autoNameCache.get(cacheKey);
275
- }
276
- const match = callSite.match(/([^/\\]+)\.(?:ts|tsx|js|jsx):(\d+):\d+/);
277
- let name;
278
- if (match) {
279
- const filename = match[1];
280
- const line = match[2];
281
- if (filename && line) {
282
- name = `${type}@${filename}:${line}`;
283
- } else {
284
- name = `${type}#${Math.random().toString(36).substring(2, 7)}`;
285
- }
286
- } else {
287
- name = `${type}#${Math.random().toString(36).substring(2, 7)}`;
288
- }
289
- this.autoNameCache.set(cacheKey, name);
290
- return name;
291
- }
292
- /**
293
- * Increments generation and schedules cleanup of old units.
294
- * Called automatically when HMR is detected.
279
+ * Schedules cleanup of units that weren't re-registered (deleted from code).
295
280
  */
296
281
  scheduleCleanup() {
297
282
  if (this.cleanupScheduled) return;
298
283
  this.cleanupScheduled = true;
299
- this.currentGeneration++;
300
284
  setTimeout(() => {
301
- this.cleanupOldGenerations();
285
+ this.cleanupDeadUnits();
302
286
  this.cleanupScheduled = false;
303
287
  }, 100);
304
288
  }
305
289
  /**
306
- * Removes units from old generations (likely orphaned by HMR).
290
+ * Removes units that weren't re-registered in the current generation.
291
+ * Uses mark-and-sweep: units that were re-registered have current generation,
292
+ * units that weren't are from old generation and should be removed.
307
293
  */
308
- cleanupOldGenerations() {
294
+ cleanupDeadUnits() {
309
295
  const toDelete = [];
310
296
  this.units.forEach((unit, key) => {
311
297
  const gen = unit._generation;
@@ -315,53 +301,46 @@ var Registry = class {
315
301
  });
316
302
  toDelete.forEach((key) => this.units.delete(key));
317
303
  if (toDelete.length > 0) {
318
- console.log(`[Pulse] Cleaned up ${toDelete.length} stale units after HMR`);
304
+ console.log(`[Pulse] Cleaned up ${toDelete.length} deleted units after HMR`);
319
305
  }
320
306
  }
321
307
  /**
322
- * Registers a new unit (Source or Guard).
323
- * Auto-assigns stable names to unnamed units for HMR stability.
308
+ * Registers a unit (only if it has an explicit name).
324
309
  */
325
- register(unit, offset = 3) {
310
+ register(unit) {
326
311
  const unitWithMetadata = unit;
327
- let name = unitWithMetadata._name;
312
+ const name = unitWithMetadata._name;
328
313
  if (!name) {
329
- const isGuard2 = "state" in unit;
330
- name = this.generateAutoName(isGuard2 ? "guard" : "source", offset);
331
- unitWithMetadata._name = name;
314
+ return;
332
315
  }
333
- unitWithMetadata._generation = this.currentGeneration;
334
- if (this.units.has(name)) {
316
+ const existingUnit = this.units.get(name);
317
+ if (existingUnit) {
318
+ const existingGen = existingUnit?._generation;
319
+ if (existingGen === this.currentGeneration) {
320
+ unitWithMetadata._generation = this.currentGeneration;
321
+ this.units.set(name, unit);
322
+ this.listeners.forEach((l) => l(unit));
323
+ return;
324
+ }
325
+ this.currentGeneration++;
335
326
  this.scheduleCleanup();
336
327
  }
328
+ unitWithMetadata._generation = this.currentGeneration;
337
329
  this.units.set(name, unit);
338
330
  this.listeners.forEach((l) => l(unit));
339
331
  }
340
- /**
341
- * Retrieves all registered units.
342
- */
343
332
  getAll() {
344
333
  return Array.from(this.units.values());
345
334
  }
346
- /**
347
- * Subscribes to new unit registrations.
348
- *
349
- * @param listener - Callback receiving the newly registered unit.
350
- * @returns Unsubscribe function.
351
- */
352
335
  onRegister(listener) {
353
336
  this.listeners.add(listener);
354
337
  return () => {
355
338
  this.listeners.delete(listener);
356
339
  };
357
340
  }
358
- /**
359
- * Clears all registered units.
360
- */
361
341
  reset() {
362
342
  this.units.clear();
363
343
  this.currentGeneration = 0;
364
- this.autoNameCache.clear();
365
344
  }
366
345
  };
367
346
  var GLOBAL_KEY = "__PULSE_REGISTRY__";
@@ -372,7 +351,7 @@ if (!globalSymbols[GLOBAL_KEY]) {
372
351
  var PulseRegistry = globalSymbols[GLOBAL_KEY];
373
352
 
374
353
  // src/source.ts
375
- function source(initialValue, options = {}, _internalOffset = 3) {
354
+ function source(initialValue, options = {}) {
376
355
  let value = initialValue;
377
356
  const subscribers = /* @__PURE__ */ new Set();
378
357
  const dependents = /* @__PURE__ */ new Set();
@@ -411,7 +390,7 @@ function compute(name, dependencies, processor) {
411
390
  return guard(name, () => {
412
391
  const values = dependencies.map((dep) => typeof dep === "function" ? dep() : dep);
413
392
  return processor(...values);
414
- }, 4);
393
+ });
415
394
  }
416
395
 
417
396
  // src/composition.ts
@@ -434,7 +413,7 @@ function guardAll(nameOrGuards, maybeGuards) {
434
413
  throw new Error(message);
435
414
  }
436
415
  return true;
437
- }, 4);
416
+ });
438
417
  }
439
418
  function guardAny(nameOrGuards, maybeGuards) {
440
419
  const name = typeof nameOrGuards === "string" ? nameOrGuards : void 0;
@@ -448,7 +427,7 @@ function guardAny(nameOrGuards, maybeGuards) {
448
427
  allFails.push(message);
449
428
  }
450
429
  throw new Error(allFails.length > 0 ? allFails.join(" and ") : "no conditions met");
451
- }, 4);
430
+ });
452
431
  }
453
432
  function guardNot(nameOrTarget, maybeTarget) {
454
433
  const name = typeof nameOrTarget === "string" ? nameOrTarget : void 0;
@@ -458,7 +437,7 @@ function guardNot(nameOrTarget, maybeTarget) {
458
437
  return !target.ok();
459
438
  }
460
439
  return !target();
461
- }, 4);
440
+ });
462
441
  }
463
442
  var guardExtensions = {
464
443
  all: guardAll,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-js/core",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "module": "dist/index.js",
5
5
  "main": "dist/index.cjs",
6
6
  "types": "dist/index.d.ts",