@pulse-js/core 0.1.6 → 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 +10 -10
- package/dist/index.cjs +129 -47
- package/dist/index.d.cts +35 -9
- package/dist/index.d.ts +35 -9
- package/dist/index.js +127 -47
- package/package.json +14 -3
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
|
|
38
|
+
var guardStack = [];
|
|
37
39
|
function runInContext(guard2, fn) {
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
47
|
+
guardStack.pop();
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
function getCurrentGuard() {
|
|
47
|
-
return
|
|
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
|
|
108
|
+
const currentDeps = /* @__PURE__ */ new Set();
|
|
109
|
+
let lastDeps = /* @__PURE__ */ new Set();
|
|
91
110
|
const node = {
|
|
92
111
|
addDependency(trackable) {
|
|
93
|
-
|
|
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
|
-
|
|
122
|
+
currentDeps.clear();
|
|
104
123
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
112
|
-
if (
|
|
113
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
|
150
|
-
if (
|
|
151
|
-
dependents.add(
|
|
152
|
-
|
|
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
|
-
|
|
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
|
|
257
|
-
if (
|
|
258
|
-
dependents.add(
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2
|
+
var guardStack = [];
|
|
3
3
|
function runInContext(guard2, fn) {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
11
|
+
guardStack.pop();
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
14
|
function getCurrentGuard() {
|
|
13
|
-
return
|
|
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
|
|
72
|
+
const currentDeps = /* @__PURE__ */ new Set();
|
|
73
|
+
let lastDeps = /* @__PURE__ */ new Set();
|
|
57
74
|
const node = {
|
|
58
75
|
addDependency(trackable) {
|
|
59
|
-
|
|
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
|
-
|
|
86
|
+
currentDeps.clear();
|
|
70
87
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
78
|
-
if (
|
|
79
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
|
116
|
-
if (
|
|
117
|
-
dependents.add(
|
|
118
|
-
|
|
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
|
-
|
|
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
|
|
223
|
-
if (
|
|
224
|
-
dependents.add(
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
63
|
+
"svelte": "^5.46.4",
|
|
64
|
+
"vitest": "^4.0.17",
|
|
65
|
+
"vue": "^3.5.26"
|
|
55
66
|
},
|
|
56
67
|
"peerDependencies": {
|
|
57
68
|
"typescript": "^5"
|