@pulse-js/core 0.2.0 → 0.2.2

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
@@ -4,7 +4,7 @@
4
4
 
5
5
  # Pulse-JS
6
6
 
7
- [![npm version](https://img.shields.io/npm/v/@pulse-js/core.svg)](https://www.npmjs.com/package/@pulse-js/core)
7
+ [![npm version](https://img.shields.io/npm/v/@pulse-js/core.svg?color=blue)](https://www.npmjs.com/package/@pulse-js/core)
8
8
 
9
9
  > A semantic reactivity system for modern applications. Separate reactive data (sources) from business conditions (guards) with a declarative, composable, and observable approach.
10
10
 
package/dist/index.cjs CHANGED
@@ -138,7 +138,12 @@ function guard(nameOrFn, fn) {
138
138
  if (currentId === evaluationId) {
139
139
  persistDependencies();
140
140
  if (resolved === false) {
141
- const reason = name ? `${name} failed` : "condition failed";
141
+ const message = name ? `${name} failed` : "condition failed";
142
+ const reason = {
143
+ code: "GUARD_FAIL",
144
+ message,
145
+ toString: () => message
146
+ };
142
147
  state = { status: "fail", reason, lastReason: reason, updatedAt: Date.now() };
143
148
  } else if (resolved === void 0) {
144
149
  state = { ...state, status: "pending", updatedAt: Date.now() };
@@ -150,13 +155,22 @@ function guard(nameOrFn, fn) {
150
155
  }).catch((err) => {
151
156
  if (currentId === evaluationId) {
152
157
  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;
158
+ let reason;
159
+ if (err && err._pulseFail) {
160
+ reason = err._reason;
161
+ } else {
162
+ const message = err instanceof Error ? err.message : String(err);
163
+ reason = err && err.meta ? {
164
+ code: err.code || "ERROR",
165
+ message,
166
+ meta: err.meta,
167
+ toString: () => message
168
+ } : {
169
+ code: "ERROR",
170
+ message,
171
+ toString: () => message
172
+ };
173
+ }
160
174
  state = {
161
175
  status: "fail",
162
176
  reason,
@@ -169,7 +183,12 @@ function guard(nameOrFn, fn) {
169
183
  } else {
170
184
  persistDependencies();
171
185
  if (result === false) {
172
- const reason = name ? `${name} failed` : "condition failed";
186
+ const message = name ? `${name} failed` : "condition failed";
187
+ const reason = {
188
+ code: "GUARD_FAIL",
189
+ message,
190
+ toString: () => message
191
+ };
173
192
  state = { status: "fail", reason, lastReason: reason, updatedAt: Date.now() };
174
193
  } else if (result === void 0) {
175
194
  state = { ...state, status: "pending", updatedAt: Date.now() };
@@ -198,7 +217,11 @@ function guard(nameOrFn, fn) {
198
217
  message,
199
218
  meta: err.meta,
200
219
  toString: () => message
201
- } : message;
220
+ } : {
221
+ code: "ERROR",
222
+ message,
223
+ toString: () => message
224
+ };
202
225
  state = {
203
226
  status: "fail",
204
227
  reason,
@@ -259,11 +282,20 @@ function guard(nameOrFn, fn) {
259
282
  lastDeps.forEach((dep) => {
260
283
  const depName = dep._name || "unnamed";
261
284
  const isG = "state" in dep;
262
- deps.push({
263
- name: depName,
264
- type: isG ? "guard" : "source",
265
- status: isG ? dep.state().status : void 0
266
- });
285
+ if (isG) {
286
+ const depState = dep.state();
287
+ deps.push({
288
+ name: depName,
289
+ type: "guard",
290
+ status: depState.status,
291
+ reason: depState.status === "fail" ? depState.reason : void 0
292
+ });
293
+ } else {
294
+ deps.push({
295
+ name: depName,
296
+ type: "source"
297
+ });
298
+ }
267
299
  });
268
300
  return {
269
301
  name: name || "guard",
@@ -288,6 +320,13 @@ function guard(nameOrFn, fn) {
288
320
  evaluate2();
289
321
  return g;
290
322
  }
323
+ guard.map = function(source2, mapper, name) {
324
+ const guardName = name || `map-${source2._name || "source"}`;
325
+ return guard(guardName, () => {
326
+ const value = source2();
327
+ return mapper(value);
328
+ });
329
+ };
291
330
 
292
331
  // src/registry.ts
293
332
  var Registry = class {
package/dist/index.d.cts CHANGED
@@ -41,6 +41,118 @@ declare function runInContext<T>(guard: GuardNode, fn: () => T): T;
41
41
  */
42
42
  declare function getCurrentGuard(): GuardNode | null;
43
43
 
44
+ /**
45
+ * Options for configuring a Pulse Source.
46
+ *
47
+ * @template T - The type of value stored in the source.
48
+ */
49
+ interface SourceOptions<T> {
50
+ /**
51
+ * A descriptive name for the source.
52
+ * Required for SSR hydration and highly recommended for debugging in DevTools.
53
+ */
54
+ name?: string;
55
+ /**
56
+ * Custom equality function to determine if a value has changed.
57
+ * By default, Pulse uses strict equality (`===`).
58
+ *
59
+ * @param a - The current value.
60
+ * @param b - The new value.
61
+ * @returns `true` if the values are considered equal, `false` otherwise.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const list = source([1], {
66
+ * equals: (a, b) => a.length === b.length
67
+ * });
68
+ * ```
69
+ */
70
+ equals?: (a: T, b: T) => boolean;
71
+ }
72
+ /**
73
+ * A Pulse Source is a reactive container for a value.
74
+ * It tracks which Guards read its value and notifies them when it changes.
75
+ *
76
+ * @template T The type of the value held by the source.
77
+ */
78
+ interface Source<T> {
79
+ /**
80
+ * Returns the current value of the source.
81
+ * If called within a Guard evaluation, it automatically registers that Guard as a dependent.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const count = source(0);
86
+ * console.log(count()); // 0
87
+ * ```
88
+ */
89
+ (): T;
90
+ /**
91
+ * Updates the source with a new value.
92
+ * If the value is different (based on strict equality or `options.equals`),
93
+ * all dependent Guards and subscribers will be notified.
94
+ *
95
+ * @param value The new value to set.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const count = source(0);
100
+ * count.set(1); // Triggers re-evaluation of dependents
101
+ * ```
102
+ *
103
+ * @error
104
+ * Common error: Mutating an object property without setting a new object reference.
105
+ * Pulse uses reference equality by default. If you mutate a property, Pulse won't know it changed.
106
+ * Solution: Always provide a new object or implement a custom `equals`.
107
+ */
108
+ set(value: T): void;
109
+ /**
110
+ * Updates the source value using a transformer function based on the current value.
111
+ * Useful for increments or toggles.
112
+ *
113
+ * @param updater A function that receives the current value and returns the new value.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * const count = source(0);
118
+ * count.update(n => n + 1);
119
+ * ```
120
+ */
121
+ update(updater: (current: T) => T): void;
122
+ /**
123
+ * Manually subscribes to changes in the source value.
124
+ *
125
+ * @param listener A callback that receives the new value.
126
+ * @returns An unsubscription function.
127
+ *
128
+ * @note Most users should use `guard()` or `usePulse()` instead of manual subscriptions.
129
+ */
130
+ subscribe(listener: Subscriber<T>): () => void;
131
+ }
132
+ /**
133
+ * Creates a new Pulse Source.
134
+ *
135
+ * Sources are the fundamental building blocks of state in Pulse. They hold a value
136
+ * and track which Guards depend on them.
137
+ *
138
+ * @template T - The type of value to store.
139
+ * @param initialValue - The initial state.
140
+ * @param options - Configuration options (name, equality).
141
+ * @returns A reactive Pulse Source.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * const user = source({ name: 'Alice' }, { name: 'user_state' });
146
+ *
147
+ * // Read value (auto-tracks if inside a guard)
148
+ * console.log(user());
149
+ *
150
+ * // Update value
151
+ * user.set({ name: 'Bob' });
152
+ * ```
153
+ */
154
+ declare function source<T>(initialValue: T, options?: SourceOptions<T>): Source<T>;
155
+
44
156
  /**
45
157
  * Status of a Pulse Guard evaluation.
46
158
  * - 'pending': Async evaluation is in progress.
@@ -67,9 +179,9 @@ interface GuardState<T> {
67
179
  /** The value returned by the evaluator (only if status is 'ok'). */
68
180
  value?: T;
69
181
  /** The reason why the guard failed. */
70
- reason?: string | GuardReason;
182
+ reason?: GuardReason;
71
183
  /** The last known failure reason, persisted even during 'pending'. */
72
- lastReason?: string | GuardReason;
184
+ lastReason?: GuardReason;
73
185
  /** The timestamp when the status last changed. */
74
186
  updatedAt?: number;
75
187
  }
@@ -79,13 +191,14 @@ interface GuardState<T> {
79
191
  interface GuardExplanation {
80
192
  name: string;
81
193
  status: GuardStatus;
82
- reason?: string | GuardReason;
83
- lastReason?: string | GuardReason;
194
+ reason?: GuardReason;
195
+ lastReason?: GuardReason;
84
196
  value?: any;
85
197
  dependencies: Array<{
86
198
  name: string;
87
199
  type: 'source' | 'guard';
88
200
  status?: GuardStatus;
201
+ reason?: GuardReason;
89
202
  }>;
90
203
  }
91
204
  /**
@@ -124,9 +237,9 @@ interface Guard<T = boolean> {
124
237
  * Returns the failure reason message if the guard is in the 'fail' state.
125
238
  * Useful for displaying semantic error messages in the UI.
126
239
  *
127
- * @returns The error message or undefined.
240
+ * @returns The error message object or undefined.
128
241
  */
129
- reason(): string | GuardReason | undefined;
242
+ reason(): GuardReason | undefined;
130
243
  /**
131
244
  * Returns a snapshot of the full internal state of the guard.
132
245
  * Useful for adapters (like React) to synchronize with the guard.
@@ -187,6 +300,9 @@ declare function guardFail(reason: string | GuardReason): never;
187
300
  */
188
301
  declare function guardOk<T>(value: T): T;
189
302
  declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>): Guard<T>;
303
+ declare namespace guard {
304
+ var map: <T, U>(source: Source<T>, mapper: (value: T) => U | Promise<U>, name?: string) => Guard<U>;
305
+ }
190
306
 
191
307
  /**
192
308
  * Utility to transform reactive dependencies into a new derived value.
@@ -251,118 +367,6 @@ declare function guardAny(nameOrGuards: string | Guard<any>[], maybeGuards?: Gua
251
367
  */
252
368
  declare function guardNot(nameOrTarget: string | Guard<any> | (() => any), maybeTarget?: Guard<any> | (() => any)): Guard<boolean>;
253
369
 
254
- /**
255
- * Options for configuring a Pulse Source.
256
- *
257
- * @template T - The type of value stored in the source.
258
- */
259
- interface SourceOptions<T> {
260
- /**
261
- * A descriptive name for the source.
262
- * Required for SSR hydration and highly recommended for debugging in DevTools.
263
- */
264
- name?: string;
265
- /**
266
- * Custom equality function to determine if a value has changed.
267
- * By default, Pulse uses strict equality (`===`).
268
- *
269
- * @param a - The current value.
270
- * @param b - The new value.
271
- * @returns `true` if the values are considered equal, `false` otherwise.
272
- *
273
- * @example
274
- * ```ts
275
- * const list = source([1], {
276
- * equals: (a, b) => a.length === b.length
277
- * });
278
- * ```
279
- */
280
- equals?: (a: T, b: T) => boolean;
281
- }
282
- /**
283
- * A Pulse Source is a reactive container for a value.
284
- * It tracks which Guards read its value and notifies them when it changes.
285
- *
286
- * @template T The type of the value held by the source.
287
- */
288
- interface Source<T> {
289
- /**
290
- * Returns the current value of the source.
291
- * If called within a Guard evaluation, it automatically registers that Guard as a dependent.
292
- *
293
- * @example
294
- * ```ts
295
- * const count = source(0);
296
- * console.log(count()); // 0
297
- * ```
298
- */
299
- (): T;
300
- /**
301
- * Updates the source with a new value.
302
- * If the value is different (based on strict equality or `options.equals`),
303
- * all dependent Guards and subscribers will be notified.
304
- *
305
- * @param value The new value to set.
306
- *
307
- * @example
308
- * ```ts
309
- * const count = source(0);
310
- * count.set(1); // Triggers re-evaluation of dependents
311
- * ```
312
- *
313
- * @error
314
- * Common error: Mutating an object property without setting a new object reference.
315
- * Pulse uses reference equality by default. If you mutate a property, Pulse won't know it changed.
316
- * Solution: Always provide a new object or implement a custom `equals`.
317
- */
318
- set(value: T): void;
319
- /**
320
- * Updates the source value using a transformer function based on the current value.
321
- * Useful for increments or toggles.
322
- *
323
- * @param updater A function that receives the current value and returns the new value.
324
- *
325
- * @example
326
- * ```ts
327
- * const count = source(0);
328
- * count.update(n => n + 1);
329
- * ```
330
- */
331
- update(updater: (current: T) => T): void;
332
- /**
333
- * Manually subscribes to changes in the source value.
334
- *
335
- * @param listener A callback that receives the new value.
336
- * @returns An unsubscription function.
337
- *
338
- * @note Most users should use `guard()` or `usePulse()` instead of manual subscriptions.
339
- */
340
- subscribe(listener: Subscriber<T>): () => void;
341
- }
342
- /**
343
- * Creates a new Pulse Source.
344
- *
345
- * Sources are the fundamental building blocks of state in Pulse. They hold a value
346
- * and track which Guards depend on them.
347
- *
348
- * @template T - The type of value to store.
349
- * @param initialValue - The initial state.
350
- * @param options - Configuration options (name, equality).
351
- * @returns A reactive Pulse Source.
352
- *
353
- * @example
354
- * ```ts
355
- * const user = source({ name: 'Alice' }, { name: 'user_state' });
356
- *
357
- * // Read value (auto-tracks if inside a guard)
358
- * console.log(user());
359
- *
360
- * // Update value
361
- * user.set({ name: 'Bob' });
362
- * ```
363
- */
364
- declare function source<T>(initialValue: T, options?: SourceOptions<T>): Source<T>;
365
-
366
370
  /**
367
371
  * Serialized state of guards for transfer from server to client.
368
372
  */
@@ -455,6 +459,17 @@ declare class Registry {
455
459
  }
456
460
  declare const PulseRegistry: Registry;
457
461
 
462
+ /**
463
+ * Extracts the result type T from a Guard<T>.
464
+ *
465
+ * @example
466
+ * ```ts
467
+ * const authGuard = guard('auth', async () => fetchUser());
468
+ * type AuthUser = InferGuardType<typeof authGuard>; // User
469
+ * ```
470
+ */
471
+ type InferGuardType<T> = T extends Guard<infer U> ? U : never;
472
+
458
473
  /**
459
474
  * Pulse Guard with integrated Composition Helpers.
460
475
  *
@@ -474,4 +489,4 @@ declare const extendedGuard: typeof guard & {
474
489
  compute: typeof compute;
475
490
  };
476
491
 
477
- 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 };
492
+ export { type Guard, type GuardExplanation, type GuardNode, type GuardReason, type GuardState, type GuardStatus, type HydrationState, type InferGuardType, 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
@@ -41,6 +41,118 @@ declare function runInContext<T>(guard: GuardNode, fn: () => T): T;
41
41
  */
42
42
  declare function getCurrentGuard(): GuardNode | null;
43
43
 
44
+ /**
45
+ * Options for configuring a Pulse Source.
46
+ *
47
+ * @template T - The type of value stored in the source.
48
+ */
49
+ interface SourceOptions<T> {
50
+ /**
51
+ * A descriptive name for the source.
52
+ * Required for SSR hydration and highly recommended for debugging in DevTools.
53
+ */
54
+ name?: string;
55
+ /**
56
+ * Custom equality function to determine if a value has changed.
57
+ * By default, Pulse uses strict equality (`===`).
58
+ *
59
+ * @param a - The current value.
60
+ * @param b - The new value.
61
+ * @returns `true` if the values are considered equal, `false` otherwise.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const list = source([1], {
66
+ * equals: (a, b) => a.length === b.length
67
+ * });
68
+ * ```
69
+ */
70
+ equals?: (a: T, b: T) => boolean;
71
+ }
72
+ /**
73
+ * A Pulse Source is a reactive container for a value.
74
+ * It tracks which Guards read its value and notifies them when it changes.
75
+ *
76
+ * @template T The type of the value held by the source.
77
+ */
78
+ interface Source<T> {
79
+ /**
80
+ * Returns the current value of the source.
81
+ * If called within a Guard evaluation, it automatically registers that Guard as a dependent.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const count = source(0);
86
+ * console.log(count()); // 0
87
+ * ```
88
+ */
89
+ (): T;
90
+ /**
91
+ * Updates the source with a new value.
92
+ * If the value is different (based on strict equality or `options.equals`),
93
+ * all dependent Guards and subscribers will be notified.
94
+ *
95
+ * @param value The new value to set.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const count = source(0);
100
+ * count.set(1); // Triggers re-evaluation of dependents
101
+ * ```
102
+ *
103
+ * @error
104
+ * Common error: Mutating an object property without setting a new object reference.
105
+ * Pulse uses reference equality by default. If you mutate a property, Pulse won't know it changed.
106
+ * Solution: Always provide a new object or implement a custom `equals`.
107
+ */
108
+ set(value: T): void;
109
+ /**
110
+ * Updates the source value using a transformer function based on the current value.
111
+ * Useful for increments or toggles.
112
+ *
113
+ * @param updater A function that receives the current value and returns the new value.
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * const count = source(0);
118
+ * count.update(n => n + 1);
119
+ * ```
120
+ */
121
+ update(updater: (current: T) => T): void;
122
+ /**
123
+ * Manually subscribes to changes in the source value.
124
+ *
125
+ * @param listener A callback that receives the new value.
126
+ * @returns An unsubscription function.
127
+ *
128
+ * @note Most users should use `guard()` or `usePulse()` instead of manual subscriptions.
129
+ */
130
+ subscribe(listener: Subscriber<T>): () => void;
131
+ }
132
+ /**
133
+ * Creates a new Pulse Source.
134
+ *
135
+ * Sources are the fundamental building blocks of state in Pulse. They hold a value
136
+ * and track which Guards depend on them.
137
+ *
138
+ * @template T - The type of value to store.
139
+ * @param initialValue - The initial state.
140
+ * @param options - Configuration options (name, equality).
141
+ * @returns A reactive Pulse Source.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * const user = source({ name: 'Alice' }, { name: 'user_state' });
146
+ *
147
+ * // Read value (auto-tracks if inside a guard)
148
+ * console.log(user());
149
+ *
150
+ * // Update value
151
+ * user.set({ name: 'Bob' });
152
+ * ```
153
+ */
154
+ declare function source<T>(initialValue: T, options?: SourceOptions<T>): Source<T>;
155
+
44
156
  /**
45
157
  * Status of a Pulse Guard evaluation.
46
158
  * - 'pending': Async evaluation is in progress.
@@ -67,9 +179,9 @@ interface GuardState<T> {
67
179
  /** The value returned by the evaluator (only if status is 'ok'). */
68
180
  value?: T;
69
181
  /** The reason why the guard failed. */
70
- reason?: string | GuardReason;
182
+ reason?: GuardReason;
71
183
  /** The last known failure reason, persisted even during 'pending'. */
72
- lastReason?: string | GuardReason;
184
+ lastReason?: GuardReason;
73
185
  /** The timestamp when the status last changed. */
74
186
  updatedAt?: number;
75
187
  }
@@ -79,13 +191,14 @@ interface GuardState<T> {
79
191
  interface GuardExplanation {
80
192
  name: string;
81
193
  status: GuardStatus;
82
- reason?: string | GuardReason;
83
- lastReason?: string | GuardReason;
194
+ reason?: GuardReason;
195
+ lastReason?: GuardReason;
84
196
  value?: any;
85
197
  dependencies: Array<{
86
198
  name: string;
87
199
  type: 'source' | 'guard';
88
200
  status?: GuardStatus;
201
+ reason?: GuardReason;
89
202
  }>;
90
203
  }
91
204
  /**
@@ -124,9 +237,9 @@ interface Guard<T = boolean> {
124
237
  * Returns the failure reason message if the guard is in the 'fail' state.
125
238
  * Useful for displaying semantic error messages in the UI.
126
239
  *
127
- * @returns The error message or undefined.
240
+ * @returns The error message object or undefined.
128
241
  */
129
- reason(): string | GuardReason | undefined;
242
+ reason(): GuardReason | undefined;
130
243
  /**
131
244
  * Returns a snapshot of the full internal state of the guard.
132
245
  * Useful for adapters (like React) to synchronize with the guard.
@@ -187,6 +300,9 @@ declare function guardFail(reason: string | GuardReason): never;
187
300
  */
188
301
  declare function guardOk<T>(value: T): T;
189
302
  declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>): Guard<T>;
303
+ declare namespace guard {
304
+ var map: <T, U>(source: Source<T>, mapper: (value: T) => U | Promise<U>, name?: string) => Guard<U>;
305
+ }
190
306
 
191
307
  /**
192
308
  * Utility to transform reactive dependencies into a new derived value.
@@ -251,118 +367,6 @@ declare function guardAny(nameOrGuards: string | Guard<any>[], maybeGuards?: Gua
251
367
  */
252
368
  declare function guardNot(nameOrTarget: string | Guard<any> | (() => any), maybeTarget?: Guard<any> | (() => any)): Guard<boolean>;
253
369
 
254
- /**
255
- * Options for configuring a Pulse Source.
256
- *
257
- * @template T - The type of value stored in the source.
258
- */
259
- interface SourceOptions<T> {
260
- /**
261
- * A descriptive name for the source.
262
- * Required for SSR hydration and highly recommended for debugging in DevTools.
263
- */
264
- name?: string;
265
- /**
266
- * Custom equality function to determine if a value has changed.
267
- * By default, Pulse uses strict equality (`===`).
268
- *
269
- * @param a - The current value.
270
- * @param b - The new value.
271
- * @returns `true` if the values are considered equal, `false` otherwise.
272
- *
273
- * @example
274
- * ```ts
275
- * const list = source([1], {
276
- * equals: (a, b) => a.length === b.length
277
- * });
278
- * ```
279
- */
280
- equals?: (a: T, b: T) => boolean;
281
- }
282
- /**
283
- * A Pulse Source is a reactive container for a value.
284
- * It tracks which Guards read its value and notifies them when it changes.
285
- *
286
- * @template T The type of the value held by the source.
287
- */
288
- interface Source<T> {
289
- /**
290
- * Returns the current value of the source.
291
- * If called within a Guard evaluation, it automatically registers that Guard as a dependent.
292
- *
293
- * @example
294
- * ```ts
295
- * const count = source(0);
296
- * console.log(count()); // 0
297
- * ```
298
- */
299
- (): T;
300
- /**
301
- * Updates the source with a new value.
302
- * If the value is different (based on strict equality or `options.equals`),
303
- * all dependent Guards and subscribers will be notified.
304
- *
305
- * @param value The new value to set.
306
- *
307
- * @example
308
- * ```ts
309
- * const count = source(0);
310
- * count.set(1); // Triggers re-evaluation of dependents
311
- * ```
312
- *
313
- * @error
314
- * Common error: Mutating an object property without setting a new object reference.
315
- * Pulse uses reference equality by default. If you mutate a property, Pulse won't know it changed.
316
- * Solution: Always provide a new object or implement a custom `equals`.
317
- */
318
- set(value: T): void;
319
- /**
320
- * Updates the source value using a transformer function based on the current value.
321
- * Useful for increments or toggles.
322
- *
323
- * @param updater A function that receives the current value and returns the new value.
324
- *
325
- * @example
326
- * ```ts
327
- * const count = source(0);
328
- * count.update(n => n + 1);
329
- * ```
330
- */
331
- update(updater: (current: T) => T): void;
332
- /**
333
- * Manually subscribes to changes in the source value.
334
- *
335
- * @param listener A callback that receives the new value.
336
- * @returns An unsubscription function.
337
- *
338
- * @note Most users should use `guard()` or `usePulse()` instead of manual subscriptions.
339
- */
340
- subscribe(listener: Subscriber<T>): () => void;
341
- }
342
- /**
343
- * Creates a new Pulse Source.
344
- *
345
- * Sources are the fundamental building blocks of state in Pulse. They hold a value
346
- * and track which Guards depend on them.
347
- *
348
- * @template T - The type of value to store.
349
- * @param initialValue - The initial state.
350
- * @param options - Configuration options (name, equality).
351
- * @returns A reactive Pulse Source.
352
- *
353
- * @example
354
- * ```ts
355
- * const user = source({ name: 'Alice' }, { name: 'user_state' });
356
- *
357
- * // Read value (auto-tracks if inside a guard)
358
- * console.log(user());
359
- *
360
- * // Update value
361
- * user.set({ name: 'Bob' });
362
- * ```
363
- */
364
- declare function source<T>(initialValue: T, options?: SourceOptions<T>): Source<T>;
365
-
366
370
  /**
367
371
  * Serialized state of guards for transfer from server to client.
368
372
  */
@@ -455,6 +459,17 @@ declare class Registry {
455
459
  }
456
460
  declare const PulseRegistry: Registry;
457
461
 
462
+ /**
463
+ * Extracts the result type T from a Guard<T>.
464
+ *
465
+ * @example
466
+ * ```ts
467
+ * const authGuard = guard('auth', async () => fetchUser());
468
+ * type AuthUser = InferGuardType<typeof authGuard>; // User
469
+ * ```
470
+ */
471
+ type InferGuardType<T> = T extends Guard<infer U> ? U : never;
472
+
458
473
  /**
459
474
  * Pulse Guard with integrated Composition Helpers.
460
475
  *
@@ -474,4 +489,4 @@ declare const extendedGuard: typeof guard & {
474
489
  compute: typeof compute;
475
490
  };
476
491
 
477
- 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 };
492
+ export { type Guard, type GuardExplanation, type GuardNode, type GuardReason, type GuardState, type GuardStatus, type HydrationState, type InferGuardType, 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
@@ -102,7 +102,12 @@ function guard(nameOrFn, fn) {
102
102
  if (currentId === evaluationId) {
103
103
  persistDependencies();
104
104
  if (resolved === false) {
105
- const reason = name ? `${name} failed` : "condition failed";
105
+ const message = name ? `${name} failed` : "condition failed";
106
+ const reason = {
107
+ code: "GUARD_FAIL",
108
+ message,
109
+ toString: () => message
110
+ };
106
111
  state = { status: "fail", reason, lastReason: reason, updatedAt: Date.now() };
107
112
  } else if (resolved === void 0) {
108
113
  state = { ...state, status: "pending", updatedAt: Date.now() };
@@ -114,13 +119,22 @@ function guard(nameOrFn, fn) {
114
119
  }).catch((err) => {
115
120
  if (currentId === evaluationId) {
116
121
  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;
122
+ let reason;
123
+ if (err && err._pulseFail) {
124
+ reason = err._reason;
125
+ } else {
126
+ const message = err instanceof Error ? err.message : String(err);
127
+ reason = err && err.meta ? {
128
+ code: err.code || "ERROR",
129
+ message,
130
+ meta: err.meta,
131
+ toString: () => message
132
+ } : {
133
+ code: "ERROR",
134
+ message,
135
+ toString: () => message
136
+ };
137
+ }
124
138
  state = {
125
139
  status: "fail",
126
140
  reason,
@@ -133,7 +147,12 @@ function guard(nameOrFn, fn) {
133
147
  } else {
134
148
  persistDependencies();
135
149
  if (result === false) {
136
- const reason = name ? `${name} failed` : "condition failed";
150
+ const message = name ? `${name} failed` : "condition failed";
151
+ const reason = {
152
+ code: "GUARD_FAIL",
153
+ message,
154
+ toString: () => message
155
+ };
137
156
  state = { status: "fail", reason, lastReason: reason, updatedAt: Date.now() };
138
157
  } else if (result === void 0) {
139
158
  state = { ...state, status: "pending", updatedAt: Date.now() };
@@ -162,7 +181,11 @@ function guard(nameOrFn, fn) {
162
181
  message,
163
182
  meta: err.meta,
164
183
  toString: () => message
165
- } : message;
184
+ } : {
185
+ code: "ERROR",
186
+ message,
187
+ toString: () => message
188
+ };
166
189
  state = {
167
190
  status: "fail",
168
191
  reason,
@@ -223,11 +246,20 @@ function guard(nameOrFn, fn) {
223
246
  lastDeps.forEach((dep) => {
224
247
  const depName = dep._name || "unnamed";
225
248
  const isG = "state" in dep;
226
- deps.push({
227
- name: depName,
228
- type: isG ? "guard" : "source",
229
- status: isG ? dep.state().status : void 0
230
- });
249
+ if (isG) {
250
+ const depState = dep.state();
251
+ deps.push({
252
+ name: depName,
253
+ type: "guard",
254
+ status: depState.status,
255
+ reason: depState.status === "fail" ? depState.reason : void 0
256
+ });
257
+ } else {
258
+ deps.push({
259
+ name: depName,
260
+ type: "source"
261
+ });
262
+ }
231
263
  });
232
264
  return {
233
265
  name: name || "guard",
@@ -252,6 +284,13 @@ function guard(nameOrFn, fn) {
252
284
  evaluate2();
253
285
  return g;
254
286
  }
287
+ guard.map = function(source2, mapper, name) {
288
+ const guardName = name || `map-${source2._name || "source"}`;
289
+ return guard(guardName, () => {
290
+ const value = source2();
291
+ return mapper(value);
292
+ });
293
+ };
255
294
 
256
295
  // src/registry.ts
257
296
  var Registry = class {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-js/core",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "module": "dist/index.js",
5
5
  "main": "dist/index.cjs",
6
6
  "types": "dist/index.d.ts",