@pulse-js/core 0.1.8 → 0.1.9

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 CHANGED
@@ -2,10 +2,14 @@
2
2
 
3
3
  <img width="200" height="200" alt="logo" src="https://raw.githubusercontent.com/ZtaMDev/Pulse/refs/heads/main/pulse.svg" />
4
4
 
5
- # Pulse
5
+ # Pulse-JS
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@pulse-js/core.svg)](https://www.npmjs.com/package/@pulse-js/core)
6
8
 
7
9
  > A semantic reactivity system for modern applications. Separate reactive data (sources) from business conditions (guards) with a declarative, composable, and observable approach.
8
10
 
11
+ Official [Documentation](https://pulse-js.vercel.app)
12
+
9
13
  Pulse differs from traditional signals or state managers by treating `Conditions` as first-class citizens. Instead of embedding complex boolean logic inside components or selectors, you define **Semantic Guards** that can be observed, composed, and debugged independently.
10
14
 
11
15
  </div>
@@ -146,37 +150,23 @@ Compare Pulse primitives:
146
150
  | **Guard** | ✅ | ✅ | ✅ | Business rules (conditioned truths). |
147
151
  | **Compute** | ❌ | ❌ | ✅ | Pure transformations (derivations). |
148
152
 
149
- ## API Reference
150
-
151
- ### `source<T>(initialValue: T, options?: SourceOptions)`
152
-
153
- Creates a reactive source.
154
-
155
- - `options.name`: Unique string name (highly recommended for debugging).
156
- - `options.equals`: Custom equality function `(prev, next) => boolean`.
157
-
158
- Methods:
159
-
160
- - `.set(value: T)`: Updates the value.
161
- - `.update(fn: (current: T) => T)`: Updates value using a transform.
162
- - `.subscribe(fn: (value: T) => void)`: Manual subscription.
163
-
164
- ### `guard<T>(name: string, evaluator: () => T | Promise<T>)`
153
+ ## Framework Integrations
165
154
 
166
- Creates a semantic guard.
155
+ Pulse provides official adapters for major frameworks to ensure seamless integration.
167
156
 
168
- Methods:
157
+ | Framework | Package | Documentation |
158
+ | :--------- | :----------------- | :----------------------------------------------------------- |
159
+ | **React** | `@pulse-js/react` | [Read Docs](https://pulse-js.vercel.app/integrations/react) |
160
+ | **Vue** | `@pulse-js/vue` | [Read Docs](https://pulse-js.vercel.app/integrations/vue) |
161
+ | **Svelte** | `@pulse-js/svelte` | [Read Docs](https://pulse-js.vercel.app/integrations/svelte) |
169
162
 
170
- - `.ok()`: Returns true if status is 'ok'.
171
- - `.fail()`: Returns true if status is 'fail'.
172
- - `.pending()`: Returns true if evaluating async.
173
- - `.reason()`: Returns the failure message.
174
- - `.state()`: Returns full `{ status, value, reason }` object.
175
- - `.subscribe(fn: (state: GuardState) => void)`: Manual subscription.
163
+ ## Developer Tools
176
164
 
177
- ---
165
+ Debug your reactive graph with **[Pulse Tools](https://pulse-js.vercel.app/guides/devtools/)**, a powerful framework-agnostic inspector.
178
166
 
179
- ## Ecosystem
167
+ ### Features
180
168
 
181
- - **@pulse-js/react**: React bindings and hooks.
182
- - **@pulse-js/tools**: Visual debugging tools.
169
+ - **Component Tree**: Visualize your entire guard dependency graph.
170
+ - **Editable Logic**: Update source values directly from the UI to test logic branches.
171
+ - **Time Travel**: (Coming Soon) Replay state changes.
172
+ - **Zero Config**: Works out of the box with `@pulse-js/tools`.
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) {
98
+ function guard(nameOrFn, fn, _internalOffset = 3) {
99
99
  const name = typeof nameOrFn === "string" ? nameOrFn : void 0;
100
100
  const evaluator = typeof nameOrFn === "function" ? nameOrFn : fn;
101
101
  if (!evaluator) {
@@ -293,13 +293,84 @@ function guard(nameOrFn, fn) {
293
293
  var Registry = class {
294
294
  units = /* @__PURE__ */ new Map();
295
295
  listeners = /* @__PURE__ */ new Set();
296
+ currentGeneration = 0;
297
+ cleanupScheduled = false;
298
+ autoNameCache = /* @__PURE__ */ new Map();
299
+ /**
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.
331
+ */
332
+ scheduleCleanup() {
333
+ if (this.cleanupScheduled) return;
334
+ this.cleanupScheduled = true;
335
+ this.currentGeneration++;
336
+ setTimeout(() => {
337
+ this.cleanupOldGenerations();
338
+ this.cleanupScheduled = false;
339
+ }, 100);
340
+ }
341
+ /**
342
+ * Removes units from old generations (likely orphaned by HMR).
343
+ */
344
+ cleanupOldGenerations() {
345
+ const toDelete = [];
346
+ this.units.forEach((unit, key) => {
347
+ const gen = unit._generation;
348
+ if (gen !== void 0 && gen < this.currentGeneration) {
349
+ toDelete.push(key);
350
+ }
351
+ });
352
+ toDelete.forEach((key) => this.units.delete(key));
353
+ if (toDelete.length > 0) {
354
+ console.log(`[Pulse] Cleaned up ${toDelete.length} stale units after HMR`);
355
+ }
356
+ }
296
357
  /**
297
358
  * Registers a new unit (Source or Guard).
298
- * Uses the unit's name as a key to prevent duplicates during HMR.
359
+ * Auto-assigns stable names to unnamed units for HMR stability.
299
360
  */
300
- register(unit) {
301
- const key = unit._name || unit;
302
- this.units.set(key, unit);
361
+ register(unit, offset = 3) {
362
+ const unitWithMetadata = unit;
363
+ let name = unitWithMetadata._name;
364
+ if (!name) {
365
+ const isGuard2 = "state" in unit;
366
+ name = this.generateAutoName(isGuard2 ? "guard" : "source", offset);
367
+ unitWithMetadata._name = name;
368
+ }
369
+ unitWithMetadata._generation = this.currentGeneration;
370
+ if (this.units.has(name)) {
371
+ this.scheduleCleanup();
372
+ }
373
+ this.units.set(name, unit);
303
374
  this.listeners.forEach((l) => l(unit));
304
375
  }
305
376
  /**
@@ -320,11 +391,24 @@ var Registry = class {
320
391
  this.listeners.delete(listener);
321
392
  };
322
393
  }
394
+ /**
395
+ * Clears all registered units.
396
+ */
397
+ reset() {
398
+ this.units.clear();
399
+ this.currentGeneration = 0;
400
+ this.autoNameCache.clear();
401
+ }
323
402
  };
324
- var PulseRegistry = new Registry();
403
+ var GLOBAL_KEY = "__PULSE_REGISTRY__";
404
+ var globalSymbols = globalThis;
405
+ if (!globalSymbols[GLOBAL_KEY]) {
406
+ globalSymbols[GLOBAL_KEY] = new Registry();
407
+ }
408
+ var PulseRegistry = globalSymbols[GLOBAL_KEY];
325
409
 
326
410
  // src/source.ts
327
- function source(initialValue, options = {}) {
411
+ function source(initialValue, options = {}, _internalOffset = 3) {
328
412
  let value = initialValue;
329
413
  const subscribers = /* @__PURE__ */ new Set();
330
414
  const dependents = /* @__PURE__ */ new Set();
@@ -363,7 +447,7 @@ function compute(name, dependencies, processor) {
363
447
  return guard(name, () => {
364
448
  const values = dependencies.map((dep) => typeof dep === "function" ? dep() : dep);
365
449
  return processor(...values);
366
- });
450
+ }, 4);
367
451
  }
368
452
 
369
453
  // src/composition.ts
@@ -386,7 +470,7 @@ function guardAll(nameOrGuards, maybeGuards) {
386
470
  throw new Error(message);
387
471
  }
388
472
  return true;
389
- });
473
+ }, 4);
390
474
  }
391
475
  function guardAny(nameOrGuards, maybeGuards) {
392
476
  const name = typeof nameOrGuards === "string" ? nameOrGuards : void 0;
@@ -400,7 +484,7 @@ function guardAny(nameOrGuards, maybeGuards) {
400
484
  allFails.push(message);
401
485
  }
402
486
  throw new Error(allFails.length > 0 ? allFails.join(" and ") : "no conditions met");
403
- });
487
+ }, 4);
404
488
  }
405
489
  function guardNot(nameOrTarget, maybeTarget) {
406
490
  const name = typeof nameOrTarget === "string" ? nameOrTarget : void 0;
@@ -410,7 +494,7 @@ function guardNot(nameOrTarget, maybeTarget) {
410
494
  return !target.ok();
411
495
  }
412
496
  return !target();
413
- });
497
+ }, 4);
414
498
  }
