@pulse-js/core 0.1.7 → 0.1.8

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
@@ -8,16 +8,6 @@
8
8
 
9
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.
10
10
 
11
- ### Mental Model
12
-
13
- Compare Pulse primitives at a glance:
14
-
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). |
20
-
21
11
  </div>
22
12
 
23
13
  ## Installation
@@ -146,6 +136,16 @@ import { hydrate } from "@pulse-js/core";
146
136
  hydrate(window.__PULSE_STATE__);
147
137
  ```
148
138
 
139
+ ### Mental Model
140
+
141
+ Compare Pulse primitives:
142
+
143
+ | Concept | Can be async | Has state | Observable | Purpose |
144
+ | :---------- | :----------: | :-------: | :--------: | :----------------------------------- |
145
+ | **Source** | ❌ | ❌ | ✅ | Reactive data (facts). |
146
+ | **Guard** | ✅ | ✅ | ✅ | Business rules (conditioned truths). |
147
+ | **Compute** | ❌ | ❌ | ✅ | Pure transformations (derivations). |
148
+
149
149
  ## API Reference
150
150
 
151
151
  ### `source<T>(initialValue: T, options?: SourceOptions)`
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,6 +81,20 @@ function hydrate(state) {
77
81
  }
78
82
 
79
83
  // src/guard.ts
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
+ }
80
98
  function guard(nameOrFn, fn) {
81
99
  const name = typeof nameOrFn === "string" ? nameOrFn : void 0;
82
100
  const evaluator = typeof nameOrFn === "function" ? nameOrFn : fn;
@@ -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
 
@@ -253,10 +329,10 @@ function source(initialValue, options = {}) {
253
329
  const subscribers = /* @__PURE__ */ new Set();
254
330
  const dependents = /* @__PURE__ */ new Set();
255
331
  const s = (() => {
256
- const activeGuard2 = getCurrentGuard();
257
- if (activeGuard2) {
258
- dependents.add(activeGuard2);
259
- activeGuard2.addDependency(s);
332
+ const activeGuard = getCurrentGuard();
333
+ if (activeGuard) {
334
+ dependents.add(activeGuard);
335
+ activeGuard.addDependency(s);
260
336
  }
261
337
  return value;
262
338
  });
@@ -305,7 +381,9 @@ function guardAll(nameOrGuards, maybeGuards) {
305
381
  }
306
382
  }
307
383
  if (firstFail) {
308
- throw new Error(firstFail.reason() || "condition failed");
384
+ const reason = firstFail.reason();
385
+ const message = typeof reason === "string" ? reason : reason?.toString() || "condition failed";
386
+ throw new Error(message);
309
387
  }
310
388
  return true;
311
389
  });
@@ -317,7 +395,9 @@ function guardAny(nameOrGuards, maybeGuards) {
317
395
  let allFails = [];
318
396
  for (const g of guards) {
319
397
  if (g.ok()) return true;
320
- allFails.push(g.reason() || "failed");
398
+ const reason = g.reason();
399
+ const message = typeof reason === "string" ? reason : reason?.toString() || "failed";
400
+ allFails.push(message);
321
401
  }
322
402
  throw new Error(allFails.length > 0 ? allFails.join(" and ") : "no conditions met");
323
403
  });
@@ -348,6 +428,8 @@ var extendedGuard = Object.assign(guard, guardExtensions);
348
428
  evaluate,
349
429
  getCurrentGuard,
350
430
  guard,
431
+ guardFail,
432
+ guardOk,
351
433
  hydrate,
352
434
  registerGuardForHydration,
353
435
  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,6 +174,18 @@ interface Guard<T = boolean> {
160
174
  * });
161
175
  * ```
162
176
  */
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;
163
189
  declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>): Guard<T>;
164
190
 
165
191
  /**
@@ -434,4 +460,4 @@ declare const extendedGuard: typeof guard & {
434
460
  compute: typeof compute;
435
461
  };
436
462
 
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 };
463
+ 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,6 +174,18 @@ interface Guard<T = boolean> {
160
174
  * });
161
175
  * ```
162
176
  */
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;
163
189
  declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>): Guard<T>;
