@pulse-js/core 0.1.5 → 0.1.6

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
@@ -6,7 +6,17 @@
6
6
 
7
7
  > A semantic reactivity system for modern applications. Separate reactive data (sources) from business conditions (guards) with a declarative, composable, and observable approach.
8
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.
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
+
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). |
10
20
 
11
21
  </div>
12
22
 
@@ -53,7 +63,7 @@ rawCount.update((n) => n + 1);
53
63
 
54
64
  ### Guards (Semantic Logic)
55
65
 
56
- Guards represent business rules or derivations. They are not just boolean flags; they track their own state including `status` (ok, fail, pending) and `reason` (why it failed).
66
+ Guards represent business rules or derivations. A Guard is not just a boolean: it is a **Semantic Guard**—an observable rule with context. They track their own state including `status` (ok, fail, pending) and `reason` (why it failed).
57
67
 
58
68
  ```typescript
59
69
  import { guard } from "@pulse-js/core";
@@ -73,56 +83,40 @@ if (isAdmin.ok()) {
73
83
  }
74
84
  ```
75
85
 
76
- ### Async Guards
86
+ ### Computed Values
77
87
 
78
- Pulse handles asynchronous logic natively. Guards can return Promises, and their status will automatically transition from `pending` to `ok` or `fail`.
88
+ You can derive new data from sources or other guards using `compute`. It works like a memoized transformation that automatically re-evaluates when dependencies change.
79
89
 
80
90
  ```typescript
81
- const isServerOnline = guard("check-server", async () => {
82
- const response = await fetch("/health");
83
- if (!response.ok) throw new Error("Server unreachable");
84
- return true;
85
- });
91
+ import { compute } from "@pulse-js/core";
86
92
 
87
- // Check status synchronously non-blocking
88
- if (isServerOnline.pending()) {
89
- showSpinner();
90
- }
93
+ const fullName = compute("full-name", [firstName, lastName], (first, last) => {
94
+ return `${first} ${last}`;
95
+ });
91
96
  ```
92
97
 
93
- ### Composition
94
-
95
- Guards can be composed using logical operators. This creates a semantic tree of conditions that is easy to debug.
98
+ ### Async Guards & Race Control
96
99
 
97
- ```typescript
98
- import { guard } from "@pulse-js/core";
99
-
100
- // .all() - Success only if ALL pass. Fails with the reason of the first failure.
101
- const canCheckout = guard.all("can-checkout", [
102
- isAuthenticated,
103
- hasItemsInCart,
104
- isServerOnline,
105
- ]);
100
+ Pulse handles asynchronous logic natively. Guards can return Promises, and their status will automatically transition from `pending` to `ok` or `fail`.
106
101
 
107
- // .any() - Success if AT LEAST ONE passes.
108
- const hasAccess = guard.any("has-access", [isAdmin, isEditor]);
102
+ Pulse implements internal **runId versioning** to automatically cancel stale async evaluations if the underlying sources change multiple times before a promise resolves, preventing race conditions.
109
103
 
110
- // .not() - Inverts the logical result.
111
- const isGuest = guard.not("is-guest", isAuthenticated);
104
+ ```typescript
105
+ const isServerOnline = guard("check-server", async () => {
106
+ const response = await fetch("/health");
107
+ if (!response.ok) throw new Error("Server unreachable");
108
+ return true;
109
+ });
112
110
  ```
113
111
 
114
- ### Computed Values
112
+ ### Explanable Guards
115
113
 
116
- You can derive new data from sources or other guards using `guard.compute`.
114
+ For complex conditions, you can call `.explain()` to get a structured tree of the current status, failure reason, and the status of all direct dependencies.
117
115
 
118
- ```typescript
119
- const fullName = guard.compute(
120
- "full-name",
121
- [firstName, lastName],
122
- (first, last) => {
123
- return `${first} ${last}`;
124
- }
125
- );
116
+ ```ts
117
+ const explanation = canCheckout.explain();
118
+ console.log(explanation);
119
+ // { status: 'fail', reason: 'auth failed', dependencies: [...] }
126
120
  ```