415
499
  var guardExtensions = {
416
500
  all: guardAll,
package/dist/index.d.cts CHANGED
@@ -186,7 +186,7 @@ declare function guardFail(reason: string | GuardReason): never;
186
186
  * Returns the value passed to it.
187
187
  */
188
188
  declare function guardOk<T>(value: T): T;
189
- declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>): Guard<T>;
189
+ declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>, _internalOffset?: number): Guard<T>;
190
190
 
191
191
  /**
192
192
  * Utility to transform reactive dependencies into a new derived value.
@@ -361,7 +361,7 @@ interface Source<T> {
361
361
  * user.set({ name: 'Bob' });
362
362
  * ```
363
363
  */
364
- declare function source<T>(initialValue: T, options?: SourceOptions<T>): Source<T>;
364
+ declare function source<T>(initialValue: T, options?: SourceOptions<T>, _internalOffset?: number): Source<T>;
365
365
 
366
366
  /**
367
367
  * Serialized state of guards for transfer from server to client.
@@ -422,11 +422,29 @@ type PulseUnit = Source<any> | Guard<any>;
422
422
  declare class Registry {
423
423
  private units;
424
424
  private listeners;
425
+ private currentGeneration;
426
+ 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
+ /**
435
+ * Increments generation and schedules cleanup of old units.
436
+ * Called automatically when HMR is detected.
437
+ */
438
+ private scheduleCleanup;
439
+ /**
440
+ * Removes units from old generations (likely orphaned by HMR).
441
+ */
442
+ private cleanupOldGenerations;
425
443
  /**
426
444
  * Registers a new unit (Source or Guard).
427
- * Uses the unit's name as a key to prevent duplicates during HMR.
445
+ * Auto-assigns stable names to unnamed units for HMR stability.
428
446
  */
