@pulse-js/core 0.1.7 → 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,21 +2,15 @@
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
6
 
7
- > A semantic reactivity system for modern applications. Separate reactive data (sources) from business conditions (guards) with a declarative, composable, and observable approach.
8
-
9
- 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.
7
+ [![npm version](https://img.shields.io/npm/v/@pulse-js/core.svg)](https://www.npmjs.com/package/@pulse-js/core)
10
8
 
11
- ### Mental Model
9
+ > A semantic reactivity system for modern applications. Separate reactive data (sources) from business conditions (guards) with a declarative, composable, and observable approach.
12
10
 
13
- Compare Pulse primitives at a glance:
11
+ Official [Documentation](https://pulse-js.vercel.app)
14
12
 
15
- | Concept | Can be async | Has state | Observable | Purpose |
16
- | :---------- | :----------: | :-------: | :--------: | :----------------------------------- |
17
- | **Source** | ❌ | ❌ | ✅ | Reactive data (facts). |
18
- | **Guard** | ✅ | ✅ | ✅ | Business rules (conditioned truths). |
19
- | **Compute** | ❌ | ❌ | ✅ | Pure transformations (derivations). |
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.
20
14
 
21
15
  </div>
22
16
 
@@ -146,37 +140,33 @@ import { hydrate } from "@pulse-js/core";
146
140
  hydrate(window.__PULSE_STATE__);
147
141
  ```
148
142
 
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`.
143
+ ### Mental Model
157
144
 
158
- Methods:
145
+ Compare Pulse primitives:
159
146
 
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.
147
+ | Concept | Can be async | Has state | Observable | Purpose |
148
+ | :---------- | :----------: | :-------: | :--------: | :----------------------------------- |
149
+ | **Source** | ❌ | ❌ | ✅ | Reactive data (facts). |
150
+ | **Guard** | ✅ | ✅ | ✅ | Business rules (conditioned truths). |
151
+ | **Compute** | ❌ | ❌ | ✅ | Pure transformations (derivations). |
163
152
 
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
@@ -25,6 +25,8 @@ __export(index_exports, {
25
25
  evaluate: () => evaluate,
26
26
  getCurrentGuard: () => getCurrentGuard,
27
27
  guard: () => extendedGuard,
28
+ guardFail: () => guardFail,
29
+ guardOk: () => guardOk,
28
30
  hydrate: () => hydrate,
29
31
  registerGuardForHydration: () => registerGuardForHydration,
30
32
  runInContext: () => runInContext,
@@ -33,18 +35,20 @@ __export(index_exports, {
33
35
  module.exports = __toCommonJS(index_exports);
34
36
 
35
37
  // src/tracking.ts
36
- var activeGuard = null;
38
+ var guardStack = [];
37
39
  function runInContext(guard2, fn) {
38
- const prev = activeGuard;
39
- activeGuard = guard2;
40
+ if (guardStack.includes(guard2)) {
41
+ throw new Error(`Cyclic guard dependency detected: ${guard2._name || "unnamed guard"}`);
42
+ }
43
+ guardStack.push(guard2);
40
44
  try {
41
45
  return fn();
42
46
  } finally {
43
- activeGuard = prev;
47
+ guardStack.pop();
44
48
  }
45
49
  }
46
50
  function getCurrentGuard() {
47
- return activeGuard;
51
+ return guardStack[guardStack.length - 1] ?? null;
48
52
  }
49
53
 
50
54
  // src/ssr.ts
@@ -77,7 +81,21 @@ function hydrate(state) {
77
81
  }
78
82
 
79
83
  // src/guard.ts
80
- function guard(nameOrFn, fn) {
84
+ function guardFail(reason) {
85
+ const guardReason = typeof reason === "string" ? {
86
+ code: "GUARD_FAIL",
87
+ message: reason,
88
+ toString: () => reason
89
+ } : reason;
90
+ const err = new Error(guardReason.message);
91
+ err._pulseFail = true;
92
+ err._reason = guardReason;
93
+ throw err;
94
+ }
95
+ function guardOk(value) {
96
+ return value;
97
+ }
98
+ function guard(nameOrFn, fn, _internalOffset = 3) {
81
99
  const name = typeof nameOrFn === "string" ? nameOrFn : void 0;
82
100
  const evaluator = typeof nameOrFn === "function" ? nameOrFn : fn;
83
101
  if (!evaluator) {
@@ -87,10 +105,11 @@ function guard(nameOrFn, fn) {
87
105
  const dependents = /* @__PURE__ */ new Set();
88
106
  const subscribers = /* @__PURE__ */ new Set();
89
107
  let evaluationId = 0;
90
- const dependencies = /* @__PURE__ */ new Set();
108
+ const currentDeps = /* @__PURE__ */ new Set();
109
+ let lastDeps = /* @__PURE__ */ new Set();
91
110
  const node = {
92
111
  addDependency(trackable) {
93
- dependencies.add(trackable);
112
+ currentDeps.add(trackable);
94
113
  },
95
114
  notify() {
96
115
  evaluate2();
@@ -100,56 +119,111 @@ function guard(nameOrFn, fn) {
100
119
  const currentId = ++evaluationId;
101
120
  const oldStatus = state.status;
102
121
  const oldValue = state.value;
103
- dependencies.clear();
122
+ currentDeps.clear();
104
123
  try {
105
- const result = runInContext(node, () => evaluator());
106
- if (result instanceof Promise) {
107
- if (state.status !== "pending") {
108
- state = { ...state, status: "pending", updatedAt: Date.now() };
109
- notifyDependents();
124
+ runInContext(node, () => {
125
+ node.isEvaluating = true;
126
+ let result;
127
+ try {
128
+ result = _evaluator();
129
+ } finally {
130
+ node.isEvaluating = false;
110
131
  }
111
- result.then((resolved) => {
112
- if (currentId === evaluationId) {
113
- if (resolved === false) {
114
- state = { status: "fail", reason: name ? `${name} failed` : "condition failed", updatedAt: Date.now() };
115
- } else {
116
- state = { status: "ok", value: resolved, updatedAt: Date.now() };
117
- }
132
+ if (result instanceof Promise) {
133
+ if (state.status !== "pending") {
134
+ state = { ...state, status: "pending", updatedAt: Date.now() };
118
135
  notifyDependents();
119
136
  }
120
- }).catch((err) => {
121
- if (currentId === evaluationId) {
122
- state = { status: "fail", reason: err instanceof Error ? err.message : String(err), updatedAt: Date.now() };
137
+ result.then((resolved) => {
138
+ if (currentId === evaluationId) {
139
+ persistDependencies();
140
+ if (resolved === false) {
141
+ const reason = name ? `${name} failed` : "condition failed";
142
+ state = { status: "fail", reason, lastReason: reason, updatedAt: Date.now() };
143
+ } else if (resolved === void 0) {
144
+ state = { ...state, status: "pending", updatedAt: Date.now() };
145
+ } else {
146
+ state = { status: "ok", value: resolved, updatedAt: Date.now() };
147
+ }
148
+ notifyDependents();
149
+ }
150
+ }).catch((err) => {
151
+ if (currentId === evaluationId) {
152
+ persistDependencies();
153
+ const message = err instanceof Error ? err.message : String(err);
154
+ const reason = err.meta ? {
155
+ code: err.code || "ERROR",
156
+ message,
157
+ meta: err.meta,
158
+ toString: () => message
159
+ } : message;
160
+ state = {
161
+ status: "fail",
162
+ reason,
163
+ lastReason: state.reason || reason,
164
+ updatedAt: Date.now()
165
+ };
166
+ notifyDependents();
167
+ }
168
+ });
169
+ } else {
170
+ persistDependencies();
171
+ if (result === false) {
172
+ const reason = name ? `${name} failed` : "condition failed";
173
+ state = { status: "fail", reason, lastReason: reason, updatedAt: Date.now() };
174
+ } else if (result === void 0) {
175
+ state = { ...state, status: "pending", updatedAt: Date.now() };
176
+ } else {
177
+ state = { status: "ok", value: result, updatedAt: Date.now() };
178
+ }
179
+ if (oldStatus !== state.status || oldValue !== state.value) {
123
180
  notifyDependents();
124
181
  }
125
- });
126
- } else {
127
- if (result === false) {
128
- state = { status: "fail", reason: name ? `${name} failed` : "condition failed", updatedAt: Date.now() };
129
- } else {
130
- state = { status: "ok", value: result, updatedAt: Date.now() };
131
- }
132
- if (oldStatus !== state.status || oldValue !== state.value) {
133
- notifyDependents();
134
182
  }
135
- }
183
+ });
136
184
  } catch (err) {
137
- state = { status: "fail", reason: err instanceof Error ? err.message : String(err), updatedAt: Date.now() };
185
+ node.isEvaluating = false;
186
+ persistDependencies();
187
+ if (err._pulseFail) {
188
+ state = {
189
+ status: "fail",
190
+ reason: err._reason,
191
+ lastReason: err._reason,
192
+ updatedAt: Date.now()
193
+ };
194
+ } else {
195
+ const message = err instanceof Error ? err.message : String(err);
196
+ const reason = err.meta ? {
197
+ code: err.code || "ERROR",
198
+ message,
199
+ meta: err.meta,
200
+ toString: () => message
201
+ } : message;
202
+ state = {
203
+ status: "fail",
204
+ reason,
205
+ lastReason: reason,
206
+ updatedAt: Date.now()
207
+ };
208
+ }
138
209
  notifyDependents();
139
210
  }
140
211
  };
212
+ const _evaluator = () => evaluator();
213
+ const persistDependencies = () => {
214
+ lastDeps = new Set(currentDeps);
215
+ };
141
216
  const notifyDependents = () => {
142
217
  const deps = Array.from(dependents);
143
218
  dependents.clear();
144
219
  deps.forEach((dep) => dep.notify());
145
220
  subscribers.forEach((sub) => sub({ ...state }));
146
221
  };
147
- evaluate2();
148
222
  const track = () => {
149
- const activeGuard2 = getCurrentGuard();
150
- if (activeGuard2 && activeGuard2 !== node) {
151
- dependents.add(activeGuard2);
152
- activeGuard2.addDependency(g);
223
+ const activeGuard = getCurrentGuard();
224
+ if (activeGuard && activeGuard !== node) {
225
+ dependents.add(activeGuard);
226
+ activeGuard.addDependency(g);
153
227
  }
154
228
  };
155
229
  const g = (() => {
@@ -170,7 +244,7 @@ function guard(nameOrFn, fn) {
170
244
  };
171
245
  g.reason = () => {
172
246
  track();
173
- return state.reason;
247
+ return state.reason || state.lastReason;
174
248
  };
175
249
  g.state = () => {
176
250
  track();
@@ -182,7 +256,7 @@ function guard(nameOrFn, fn) {
182
256
  };
183
257
  g.explain = () => {
184
258
  const deps = [];
185
- dependencies.forEach((dep) => {
259
+ lastDeps.forEach((dep) => {
186
260
  const depName = dep._name || "unnamed";
187
261
  const isG = "state" in dep;
188
262
  deps.push({
@@ -195,6 +269,7 @@ function guard(nameOrFn, fn) {
195
269
  name: name || "guard",
196
270
  status: state.status,
197
271
  reason: state.reason,
272
+ lastReason: state.lastReason,
198
273
  value: state.value,
199
274
  dependencies: deps
200
275
  };
@@ -210,6 +285,7 @@ function guard(nameOrFn, fn) {
210
285
  registerGuardForHydration(name, g);
211
286
  }
212
287
  PulseRegistry.register(g);
288
+ evaluate2();
213
289
  return g;
214
290
  }
215
291
 
@@ -217,13 +293,84 @@ function guard(nameOrFn, fn) {
217
293
  var Registry = class {
218
294
  units = /* @__PURE__ */ new Map();
219
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
+ }
220
357
  /**
221
358
  * Registers a new unit (Source or Guard).
222
- * Uses the unit's name as a key to prevent duplicates during HMR.
359
+ * Auto-assigns stable names to unnamed units for HMR stability.
223
360
  */
224
- register(unit) {
225
- const key = unit._name || unit;
226
- 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);
227
374
  this.listeners.forEach((l) => l(unit));
228
375
  }
229
376
  /**
@@ -244,19 +391,32 @@ var Registry = class {
244
391
  this.listeners.delete(listener);
245
392
  };
246
393
  }
394
+ /**
395
+ * Clears all registered units.
396
+ */
397
+ reset() {
398
+ this.units.clear();
399
+ this.currentGeneration = 0;
400
+ this.autoNameCache.clear();
401
+ }
247
402
  };
248
- 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];
249
409
 
250
410
  // src/source.ts
251
- function source(initialValue, options = {}) {
411
+ function source(initialValue, options = {}, _internalOffset = 3) {
252
412
  let value = initialValue;
253
413
  const subscribers = /* @__PURE__ */ new Set();
254
414
  const dependents = /* @__PURE__ */ new Set();
255
415
  const s = (() => {
256
- const activeGuard2 = getCurrentGuard();
257
- if (activeGuard2) {
258
- dependents.add(activeGuard2);
259
- activeGuard2.addDependency(s);
416
+ const activeGuard = getCurrentGuard();
417
+ if (activeGuard) {
418
+ dependents.add(activeGuard);
419
+ activeGuard.addDependency(s);
260
420
  }
261
421
  return value;
262
422
  });
@@ -287,7 +447,7 @@ function compute(name, dependencies, processor) {
287
447
  return guard(name, () => {
288
448
  const values = dependencies.map((dep) => typeof dep === "function" ? dep() : dep);
289
449
  return processor(...values);
290
- });
450
+ }, 4);
291
451
  }
292
452
 
293
453
  // src/composition.ts
@@ -305,10 +465,12 @@ function guardAll(nameOrGuards, maybeGuards) {
305
465
  }
306
466
  }
307
467
  if (firstFail) {
308
- throw new Error(firstFail.reason() || "condition failed");
468
+ const reason = firstFail.reason();
469
+ const message = typeof reason === "string" ? reason : reason?.toString() || "condition failed";
470
+ throw new Error(message);
309
471
  }
310
472
  return true;
311
- });
473
+ }, 4);
312
474
  }
313
475
  function guardAny(nameOrGuards, maybeGuards) {
314
476
  const name = typeof nameOrGuards === "string" ? nameOrGuards : void 0;
@@ -317,10 +479,12 @@ function guardAny(nameOrGuards, maybeGuards) {
317
479
  let allFails = [];
318
480
  for (const g of guards) {
319
481
  if (g.ok()) return true;
320
- allFails.push(g.reason() || "failed");
482
+ const reason = g.reason();
483
+ const message = typeof reason === "string" ? reason : reason?.toString() || "failed";
484
+ allFails.push(message);
321
485
  }
322
486
  throw new Error(allFails.length > 0 ? allFails.join(" and ") : "no conditions met");
323
- });
487
+ }, 4);
324
488
  }
325
489
  function guardNot(nameOrTarget, maybeTarget) {
326
490
  const name = typeof nameOrTarget === "string" ? nameOrTarget : void 0;
@@ -330,7 +494,7 @@ function guardNot(nameOrTarget, maybeTarget) {
330
494
  return !target.ok();
331
495
  }
332
496
  return !target();
333
- });
497
+ }, 4);
334
498
  }
335
499
  var guardExtensions = {
336
500
  all: guardAll,
@@ -348,6 +512,8 @@ var extendedGuard = Object.assign(guard, guardExtensions);
348
512
  evaluate,
349
513
  getCurrentGuard,
350
514
  guard,
515
+ guardFail,
516
+ guardOk,
351
517
  hydrate,
352
518
  registerGuardForHydration,
353
519
  runInContext,
package/dist/index.d.cts CHANGED
@@ -19,6 +19,8 @@ interface GuardNode extends Trackable {
19
19
  * Internal use only.
20
20
  */
21
21
  addDependency(trackable: any): void;
22
+ /** Whether the guard is currently evaluating. */
23
+ isEvaluating?: boolean;
22
24
  }
23
25
  /**
24
26
  * Executes a function within the context of a specific Guard.
@@ -28,6 +30,7 @@ interface GuardNode extends Trackable {
28
30
  * @param guard The guard node to set as active.
29
31
  * @param fn The function to execute.
30
32
  * @returns The result of the function.
33
+ * @throws Error if a cyclic dependency is detected.
31
34
  */
32
35
  declare function runInContext<T>(guard: GuardNode, fn: () => T): T;
33
36
  /**
@@ -41,10 +44,20 @@ declare function getCurrentGuard(): GuardNode | null;
41
44
  /**
42
45
  * Status of a Pulse Guard evaluation.
43
46
  * - 'pending': Async evaluation is in progress.
44
- * - 'ok': Evaluation completed successfully (return value was not `false`).
45
- * - 'fail': Evaluation encountered an error or return value was `false`.
47
+ * - 'ok': Evaluation completed successfully.
48
+ * - 'fail': Evaluation encountered an error or return value was explicitly `false`.
46
49
  */
47
50
  type GuardStatus = 'ok' | 'fail' | 'pending';
51
+ /**
52
+ * Structured reason for a guard failure.
53
+ * Includes toString() so it can be rendered directly in React and other UI frameworks.
54
+ */
55
+ interface GuardReason {
56
+ code: string;
57
+ message: string;
58
+ meta?: any;
59
+ toString(): string;
60
+ }
48
61
  /**
49
62
  * The internal state of a Pulse Guard.
50
63
  */
@@ -53,8 +66,10 @@ interface GuardState<T> {
53
66
  status: GuardStatus;
54
67
  /** The value returned by the evaluator (only if status is 'ok'). */
55
68
  value?: T;
56
- /** The message explaining why the guard failed (only if status is 'fail'). */
57
- reason?: string;
69
+ /** The reason why the guard failed. */
70
+ reason?: string | GuardReason;
71
+ /** The last known failure reason, persisted even during 'pending'. */
72
+ lastReason?: string | GuardReason;
58
73
  /** The timestamp when the status last changed. */
59
74
  updatedAt?: number;
60
75
  }
@@ -64,7 +79,8 @@ interface GuardState<T> {
64
79
  interface GuardExplanation {
65
80
  name: string;
66
81
  status: GuardStatus;
67
- reason?: string;
82
+ reason?: string | GuardReason;
83
+ lastReason?: string | GuardReason;
68
84
  value?: any;
69
85
  dependencies: Array<{
70
86
  name: string;
@@ -110,7 +126,7 @@ interface Guard<T = boolean> {
110
126
  *
111
127
  * @returns The error message or undefined.
112
128
  */
113
- reason(): string | undefined;
129
+ reason(): string | GuardReason | undefined;
114
130
  /**
115
131
  * Returns a snapshot of the full internal state of the guard.
116
132
  * Useful for adapters (like React) to synchronize with the guard.
@@ -127,12 +143,10 @@ interface Guard<T = boolean> {
127
143
  subscribe(listener: Subscriber<GuardState<T>>): () => void;
128
144
  /**
129
145
  * Returns a structured explanation of the guard's state and its direct dependencies.
130
- * Useful for DevTools and sophisticated error reporting.
131
146
  */
132
147
  explain(): GuardExplanation;
133
148
  /**
134
149
  * Manually forces a re-evaluation of the guard.
135
- * Typically used internally by Pulse or for debugging.
136
150
  * @internal
137
151
  */
138
152
  _evaluate(): void;
@@ -160,7 +174,19 @@ interface Guard<T = boolean> {
160
174
  * });
161
175
  * ```
162
176
  */
163
- declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>): Guard<T>;
177
+ /**
178
+ * Signals that a Guard should fail with a specific reason.
179
+ *
180
+ * @param reason - The reason for failure.
181
+ * @throws An internal signal error caught by the Guard evaluator.
182
+ */
183
+ declare function guardFail(reason: string | GuardReason): never;
184
+ /**
185
+ * Explicitly signals a successful Guard evaluation.
186
+ * Returns the value passed to it.
187
+ */
188
+ 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>;
164
190
 
165
191
  /**
166
192
  * Utility to transform reactive dependencies into a new derived value.
@@ -335,7 +361,7 @@ interface Source<T> {
335
361
  * user.set({ name: 'Bob' });
336
362
  * ```
337
363
  */
338
- 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>;
339
365
 
340
366
  /**
341
367
  * Serialized state of guards for transfer from server to client.
@@ -396,11 +422,29 @@ type PulseUnit = Source<any> | Guard<any>;
396
422
  declare class Registry {
397
423
  private units;
398
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;
399
443
  /**
400
444
  * Registers a new unit (Source or Guard).
401
- * Uses the unit's name as a key to prevent duplicates during HMR.
445
+ * Auto-assigns stable names to unnamed units for HMR stability.
402
446
  */
403
- register(unit: PulseUnit): void;
447
+ register(unit: PulseUnit, offset?: number): void;
404
448
  /**
405
449
  * Retrieves all registered units.
406
450
  */
@@ -412,6 +456,10 @@ declare class Registry {
412
456
  * @returns Unsubscribe function.
413
457
  */
414
458
  onRegister(listener: (unit: PulseUnit) => void): () => void;
459
+ /**
460
+ * Clears all registered units.
461
+ */
462
+ reset(): void;
415
463
  }
416
464
  declare const PulseRegistry: Registry;
417
465
 
@@ -434,4 +482,4 @@ declare const extendedGuard: typeof guard & {
434
482
  compute: typeof compute;
435
483
  };
436
484
 
437
- export { type Guard, type GuardExplanation, type GuardNode, type GuardState, type GuardStatus, type HydrationState, PulseRegistry, type PulseUnit, type Source, type SourceOptions, type Subscriber, type Trackable, compute, evaluate, getCurrentGuard, extendedGuard as guard, hydrate, registerGuardForHydration, runInContext, source };
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 };
package/dist/index.d.ts CHANGED
@@ -19,6 +19,8 @@ interface GuardNode extends Trackable {
19
19
  * Internal use only.
20
20
  */
21
21
  addDependency(trackable: any): void;
22
+ /** Whether the guard is currently evaluating. */
23
+ isEvaluating?: boolean;
22
24
  }
23
25
  /**
24
26
  * Executes a function within the context of a specific Guard.
@@ -28,6 +30,7 @@ interface GuardNode extends Trackable {
28
30
  * @param guard The guard node to set as active.
29
31
  * @param fn The function to execute.
30
32
  * @returns The result of the function.
33
+ * @throws Error if a cyclic dependency is detected.
31
34
  */
32
35
  declare function runInContext<T>(guard: GuardNode, fn: () => T): T;
33
36
  /**
@@ -41,10 +44,20 @@ declare function getCurrentGuard(): GuardNode | null;
41
44
  /**
42
45
  * Status of a Pulse Guard evaluation.
43
46
  * - 'pending': Async evaluation is in progress.
44
- * - 'ok': Evaluation completed successfully (return value was not `false`).
45
- * - 'fail': Evaluation encountered an error or return value was `false`.
47
+ * - 'ok': Evaluation completed successfully.
48
+ * - 'fail': Evaluation encountered an error or return value was explicitly `false`.
46
49
  */
47
50
  type GuardStatus = 'ok' | 'fail' | 'pending';
51
+ /**
52
+ * Structured reason for a guard failure.
53
+ * Includes toString() so it can be rendered directly in React and other UI frameworks.
54
+ */
55
+ interface GuardReason {
56
+ code: string;
57
+ message: string;
58
+ meta?: any;
59
+ toString(): string;
60
+ }
48
61
  /**
49
62
  * The internal state of a Pulse Guard.
50
63
  */
@@ -53,8 +66,10 @@ interface GuardState<T> {
53
66
  status: GuardStatus;
54
67
  /** The value returned by the evaluator (only if status is 'ok'). */
55
68
  value?: T;
56
- /** The message explaining why the guard failed (only if status is 'fail'). */
57
- reason?: string;
69
+ /** The reason why the guard failed. */
70
+ reason?: string | GuardReason;
71
+ /** The last known failure reason, persisted even during 'pending'. */
72
+ lastReason?: string | GuardReason;
58
73
  /** The timestamp when the status last changed. */
59
74
  updatedAt?: number;
60
75
  }
@@ -64,7 +79,8 @@ interface GuardState<T> {
64
79
  interface GuardExplanation {
65
80
  name: string;
66
81
  status: GuardStatus;
67
- reason?: string;
82
+ reason?: string | GuardReason;
83
+ lastReason?: string | GuardReason;
68
84
  value?: any;
69
85
  dependencies: Array<{
70
86
  name: string;
@@ -110,7 +126,7 @@ interface Guard<T = boolean> {
110
126
  *
111
127
  * @returns The error message or undefined.
112
128
  */
113
- reason(): string | undefined;
129
+ reason(): string | GuardReason | undefined;
114
130
  /**
115
131
  * Returns a snapshot of the full internal state of the guard.
116
132
  * Useful for adapters (like React) to synchronize with the guard.
@@ -127,12 +143,10 @@ interface Guard<T = boolean> {
127
143
  subscribe(listener: Subscriber<GuardState<T>>): () => void;
128
144
  /**
129
145
  * Returns a structured explanation of the guard's state and its direct dependencies.
130
- * Useful for DevTools and sophisticated error reporting.
131
146
  */
132
147
  explain(): GuardExplanation;
133
148
  /**
134
149
  * Manually forces a re-evaluation of the guard.
135
- * Typically used internally by Pulse or for debugging.
136
150
  * @internal
137
151
  */
138
152
  _evaluate(): void;
@@ -160,7 +174,19 @@ interface Guard<T = boolean> {
160
174
  * });
161
175
  * ```
162
176
  */
163
- declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>): Guard<T>;
177
+ /**
178
+ * Signals that a Guard should fail with a specific reason.
179
+ *
180
+ * @param reason - The reason for failure.
181
+ * @throws An internal signal error caught by the Guard evaluator.
182
+ */
183
+ declare function guardFail(reason: string | GuardReason): never;
184
+ /**
185
+ * Explicitly signals a successful Guard evaluation.
186
+ * Returns the value passed to it.
187
+ */
188
+ 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>;
164
190
 
165
191
  /**
166
192
  * Utility to transform reactive dependencies into a new derived value.
@@ -335,7 +361,7 @@ interface Source<T> {
335
361
  * user.set({ name: 'Bob' });
336
362
  * ```
337
363
  */
338
- 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>;
339
365
 
340
366
  /**
341
367
  * Serialized state of guards for transfer from server to client.
@@ -396,11 +422,29 @@ type PulseUnit = Source<any> | Guard<any>;
396
422
  declare class Registry {
397
423
  private units;
398
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;
399
443
  /**
400
444
  * Registers a new unit (Source or Guard).
401
- * Uses the unit's name as a key to prevent duplicates during HMR.
445
+ * Auto-assigns stable names to unnamed units for HMR stability.
402
446
  */
403
- register(unit: PulseUnit): void;
447
+ register(unit: PulseUnit, offset?: number): void;
404
448
  /**
405
449
  * Retrieves all registered units.
406
450
  */
@@ -412,6 +456,10 @@ declare class Registry {
412
456
  * @returns Unsubscribe function.
413
457
  */
414
458
  onRegister(listener: (unit: PulseUnit) => void): () => void;
459
+ /**
460
+ * Clears all registered units.
461
+ */
462
+ reset(): void;
415
463
  }
416
464
  declare const PulseRegistry: Registry;
417
465
 
@@ -434,4 +482,4 @@ declare const extendedGuard: typeof guard & {
434
482
  compute: typeof compute;
435
483
  };
436
484
 
437
- export { type Guard, type GuardExplanation, type GuardNode, type GuardState, type GuardStatus, type HydrationState, PulseRegistry, type PulseUnit, type Source, type SourceOptions, type Subscriber, type Trackable, compute, evaluate, getCurrentGuard, extendedGuard as guard, hydrate, registerGuardForHydration, runInContext, source };
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 };
package/dist/index.js CHANGED
@@ -1,16 +1,18 @@
1
1
  // src/tracking.ts
2
- var activeGuard = null;
2
+ var guardStack = [];
3
3
  function runInContext(guard2, fn) {
4
- const prev = activeGuard;
5
- activeGuard = guard2;
4
+ if (guardStack.includes(guard2)) {
5
+ throw new Error(`Cyclic guard dependency detected: ${guard2._name || "unnamed guard"}`);
6
+ }
7
+ guardStack.push(guard2);
6
8
  try {
7
9
  return fn();
8
10
  } finally {
9
- activeGuard = prev;
11
+ guardStack.pop();
10
12
  }
11
13
  }
12
14
  function getCurrentGuard() {
13
- return activeGuard;
15
+ return guardStack[guardStack.length - 1] ?? null;
14
16
  }
15
17
 
16
18
  // src/ssr.ts
@@ -43,7 +45,21 @@ function hydrate(state) {
43
45
  }
44
46
 
45
47
  // src/guard.ts
46
- function guard(nameOrFn, fn) {
48
+ function guardFail(reason) {
49
+ const guardReason = typeof reason === "string" ? {
50
+ code: "GUARD_FAIL",
51
+ message: reason,
52
+ toString: () => reason
53
+ } : reason;
54
+ const err = new Error(guardReason.message);
55
+ err._pulseFail = true;
56
+ err._reason = guardReason;
57
+ throw err;
58
+ }
59
+ function guardOk(value) {
60
+ return value;
61
+ }
62
+ function guard(nameOrFn, fn, _internalOffset = 3) {
47
63
  const name = typeof nameOrFn === "string" ? nameOrFn : void 0;
48
64
  const evaluator = typeof nameOrFn === "function" ? nameOrFn : fn;
49
65
  if (!evaluator) {
@@ -53,10 +69,11 @@ function guard(nameOrFn, fn) {
53
69
  const dependents = /* @__PURE__ */ new Set();
54
70
  const subscribers = /* @__PURE__ */ new Set();
55
71
  let evaluationId = 0;
56
- const dependencies = /* @__PURE__ */ new Set();
72
+ const currentDeps = /* @__PURE__ */ new Set();
73
+ let lastDeps = /* @__PURE__ */ new Set();
57
74
  const node = {
58
75
  addDependency(trackable) {
59
- dependencies.add(trackable);
76
+ currentDeps.add(trackable);
60
77
  },
61
78
  notify() {
62
79
  evaluate2();
@@ -66,56 +83,111 @@ function guard(nameOrFn, fn) {
66
83
  const currentId = ++evaluationId;
67
84
  const oldStatus = state.status;
68
85
  const oldValue = state.value;
69
- dependencies.clear();
86
+ currentDeps.clear();
70
87
  try {
71
- const result = runInContext(node, () => evaluator());
72
- if (result instanceof Promise) {
73
- if (state.status !== "pending") {
74
- state = { ...state, status: "pending", updatedAt: Date.now() };
75
- notifyDependents();
88
+ runInContext(node, () => {
89
+ node.isEvaluating = true;
90
+ let result;
91
+ try {
92
+ result = _evaluator();
93
+ } finally {
94
+ node.isEvaluating = false;
76
95
  }
77
- result.then((resolved) => {
78
- if (currentId === evaluationId) {
79
- if (resolved === false) {
80
- state = { status: "fail", reason: name ? `${name} failed` : "condition failed", updatedAt: Date.now() };
81
- } else {
82
- state = { status: "ok", value: resolved, updatedAt: Date.now() };
83
- }
96
+ if (result instanceof Promise) {
97
+ if (state.status !== "pending") {
98
+ state = { ...state, status: "pending", updatedAt: Date.now() };
84
99
  notifyDependents();
85
100
  }
86
- }).catch((err) => {
87
- if (currentId === evaluationId) {
88
- state = { status: "fail", reason: err instanceof Error ? err.message : String(err), updatedAt: Date.now() };
101
+ result.then((resolved) => {
102
+ if (currentId === evaluationId) {
103
+ persistDependencies();
104
+ if (resolved === false) {
105
+ const reason = name ? `${name} failed` : "condition failed";
106
+ state = { status: "fail", reason, lastReason: reason, updatedAt: Date.now() };
107
+ } else if (resolved === void 0) {
108
+ state = { ...state, status: "pending", updatedAt: Date.now() };
109
+ } else {
110
+ state = { status: "ok", value: resolved, updatedAt: Date.now() };
111
+ }
112
+ notifyDependents();
113
+ }
114
+ }).catch((err) => {
115
+ if (currentId === evaluationId) {
116
+ persistDependencies();
117
+ const message = err instanceof Error ? err.message : String(err);
118
+ const reason = err.meta ? {
119
+ code: err.code || "ERROR",
120
+ message,
121
+ meta: err.meta,
122
+ toString: () => message
123
+ } : message;
124
+ state = {
125
+ status: "fail",
126
+ reason,
127
+ lastReason: state.reason || reason,
128
+ updatedAt: Date.now()
129
+ };
130
+ notifyDependents();
131
+ }
132
+ });
133
+ } else {
134
+ persistDependencies();
135
+ if (result === false) {
136
+ const reason = name ? `${name} failed` : "condition failed";
137
+ state = { status: "fail", reason, lastReason: reason, updatedAt: Date.now() };
138
+ } else if (result === void 0) {
139
+ state = { ...state, status: "pending", updatedAt: Date.now() };
140
+ } else {
141
+ state = { status: "ok", value: result, updatedAt: Date.now() };
142
+ }
143
+ if (oldStatus !== state.status || oldValue !== state.value) {
89
144
  notifyDependents();
90
145
  }
91
- });
92
- } else {
93
- if (result === false) {
94
- state = { status: "fail", reason: name ? `${name} failed` : "condition failed", updatedAt: Date.now() };
95
- } else {
96
- state = { status: "ok", value: result, updatedAt: Date.now() };
97
- }
98
- if (oldStatus !== state.status || oldValue !== state.value) {
99
- notifyDependents();
100
146
  }
101
- }
147
+ });
102
148
  } catch (err) {
103
- state = { status: "fail", reason: err instanceof Error ? err.message : String(err), updatedAt: Date.now() };
149
+ node.isEvaluating = false;
150
+ persistDependencies();
151
+ if (err._pulseFail) {
152
+ state = {
153
+ status: "fail",
154
+ reason: err._reason,
155
+ lastReason: err._reason,
156
+ updatedAt: Date.now()
157
+ };
158
+ } else {
159
+ const message = err instanceof Error ? err.message : String(err);
160
+ const reason = err.meta ? {
161
+ code: err.code || "ERROR",
162
+ message,
163
+ meta: err.meta,
164
+ toString: () => message
165
+ } : message;
166
+ state = {
167
+ status: "fail",
168
+ reason,
169
+ lastReason: reason,
170
+ updatedAt: Date.now()
171
+ };
172
+ }
104
173
  notifyDependents();
105
174
  }
106
175
  };
176
+ const _evaluator = () => evaluator();
177
+ const persistDependencies = () => {
178
+ lastDeps = new Set(currentDeps);
179
+ };
107
180
  const notifyDependents = () => {
108
181
  const deps = Array.from(dependents);
109
182
  dependents.clear();
110
183
  deps.forEach((dep) => dep.notify());
111
184
  subscribers.forEach((sub) => sub({ ...state }));
112
185
  };
113
- evaluate2();
114
186
  const track = () => {
115
- const activeGuard2 = getCurrentGuard();
116
- if (activeGuard2 && activeGuard2 !== node) {
117
- dependents.add(activeGuard2);
118
- activeGuard2.addDependency(g);
187
+ const activeGuard = getCurrentGuard();
188
+ if (activeGuard && activeGuard !== node) {
189
+ dependents.add(activeGuard);
190
+ activeGuard.addDependency(g);
119
191
  }
120
192
  };
121
193
  const g = (() => {
@@ -136,7 +208,7 @@ function guard(nameOrFn, fn) {
136
208
  };
137
209
  g.reason = () => {
138
210
  track();
139
- return state.reason;
211
+ return state.reason || state.lastReason;
140
212
  };
141
213
  g.state = () => {
142
214
  track();
@@ -148,7 +220,7 @@ function guard(nameOrFn, fn) {
148
220
  };
149
221
  g.explain = () => {
150
222
  const deps = [];
151
- dependencies.forEach((dep) => {
223
+ lastDeps.forEach((dep) => {
152
224
  const depName = dep._name || "unnamed";
153
225
  const isG = "state" in dep;
154
226
  deps.push({
@@ -161,6 +233,7 @@ function guard(nameOrFn, fn) {
161
233
  name: name || "guard",
162
234
  status: state.status,
163
235
  reason: state.reason,
236
+ lastReason: state.lastReason,
164
237
  value: state.value,
165
238
  dependencies: deps
166
239
  };
@@ -176,6 +249,7 @@ function guard(nameOrFn, fn) {
176
249
  registerGuardForHydration(name, g);
177
250
  }
178
251
  PulseRegistry.register(g);
252
+ evaluate2();
179
253
  return g;
180
254
  }
181
255
 
@@ -183,13 +257,84 @@ function guard(nameOrFn, fn) {
183
257
  var Registry = class {
184
258
  units = /* @__PURE__ */ new Map();
185
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
+ }
186
321
  /**
187
322
  * Registers a new unit (Source or Guard).
188
- * Uses the unit's name as a key to prevent duplicates during HMR.
323
+ * Auto-assigns stable names to unnamed units for HMR stability.
189
324
  */
190
- register(unit) {
191
- const key = unit._name || unit;
192
- 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);
193
338
  this.listeners.forEach((l) => l(unit));
194
339
  }
195
340
  /**
@@ -210,19 +355,32 @@ var Registry = class {
210
355
  this.listeners.delete(listener);
211
356
  };
212
357
  }
358
+ /**
359
+ * Clears all registered units.
360
+ */
361
+ reset() {
362
+ this.units.clear();
363
+ this.currentGeneration = 0;
364
+ this.autoNameCache.clear();
365
+ }
213
366
  };
214
- 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];
215
373
 
216
374
  // src/source.ts
217
- function source(initialValue, options = {}) {
375
+ function source(initialValue, options = {}, _internalOffset = 3) {
218
376
  let value = initialValue;
219
377
  const subscribers = /* @__PURE__ */ new Set();
220
378
  const dependents = /* @__PURE__ */ new Set();
221
379
  const s = (() => {
222
- const activeGuard2 = getCurrentGuard();
223
- if (activeGuard2) {
224
- dependents.add(activeGuard2);
225
- activeGuard2.addDependency(s);
380
+ const activeGuard = getCurrentGuard();
381
+ if (activeGuard) {
382
+ dependents.add(activeGuard);
383
+ activeGuard.addDependency(s);
226
384
  }
227
385
  return value;
228
386
  });
@@ -253,7 +411,7 @@ function compute(name, dependencies, processor) {
253
411
  return guard(name, () => {
254
412
  const values = dependencies.map((dep) => typeof dep === "function" ? dep() : dep);
255
413
  return processor(...values);
256
- });
414
+ }, 4);
257
415
  }
258
416
 
259
417
  // src/composition.ts
@@ -271,10 +429,12 @@ function guardAll(nameOrGuards, maybeGuards) {
271
429
  }
272
430
  }
273
431
  if (firstFail) {
274
- throw new Error(firstFail.reason() || "condition failed");
432
+ const reason = firstFail.reason();
433
+ const message = typeof reason === "string" ? reason : reason?.toString() || "condition failed";
434
+ throw new Error(message);
275
435
  }
276
436
  return true;
277
- });
437
+ }, 4);
278
438
  }
279
439
  function guardAny(nameOrGuards, maybeGuards) {
280
440
  const name = typeof nameOrGuards === "string" ? nameOrGuards : void 0;
@@ -283,10 +443,12 @@ function guardAny(nameOrGuards, maybeGuards) {
283
443
  let allFails = [];
284
444
  for (const g of guards) {
285
445
  if (g.ok()) return true;
286
- allFails.push(g.reason() || "failed");
446
+ const reason = g.reason();
447
+ const message = typeof reason === "string" ? reason : reason?.toString() || "failed";
448
+ allFails.push(message);
287
449
  }
288
450
  throw new Error(allFails.length > 0 ? allFails.join(" and ") : "no conditions met");
289
- });
451
+ }, 4);
290
452
  }
291
453
  function guardNot(nameOrTarget, maybeTarget) {
292
454
  const name = typeof nameOrTarget === "string" ? nameOrTarget : void 0;
@@ -296,7 +458,7 @@ function guardNot(nameOrTarget, maybeTarget) {
296
458
  return !target.ok();
297
459
  }
298
460
  return !target();
299
- });
461
+ }, 4);
300
462
  }
301
463
  var guardExtensions = {
302
464
  all: guardAll,
@@ -313,6 +475,8 @@ export {
313
475
  evaluate,
314
476
  getCurrentGuard,
315
477
  extendedGuard as guard,
478
+ guardFail,
479
+ guardOk,
316
480
  hydrate,
317
481
  registerGuardForHydration,
318
482
  runInContext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-js/core",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "module": "dist/index.js",
5
5
  "main": "dist/index.cjs",
6
6
  "types": "dist/index.d.ts",
@@ -17,7 +17,9 @@
17
17
  "type": "module",
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
- "packages/*"
20
+ "packages/*",
21
+ "tests",
22
+ "tests/*"
21
23
  ],
22
24
  "keywords": [
23
25
  "reactivity",
@@ -36,7 +38,7 @@
36
38
  "directory": "packages/core"
37
39
  },
38
40
  "bugs": {
39
- "url": "https://github.com/ZtaMDev/pulse/issues"
41
+ "url": "https://github.com/ZtaMDev/pulse-js/issues"
40
42
  },
41
43
  "license": "MIT",
42
44
  "scripts": {
@@ -46,12 +48,22 @@
46
48
  "test": "vitest run"
47
49
  },
48
50
  "devDependencies": {
51
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
52
+ "@testing-library/jest-dom": "^6.9.1",
53
+ "@testing-library/svelte": "^5.3.1",
54
+ "@testing-library/vue": "^8.1.0",
49
55
  "@types/bun": "latest",
50
56
  "@types/react": "^19.2.8",
51
57
  "@types/react-dom": "^19.2.3",
58
+ "@vitejs/plugin-react": "^5.1.2",
59
+ "@vitejs/plugin-vue": "^6.0.3",
60
+ "@vue/test-utils": "^2.4.6",
61
+ "jsdom": "^27.4.0",
52
62
  "react": "^19.2.3",
53
63
  "react-dom": "^19.2.3",
54
- "vitest": "^4.0.17"
64
+ "svelte": "^5.46.4",
65
+ "vitest": "^4.0.17",
66
+ "vue": "^3.5.26"
55
67
  },
56
68
  "peerDependencies": {
57
69
  "typescript": "^5"