164
190
 
165
191
  /**
@@ -434,4 +460,4 @@ declare const extendedGuard: typeof guard & {
434
460
  compute: typeof compute;
435
461
  };
436
462
 
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 };
463
+ 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,6 +45,20 @@ function hydrate(state) {
43
45
  }
44
46
 
45
47
  // src/guard.ts
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
+ }
46
62
  function guard(nameOrFn, fn) {
47
63
  const name = typeof nameOrFn === "string" ? nameOrFn : void 0;
48
64
  const evaluator = typeof nameOrFn === "function" ? nameOrFn : fn;
@@ -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
 
@@ -219,10 +293,10 @@ function source(initialValue, options = {}) {
219
293
  const subscribers = /* @__PURE__ */ new Set();
220
294
  const dependents = /* @__PURE__ */ new Set();
221
295
  const s = (() => {
222
- const activeGuard2 = getCurrentGuard();
223
- if (activeGuard2) {
224
- dependents.add(activeGuard2);
225
- activeGuard2.addDependency(s);
296
+ const activeGuard = getCurrentGuard();
297
+ if (activeGuard) {
298
+ dependents.add(activeGuard);
299
+ activeGuard.addDependency(s);
226
300
  }
227
301
  return value;
228
302
  });
@@ -271,7 +345,9 @@ function guardAll(nameOrGuards, maybeGuards) {
271
345
  }
272
346
  }
273
347
  if (firstFail) {
274
- throw new Error(firstFail.reason() || "condition failed");
348
+ const reason = firstFail.reason();
349
+ const message = typeof reason === "string" ? reason : reason?.toString() || "condition failed";
350
+ throw new Error(message);
275
351
  }
276
352
  return true;
277
353
  });
@@ -283,7 +359,9 @@ function guardAny(nameOrGuards, maybeGuards) {
283
359
  let allFails = [];
284
360
  for (const g of guards) {
285
361
  if (g.ok()) return true;
286
- allFails.push(g.reason() || "failed");
362
+ const reason = g.reason();
363
+ const message = typeof reason === "string" ? reason : reason?.toString() || "failed";
364
+ allFails.push(message);
287
365
  }
288
366
  throw new Error(allFails.length > 0 ? allFails.join(" and ") : "no conditions met");
289
367
  });
@@ -313,6 +391,8 @@ export {
313
391
  evaluate,
314
392
  getCurrentGuard,
315
393
  extendedGuard as guard,
394
+ guardFail,
395
+ guardOk,
316
396
  hydrate,
317
397
  registerGuardForHydration,
318
398
  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.8",
4
4
  "module": "dist/index.js",
5
5
  "main": "dist/index.cjs",
6
6
  "types": "dist/index.d.ts",
@@ -17,7 +17,8 @@
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"
21
22
  ],
22
23
  "keywords": [
23
24
  "reactivity",
@@ -46,12 +47,22 @@
46
47
  "test": "vitest run"
47
48
  },
48
49
  "devDependencies": {
50
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
51
+ "@testing-library/jest-dom": "^6.9.1",
52
+ "@testing-library/svelte": "^5.3.1",
53
+ "@testing-library/vue": "^8.1.0",
49
54
  "@types/bun": "latest",
50
55
  "@types/react": "^19.2.8",
51
56
  "@types/react-dom": "^19.2.3",
57
+ "@vitejs/plugin-react": "^5.1.2",
58
+ "@vitejs/plugin-vue": "^6.0.3",
59
+ "@vue/test-utils": "^2.4.6",
60
+ "jsdom": "^27.4.0",
52
61
  "react": "^19.2.3",
53
62
  "react-dom": "^19.2.3",
54
- "vitest": "^4.0.17"
63
+ "svelte": "^5.46.4",
64
+ "vitest": "^4.0.17",
65
+ "vue": "^3.5.26"
55
66
  },
56
67
  "peerDependencies": {
57
68
  "typescript": "^5"