429
- register(unit: PulseUnit): void;
447
+ register(unit: PulseUnit, offset?: number): void;
430
448
  /**
431
449
  * Retrieves all registered units.
432
450
  */
@@ -438,6 +456,10 @@ declare class Registry {
438
456
  * @returns Unsubscribe function.
439
457
  */
440
458
  onRegister(listener: (unit: PulseUnit) => void): () => void;
459
+ /**
460
+ * Clears all registered units.
461
+ */
462
+ reset(): void;
441
463
  }
442
464
  declare const PulseRegistry: Registry;
443
465
 
package/dist/index.d.ts CHANGED
@@ -186,7 +186,7 @@ declare function guardFail(reason: string | GuardReason): never;
186
186
  * Returns the value passed to it.
187
187
  */
188
188
  declare function guardOk<T>(value: T): T;
189
- declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>): Guard<T>;
189
+ declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>, _internalOffset?: number): Guard<T>;
190
190
 
191
191
  /**
192
192
  * Utility to transform reactive dependencies into a new derived value.
@@ -361,7 +361,7 @@ interface Source<T> {
361
361
  * user.set({ name: 'Bob' });
362
362
  * ```
363
363
  */
364
- declare function source<T>(initialValue: T, options?: SourceOptions<T>): Source<T>;
364
+ declare function source<T>(initialValue: T, options?: SourceOptions<T>, _internalOffset?: number): Source<T>;
365
365
 
