@pulse-js/core 0.1.8 → 0.2.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/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
@@ -293,35 +293,82 @@ 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;
296
298
  /**
297
- * Registers a new unit (Source or Guard).
298
- * Uses the unit's name as a key to prevent duplicates during HMR.
299
+ * Schedules cleanup of units that weren't re-registered (deleted from code).
299
300
  */
300
- register(unit) {
301
- const key = unit._name || unit;
302
- this.units.set(key, unit);
303
- this.listeners.forEach((l) => l(unit));
301
+ scheduleCleanup() {
302
+ if (this.cleanupScheduled) return;
303
+ this.cleanupScheduled = true;
304
+ setTimeout(() => {
305
+ this.cleanupDeadUnits();
306
+ this.cleanupScheduled = false;
307
+ }, 100);
304
308
  }
305
309
  /**
306
- * Retrieves all registered units.
310
+ * Removes units that weren't re-registered in the current generation.
311
+ * Uses mark-and-sweep: units that were re-registered have current generation,
312
+ * units that weren't are from old generation and should be removed.
307
313
  */
308
- getAll() {
309
- return Array.from(this.units.values());
314
+ cleanupDeadUnits() {
315
+ const toDelete = [];
316
+ this.units.forEach((unit, key) => {
317
+ const gen = unit._generation;
318
+ if (gen !== void 0 && gen < this.currentGeneration) {
319
+ toDelete.push(key);
320
+ }
321
+ });
322
+ toDelete.forEach((key) => this.units.delete(key));
323
+ if (toDelete.length > 0) {
324
+ console.log(`[Pulse] Cleaned up ${toDelete.length} deleted units after HMR`);
325
+ }
310
326
  }
311
327
  /**
312
- * Subscribes to new unit registrations.
313
- *
314
- * @param listener - Callback receiving the newly registered unit.
315
- * @returns Unsubscribe function.
328
+ * Registers a unit (only if it has an explicit name).
316
329
  */
330
+ register(unit) {
331
+ const unitWithMetadata = unit;
332
+ const name = unitWithMetadata._name;
333
+ if (!name) {
334
+ return;
335
+ }
336
+ const existingUnit = this.units.get(name);
337
+ if (existingUnit) {
338
+ const existingGen = existingUnit?._generation;
339
+ if (existingGen === this.currentGeneration) {
340
+ unitWithMetadata._generation = this.currentGeneration;
341
+ this.units.set(name, unit);
342
+ this.listeners.forEach((l) => l(unit));
343
+ return;
344
+ }
345
+ this.currentGeneration++;
346
+ this.scheduleCleanup();
347
+ }
348
+ unitWithMetadata._generation = this.currentGeneration;
349
+ this.units.set(name, unit);
350
+ this.listeners.forEach((l) => l(unit));
351
+ }
352
+ getAll() {
353
+ return Array.from(this.units.values());
354
+ }
317
355
  onRegister(listener) {
318
356
  this.listeners.add(listener);
319
357
  return () => {
320
358
  this.listeners.delete(listener);
321
359
  };
322
360
  }
361
+ reset() {
362
+ this.units.clear();
363
+ this.currentGeneration = 0;
364
+ }
323
365
  };
324
- var PulseRegistry = new Registry();
366
+ var GLOBAL_KEY = "__PULSE_REGISTRY__";
367
+ var globalSymbols = globalThis;
368
+ if (!globalSymbols[GLOBAL_KEY]) {
369
+ globalSymbols[GLOBAL_KEY] = new Registry();
370
+ }
371
+ var PulseRegistry = globalSymbols[GLOBAL_KEY];
325
372
 
326
373
  // src/source.ts
327
374
  function source(initialValue, options = {}) {
package/dist/index.d.cts CHANGED
@@ -416,28 +416,42 @@ type PulseUnit = Source<any> | Guard<any>;
416
416
  /**
417
417
  * Root Registry for Pulse.
418
418
  *
419
- * It tracks all registered Units (Sources and Guards) globally, providing
420
- * the data source for DevTools and HMR stability.
419
+ * Tracks all registered Units (Sources and Guards) globally for DevTools.
420
+ *
421
+ * **IMPORTANT**: Only units with explicit names are registered and visible in DevTools.
422
+ * Unnamed units work perfectly but are not tracked to avoid HMR instability.
423
+ *
424
+ * @example
425
+ * ```ts
426
+ * // ✅ Visible in DevTools
427
+ * const count = source(0, { name: 'count' });
428
+ *
429
+ * // ❌ Not visible in DevTools (but works fine)
430
+ * const temp = source(0);
431
+ * ```
421
432
  */
422
433
  declare class Registry {
423
434
  private units;
424
435
  private listeners;
436
+ private currentGeneration;
437
+ private cleanupScheduled;
425
438
  /**
426
- * Registers a new unit (Source or Guard).
427
- * Uses the unit's name as a key to prevent duplicates during HMR.
439
+ * Schedules cleanup of units that weren't re-registered (deleted from code).
428
440
  */
429
- register(unit: PulseUnit): void;
441
+ private scheduleCleanup;
430
442
  /**
431
- * Retrieves all registered units.
443
+ * Removes units that weren't re-registered in the current generation.
444
+ * Uses mark-and-sweep: units that were re-registered have current generation,
445
+ * units that weren't are from old generation and should be removed.
432
446
  */
433
- getAll(): PulseUnit[];
447
+ private cleanupDeadUnits;
434
448
  /**
435
- * Subscribes to new unit registrations.
436
- *
437
- * @param listener - Callback receiving the newly registered unit.
438
- * @returns Unsubscribe function.
449
+ * Registers a unit (only if it has an explicit name).
439
450
  */
451
+ register(unit: PulseUnit): void;
452
+ getAll(): PulseUnit[];
440
453
  onRegister(listener: (unit: PulseUnit) => void): () => void;
454
+ reset(): void;
441
455
  }
442
456
  declare const PulseRegistry: Registry;
443
457
 
package/dist/index.d.ts CHANGED
@@ -416,28 +416,42 @@ type PulseUnit = Source<any> | Guard<any>;
416
416
  /**
417
417
  * Root Registry for Pulse.
418
418
  *
419
- * It tracks all registered Units (Sources and Guards) globally, providing
420
- * the data source for DevTools and HMR stability.
419
+ * Tracks all registered Units (Sources and Guards) globally for DevTools.
420
+ *
421
+ * **IMPORTANT**: Only units with explicit names are registered and visible in DevTools.
422
+ * Unnamed units work perfectly but are not tracked to avoid HMR instability.
423
+ *
424
+ * @example
425
+ * ```ts
426
+ * // ✅ Visible in DevTools
427
+ * const count = source(0, { name: 'count' });
428
+ *
429
+ * // ❌ Not visible in DevTools (but works fine)
430
+ * const temp = source(0);
431
+ * ```
421
432
  */
422
433
  declare class Registry {
423
434
  private units;
424
435
  private listeners;
436
+ private currentGeneration;
437
+ private cleanupScheduled;
425
438
  /**
426
- * Registers a new unit (Source or Guard).
427
- * Uses the unit's name as a key to prevent duplicates during HMR.
439
+ * Schedules cleanup of units that weren't re-registered (deleted from code).
428
440
  */
429
- register(unit: PulseUnit): void;
441
+ private scheduleCleanup;
430
442
  /**
431
- * Retrieves all registered units.
443
+ * Removes units that weren't re-registered in the current generation.
444
+ * Uses mark-and-sweep: units that were re-registered have current generation,
445
+ * units that weren't are from old generation and should be removed.
432
446
  */
433
- getAll(): PulseUnit[];
447
+ private cleanupDeadUnits;
434
448
  /**
435
- * Subscribes to new unit registrations.
436
- *
437
- * @param listener - Callback receiving the newly registered unit.
438
- * @returns Unsubscribe function.
449
+ * Registers a unit (only if it has an explicit name).
439
450
  */
451
+ register(unit: PulseUnit): void;
452
+ getAll(): PulseUnit[];
440
453
  onRegister(listener: (unit: PulseUnit) => void): () => void;
454
+ reset(): void;
441
455
  }
442
456
  declare const PulseRegistry: Registry;
443
457
 
package/dist/index.js CHANGED
@@ -257,35 +257,82 @@ 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;
260
262
  /**
261
- * Registers a new unit (Source or Guard).
262
- * Uses the unit's name as a key to prevent duplicates during HMR.
263
+ * Schedules cleanup of units that weren't re-registered (deleted from code).
263
264
  */
264
- register(unit) {
265
- const key = unit._name || unit;
266
- this.units.set(key, unit);
267
- this.listeners.forEach((l) => l(unit));
265
+ scheduleCleanup() {
266
+ if (this.cleanupScheduled) return;
267
+ this.cleanupScheduled = true;
268
+ setTimeout(() => {
269
+ this.cleanupDeadUnits();
270
+ this.cleanupScheduled = false;
271
+ }, 100);
268
272
  }
269
273
  /**
270
- * Retrieves all registered units.
274
+ * Removes units that weren't re-registered in the current generation.
275
+ * Uses mark-and-sweep: units that were re-registered have current generation,
276
+ * units that weren't are from old generation and should be removed.
271
277
  */
272
- getAll() {
273
- return Array.from(this.units.values());
278
+ cleanupDeadUnits() {
279
+ const toDelete = [];
280
+ this.units.forEach((unit, key) => {
281
+ const gen = unit._generation;
282
+ if (gen !== void 0 && gen < this.currentGeneration) {
283
+ toDelete.push(key);
284
+ }
285
+ });
286
+ toDelete.forEach((key) => this.units.delete(key));
287
+ if (toDelete.length > 0) {
288
+ console.log(`[Pulse] Cleaned up ${toDelete.length} deleted units after HMR`);
289
+ }
274
290
  }
275
291
  /**
276
- * Subscribes to new unit registrations.
277
- *
278
- * @param listener - Callback receiving the newly registered unit.
279
- * @returns Unsubscribe function.
292
+ * Registers a unit (only if it has an explicit name).
280
293
  */
294
+ register(unit) {
295
+ const unitWithMetadata = unit;
296
+ const name = unitWithMetadata._name;
297
+ if (!name) {
298
+ return;
299
+ }
300
+ const existingUnit = this.units.get(name);
301
+ if (existingUnit) {
302
+ const existingGen = existingUnit?._generation;
303
+ if (existingGen === this.currentGeneration) {
304
+ unitWithMetadata._generation = this.currentGeneration;
305
+ this.units.set(name, unit);
306
+ this.listeners.forEach((l) => l(unit));
307
+ return;
308
+ }
309
+ this.currentGeneration++;
310
+ this.scheduleCleanup();
311
+ }
312
+ unitWithMetadata._generation = this.currentGeneration;
313
+ this.units.set(name, unit);
314
+ this.listeners.forEach((l) => l(unit));
315
+ }
316
+ getAll() {
317
+ return Array.from(this.units.values());
318
+ }
281
319
  onRegister(listener) {
282
320
  this.listeners.add(listener);
283
321
  return () => {
284
322
  this.listeners.delete(listener);
285
323
  };
286
324
  }
325
+ reset() {
326
+ this.units.clear();
327
+ this.currentGeneration = 0;
328
+ }
287
329
  };
288
- var PulseRegistry = new Registry();
330
+ var GLOBAL_KEY = "__PULSE_REGISTRY__";
331
+ var globalSymbols = globalThis;
332
+ if (!globalSymbols[GLOBAL_KEY]) {
333
+ globalSymbols[GLOBAL_KEY] = new Registry();
334
+ }
335
+ var PulseRegistry = globalSymbols[GLOBAL_KEY];
289
336
 
290
337
  // src/source.ts
291
338
  function source(initialValue, options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-js/core",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
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": {