@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 +19 -29
- package/dist/index.cjs +61 -14
- package/dist/index.d.cts +25 -11
- package/dist/index.d.ts +25 -11
- package/dist/index.js +61 -14
- package/package.json +4 -3
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
|
+
[](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
|
-
##
|
|
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
|
-
|
|
155
|
+
Pulse provides official adapters for major frameworks to ensure seamless integration.
|
|
167
156
|
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
+
### Features
|
|
180
168
|
|
|
181
|
-
-
|
|
182
|
-
-
|
|
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
|
-
*
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
this.
|
|
303
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
309
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
420
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
441
|
+
private scheduleCleanup;
|
|
430
442
|
/**
|
|
431
|
-
*
|
|
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
|
-
|
|
447
|
+
private cleanupDeadUnits;
|
|
434
448
|
/**
|
|
435
|
-
*
|
|
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
|
-
*
|
|
420
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
441
|
+
private scheduleCleanup;
|
|
430
442
|
/**
|
|
431
|
-
*
|
|
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
|
-
|
|
447
|
+
private cleanupDeadUnits;
|
|
434
448
|
/**
|
|
435
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
this.
|
|
267
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
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": {
|