127
121
 
128
122
  ## Server-Side Rendering (SSR)
package/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  PulseRegistry: () => PulseRegistry,
24
+ compute: () => compute,
24
25
  evaluate: () => evaluate,
25
26
  getCurrentGuard: () => getCurrentGuard,
26
27
  guard: () => extendedGuard,
@@ -86,8 +87,10 @@ function guard(nameOrFn, fn) {
86
87
  const dependents = /* @__PURE__ */ new Set();
87
88
  const subscribers = /* @__PURE__ */ new Set();
88
89
  let evaluationId = 0;
90
+ const dependencies = /* @__PURE__ */ new Set();
89
91
  const node = {
90
92
  addDependency(trackable) {
93
+ dependencies.add(trackable);
91
94
  },
92
95
  notify() {
93
96
  evaluate2();
@@ -97,37 +100,41 @@ function guard(nameOrFn, fn) {
97
100
  const currentId = ++evaluationId;
98
101
  const oldStatus = state.status;
99
102
  const oldValue = state.value;
103
+ dependencies.clear();
100
104
  try {
101
105
  const result = runInContext(node, () => evaluator());
102
106
  if (result instanceof Promise) {
103
- state = { status: "pending" };
107
+ if (state.status !== "pending") {
108
+ state = { ...state, status: "pending", updatedAt: Date.now() };
109
+ notifyDependents();
110
+ }
104
111
  result.then((resolved) => {
105
112
  if (currentId === evaluationId) {
106
113
  if (resolved === false) {
107
- state = { status: "fail", reason: name ? `${name} failed` : "condition failed" };
114
+ state = { status: "fail", reason: name ? `${name} failed` : "condition failed", updatedAt: Date.now() };
108
115
  } else {
109
- state = { status: "ok", value: resolved };
116
+ state = { status: "ok", value: resolved, updatedAt: Date.now() };
110
117
  }
111
118
  notifyDependents();
112
119
  }
113
120
  }).catch((err) => {
114
121
  if (currentId === evaluationId) {
115
- state = { status: "fail", reason: err instanceof Error ? err.message : String(err) };
122
+ state = { status: "fail", reason: err instanceof Error ? err.message : String(err), updatedAt: Date.now() };
116
123
  notifyDependents();
117
124
  }
118
125
  });
119
126
  } else {
120
127
  if (result === false) {
121
- state = { status: "fail", reason: name ? `${name} failed` : "condition failed" };
128
+ state = { status: "fail", reason: name ? `${name} failed` : "condition failed", updatedAt: Date.now() };
122
129
  } else {
123
- state = { status: "ok", value: result };
130
+ state = { status: "ok", value: result, updatedAt: Date.now() };
124
131
  }
125
132
  if (oldStatus !== state.status || oldValue !== state.value) {
126
133
  notifyDependents();
127
134
  }
128
135
  }
129
136
  } catch (err) {
130
- state = { status: "fail", reason: err instanceof Error ? err.message : String(err) };
137
+ state = { status: "fail", reason: err instanceof Error ? err.message : String(err), updatedAt: Date.now() };
131
138
  notifyDependents();
132
139
  }
133
140
  };
@@ -138,40 +145,60 @@ function guard(nameOrFn, fn) {
138
145
  subscribers.forEach((sub) => sub({ ...state }));
139
146
  };
140
147
  evaluate2();
141
- const handleRead = () => {
148
+ const track = () => {
142
149
  const activeGuard2 = getCurrentGuard();
143
150
  if (activeGuard2 && activeGuard2 !== node) {
144
151
  dependents.add(activeGuard2);
152
+ activeGuard2.addDependency(g);
145
153
  }
146
154
  };
147
155
  const g = (() => {
148
- handleRead();
156
+ track();
149
157
  return state.status === "ok" ? state.value : void 0;
150
158
  });
151
159
  g.ok = () => {
152
- handleRead();
160
+ track();
153
161
  return state.status === "ok";
154
162
  };
155
163
  g.fail = () => {
156
- handleRead();
164
+ track();
157
165
  return state.status === "fail";
158
166
  };
159
167
  g.pending = () => {
160
- handleRead();
168
+ track();
161
169
  return state.status === "pending";
162
170
  };
163
171
  g.reason = () => {
164
- handleRead();
172
+ track();
165
173
  return state.reason;
166
174
  };
167
175
  g.state = () => {
168
- handleRead();
176
+ track();
169
177
  return state;
170
178
  };
171
179
  g.subscribe = (listener) => {
172
180
  subscribers.add(listener);
173
181
  return () => subscribers.delete(listener);
174
182
  };
183
+ g.explain = () => {
184
+ const deps = [];
185
+ dependencies.forEach((dep) => {
186
+ const depName = dep._name || "unnamed";
187
+ const isG = "state" in dep;
188
+ deps.push({
189
+ name: depName,
190
+ type: isG ? "guard" : "source",
191
+ status: isG ? dep.state().status : void 0
192
+ });
193
+ });
194
+ return {
195
+ name: name || "guard",
196
+ status: state.status,
197
+ reason: state.reason,
198
+ value: state.value,
199
+ dependencies: deps
200
+ };
201
+ };
175
202
  g._evaluate = () => evaluate2();
176
203
  g._name = name;
177
204
  g._hydrate = (newState) => {
@@ -229,6 +256,7 @@ function source(initialValue, options = {}) {
229
256
  const activeGuard2 = getCurrentGuard();
230
257
  if (activeGuard2) {
231
258
  dependents.add(activeGuard2);
259
+ activeGuard2.addDependency(s);
232
260
  }
233
261
  return value;
234
262
  });
@@ -254,6 +282,14 @@ function source(initialValue, options = {}) {
254
282
  return s;
255
283
  }
256
284
 
285
+ // src/compute.ts
286
+ function compute(name, dependencies, processor) {
287
+ return guard(name, () => {
288
+ const values = dependencies.map((dep) => typeof dep === "function" ? dep() : dep);
289
+ return processor(...values);
290
+ });
291
+ }
292
+
257
293
  // src/composition.ts
258
294
  function isGuard(target) {
259
295
  return typeof target === "function" && "ok" in target;
@@ -296,17 +332,11 @@ function guardNot(nameOrTarget, maybeTarget) {
296
332
  return !target();
297
333
  });
298
334
  }
299
- function guardCompute(name, dependencies, processor) {
300
- return guard(name, () => {
301
- const values = dependencies.map((dep) => typeof dep === "function" ? dep() : dep);
302
- return processor(...values);
303
- });
304
- }
305
335
  var guardExtensions = {
306
336
  all: guardAll,
307
337
  any: guardAny,
308
338
  not: guardNot,
309
- compute: guardCompute
339
+ compute
310
340
  };
311
341
 
312
342
  // src/index.ts
@@ -314,6 +344,7 @@ var extendedGuard = Object.assign(guard, guardExtensions);
314
344
  // Annotate the CommonJS export names for ESM import in node:
315
345
  0 && (module.exports = {
316
346
  PulseRegistry,
347
+ compute,
317
348
  evaluate,
318
349
  getCurrentGuard,
319
350
  guard,
package/dist/index.d.cts CHANGED
@@ -18,7 +18,7 @@ interface GuardNode extends Trackable {
18
18
  * Registers a dependency for this guard.
19
19
  * Internal use only.
20
20
  */
21
- addDependency(trackable: Trackable): void;
21
+ addDependency(trackable: any): void;
22
22
  }
23
23
  /**
24
24
  * Executes a function within the context of a specific Guard.
@@ -55,6 +55,22 @@ interface GuardState<T> {
55
55
  value?: T;
56
56
  /** The message explaining why the guard failed (only if status is 'fail'). */
57
57
  reason?: string;
58
+ /** The timestamp when the status last changed. */
59
+ updatedAt?: number;
60
+ }
61
+ /**
62
+ * Detailed explanation of the Guard's current state and its dependencies.
63
+ */
64
+ interface GuardExplanation {
65
+ name: string;
66
+ status: GuardStatus;
67
+ reason?: string;
68
+ value?: any;
69
+ dependencies: Array<{
70
+ name: string;
71
+ type: 'source' | 'guard';
72
+ status?: GuardStatus;
73
+ }>;
58
74
  }
59
75
  /**
60
76
  * A Pulse Guard is a reactive semantic condition.
@@ -109,6 +125,11 @@ interface Guard<T = boolean> {
109
125
  * @returns An unsubscription function.
110
126
  */
111
127
  subscribe(listener: Subscriber<GuardState<T>>): () => void;
128
+ /**
129
+ * Returns a structured explanation of the guard's state and its direct dependencies.
130
+ * Useful for DevTools and sophisticated error reporting.
131
+ */
132
+ explain(): GuardExplanation;
112
133
  /**
113
134
  * Manually forces a re-evaluation of the guard.
114
135
  * Typically used internally by Pulse or for debugging.
@@ -141,6 +162,27 @@ interface Guard<T = boolean> {
141
162
  */
142
163
  declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>): Guard<T>;
143
164
 
165
+ /**
166
+ * Utility to transform reactive dependencies into a new derived value.
167
+ *
168
+ * Works like a memoized computation that automatically re-evaluates when
169
+ * any of its dependencies change. Unlike a Guard, compute is intended for
170
+ * pure transformations and does not have a failure reason by default.
171
+ *
172
+ * @template T - The type of input values.
173
+ * @template R - The type of the computed result.
174
+ * @param name - A unique name for the computation (required for SSR).
175
+ * @param dependencies - An array of sources or guards to observe.
176
+ * @param processor - A function that derives the new value.
177
+ * @returns A Pulse Guard holding the computed result.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * const fullName = compute('full-name', [firstName, lastName], (f, l) => `${f} ${l}`);
182
+ * ```
183
+ */
184
+ declare function compute<R>(name: string, dependencies: any[], processor: (...args: any[]) => R): Guard<R>;
185
+
144
186
  /**
145
187
  * Creates a composite guard that is 'ok' only if ALL provided guards are 'ok'.
146
188
  * If any guard fails, this guard also fails and adopts the reason of the FIRST failing guard.
@@ -182,25 +224,6 @@ declare function guardAny(nameOrGuards: string | Guard<any>[], maybeGuards?: Gua
182
224
  * ```
183
225
  */
184
226
  declare function guardNot(nameOrTarget: string | Guard<any> | (() => any), maybeTarget?: Guard<any> | (() => any)): Guard<boolean>;
185
- /**
186
- * Utility to transform reactive dependencies into a new derived value.
187
- *
188
- * Works like a memoized computation that automatically re-evaluates when
189
- * any of its dependencies change.
190
- *
191
- * @template T - The type of input values.
192
- * @template R - The type of the computed result.
193
- * @param name - A unique name for the computation (required for SSR).
194
- * @param dependencies - An array of sources or guards to observe.
195
- * @param processor - A function that derives the new value.
196
- * @returns A Pulse Guard holding the computed result.
197
- *
198
- * @example
199
- * ```ts
200
- * const fullName = guard.compute('full-name', [firstName, lastName], (f, l) => `${f} ${l}`);
201
- * ```
202
- */
203
- declare function guardCompute<T, R>(name: string, dependencies: any[], processor: (...args: any[]) => R): Guard<R>;
204
227
 
205
228
  /**
206
229
  * Options for configuring a Pulse Source.
@@ -408,7 +431,7 @@ declare const extendedGuard: typeof guard & {
408
431
  all: typeof guardAll;
409
432
  any: typeof guardAny;
410
433
  not: typeof guardNot;
411
- compute: typeof guardCompute;
434
+ compute: typeof compute;
412
435
  };
413
436
 
414
- export { type Guard, type GuardNode, type GuardState, type GuardStatus, type HydrationState, PulseRegistry, type PulseUnit, type Source, type SourceOptions, type Subscriber, type Trackable, evaluate, getCurrentGuard, extendedGuard as guard, hydrate, registerGuardForHydration, runInContext, source };
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 };
package/dist/index.d.ts CHANGED
@@ -18,7 +18,7 @@ interface GuardNode extends Trackable {
18
18
  * Registers a dependency for this guard.
19
19
  * Internal use only.
20
20
  */
21
- addDependency(trackable: Trackable): void;
21
+ addDependency(trackable: any): void;
22
22
  }
23
23
  /**
24
24
  * Executes a function within the context of a specific Guard.
@@ -55,6 +55,22 @@ interface GuardState<T> {
55
55
  value?: T;
56
56
  /** The message explaining why the guard failed (only if status is 'fail'). */
57
57
  reason?: string;
58
+ /** The timestamp when the status last changed. */
59
+ updatedAt?: number;
60
+ }
61
+ /**
62
+ * Detailed explanation of the Guard's current state and its dependencies.
63
+ */
64
+ interface GuardExplanation {
65
+ name: string;
66
+ status: GuardStatus;
67
+ reason?: string;
68
+ value?: any;
69
+ dependencies: Array<{
70
+ name: string;
71
+ type: 'source' | 'guard';
72
+ status?: GuardStatus;
73
+ }>;
58
74
  }
59
75
  /**
60
76
  * A Pulse Guard is a reactive semantic condition.
@@ -109,6 +125,11 @@ interface Guard<T = boolean> {
109
125
  * @returns An unsubscription function.
110
126
  */
111
127
  subscribe(listener: Subscriber<GuardState<T>>): () => void;
128
+ /**
129
+ * Returns a structured explanation of the guard's state and its direct dependencies.
130
+ * Useful for DevTools and sophisticated error reporting.
131
+ */
132
+ explain(): GuardExplanation;
112
133
  /**
113
134
  * Manually forces a re-evaluation of the guard.
114
135
  * Typically used internally by Pulse or for debugging.
@@ -141,6 +162,27 @@ interface Guard<T = boolean> {
141
162
  */
142
163
  declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>): Guard<T>;