366
366
  /**
367
367
  * Serialized state of guards for transfer from server to client.
@@ -422,11 +422,29 @@ type PulseUnit = Source<any> | Guard<any>;
422
422
  declare class Registry {
423
423
  private units;
424
424
  private listeners;
425
+ private currentGeneration;
426
+ 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
+ /**
435
+ * Increments generation and schedules cleanup of old units.
436
+ * Called automatically when HMR is detected.
437
+ */
438
+ private scheduleCleanup;
439
+ /**
440
+ * Removes units from old generations (likely orphaned by HMR).
441
+ */
442
+ private cleanupOldGenerations;
425
443
  /**
426
444
  * Registers a new unit (Source or Guard).
427
- * Uses the unit's name as a key to prevent duplicates during HMR.
445
+ * Auto-assigns stable names to unnamed units for HMR stability.
428
446
  */
429
- register(unit: PulseUnit): void;
447
+ register(unit: PulseUnit, offset?: number): void;
430
448
  /**
431
449
  * Retrieves all registered units.
432
450
  */
@@ -438,6 +456,10 @@ declare class Registry {
438
456
  * @returns Unsubscribe function.
439
457
  */
440
458
  onRegister(listener: (unit: PulseUnit) => void): () => void;
459
+ /**
460
+ * Clears all registered units.
461
+ */
462
+ reset(): void;
441
463
  }
442
464
  declare const PulseRegistry: Registry;
443
465
 
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) {
62
+ function guard(nameOrFn, fn, _internalOffset = 3) {
63
63
  const name = typeof nameOrFn === "string" ? nameOrFn : void 0;
64
64
  const evaluator = typeof nameOrFn === "function" ? nameOrFn : fn;
65
65
  if (!evaluator) {
@@ -257,13 +257,84 @@ function guard(nameOrFn, fn) {
257
257
  var Registry = class {
258
258
  units = /* @__PURE__ */ new Map();
259
259
  listeners = /* @__PURE__ */ new Set();
260
+ currentGeneration = 0;
261
+ cleanupScheduled = false;
262
+ autoNameCache = /* @__PURE__ */ new Map();
263
+ /**
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.
295
+ */
296
+ scheduleCleanup() {
297
+ if (this.cleanupScheduled) return;
298
+ this.cleanupScheduled = true;
299
+ this.currentGeneration++;
300
+ setTimeout(() => {
301
+ this.cleanupOldGenerations();
302
+ this.cleanupScheduled = false;
303
+ }, 100);
304
+ }
305
+ /**
306
+ * Removes units from old generations (likely orphaned by HMR).
307
+ */
308
+ cleanupOldGenerations() {
309
+ const toDelete = [];
310
+ this.units.forEach((unit, key) => {
311
+ const gen = unit._generation;
312
+ if (gen !== void 0 && gen < this.currentGeneration) {
313
+ toDelete.push(key);
314
+ }
315
+ });
316
+ toDelete.forEach((key) => this.units.delete(key));
317
+ if (toDelete.length > 0) {
318
+ console.log(`[Pulse] Cleaned up ${toDelete.length} stale units after HMR`);
319
+ }
320
+ }
260
321
  /**
261
322
  * Registers a new unit (Source or Guard).
262
- * Uses the unit's name as a key to prevent duplicates during HMR.
323
+ * Auto-assigns stable names to unnamed units for HMR stability.
263
324
  */
264
- register(unit) {
265
- const key = unit._name || unit;
266
- this.units.set(key, unit);
325
+ register(unit, offset = 3) {
326
+ const unitWithMetadata = unit;
327
+ let name = unitWithMetadata._name;
328
+ if (!name) {
329
+ const isGuard2 = "state" in unit;
330
+ name = this.generateAutoName(isGuard2 ? "guard" : "source", offset);
331
+ unitWithMetadata._name = name;
332
+ }
333
+ unitWithMetadata._generation = this.currentGeneration;
334
+ if (this.units.has(name)) {
335
+ this.scheduleCleanup();
336
+ }
337
+ this.units.set(name, unit);
267
338
  this.listeners.forEach((l) => l(unit));
268
339
  }
269
340
  /**
@@ -284,11 +355,24 @@ var Registry = class {
284
355
  this.listeners.delete(listener);
285
356
  };
286
357
  }
358
+ /**
359
+ * Clears all registered units.
360
+ */
361
+ reset() {
362
+ this.units.clear();
363
+ this.currentGeneration = 0;
364
+ this.autoNameCache.clear();
365
+ }
287
366
  };
288
- var PulseRegistry = new Registry();
367
+ var GLOBAL_KEY = "__PULSE_REGISTRY__";
368
+ var globalSymbols = globalThis;
369
+ if (!globalSymbols[GLOBAL_KEY]) {
370
+ globalSymbols[GLOBAL_KEY] = new Registry();
371
+ }
372
+ var PulseRegistry = globalSymbols[GLOBAL_KEY];
289
373
 
290
374
  // src/source.ts
291
- function source(initialValue, options = {}) {
375
+ function source(initialValue, options = {}, _internalOffset = 3) {
292
376
  let value = initialValue;
293
377
  const subscribers = /* @__PURE__ */ new Set();
294
378
  const dependents = /* @__PURE__ */ new Set();
@@ -327,7 +411,7 @@ function compute(name, dependencies, processor) {
327
411
  return guard(name, () => {
328
412
  const values = dependencies.map((dep) => typeof dep === "function" ? dep() : dep);
329
413
  return processor(...values);
330
- });
414
+ }, 4);
331
415
  }
332
416
 
333
417
  // src/composition.ts
@@ -350,7 +434,7 @@ function guardAll(nameOrGuards, maybeGuards) {
350
434
  throw new Error(message);
351
435
  }
352
436
  return true;
353
- });
437
+ }, 4);
354
438
  }
