@pulse-js/core 0.2.2 → 0.3.0
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 +42 -117
- package/dist/index.cjs +313 -52
- package/dist/index.d.cts +160 -24
- package/dist/index.d.ts +160 -24
- package/dist/index.js +305 -51
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,172 +1,97 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
|
|
3
2
|
<img width="200" height="200" alt="logo" src="https://raw.githubusercontent.com/ZtaMDev/Pulse/refs/heads/main/pulse.svg" />
|
|
4
3
|
|
|
5
|
-
# Pulse-JS
|
|
4
|
+
# Pulse-JS v2.0
|
|
6
5
|
|
|
7
6
|
[](https://www.npmjs.com/package/@pulse-js/core)
|
|
8
7
|
|
|
9
|
-
> A semantic reactivity system for modern applications. Separate reactive
|
|
8
|
+
> A semantic reactivity system for modern applications. Separate reactive state (pulse) from business conditions (guards) with a declarative, composable, and proxied approach.
|
|
10
9
|
|
|
11
10
|
Official [Documentation](https://pulse-js.vercel.app)
|
|
12
11
|
|
|
13
|
-
Pulse differs from traditional signals or state managers by treating `Conditions` as first-class citizens. Instead of embedding complex boolean logic inside components
|
|
12
|
+
Pulse differs from traditional signals or state managers by treating `Conditions` as first-class citizens. Instead of embedding complex boolean logic inside components, you define **Semantic Guards** that are observed, composed, and debugged independently.
|
|
14
13
|
|
|
15
14
|
</div>
|
|
16
15
|
|
|
17
16
|
## Installation
|
|
18
17
|
|
|
19
|
-
```bash
|
|
20
|
-
npm install @pulse-js/core @pulse-js/tools
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
### or
|
|
24
|
-
|
|
25
18
|
```bash
|
|
26
19
|
bun add @pulse-js/core @pulse-js/tools
|
|
27
20
|
```
|
|
28
21
|
|
|
29
|
-
Pulse works with React via adapters like `@pulse-js/react`.
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
bun add @pulse-js/react
|
|
33
|
-
```
|
|
34
|
-
|
|
35
22
|
## Core Concepts
|
|
36
23
|
|
|
37
|
-
###
|
|
24
|
+
### Pulse Objects (Universal Reactivity)
|
|
38
25
|
|
|
39
|
-
|
|
26
|
+
`pulse()` creates a deep reactive Proxy. Use it for complex objects and arrays. Track property access automatically and mutate state directly.
|
|
40
27
|
|
|
41
28
|
```typescript
|
|
42
|
-
import {
|
|
29
|
+
import { pulse } from "@pulse-js/core";
|
|
30
|
+
|
|
31
|
+
const user = pulse({
|
|
32
|
+
name: "Alice",
|
|
33
|
+
role: "admin",
|
|
34
|
+
increment() {
|
|
35
|
+
this.stats.clicks++;
|
|
36
|
+
},
|
|
37
|
+
stats: { clicks: 0 },
|
|
38
|
+
});
|
|
43
39
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
// Mutate directly - it's reactive!
|
|
41
|
+
user.name = "Bob";
|
|
42
|
+
user.increment();
|
|
43
|
+
```
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
console.log(user());
|
|
45
|
+
### Sources (Primitive Records)
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
user.set({ name: "Bob", id: 1 });
|
|
47
|
+
Sources are primitive containers for your application state. They hold values and notify dependents when those values change.
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
```typescript
|
|
50
|
+
import { source } from "@pulse-js/core";
|
|
51
|
+
const count = source(0);
|
|
52
|
+
count.set(5);
|
|
56
53
|
```
|
|
57
54
|
|
|
58
55
|
### Guards (Semantic Logic)
|
|
59
56
|
|
|
60
|
-
Guards represent business rules or derivations.
|
|
57
|
+
Guards represent business rules or derivations. They act as high-performance **Selectors** with built-in dependency tracking and rich failure context.
|
|
61
58
|
|
|
62
59
|
```typescript
|
|
63
60
|
import { guard } from "@pulse-js/core";
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
const isAdmin = guard("is-admin", () => {
|
|
62
|
+
const isAuthorized = guard("auth-check", async () => {
|
|
67
63
|
const u = user();
|
|
68
|
-
if (u
|
|
69
|
-
return true;
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Guards can be checked explicitly
|
|
73
|
-
if (isAdmin.ok()) {
|
|
74
|
-
// Grant access
|
|
75
|
-
} else {
|
|
76
|
-
console.log(isAdmin.reason()); // e.g. "is-admin failed"
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Computed Values
|
|
81
|
-
|
|
82
|
-
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.
|
|
83
|
-
|
|
84
|
-
```typescript
|
|
85
|
-
import { compute } from "@pulse-js/core";
|
|
86
|
-
|
|
87
|
-
const fullName = compute("full-name", [firstName, lastName], (first, last) => {
|
|
88
|
-
return `${first} ${last}`;
|
|
89
|
-
});
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Async Guards & Race Control
|
|
93
|
-
|
|
94
|
-
Pulse handles asynchronous logic natively. Guards can return Promises, and their status will automatically transition from `pending` to `ok` or `fail`.
|
|
95
|
-
|
|
96
|
-
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.
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
const isServerOnline = guard("check-server", async () => {
|
|
100
|
-
const response = await fetch("/health");
|
|
101
|
-
if (!response.ok) throw new Error("Server unreachable");
|
|
64
|
+
if (!u) return false;
|
|
102
65
|
return true;
|
|
103
66
|
});
|
|
104
67
|
```
|
|
105
68
|
|
|
106
69
|
### Explanable Guards
|
|
107
70
|
|
|
108
|
-
|
|
71
|
+
Get a structured tree of the current status, failure reasons, and the status of all direct dependencies.
|
|
109
72
|
|
|
110
73
|
```ts
|
|
111
74
|
const explanation = canCheckout.explain();
|
|
112
|
-
|
|
113
|
-
// { status: 'fail', reason: 'auth failed', dependencies: [...] }
|
|
75
|
+
// { status: 'fail', reason: { code: 'AUTH', message: '...' }, dependencies: [...] }
|
|
114
76
|
```
|
|
115
77
|
|
|
116
|
-
## Server-Side Rendering (SSR)
|
|
117
|
-
|
|
118
|
-
Pulse is designed with SSR in mind. It supports isomorphic rendering where async guards can be evaluated on the server, their state serialized, and then hydrated on the client.
|
|
119
|
-
|
|
120
|
-
### Server Side
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
import { evaluate } from "@pulse-js/core";
|
|
124
|
-
|
|
125
|
-
// 1. Evaluate critical guards on the server
|
|
126
|
-
const hydrationState = await evaluate([isUserAuthenticated, appSettings]);
|
|
127
|
-
|
|
128
|
-
// 2. Serialize this state into your HTML
|
|
129
|
-
const html = `
|
|
130
|
-
<script>window.__PULSE_STATE__ = ${JSON.stringify(hydrationState)}</script>
|
|
131
|
-
`;
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### Client Side (Hydration)
|
|
135
|
-
|
|
136
|
-
```typescript
|
|
137
|
-
import { hydrate } from "@pulse-js/core";
|
|
138
|
-
|
|
139
|
-
// 1. Hydrate before rendering
|
|
140
|
-
hydrate(window.__PULSE_STATE__);
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### Mental Model
|
|
144
|
-
|
|
145
|
-
Compare Pulse primitives:
|
|
146
|
-
|
|
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). |
|
|
152
|
-
|
|
153
78
|
## Framework Integrations
|
|
154
79
|
|
|
155
|
-
Pulse provides official adapters for major frameworks
|
|
80
|
+
Pulse provides official adapters for all major frameworks.
|
|
156
81
|
|
|
157
|
-
| Framework
|
|
158
|
-
|
|
|
159
|
-
| **
|
|
160
|
-
| **
|
|
161
|
-
| **
|
|
82
|
+
| Framework | Package | Status |
|
|
83
|
+
| :----------- | :------------------- | :----- |
|
|
84
|
+
| **Astro** | `@pulse-js/astro` | NEW |
|
|
85
|
+
| **React** | `@pulse-js/react` | Stable |
|
|
86
|
+
| **Vue** | `@pulse-js/vue` | Stable |
|
|
87
|
+
| **Svelte** | `@pulse-js/svelte` | Stable |
|
|
88
|
+
| **TanStack** | `@pulse-js/tanstack` | NEW |
|
|
162
89
|
|
|
163
90
|
## Developer Tools
|
|
164
91
|
|
|
165
|
-
Debug your reactive graph with **[Pulse Tools](https://pulse-js.vercel.app/guides/devtools/)**, a
|
|
166
|
-
|
|
167
|
-
### Features
|
|
92
|
+
Debug your reactive graph with **[Pulse Tools](https://pulse-js.vercel.app/guides/devtools/)**, a decoupled Agent-Client inspector.
|
|
168
93
|
|
|
169
|
-
- **
|
|
170
|
-
- **
|
|
171
|
-
- **
|
|
94
|
+
- **Agent-Client Architecture**: Isolated UI that doesn't restart your app.
|
|
95
|
+
- **Dependency Graph**: Visualize how your guards are connected.
|
|
96
|
+
- **Glassmorphism UI**: A premium, draggable development experience.
|
|
172
97
|
- **Zero Config**: Works out of the box with `@pulse-js/tools`.
|
package/dist/index.cjs
CHANGED
|
@@ -21,16 +21,23 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
PulseRegistry: () => PulseRegistry,
|
|
24
|
+
batch: () => batch,
|
|
24
25
|
compute: () => compute,
|
|
26
|
+
createSignal: () => createSignal,
|
|
27
|
+
effect: () => effect,
|
|
25
28
|
evaluate: () => evaluate,
|
|
26
29
|
getCurrentGuard: () => getCurrentGuard,
|
|
27
30
|
guard: () => extendedGuard,
|
|
28
31
|
guardFail: () => guardFail,
|
|
29
32
|
guardOk: () => guardOk,
|
|
30
33
|
hydrate: () => hydrate,
|
|
34
|
+
isPulseObject: () => isPulseObject,
|
|
35
|
+
pulse: () => pulse,
|
|
36
|
+
readonly: () => readonly,
|
|
31
37
|
registerGuardForHydration: () => registerGuardForHydration,
|
|
32
38
|
runInContext: () => runInContext,
|
|
33
|
-
source: () => source
|
|
39
|
+
source: () => source,
|
|
40
|
+
toRaw: () => toRaw
|
|
34
41
|
});
|
|
35
42
|
module.exports = __toCommonJS(index_exports);
|
|
36
43
|
|
|
@@ -316,9 +323,8 @@ function guard(nameOrFn, fn) {
|
|
|
316
323
|
if (name) {
|
|
317
324
|
registerGuardForHydration(name, g);
|
|
318
325
|
}
|
|
319
|
-
PulseRegistry.register(g);
|
|
320
326
|
evaluate2();
|
|
321
|
-
return g;
|
|
327
|
+
return PulseRegistry.register(g);
|
|
322
328
|
}
|
|
323
329
|
guard.map = function(source2, mapper, name) {
|
|
324
330
|
const guardName = name || `map-${source2._name || "source"}`;
|
|
@@ -327,69 +333,132 @@ guard.map = function(source2, mapper, name) {
|
|
|
327
333
|
return mapper(value);
|
|
328
334
|
});
|
|
329
335
|
};
|
|
336
|
+
guard.select = function(pulseObj, selector, name) {
|
|
337
|
+
const guardName = name || `select-${pulseObj._name || "pulse"}`;
|
|
338
|
+
return guard(guardName, () => {
|
|
339
|
+
return selector(pulseObj);
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
guard.from = function(getValue, options) {
|
|
343
|
+
const name = options?.name || "from-external";
|
|
344
|
+
return guard(name, () => {
|
|
345
|
+
const result = getValue();
|
|
346
|
+
if (result && typeof result === "object" && ("value" in result || "isLoading" in result || "error" in result)) {
|
|
347
|
+
const wrapped = result;
|
|
348
|
+
if (wrapped.isLoading) {
|
|
349
|
+
return void 0;
|
|
350
|
+
}
|
|
351
|
+
if (wrapped.error) {
|
|
352
|
+
guardFail(wrapped.error?.message || "External error");
|
|
353
|
+
}
|
|
354
|
+
return wrapped.value;
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
});
|
|
358
|
+
};
|
|
330
359
|
|
|
331
360
|
// src/registry.ts
|
|
361
|
+
function generateUID(name, sourceInfo) {
|
|
362
|
+
if (sourceInfo?.file && sourceInfo?.line) {
|
|
363
|
+
return `${sourceInfo.file}:${sourceInfo.line}:${name}`;
|
|
364
|
+
}
|
|
365
|
+
return `pulse:${name}`;
|
|
366
|
+
}
|
|
332
367
|
var Registry = class {
|
|
333
|
-
|
|
368
|
+
targets = /* @__PURE__ */ new Map();
|
|
369
|
+
proxies = /* @__PURE__ */ new Map();
|
|
334
370
|
listeners = /* @__PURE__ */ new Set();
|
|
335
371
|
currentGeneration = 0;
|
|
336
372
|
cleanupScheduled = false;
|
|
373
|
+
hmrDebounce = null;
|
|
374
|
+
/**
|
|
375
|
+
* Registers a unit and returns a stable Identity Proxy.
|
|
376
|
+
*
|
|
377
|
+
* If a unit with the same UID already exists, it updates the internal
|
|
378
|
+
* target of the existing proxy and returns that same proxy.
|
|
379
|
+
*/
|
|
380
|
+
register(unit) {
|
|
381
|
+
const meta = unit;
|
|
382
|
+
const name = meta._name;
|
|
383
|
+
if (!name) return unit;
|
|
384
|
+
const uid = generateUID(name, meta._sourceInfo);
|
|
385
|
+
meta._uid = uid;
|
|
386
|
+
const existingTarget = this.targets.get(uid);
|
|
387
|
+
const existingProxy = this.proxies.get(uid);
|
|
388
|
+
if (existingProxy) {
|
|
389
|
+
if (this.targets.get(uid) !== unit) {
|
|
390
|
+
if (this.currentGeneration === unit._generation) {
|
|
391
|
+
}
|
|
392
|
+
this.targets.set(uid, unit);
|
|
393
|
+
this.notifyListeners(unit, "update");
|
|
394
|
+
}
|
|
395
|
+
return existingProxy;
|
|
396
|
+
}
|
|
397
|
+
this.targets.set(uid, unit);
|
|
398
|
+
const self = this;
|
|
399
|
+
const proxy = new Proxy((() => {
|
|
400
|
+
}), {
|
|
401
|
+
get(_, prop) {
|
|
402
|
+
const target = self.targets.get(uid);
|
|
403
|
+
if (!target) return void 0;
|
|
404
|
+
const value = target[prop];
|
|
405
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
406
|
+
},
|
|
407
|
+
apply(_, thisArg, args) {
|
|
408
|
+
const target = self.targets.get(uid);
|
|
409
|
+
if (typeof target !== "function") return void 0;
|
|
410
|
+
return Reflect.apply(target, thisArg, args);
|
|
411
|
+
},
|
|
412
|
+
// Ensure type checking and other proxy traps work
|
|
413
|
+
getPrototypeOf(_) {
|
|
414
|
+
return Object.getPrototypeOf(self.targets.get(uid) || {});
|
|
415
|
+
},
|
|
416
|
+
has(_, prop) {
|
|
417
|
+
return Reflect.has(self.targets.get(uid) || {}, prop);
|
|
418
|
+
},
|
|
419
|
+
ownKeys(_) {
|
|
420
|
+
return Reflect.ownKeys(self.targets.get(uid) || {});
|
|
421
|
+
},
|
|
422
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
423
|
+
return Reflect.getOwnPropertyDescriptor(self.targets.get(uid) || {}, prop);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
this.proxies.set(uid, proxy);
|
|
427
|
+
this.notifyListeners(unit, "add");
|
|
428
|
+
return proxy;
|
|
429
|
+
}
|
|
337
430
|
/**
|
|
338
|
-
* Schedules cleanup of units that weren't re-registered
|
|
431
|
+
* Schedules cleanup of units that weren't re-registered.
|
|
339
432
|
*/
|
|
340
433
|
scheduleCleanup() {
|
|
341
434
|
if (this.cleanupScheduled) return;
|
|
342
435
|
this.cleanupScheduled = true;
|
|
343
|
-
|
|
436
|
+
if (this.hmrDebounce) clearTimeout(this.hmrDebounce);
|
|
437
|
+
this.hmrDebounce = setTimeout(() => {
|
|
344
438
|
this.cleanupDeadUnits();
|
|
345
439
|
this.cleanupScheduled = false;
|
|
346
|
-
|
|
440
|
+
this.hmrDebounce = null;
|
|
441
|
+
}, 150);
|
|
347
442
|
}
|
|
348
|
-
/**
|
|
349
|
-
* Removes units that weren't re-registered in the current generation.
|
|
350
|
-
* Uses mark-and-sweep: units that were re-registered have current generation,
|
|
351
|
-
* units that weren't are from old generation and should be removed.
|
|
352
|
-
*/
|
|
353
443
|
cleanupDeadUnits() {
|
|
354
|
-
const toDelete = [];
|
|
355
|
-
this.units.forEach((unit, key) => {
|
|
356
|
-
const gen = unit._generation;
|
|
357
|
-
if (gen !== void 0 && gen < this.currentGeneration) {
|
|
358
|
-
toDelete.push(key);
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
toDelete.forEach((key) => this.units.delete(key));
|
|
362
|
-
if (toDelete.length > 0) {
|
|
363
|
-
console.log(`[Pulse] Cleaned up ${toDelete.length} deleted units after HMR`);
|
|
364
|
-
}
|
|
365
444
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
const existingUnit = this.units.get(name);
|
|
376
|
-
if (existingUnit) {
|
|
377
|
-
const existingGen = existingUnit?._generation;
|
|
378
|
-
if (existingGen === this.currentGeneration) {
|
|
379
|
-
unitWithMetadata._generation = this.currentGeneration;
|
|
380
|
-
this.units.set(name, unit);
|
|
381
|
-
this.listeners.forEach((l) => l(unit));
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
this.currentGeneration++;
|
|
385
|
-
this.scheduleCleanup();
|
|
386
|
-
}
|
|
387
|
-
unitWithMetadata._generation = this.currentGeneration;
|
|
388
|
-
this.units.set(name, unit);
|
|
389
|
-
this.listeners.forEach((l) => l(unit));
|
|
445
|
+
notifyListeners(unit, event) {
|
|
446
|
+
this.listeners.forEach((l) => l(unit, event));
|
|
447
|
+
}
|
|
448
|
+
get(nameOrUid) {
|
|
449
|
+
const proxy = this.proxies.get(nameOrUid);
|
|
450
|
+
if (proxy) return proxy;
|
|
451
|
+
const uid = generateUID(nameOrUid);
|
|
452
|
+
return this.proxies.get(uid);
|
|
390
453
|
}
|
|
391
454
|
getAll() {
|
|
392
|
-
return Array.from(this.
|
|
455
|
+
return Array.from(this.proxies.values());
|
|
456
|
+
}
|
|
457
|
+
getAllWithMeta() {
|
|
458
|
+
return Array.from(this.proxies.entries()).map(([uid, unit]) => ({
|
|
459
|
+
unit,
|
|
460
|
+
uid
|
|
461
|
+
}));
|
|
393
462
|
}
|
|
394
463
|
onRegister(listener) {
|
|
395
464
|
this.listeners.add(listener);
|
|
@@ -398,7 +467,8 @@ var Registry = class {
|
|
|
398
467
|
};
|
|
399
468
|
}
|
|
400
469
|
reset() {
|
|
401
|
-
this.
|
|
470
|
+
this.targets.clear();
|
|
471
|
+
this.proxies.clear();
|
|
402
472
|
this.currentGeneration = 0;
|
|
403
473
|
}
|
|
404
474
|
};
|
|
@@ -440,8 +510,192 @@ function source(initialValue, options = {}) {
|
|
|
440
510
|
return () => subscribers.delete(listener);
|
|
441
511
|
};
|
|
442
512
|
s._name = options.name;
|
|
443
|
-
PulseRegistry.register(s);
|
|
444
|
-
|
|
513
|
+
return PulseRegistry.register(s);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/signal.ts
|
|
517
|
+
var batchQueue = null;
|
|
518
|
+
var batchDepth = 0;
|
|
519
|
+
function batch(fn) {
|
|
520
|
+
batchDepth++;
|
|
521
|
+
if (!batchQueue) {
|
|
522
|
+
batchQueue = /* @__PURE__ */ new Set();
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
fn();
|
|
526
|
+
} finally {
|
|
527
|
+
batchDepth--;
|
|
528
|
+
if (batchDepth === 0 && batchQueue) {
|
|
529
|
+
const queue = batchQueue;
|
|
530
|
+
batchQueue = null;
|
|
531
|
+
queue.forEach((notify) => notify());
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function scheduleNotification(notify) {
|
|
536
|
+
if (batchQueue) {
|
|
537
|
+
batchQueue.add(notify);
|
|
538
|
+
} else {
|
|
539
|
+
notify();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function createSignal(initialValue, equals = (a, b) => a === b) {
|
|
543
|
+
let value = initialValue;
|
|
544
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
545
|
+
const dependents = /* @__PURE__ */ new Set();
|
|
546
|
+
const notify = () => {
|
|
547
|
+
subscribers.forEach((sub) => sub(value));
|
|
548
|
+
const deps = Array.from(dependents);
|
|
549
|
+
dependents.clear();
|
|
550
|
+
deps.forEach((dep) => dep.notify());
|
|
551
|
+
};
|
|
552
|
+
const signal = {
|
|
553
|
+
get() {
|
|
554
|
+
const activeGuard = getCurrentGuard();
|
|
555
|
+
if (activeGuard) {
|
|
556
|
+
dependents.add(activeGuard);
|
|
557
|
+
activeGuard.addDependency(signal);
|
|
558
|
+
}
|
|
559
|
+
return value;
|
|
560
|
+
},
|
|
561
|
+
peek() {
|
|
562
|
+
return value;
|
|
563
|
+
},
|
|
564
|
+
set(newValue) {
|
|
565
|
+
if (!equals(value, newValue)) {
|
|
566
|
+
value = newValue;
|
|
567
|
+
scheduleNotification(notify);
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
update(fn) {
|
|
571
|
+
signal.set(fn(value));
|
|
572
|
+
},
|
|
573
|
+
subscribe(listener) {
|
|
574
|
+
subscribers.add(listener);
|
|
575
|
+
return () => subscribers.delete(listener);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
return signal;
|
|
579
|
+
}
|
|
580
|
+
function effect(fn) {
|
|
581
|
+
let cleanup;
|
|
582
|
+
let disposed = false;
|
|
583
|
+
const run = () => {
|
|
584
|
+
if (disposed) return;
|
|
585
|
+
if (cleanup) cleanup();
|
|
586
|
+
cleanup = fn();
|
|
587
|
+
};
|
|
588
|
+
run();
|
|
589
|
+
return () => {
|
|
590
|
+
disposed = true;
|
|
591
|
+
if (cleanup) cleanup();
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/pulse.ts
|
|
596
|
+
var PULSE_META = /* @__PURE__ */ Symbol("PULSE_META");
|
|
597
|
+
function pulse(target, options = {}) {
|
|
598
|
+
const { name, deep = true } = options;
|
|
599
|
+
const signals = /* @__PURE__ */ new Map();
|
|
600
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
601
|
+
const dependents = /* @__PURE__ */ new Set();
|
|
602
|
+
const nestedCache = /* @__PURE__ */ new Map();
|
|
603
|
+
const meta = {
|
|
604
|
+
signals,
|
|
605
|
+
subscribers,
|
|
606
|
+
dependents,
|
|
607
|
+
name,
|
|
608
|
+
target
|
|
609
|
+
};
|
|
610
|
+
function getSignal(key) {
|
|
611
|
+
if (!signals.has(key)) {
|
|
612
|
+
const initialValue = target[key];
|
|
613
|
+
signals.set(key, createSignal(initialValue));
|
|
614
|
+
}
|
|
615
|
+
return signals.get(key);
|
|
616
|
+
}
|
|
617
|
+
function notify() {
|
|
618
|
+
subscribers.forEach((sub) => sub(proxy));
|
|
619
|
+
const deps = Array.from(dependents);
|
|
620
|
+
dependents.clear();
|
|
621
|
+
deps.forEach((dep) => dep.notify());
|
|
622
|
+
}
|
|
623
|
+
function trackAccess() {
|
|
624
|
+
const activeGuard = getCurrentGuard();
|
|
625
|
+
if (activeGuard) {
|
|
626
|
+
dependents.add(activeGuard);
|
|
627
|
+
activeGuard.addDependency(proxy);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const proxy = new Proxy(target, {
|
|
631
|
+
get(obj, prop) {
|
|
632
|
+
if (prop === PULSE_META) return meta;
|
|
633
|
+
if (prop === "$raw") return target;
|
|
634
|
+
if (prop === "$subscribe") {
|
|
635
|
+
return (listener) => {
|
|
636
|
+
subscribers.add(listener);
|
|
637
|
+
return () => subscribers.delete(listener);
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
if (prop === "$snapshot") {
|
|
641
|
+
return () => ({ ...target });
|
|
642
|
+
}
|
|
643
|
+
const value = obj[prop];
|
|
644
|
+
if (typeof value === "function") {
|
|
645
|
+
return value.bind(proxy);
|
|
646
|
+
}
|
|
647
|
+
trackAccess();
|
|
648
|
+
const signal = getSignal(prop);
|
|
649
|
+
const currentValue = signal.get();
|
|
650
|
+
if (deep && currentValue !== null && typeof currentValue === "object" && !isPulseObject(currentValue)) {
|
|
651
|
+
if (!nestedCache.has(prop)) {
|
|
652
|
+
nestedCache.set(prop, pulse(currentValue, { deep, name: name ? `${name}.${String(prop)}` : void 0 }));
|
|
653
|
+
}
|
|
654
|
+
return nestedCache.get(prop);
|
|
655
|
+
}
|
|
656
|
+
return currentValue;
|
|
657
|
+
},
|
|
658
|
+
set(obj, prop, value) {
|
|
659
|
+
const oldValue = obj[prop];
|
|
660
|
+
if (oldValue === value) return true;
|
|
661
|
+
obj[prop] = value;
|
|
662
|
+
getSignal(prop).set(value);
|
|
663
|
+
notify();
|
|
664
|
+
return true;
|
|
665
|
+
},
|
|
666
|
+
has(obj, prop) {
|
|
667
|
+
if (prop === PULSE_META) return true;
|
|
668
|
+
trackAccess();
|
|
669
|
+
return Reflect.has(obj, prop);
|
|
670
|
+
},
|
|
671
|
+
ownKeys(obj) {
|
|
672
|
+
trackAccess();
|
|
673
|
+
return Reflect.ownKeys(obj);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
return PulseRegistry.register(proxy);
|
|
677
|
+
}
|
|
678
|
+
function isPulseObject(value) {
|
|
679
|
+
return value !== null && typeof value === "object" && PULSE_META in value;
|
|
680
|
+
}
|
|
681
|
+
function toRaw(pulseObj) {
|
|
682
|
+
return pulseObj.$raw;
|
|
683
|
+
}
|
|
684
|
+
function readonly(pulseObj) {
|
|
685
|
+
return new Proxy(pulseObj, {
|
|
686
|
+
set() {
|
|
687
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
688
|
+
console.warn("[Pulse] Attempted to mutate a readonly pulse object");
|
|
689
|
+
}
|
|
690
|
+
return false;
|
|
691
|
+
},
|
|
692
|
+
deleteProperty() {
|
|
693
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
694
|
+
console.warn("[Pulse] Attempted to delete from a readonly pulse object");
|
|
695
|
+
}
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
});
|
|
445
699
|
}
|
|
446
700
|
|
|
447
701
|
// src/compute.ts
|
|
@@ -510,14 +764,21 @@ var extendedGuard = Object.assign(guard, guardExtensions);
|
|
|
510
764
|
// Annotate the CommonJS export names for ESM import in node:
|
|
511
765
|
0 && (module.exports = {
|
|
512
766
|
PulseRegistry,
|
|
767
|
+
batch,
|
|
513
768
|
compute,
|
|
769
|
+
createSignal,
|
|
770
|
+
effect,
|
|
514
771
|
evaluate,
|
|
515
772
|
getCurrentGuard,
|
|
516
773
|
guard,
|
|
517
774
|
guardFail,
|
|
518
775
|
guardOk,
|
|
519
776
|
hydrate,
|
|
777
|
+
isPulseObject,
|
|
778
|
+
pulse,
|
|
779
|
+
readonly,
|
|
520
780
|
registerGuardForHydration,
|
|
521
781
|
runInContext,
|
|
522
|
-
source
|
|
782
|
+
source,
|
|
783
|
+
toRaw
|
|
523
784
|
});
|