143
164
 
165
+ /**
166
+ * Utility to transform reactive dependencies into a new derived value.
167
+ *
168
+ * Works like a memoized computation that automatically re-evaluates when
169
+ * any of its dependencies change. Unlike a Guard, compute is intended for
170
+ * pure transformations and does not have a failure reason by default.
171
+ *
172
+ * @template T - The type of input values.
173
+ * @template R - The type of the computed result.
174
+ * @param name - A unique name for the computation (required for SSR).
175
+ * @param dependencies - An array of sources or guards to observe.
176
+ * @param processor - A function that derives the new value.
177
+ * @returns A Pulse Guard holding the computed result.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * const fullName = compute('full-name', [firstName, lastName], (f, l) => `${f} ${l}`);
182
+ * ```
183
+ */
184
+ declare function compute<R>(name: string, dependencies: any[], processor: (...args: any[]) => R): Guard<R>;
185
+
144
186
  /**
145
187
  * Creates a composite guard that is 'ok' only if ALL provided guards are 'ok'.
146
188
  * If any guard fails, this guard also fails and adopts the reason of the FIRST failing guard.
@@ -182,25 +224,6 @@ declare function guardAny(nameOrGuards: string | Guard<any>[], maybeGuards?: Gua
182
224
  * ```
183
225
  */