355
439
  function guardAny(nameOrGuards, maybeGuards) {
356
440
  const name = typeof nameOrGuards === "string" ? nameOrGuards : void 0;
@@ -364,7 +448,7 @@ function guardAny(nameOrGuards, maybeGuards) {
364
448
  allFails.push(message);
365
449
  }
366
450
  throw new Error(allFails.length > 0 ? allFails.join(" and ") : "no conditions met");
367
- });
451
+ }, 4);
368
452
  }
369
453
  function guardNot(nameOrTarget, maybeTarget) {
370
454
  const name = typeof nameOrTarget === "string" ? nameOrTarget : void 0;
@@ -374,7 +458,7 @@ function guardNot(nameOrTarget, maybeTarget) {
374
458
  return !target.ok();
375
459
  }
376
460
  return !target();
377
- });
461
+ }, 4);
378
462
  }
379
463
  var guardExtensions = {
380
464
  all: guardAll,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-js/core",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "module": "dist/index.js",
5
5
  "main": "dist/index.cjs",
6
6
  "types": "dist/index.d.ts",
@@ -18,7 +18,8 @@
18
18
  "description": "A semantic reactivity system for modern applications. Separate reactive data (sources) from business conditions (guards) with a declarative, composable, and observable approach.",
19
19
  "workspaces": [
20
20
  "packages/*",
21
- "tests"
21
+ "tests",
22
+ "tests/*"
22
23
  ],
23
24
  "keywords": [
24
25
  "reactivity",
@@ -37,7 +38,7 @@
37
38
  "directory": "packages/core"
38
39
  },
39
40
  "bugs": {
40
- "url": "https://github.com/ZtaMDev/pulse/issues"
41
+ "url": "https://github.com/ZtaMDev/pulse-js/issues"
41
42
  },
42
43
  "license": "MIT",
43
44
  "scripts": {