@pulse-js/core 0.1.7 → 0.1.9
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 +26 -36
- package/dist/index.cjs +224 -58
- package/dist/index.d.cts +61 -13
- package/dist/index.d.ts +61 -13
- package/dist/index.js +222 -58
- package/package.json +16 -4
package/README.md
CHANGED
|
@@ -2,21 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
<img width="200" height="200" alt="logo" src="https://raw.githubusercontent.com/ZtaMDev/Pulse/refs/heads/main/pulse.svg" />
|
|
4
4
|
|
|
5
|
-
# Pulse
|
|
5
|
+
# Pulse-JS
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Pulse differs from traditional signals or state managers by treating `Conditions` as first-class citizens. Instead of embedding complex boolean logic inside components or selectors, you define **Semantic Guards** that can be observed, composed, and debugged independently.
|
|
7
|
+
[](https://www.npmjs.com/package/@pulse-js/core)
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
> A semantic reactivity system for modern applications. Separate reactive data (sources) from business conditions (guards) with a declarative, composable, and observable approach.
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
Official [Documentation](https://pulse-js.vercel.app)
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
| :---------- | :----------: | :-------: | :--------: | :----------------------------------- |
|
|
17
|
-
| **Source** | ❌ | ❌ | ✅ | Reactive data (facts). |
|
|
18
|
-
| **Guard** | ✅ | ✅ | ✅ | Business rules (conditioned truths). |
|
|
19
|
-
| **Compute** | ❌ | ❌ | ✅ | Pure transformations (derivations). |
|
|
13
|
+
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.
|
|
20
14
|
|
|
21
15
|
</div>
|
|
22
16
|
|
|
@@ -146,37 +140,33 @@ import { hydrate } from "@pulse-js/core";
|
|
|
146
140
|
hydrate(window.__PULSE_STATE__);
|
|
147
141
|
```
|
|
148
142
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
### `source<T>(initialValue: T, options?: SourceOptions)`
|
|
152
|
-
|
|
153
|
-
Creates a reactive source.
|
|
154
|
-
|
|
155
|
-
- `options.name`: Unique string name (highly recommended for debugging).
|
|
156
|
-
- `options.equals`: Custom equality function `(prev, next) => boolean`.
|
|
143
|
+
### Mental Model
|
|
157
144
|
|
|
158
|
-
|
|
145
|
+
Compare Pulse primitives:
|
|
159
146
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
147
|
+
| Concept | Can be async | Has state | Observable | Purpose |
|
|
148
|
+
| :---------- | :----------: | :-------: | :--------: | :----------------------------------- |
|
|
149
|
+
| **Source** | ❌ | ❌ | ✅ | Reactive data (facts). |
|
|
150
|
+
| **Guard** | ✅ | ✅ | ✅ | Business rules (conditioned truths). |
|
|
151
|
+
| **Compute** | ❌ | ❌ | ✅ | Pure transformations (derivations). |
|
|
163
152
|
|
|
164
|
-
|
|
153
|
+
## Framework Integrations
|
|
165
154
|
|
|
166
|
-
|
|
155
|
+
Pulse provides official adapters for major frameworks to ensure seamless integration.
|
|
167
156
|
|
|
168
|
-
|
|
157
|
+
| Framework | Package | Documentation |
|
|
158
|
+
| :--------- | :----------------- | :----------------------------------------------------------- |
|
|
159
|
+
| **React** | `@pulse-js/react` | [Read Docs](https://pulse-js.vercel.app/integrations/react) |
|
|
160
|
+
| **Vue** | `@pulse-js/vue` | [Read Docs](https://pulse-js.vercel.app/integrations/vue) |
|
|
161
|
+
| **Svelte** | `@pulse-js/svelte` | [Read Docs](https://pulse-js.vercel.app/integrations/svelte) |
|
|
169
162
|
|
|
170
|
-
|
|
171
|
-
- `.fail()`: Returns true if status is 'fail'.
|
|
172
|
-
- `.pending()`: Returns true if evaluating async.
|
|
173
|
-
- `.reason()`: Returns the failure message.
|
|
174
|
-
- `.state()`: Returns full `{ status, value, reason }` object.
|
|
175
|
-
- `.subscribe(fn: (state: GuardState) => void)`: Manual subscription.
|
|
163
|
+
## Developer Tools
|
|
176
164
|
|
|
177
|
-
|
|
165
|
+
Debug your reactive graph with **[Pulse Tools](https://pulse-js.vercel.app/guides/devtools/)**, a powerful framework-agnostic inspector.
|
|
178
166
|
|
|
179
|
-
|
|
167
|
+
### Features
|
|
180
168
|
|
|
181
|
-
-
|
|
182
|
-
-
|
|
169
|
+
- **Component Tree**: Visualize your entire guard dependency graph.
|
|
170
|
+
- **Editable Logic**: Update source values directly from the UI to test logic branches.
|
|
171
|
+
- **Time Travel**: (Coming Soon) Replay state changes.
|
|
172
|
+
- **Zero Config**: Works out of the box with `@pulse-js/tools`.
|
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,7 +81,21 @@ function hydrate(state) {
|
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
// src/guard.ts
|
|
80
|
-
function
|
|
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
|
+
}
|
|
98
|
+
function guard(nameOrFn, fn, _internalOffset = 3) {
|
|
81
99
|
const name = typeof nameOrFn === "string" ? nameOrFn : void 0;
|
|
82
100
|
const evaluator = typeof nameOrFn === "function" ? nameOrFn : fn;
|
|
83
101
|
if (!evaluator) {
|
|
@@ -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
|
|
|
@@ -217,13 +293,84 @@ function guard(nameOrFn, fn) {
|
|
|
217
293
|
var Registry = class {
|
|
218
294
|
units = /* @__PURE__ */ new Map();
|
|
219
295
|
listeners = /* @__PURE__ */ new Set();
|
|
296
|
+
currentGeneration = 0;
|
|
297
|
+
cleanupScheduled = false;
|
|
298
|
+
autoNameCache = /* @__PURE__ */ new Map();
|
|
299
|
+
/**
|
|
300
|
+
* Generates a stable auto-name based on source code location.
|
|
301
|
+
* Uses file path and line number to ensure the same location always gets the same name.
|
|
302
|
+
* Cached to avoid repeated stack trace parsing.
|
|
303
|
+
*/
|
|
304
|
+
generateAutoName(type, offset = 3) {
|
|
305
|
+
const err = new Error();
|
|
306
|
+
const stack = err.stack?.split("\n") || [];
|
|
307
|
+
let callSite = stack[offset]?.trim() || "";
|
|
308
|
+
const cacheKey = `${type}:${callSite}`;
|
|
309
|
+
if (this.autoNameCache.has(cacheKey)) {
|
|
310
|
+
return this.autoNameCache.get(cacheKey);
|
|
311
|
+
}
|
|
312
|
+
const match = callSite.match(/([^/\\]+)\.(?:ts|tsx|js|jsx):(\d+):\d+/);
|
|
313
|
+
let name;
|
|
314
|
+
if (match) {
|
|
315
|
+
const filename = match[1];
|
|
316
|
+
const line = match[2];
|
|
317
|
+
if (filename && line) {
|
|
318
|
+
name = `${type}@${filename}:${line}`;
|
|
319
|
+
} else {
|
|
320
|
+
name = `${type}#${Math.random().toString(36).substring(2, 7)}`;
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
name = `${type}#${Math.random().toString(36).substring(2, 7)}`;
|
|
324
|
+
}
|
|
325
|
+
this.autoNameCache.set(cacheKey, name);
|
|
326
|
+
return name;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Increments generation and schedules cleanup of old units.
|
|
330
|
+
* Called automatically when HMR is detected.
|
|
331
|
+
*/
|
|
332
|
+
scheduleCleanup() {
|
|
333
|
+
if (this.cleanupScheduled) return;
|
|
334
|
+
this.cleanupScheduled = true;
|
|
335
|
+
this.currentGeneration++;
|
|
336
|
+
setTimeout(() => {
|
|
337
|
+
this.cleanupOldGenerations();
|
|
338
|
+
this.cleanupScheduled = false;
|
|
339
|
+
}, 100);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Removes units from old generations (likely orphaned by HMR).
|
|
343
|
+
*/
|
|
344
|
+
cleanupOldGenerations() {
|
|
345
|
+
const toDelete = [];
|
|
346
|
+
this.units.forEach((unit, key) => {
|
|
347
|
+
const gen = unit._generation;
|
|
348
|
+
if (gen !== void 0 && gen < this.currentGeneration) {
|
|
349
|
+
toDelete.push(key);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
toDelete.forEach((key) => this.units.delete(key));
|
|
353
|
+
if (toDelete.length > 0) {
|
|
354
|
+
console.log(`[Pulse] Cleaned up ${toDelete.length} stale units after HMR`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
220
357
|
/**
|
|
221
358
|
* Registers a new unit (Source or Guard).
|
|
222
|
-
*
|
|
359
|
+
* Auto-assigns stable names to unnamed units for HMR stability.
|
|
223
360
|
*/
|
|
224
|
-
register(unit) {
|
|
225
|
-
const
|
|
226
|
-
|
|
361
|
+
register(unit, offset = 3) {
|
|
362
|
+
const unitWithMetadata = unit;
|
|
363
|
+
let name = unitWithMetadata._name;
|
|
364
|
+
if (!name) {
|
|
365
|
+
const isGuard2 = "state" in unit;
|
|
366
|
+
name = this.generateAutoName(isGuard2 ? "guard" : "source", offset);
|
|
367
|
+
unitWithMetadata._name = name;
|
|
368
|
+
}
|
|
369
|
+
unitWithMetadata._generation = this.currentGeneration;
|
|
370
|
+
if (this.units.has(name)) {
|
|
371
|
+
this.scheduleCleanup();
|
|
372
|
+
}
|
|
373
|
+
this.units.set(name, unit);
|
|
227
374
|
this.listeners.forEach((l) => l(unit));
|
|
228
375
|
}
|
|
229
376
|
/**
|
|
@@ -244,19 +391,32 @@ var Registry = class {
|
|
|
244
391
|
this.listeners.delete(listener);
|
|
245
392
|
};
|
|
246
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* Clears all registered units.
|
|
396
|
+
*/
|
|
397
|
+
reset() {
|
|
398
|
+
this.units.clear();
|
|
399
|
+
this.currentGeneration = 0;
|
|
400
|
+
this.autoNameCache.clear();
|
|
401
|
+
}
|
|
247
402
|
};
|
|
248
|
-
var
|
|
403
|
+
var GLOBAL_KEY = "__PULSE_REGISTRY__";
|
|
404
|
+
var globalSymbols = globalThis;
|
|
405
|
+
if (!globalSymbols[GLOBAL_KEY]) {
|
|
406
|
+
globalSymbols[GLOBAL_KEY] = new Registry();
|
|
407
|
+
}
|
|
408
|
+
var PulseRegistry = globalSymbols[GLOBAL_KEY];
|
|
249
409
|
|
|
250
410
|
// src/source.ts
|
|
251
|
-
function source(initialValue, options = {}) {
|
|
411
|
+
function source(initialValue, options = {}, _internalOffset = 3) {
|
|
252
412
|
let value = initialValue;
|
|
253
413
|
const subscribers = /* @__PURE__ */ new Set();
|
|
254
414
|
const dependents = /* @__PURE__ */ new Set();
|
|
255
415
|
const s = (() => {
|
|
256
|
-
const
|
|
257
|
-
if (
|
|
258
|
-
dependents.add(
|
|
259
|
-
|
|
416
|
+
const activeGuard = getCurrentGuard();
|
|
417
|
+
if (activeGuard) {
|
|
418
|
+
dependents.add(activeGuard);
|
|
419
|
+
activeGuard.addDependency(s);
|
|
260
420
|
}
|
|
261
421
|
return value;
|
|
262
422
|
});
|
|
@@ -287,7 +447,7 @@ function compute(name, dependencies, processor) {
|
|
|
287
447
|
return guard(name, () => {
|
|
288
448
|
const values = dependencies.map((dep) => typeof dep === "function" ? dep() : dep);
|
|
289
449
|
return processor(...values);
|
|
290
|
-
});
|
|
450
|
+
}, 4);
|
|
291
451
|
}
|
|
292
452
|
|
|
293
453
|
// src/composition.ts
|
|
@@ -305,10 +465,12 @@ function guardAll(nameOrGuards, maybeGuards) {
|
|
|
305
465
|
}
|
|
306
466
|
}
|
|
307
467
|
if (firstFail) {
|
|
308
|
-
|
|
468
|
+
const reason = firstFail.reason();
|
|
469
|
+
const message = typeof reason === "string" ? reason : reason?.toString() || "condition failed";
|
|
470
|
+
throw new Error(message);
|
|
309
471
|
}
|
|
310
472
|
return true;
|
|
311
|
-
});
|
|
473
|
+
}, 4);
|
|
312
474
|
}
|
|
313
475
|
function guardAny(nameOrGuards, maybeGuards) {
|
|
314
476
|
const name = typeof nameOrGuards === "string" ? nameOrGuards : void 0;
|
|
@@ -317,10 +479,12 @@ function guardAny(nameOrGuards, maybeGuards) {
|
|
|
317
479
|
let allFails = [];
|
|
318
480
|
for (const g of guards) {
|
|
319
481
|
if (g.ok()) return true;
|
|
320
|
-
|
|
482
|
+
const reason = g.reason();
|
|
483
|
+
const message = typeof reason === "string" ? reason : reason?.toString() || "failed";
|
|
484
|
+
allFails.push(message);
|
|
321
485
|
}
|
|
322
486
|
throw new Error(allFails.length > 0 ? allFails.join(" and ") : "no conditions met");
|
|
323
|
-
});
|
|
487
|
+
}, 4);
|
|
324
488
|
}
|
|
325
489
|
function guardNot(nameOrTarget, maybeTarget) {
|
|
326
490
|
const name = typeof nameOrTarget === "string" ? nameOrTarget : void 0;
|
|
@@ -330,7 +494,7 @@ function guardNot(nameOrTarget, maybeTarget) {
|
|
|
330
494
|
return !target.ok();
|
|
331
495
|
}
|
|
332
496
|
return !target();
|
|
333
|
-
});
|
|
497
|
+
}, 4);
|
|
334
498
|
}
|
|
335
499
|
var guardExtensions = {
|
|
336
500
|
all: guardAll,
|
|
@@ -348,6 +512,8 @@ var extendedGuard = Object.assign(guard, guardExtensions);
|
|
|
348
512
|
evaluate,
|
|
349
513
|
getCurrentGuard,
|
|
350
514
|
guard,
|
|
515
|
+
guardFail,
|
|
516
|
+
guardOk,
|
|
351
517
|
hydrate,
|
|
352
518
|
registerGuardForHydration,
|
|
353
519
|
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,7 +174,19 @@ interface Guard<T = boolean> {
|
|
|
160
174
|
* });
|
|
161
175
|
* ```
|
|
162
176
|
*/
|
|
163
|
-
|
|
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;
|
|
189
|
+
declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>, _internalOffset?: number): Guard<T>;
|
|
164
190
|
|
|
165
191
|
/**
|
|
166
192
|
* Utility to transform reactive dependencies into a new derived value.
|
|
@@ -335,7 +361,7 @@ interface Source<T> {
|
|
|
335
361
|
* user.set({ name: 'Bob' });
|
|
336
362
|
* ```
|
|
337
363
|
*/
|
|
338
|
-
declare function source<T>(initialValue: T, options?: SourceOptions<T
|
|
364
|
+
declare function source<T>(initialValue: T, options?: SourceOptions<T>, _internalOffset?: number): Source<T>;
|
|
339
365
|
|
|
340
366
|
/**
|
|
341
367
|
* Serialized state of guards for transfer from server to client.
|
|
@@ -396,11 +422,29 @@ type PulseUnit = Source<any> | Guard<any>;
|
|
|
396
422
|
declare class Registry {
|
|
397
423
|
private units;
|
|
398
424
|
private listeners;
|
|
425
|
+
private currentGeneration;
|
|
426
|
+
private cleanupScheduled;
|
|
427
|
+
private autoNameCache;
|
|
428
|
+
/**
|
|
429
|
+
* Generates a stable auto-name based on source code location.
|
|
430
|
+
* Uses file path and line number to ensure the same location always gets the same name.
|
|
431
|
+
* Cached to avoid repeated stack trace parsing.
|
|
432
|
+
*/
|
|
433
|
+
generateAutoName(type: 'source' | 'guard', offset?: number): string;
|
|
434
|
+
/**
|
|
435
|
+
* Increments generation and schedules cleanup of old units.
|
|
436
|
+
* Called automatically when HMR is detected.
|
|
437
|
+
*/
|
|
438
|
+
private scheduleCleanup;
|
|
439
|
+
/**
|
|
440
|
+
* Removes units from old generations (likely orphaned by HMR).
|
|
441
|
+
*/
|
|
442
|
+
private cleanupOldGenerations;
|
|
399
443
|
/**
|
|
400
444
|
* Registers a new unit (Source or Guard).
|
|
401
|
-
*
|
|
445
|
+
* Auto-assigns stable names to unnamed units for HMR stability.
|
|
402
446
|
*/
|
|
403
|
-
register(unit: PulseUnit): void;
|
|
447
|
+
register(unit: PulseUnit, offset?: number): void;
|
|
404
448
|
/**
|
|
405
449
|
* Retrieves all registered units.
|
|
406
450
|
*/
|
|
@@ -412,6 +456,10 @@ declare class Registry {
|
|
|
412
456
|
* @returns Unsubscribe function.
|
|
413
457
|
*/
|
|
414
458
|
onRegister(listener: (unit: PulseUnit) => void): () => void;
|
|
459
|
+
/**
|
|
460
|
+
* Clears all registered units.
|
|
461
|
+
*/
|
|
462
|
+
reset(): void;
|
|
415
463
|
}
|
|
416
464
|
declare const PulseRegistry: Registry;
|
|
417
465
|
|
|
@@ -434,4 +482,4 @@ declare const extendedGuard: typeof guard & {
|
|
|
434
482
|
compute: typeof compute;
|
|
435
483
|
};
|
|
436
484
|
|
|
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 };
|
|
485
|
+
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,7 +174,19 @@ interface Guard<T = boolean> {
|
|
|
160
174
|
* });
|
|
161
175
|
* ```
|
|
162
176
|
*/
|
|
163
|
-
|
|
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;
|
|
189
|
+
declare function guard<T = boolean>(nameOrFn?: string | (() => T | Promise<T>), fn?: () => T | Promise<T>, _internalOffset?: number): Guard<T>;
|
|
164
190
|
|
|
165
191
|
/**
|
|
166
192
|
* Utility to transform reactive dependencies into a new derived value.
|
|
@@ -335,7 +361,7 @@ interface Source<T> {
|
|
|
335
361
|
* user.set({ name: 'Bob' });
|
|
336
362
|
* ```
|
|
337
363
|
*/
|
|
338
|
-
declare function source<T>(initialValue: T, options?: SourceOptions<T
|
|
364
|
+
declare function source<T>(initialValue: T, options?: SourceOptions<T>, _internalOffset?: number): Source<T>;
|
|
339
365
|
|
|
340
366
|
/**
|
|
341
367
|
* Serialized state of guards for transfer from server to client.
|
|
@@ -396,11 +422,29 @@ type PulseUnit = Source<any> | Guard<any>;
|
|
|
396
422
|
declare class Registry {
|
|
397
423
|
private units;
|
|
398
424
|
private listeners;
|
|
425
|
+
private currentGeneration;
|
|
426
|
+
private cleanupScheduled;
|
|
427
|
+
private autoNameCache;
|
|
428
|
+
/**
|
|
429
|
+
* Generates a stable auto-name based on source code location.
|
|
430
|
+
* Uses file path and line number to ensure the same location always gets the same name.
|
|
431
|
+
* Cached to avoid repeated stack trace parsing.
|
|
432
|
+
*/
|
|
433
|
+
generateAutoName(type: 'source' | 'guard', offset?: number): string;
|
|
434
|
+
/**
|
|
435
|
+
* Increments generation and schedules cleanup of old units.
|
|
436
|
+
* Called automatically when HMR is detected.
|
|
437
|
+
*/
|
|
438
|
+
private scheduleCleanup;
|
|
439
|
+
/**
|
|
440
|
+
* Removes units from old generations (likely orphaned by HMR).
|
|
441
|
+
*/
|
|
442
|
+
private cleanupOldGenerations;
|
|
399
443
|
/**
|
|
400
444
|
* Registers a new unit (Source or Guard).
|
|
401
|
-
*
|
|
445
|
+
* Auto-assigns stable names to unnamed units for HMR stability.
|
|
402
446
|
*/
|
|
403
|
-
register(unit: PulseUnit): void;
|
|
447
|
+
register(unit: PulseUnit, offset?: number): void;
|
|
404
448
|
/**
|
|
405
449
|
* Retrieves all registered units.
|
|
406
450
|
*/
|
|
@@ -412,6 +456,10 @@ declare class Registry {
|
|
|
412
456
|
* @returns Unsubscribe function.
|
|
413
457
|
*/
|
|
414
458
|
onRegister(listener: (unit: PulseUnit) => void): () => void;
|
|
459
|
+
/**
|
|
460
|
+
* Clears all registered units.
|
|
461
|
+
*/
|
|
462
|
+
reset(): void;
|
|
415
463
|
}
|
|
416
464
|
declare const PulseRegistry: Registry;
|
|
417
465
|
|
|
@@ -434,4 +482,4 @@ declare const extendedGuard: typeof guard & {
|
|
|
434
482
|
compute: typeof compute;
|
|
435
483
|
};
|
|
436
484
|
|
|
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 };
|
|
485
|
+
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,7 +45,21 @@ function hydrate(state) {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
// src/guard.ts
|
|
46
|
-
function
|
|
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
|
+
}
|
|
62
|
+
function guard(nameOrFn, fn, _internalOffset = 3) {
|
|
47
63
|
const name = typeof nameOrFn === "string" ? nameOrFn : void 0;
|
|
48
64
|
const evaluator = typeof nameOrFn === "function" ? nameOrFn : fn;
|
|
49
65
|
if (!evaluator) {
|
|
@@ -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
|
|
|
@@ -183,13 +257,84 @@ function guard(nameOrFn, fn) {
|
|
|
183
257
|
var Registry = class {
|
|
184
258
|
units = /* @__PURE__ */ new Map();
|
|
185
259
|
listeners = /* @__PURE__ */ new Set();
|
|
260
|
+
currentGeneration = 0;
|
|
261
|
+
cleanupScheduled = false;
|
|
262
|
+
autoNameCache = /* @__PURE__ */ new Map();
|
|
263
|
+
/**
|
|
264
|
+
* Generates a stable auto-name based on source code location.
|
|
265
|
+
* Uses file path and line number to ensure the same location always gets the same name.
|
|
266
|
+
* Cached to avoid repeated stack trace parsing.
|
|
267
|
+
*/
|
|
268
|
+
generateAutoName(type, offset = 3) {
|
|
269
|
+
const err = new Error();
|
|
270
|
+
const stack = err.stack?.split("\n") || [];
|
|
271
|
+
let callSite = stack[offset]?.trim() || "";
|
|
272
|
+
const cacheKey = `${type}:${callSite}`;
|
|
273
|
+
if (this.autoNameCache.has(cacheKey)) {
|
|
274
|
+
return this.autoNameCache.get(cacheKey);
|
|
275
|
+
}
|
|
276
|
+
const match = callSite.match(/([^/\\]+)\.(?:ts|tsx|js|jsx):(\d+):\d+/);
|
|
277
|
+
let name;
|
|
278
|
+
if (match) {
|
|
279
|
+
const filename = match[1];
|
|
280
|
+
const line = match[2];
|
|
281
|
+
if (filename && line) {
|
|
282
|
+
name = `${type}@${filename}:${line}`;
|
|
283
|
+
} else {
|
|
284
|
+
name = `${type}#${Math.random().toString(36).substring(2, 7)}`;
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
name = `${type}#${Math.random().toString(36).substring(2, 7)}`;
|
|
288
|
+
}
|
|
289
|
+
this.autoNameCache.set(cacheKey, name);
|
|
290
|
+
return name;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Increments generation and schedules cleanup of old units.
|
|
294
|
+
* Called automatically when HMR is detected.
|
|
295
|
+
*/
|
|
296
|
+
scheduleCleanup() {
|
|
297
|
+
if (this.cleanupScheduled) return;
|
|
298
|
+
this.cleanupScheduled = true;
|
|
299
|
+
this.currentGeneration++;
|
|
300
|
+
setTimeout(() => {
|
|
301
|
+
this.cleanupOldGenerations();
|
|
302
|
+
this.cleanupScheduled = false;
|
|
303
|
+
}, 100);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Removes units from old generations (likely orphaned by HMR).
|
|
307
|
+
*/
|
|
308
|
+
cleanupOldGenerations() {
|
|
309
|
+
const toDelete = [];
|
|
310
|
+
this.units.forEach((unit, key) => {
|
|
311
|
+
const gen = unit._generation;
|
|
312
|
+
if (gen !== void 0 && gen < this.currentGeneration) {
|
|
313
|
+
toDelete.push(key);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
toDelete.forEach((key) => this.units.delete(key));
|
|
317
|
+
if (toDelete.length > 0) {
|
|
318
|
+
console.log(`[Pulse] Cleaned up ${toDelete.length} stale units after HMR`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
186
321
|
/**
|
|
187
322
|
* Registers a new unit (Source or Guard).
|
|
188
|
-
*
|
|
323
|
+
* Auto-assigns stable names to unnamed units for HMR stability.
|
|
189
324
|
*/
|
|
190
|
-
register(unit) {
|
|
191
|
-
const
|
|
192
|
-
|
|
325
|
+
register(unit, offset = 3) {
|
|
326
|
+
const unitWithMetadata = unit;
|
|
327
|
+
let name = unitWithMetadata._name;
|
|
328
|
+
if (!name) {
|
|
329
|
+
const isGuard2 = "state" in unit;
|
|
330
|
+
name = this.generateAutoName(isGuard2 ? "guard" : "source", offset);
|
|
331
|
+
unitWithMetadata._name = name;
|
|
332
|
+
}
|
|
333
|
+
unitWithMetadata._generation = this.currentGeneration;
|
|
334
|
+
if (this.units.has(name)) {
|
|
335
|
+
this.scheduleCleanup();
|
|
336
|
+
}
|
|
337
|
+
this.units.set(name, unit);
|
|
193
338
|
this.listeners.forEach((l) => l(unit));
|
|
194
339
|
}
|
|
195
340
|
/**
|
|
@@ -210,19 +355,32 @@ var Registry = class {
|
|
|
210
355
|
this.listeners.delete(listener);
|
|
211
356
|
};
|
|
212
357
|
}
|
|
358
|
+
/**
|
|
359
|
+
* Clears all registered units.
|
|
360
|
+
*/
|
|
361
|
+
reset() {
|
|
362
|
+
this.units.clear();
|
|
363
|
+
this.currentGeneration = 0;
|
|
364
|
+
this.autoNameCache.clear();
|
|
365
|
+
}
|
|
213
366
|
};
|
|
214
|
-
var
|
|
367
|
+
var GLOBAL_KEY = "__PULSE_REGISTRY__";
|
|
368
|
+
var globalSymbols = globalThis;
|
|
369
|
+
if (!globalSymbols[GLOBAL_KEY]) {
|
|
370
|
+
globalSymbols[GLOBAL_KEY] = new Registry();
|
|
371
|
+
}
|
|
372
|
+
var PulseRegistry = globalSymbols[GLOBAL_KEY];
|
|
215
373
|
|
|
216
374
|
// src/source.ts
|
|
217
|
-
function source(initialValue, options = {}) {
|
|
375
|
+
function source(initialValue, options = {}, _internalOffset = 3) {
|
|
218
376
|
let value = initialValue;
|
|
219
377
|
const subscribers = /* @__PURE__ */ new Set();
|
|
220
378
|
const dependents = /* @__PURE__ */ new Set();
|
|
221
379
|
const s = (() => {
|
|
222
|
-
const
|
|
223
|
-
if (
|
|
224
|
-
dependents.add(
|
|
225
|
-
|
|
380
|
+
const activeGuard = getCurrentGuard();
|
|
381
|
+
if (activeGuard) {
|
|
382
|
+
dependents.add(activeGuard);
|
|
383
|
+
activeGuard.addDependency(s);
|
|
226
384
|
}
|
|
227
385
|
return value;
|
|
228
386
|
});
|
|
@@ -253,7 +411,7 @@ function compute(name, dependencies, processor) {
|
|
|
253
411
|
return guard(name, () => {
|
|
254
412
|
const values = dependencies.map((dep) => typeof dep === "function" ? dep() : dep);
|
|
255
413
|
return processor(...values);
|
|
256
|
-
});
|
|
414
|
+
}, 4);
|
|
257
415
|
}
|
|
258
416
|
|
|
259
417
|
// src/composition.ts
|
|
@@ -271,10 +429,12 @@ function guardAll(nameOrGuards, maybeGuards) {
|
|
|
271
429
|
}
|
|
272
430
|
}
|
|
273
431
|
if (firstFail) {
|
|
274
|
-
|
|
432
|
+
const reason = firstFail.reason();
|
|
433
|
+
const message = typeof reason === "string" ? reason : reason?.toString() || "condition failed";
|
|
434
|
+
throw new Error(message);
|
|
275
435
|
}
|
|
276
436
|
return true;
|
|
277
|
-
});
|
|
437
|
+
}, 4);
|
|
278
438
|
}
|
|
279
439
|
function guardAny(nameOrGuards, maybeGuards) {
|
|
280
440
|
const name = typeof nameOrGuards === "string" ? nameOrGuards : void 0;
|
|
@@ -283,10 +443,12 @@ function guardAny(nameOrGuards, maybeGuards) {
|
|
|
283
443
|
let allFails = [];
|
|
284
444
|
for (const g of guards) {
|
|
285
445
|
if (g.ok()) return true;
|
|
286
|
-
|
|
446
|
+
const reason = g.reason();
|
|
447
|
+
const message = typeof reason === "string" ? reason : reason?.toString() || "failed";
|
|
448
|
+
allFails.push(message);
|
|
287
449
|
}
|
|
288
450
|
throw new Error(allFails.length > 0 ? allFails.join(" and ") : "no conditions met");
|
|
289
|
-
});
|
|
451
|
+
}, 4);
|
|
290
452
|
}
|
|
291
453
|
function guardNot(nameOrTarget, maybeTarget) {
|
|
292
454
|
const name = typeof nameOrTarget === "string" ? nameOrTarget : void 0;
|
|
@@ -296,7 +458,7 @@ function guardNot(nameOrTarget, maybeTarget) {
|
|
|
296
458
|
return !target.ok();
|
|
297
459
|
}
|
|
298
460
|
return !target();
|
|
299
|
-
});
|
|
461
|
+
}, 4);
|
|
300
462
|
}
|
|
301
463
|
var guardExtensions = {
|
|
302
464
|
all: guardAll,
|
|
@@ -313,6 +475,8 @@ export {
|
|
|
313
475
|
evaluate,
|
|
314
476
|
getCurrentGuard,
|
|
315
477
|
extendedGuard as guard,
|
|
478
|
+
guardFail,
|
|
479
|
+
guardOk,
|
|
316
480
|
hydrate,
|
|
317
481
|
registerGuardForHydration,
|
|
318
482
|
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.9",
|
|
4
4
|
"module": "dist/index.js",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -17,7 +17,9 @@
|
|
|
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",
|
|
22
|
+
"tests/*"
|
|
21
23
|
],
|
|
22
24
|
"keywords": [
|
|
23
25
|
"reactivity",
|
|
@@ -36,7 +38,7 @@
|
|
|
36
38
|
"directory": "packages/core"
|
|
37
39
|
},
|
|
38
40
|
"bugs": {
|
|
39
|
-
"url": "https://github.com/ZtaMDev/pulse/issues"
|
|
41
|
+
"url": "https://github.com/ZtaMDev/pulse-js/issues"
|
|
40
42
|
},
|
|
41
43
|
"license": "MIT",
|
|
42
44
|
"scripts": {
|
|
@@ -46,12 +48,22 @@
|
|
|
46
48
|
"test": "vitest run"
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
51
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
52
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
53
|
+
"@testing-library/svelte": "^5.3.1",
|
|
54
|
+
"@testing-library/vue": "^8.1.0",
|
|
49
55
|
"@types/bun": "latest",
|
|
50
56
|
"@types/react": "^19.2.8",
|
|
51
57
|
"@types/react-dom": "^19.2.3",
|
|
58
|
+
"@vitejs/plugin-react": "^5.1.2",
|
|
59
|
+
"@vitejs/plugin-vue": "^6.0.3",
|
|
60
|
+
"@vue/test-utils": "^2.4.6",
|
|
61
|
+
"jsdom": "^27.4.0",
|
|
52
62
|
"react": "^19.2.3",
|
|
53
63
|
"react-dom": "^19.2.3",
|
|
54
|
-
"
|
|
64
|
+
"svelte": "^5.46.4",
|
|
65
|
+
"vitest": "^4.0.17",
|
|
66
|
+
"vue": "^3.5.26"
|
|
55
67
|
},
|
|
56
68
|
"peerDependencies": {
|
|
57
69
|
"typescript": "^5"
|