184
226
  declare function guardNot(nameOrTarget: string | Guard<any> | (() => any), maybeTarget?: Guard<any> | (() => any)): Guard<boolean>;
185
- /**
186
- * Utility to transform reactive dependencies into a new derived value.
187
- *
188
- * Works like a memoized computation that automatically re-evaluates when
189
- * any of its dependencies change.
190
- *
191
- * @template T - The type of input values.
192
- * @template R - The type of the computed result.
193
- * @param name - A unique name for the computation (required for SSR).
194
- * @param dependencies - An array of sources or guards to observe.
195
- * @param processor - A function that derives the new value.
196
- * @returns A Pulse Guard holding the computed result.
197
- *
198
- * @example
199
- * ```ts
200
- * const fullName = guard.compute('full-name', [firstName, lastName], (f, l) => `${f} ${l}`);
201
- * ```
202
- */
203
- declare function guardCompute<T, R>(name: string, dependencies: any[], processor: (...args: any[]) => R): Guard<R>;
204
227
 
205
228
  /**
206
229
  * Options for configuring a Pulse Source.
@@ -408,7 +431,7 @@ declare const extendedGuard: typeof guard & {
408
431
  all: typeof guardAll;
409
432
  any: typeof guardAny;
410
433
  not: typeof guardNot;
411
- compute: typeof guardCompute;
434
+ compute: typeof compute;
412
435
  };
413
436
 
414
- export { type Guard, type GuardNode, type GuardState, type GuardStatus, type HydrationState, PulseRegistry, type PulseUnit, type Source, type SourceOptions, type Subscriber, type Trackable, evaluate, getCurrentGuard, extendedGuard as guard, hydrate, registerGuardForHydration, runInContext, source };
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 };
package/dist/index.js CHANGED
@@ -53,8 +53,10 @@ function guard(nameOrFn, fn) {
53
53
  const dependents = /* @__PURE__ */ new Set();
