@pulse-js/core 0.1.4 → 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 +33 -39
- package/dist/index.cjs +52 -21
- package/dist/index.d.cts +45 -22
- package/dist/index.d.ts +45 -22
- package/dist/index.js +51 -21
- package/package.json +1 -1
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
|
|
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.
|
|
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
|
-
###
|
|
86
|
+
### Computed Values
|
|
77
87
|
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
93
|
+
const fullName = compute("full-name", [firstName, lastName], (first, last) => {
|
|
94
|
+
return `${first} ${last}`;
|
|
95
|
+
});
|
|
91
96
|
```
|
|
92
97
|
|
|
93
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
const
|
|
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
|
-
###
|
|
112
|
+
### Explanable Guards
|
|
115
113
|
|
|
116
|
-
|
|
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
|
-
```
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
156
|
+
track();
|
|
149
157
|
return state.status === "ok" ? state.value : void 0;
|
|
150
158
|
});
|
|
151
159
|
g.ok = () => {
|
|
152
|
-
|
|
160
|
+
track();
|
|
153
161
|
return state.status === "ok";
|
|
154
162
|
};
|
|
155
163
|
g.fail = () => {
|
|
156
|
-
|
|
164
|
+
track();
|
|
157
165
|
return state.status === "fail";
|
|
158
166
|
};
|
|
159
167
|
g.pending = () => {
|
|
160
|
-
|
|
168
|
+
track();
|
|
161
169
|
return state.status === "pending";
|
|
162
170
|
};
|
|
163
171
|
g.reason = () => {
|
|
164
|
-
|
|
172
|
+
track();
|
|
165
173
|
return state.reason;
|
|
166
174
|
};
|
|
167
175
|
g.state = () => {
|
|
168
|
-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
122
|
+
track();
|
|
116
123
|
return state.status === "ok" ? state.value : void 0;
|
|
117
124
|
});
|
|
118
125
|
g.ok = () => {
|
|
119
|
-
|
|
126
|
+
track();
|
|
120
127
|
return state.status === "ok";
|
|
121
128
|
};
|
|
122
129
|
g.fail = () => {
|
|
123
|
-
|
|
130
|
+
track();
|
|
124
131
|
return state.status === "fail";
|
|
125
132
|
};
|
|
126
133
|
g.pending = () => {
|
|
127
|
-
|
|
134
|
+
track();
|
|
128
135
|
return state.status === "pending";
|
|
129
136
|
};
|
|
130
137
|
g.reason = () => {
|
|
131
|
-
|
|
138
|
+
track();
|
|
132
139
|
return state.reason;
|
|
133
140
|
};
|
|
134
141
|
g.state = () => {
|
|
135
|
-
|
|
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
|
|
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,
|