54
54
  const subscribers = /* @__PURE__ */ new Set();
55
55
  let evaluationId = 0;
56
+ const dependencies = /* @__PURE__ */ new Set();
56
57
  const node = {
57
58
  addDependency(trackable) {
59
+ dependencies.add(trackable);
58
60
  },
59
61
  notify() {
60
62
  evaluate2();
@@ -64,37 +66,41 @@ function guard(nameOrFn, fn) {
64
66
  const currentId = ++evaluationId;
65
67
  const oldStatus = state.status;
66
68
  const oldValue = state.value;
69
+ dependencies.clear();
67
70
  try {
68
71
  const result = runInContext(node, () => evaluator());
69
72
  if (result instanceof Promise) {
70
- state = { status: "pending" };
73
+ if (state.status !== "pending") {
74
+ state = { ...state, status: "pending", updatedAt: Date.now() };
75
+ notifyDependents();
76
+ }
71
77
  result.then((resolved) => {
72
78
  if (currentId === evaluationId) {
73
79
  if (resolved === false) {
74
- state = { status: "fail", reason: name ? `${name} failed` : "condition failed" };
80
+ state = { status: "fail", reason: name ? `${name} failed` : "condition failed", updatedAt: Date.now() };
75
81
  } else {
76
- state = { status: "ok", value: resolved };
82
+ state = { status: "ok", value: resolved, updatedAt: Date.now() };
77
83
  }
78
84
  notifyDependents();
79
85
  }
80
86
  }).catch((err) => {
81
87
  if (currentId === evaluationId) {
82
- state = { status: "fail", reason: err instanceof Error ? err.message : String(err) };
88
+ state = { status: "fail", reason: err instanceof Error ? err.message : String(err), updatedAt: Date.now() };
83
89
  notifyDependents();
84
90
  }
85
91
  });
86
92
  } else {
87
93
  if (result === false) {
88
- state = { status: "fail", reason: name ? `${name} failed` : "condition failed" };
94
+ state = { status: "fail", reason: name ? `${name} failed` : "condition failed", updatedAt: Date.now() };
89
95
  } else {
90
- state = { status: "ok", value: result };
96
+ state = { status: "ok", value: result, updatedAt: Date.now() };
91
97
  }
92
98
  if (oldStatus !== state.status || oldValue !== state.value) {
93
99
  notifyDependents();
94
100
  }
95
101
  }
96
102
  } catch (err) {
97
- state = { status: "fail", reason: err instanceof Error ? err.message : String(err) };
103
+ state = { status: "fail", reason: err instanceof Error ? err.message : String(err), updatedAt: Date.now() };
98
104
  notifyDependents();
99
105
  }
100
106
  };
@@ -105,40 +111,60 @@ function guard(nameOrFn, fn) {
105
111
  subscribers.forEach((sub) => sub({ ...state }));
106
112
  };
107
113
  evaluate2();
108
- const handleRead = () => {
114
+ const track = () => {
109
115
  const activeGuard2 = getCurrentGuard();
110
116
  if (activeGuard2 && activeGuard2 !== node) {
111
117
  dependents.add(activeGuard2);
118
+ activeGuard2.addDependency(g);
112
119
  }
113
120
  };
114
121
  const g = (() => {
115
- handleRead();
122
+ track();
116
123
  return state.status === "ok" ? state.value : void 0;
117
124
  });
118
125
  g.ok = () => {
119
- handleRead();
126
+ track();
120
127
  return state.status === "ok";
121
128
  };
122
129
  g.fail = () => {
123
- handleRead();
130
+ track();
124
131
  return state.status === "fail";
125
132
  };
126
133
  g.pending = () => {
127
- handleRead();
134
+ track();
128
135
  return state.status === "pending";
129
136
  };
130
137
  g.reason = () => {
131
- handleRead();
138
+ track();
132
139
  return state.reason;
133
140
  };
134
141
  g.state = () => {
135
- handleRead();
142
+ track();
136
143
  return state;
137
144
  };
138
145
  g.subscribe = (listener) => {
139
146
  subscribers.add(listener);
140
147
  return () => subscribers.delete(listener);
141
148
  };
149
+ g.explain = () => {
150
+ const deps = [];
151
+ dependencies.forEach((dep) => {
152
+ const depName = dep._name || "unnamed";
153
+ const isG = "state" in dep;
154
+ deps.push({
155
+ name: depName,
156
+ type: isG ? "guard" : "source",
157
+ status: isG ? dep.state().status : void 0
158
+ });
159
+ });
160
+ return {
161
+ name: name || "guard",
162
+ status: state.status,
163
+ reason: state.reason,
164
+ value: state.value,
165
+ dependencies: deps
166
+ };
167
+ };
142
168
  g._evaluate = () => evaluate2();
143
169
  g._name = name;
144
170
  g._hydrate = (newState) => {
@@ -196,6 +222,7 @@ function source(initialValue, options = {}) {
196
222
  const activeGuard2 = getCurrentGuard();
197
223
  if (activeGuard2) {
198
224
  dependents.add(activeGuard2);
225
+ activeGuard2.addDependency(s);
199
226
  }
200
227
  return value;
201
228
  });
@@ -221,6 +248,14 @@ function source(initialValue, options = {}) {
221
248
  return s;
222
249
  }
223
250
 
251
+ // src/compute.ts
252
+ function compute(name, dependencies, processor) {
253
+ return guard(name, () => {
254
+ const values = dependencies.map((dep) => typeof dep === "function" ? dep() : dep);
255
+ return processor(...values);
256
+ });
257
+ }
258
+
224
259
  // src/composition.ts
225
260
  function isGuard(target) {
226
261
  return typeof target === "function" && "ok" in target;
@@ -263,23 +298,18 @@ function guardNot(nameOrTarget, maybeTarget) {
263
298
  return !target();
264
299
  });
265
300
  }
266
- function guardCompute(name, dependencies, processor) {
267
- return guard(name, () => {
268
- const values = dependencies.map((dep) => typeof dep === "function" ? dep() : dep);
269
- return processor(...values);
270
- });
271
- }
272
301
  var guardExtensions = {
273
302
  all: guardAll,
274
303
  any: guardAny,
275
304
  not: guardNot,
276
- compute: guardCompute
305
+ compute
277
306
  };
278
307
 
279
308
  // src/index.ts
280
309
  var extendedGuard = Object.assign(guard, guardExtensions);
281
310
  export {
282
311
  PulseRegistry,
312
+ compute,
283
313
  evaluate,
284
314
  getCurrentGuard,
285
315
  extendedGuard as guard,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-js/core",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "module": "dist/index.js",
5
5
  "main": "dist/index.cjs",
6
6
  "types": "dist/index.d.ts",