@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/dist/index.js
CHANGED
|
@@ -280,9 +280,8 @@ function guard(nameOrFn, fn) {
|
|
|
280
280
|
if (name) {
|
|
281
281
|
registerGuardForHydration(name, g);
|
|
282
282
|
}
|
|
283
|
-
PulseRegistry.register(g);
|
|
284
283
|
evaluate2();
|
|
285
|
-
return g;
|
|
284
|
+
return PulseRegistry.register(g);
|
|
286
285
|
}
|
|
287
286
|
guard.map = function(source2, mapper, name) {
|
|
288
287
|
const guardName = name || `map-${source2._name || "source"}`;
|
|
@@ -291,69 +290,132 @@ guard.map = function(source2, mapper, name) {
|
|
|
291
290
|
return mapper(value);
|
|
292
291
|
});
|
|
293
292
|
};
|
|
293
|
+
guard.select = function(pulseObj, selector, name) {
|
|
294
|
+
const guardName = name || `select-${pulseObj._name || "pulse"}`;
|
|
295
|
+
return guard(guardName, () => {
|
|
296
|
+
return selector(pulseObj);
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
guard.from = function(getValue, options) {
|
|
300
|
+
const name = options?.name || "from-external";
|
|
301
|
+
return guard(name, () => {
|
|
302
|
+
const result = getValue();
|
|
303
|
+
if (result && typeof result === "object" && ("value" in result || "isLoading" in result || "error" in result)) {
|
|
304
|
+
const wrapped = result;
|
|
305
|
+
if (wrapped.isLoading) {
|
|
306
|
+
return void 0;
|
|
307
|
+
}
|
|
308
|
+
if (wrapped.error) {
|
|
309
|
+
guardFail(wrapped.error?.message || "External error");
|
|
310
|
+
}
|
|
311
|
+
return wrapped.value;
|
|
312
|
+
}
|
|
313
|
+
return result;
|
|
314
|
+
});
|
|
315
|
+
};
|
|
294
316
|
|
|
295
317
|
// src/registry.ts
|
|
318
|
+
function generateUID(name, sourceInfo) {
|
|
319
|
+
if (sourceInfo?.file && sourceInfo?.line) {
|
|
320
|
+
return `${sourceInfo.file}:${sourceInfo.line}:${name}`;
|
|
321
|
+
}
|
|
322
|
+
return `pulse:${name}`;
|
|
323
|
+
}
|
|
296
324
|
var Registry = class {
|
|
297
|
-
|
|
325
|
+
targets = /* @__PURE__ */ new Map();
|
|
326
|
+
proxies = /* @__PURE__ */ new Map();
|
|
298
327
|
listeners = /* @__PURE__ */ new Set();
|
|
299
328
|
currentGeneration = 0;
|
|
300
329
|
cleanupScheduled = false;
|
|
330
|
+
hmrDebounce = null;
|
|
331
|
+
/**
|
|
332
|
+
* Registers a unit and returns a stable Identity Proxy.
|
|
333
|
+
*
|
|
334
|
+
* If a unit with the same UID already exists, it updates the internal
|
|
335
|
+
* target of the existing proxy and returns that same proxy.
|
|
336
|
+
*/
|
|
337
|
+
register(unit) {
|
|
338
|
+
const meta = unit;
|
|
339
|
+
const name = meta._name;
|
|
340
|
+
if (!name) return unit;
|
|
341
|
+
const uid = generateUID(name, meta._sourceInfo);
|
|
342
|
+
meta._uid = uid;
|
|
343
|
+
const existingTarget = this.targets.get(uid);
|
|
344
|
+
const existingProxy = this.proxies.get(uid);
|
|
345
|
+
if (existingProxy) {
|
|
346
|
+
if (this.targets.get(uid) !== unit) {
|
|
347
|
+
if (this.currentGeneration === unit._generation) {
|
|
348
|
+
}
|
|
349
|
+
this.targets.set(uid, unit);
|
|
350
|
+
this.notifyListeners(unit, "update");
|
|
351
|
+
}
|
|
352
|
+
return existingProxy;
|
|
353
|
+
}
|
|
354
|
+
this.targets.set(uid, unit);
|
|
355
|
+
const self = this;
|
|
356
|
+
const proxy = new Proxy((() => {
|
|
357
|
+
}), {
|
|
358
|
+
get(_, prop) {
|
|
359
|
+
const target = self.targets.get(uid);
|
|
360
|
+
if (!target) return void 0;
|
|
361
|
+
const value = target[prop];
|
|
362
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
363
|
+
},
|
|
364
|
+
apply(_, thisArg, args) {
|
|
365
|
+
const target = self.targets.get(uid);
|
|
366
|
+
if (typeof target !== "function") return void 0;
|
|
367
|
+
return Reflect.apply(target, thisArg, args);
|
|
368
|
+
},
|
|
369
|
+
// Ensure type checking and other proxy traps work
|
|
370
|
+
getPrototypeOf(_) {
|
|
371
|
+
return Object.getPrototypeOf(self.targets.get(uid) || {});
|
|
372
|
+
},
|
|
373
|
+
has(_, prop) {
|
|
374
|
+
return Reflect.has(self.targets.get(uid) || {}, prop);
|
|
375
|
+
},
|
|
376
|
+
ownKeys(_) {
|
|
377
|
+
return Reflect.ownKeys(self.targets.get(uid) || {});
|
|
378
|
+
},
|
|
379
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
380
|
+
return Reflect.getOwnPropertyDescriptor(self.targets.get(uid) || {}, prop);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
this.proxies.set(uid, proxy);
|
|
384
|
+
this.notifyListeners(unit, "add");
|
|
385
|
+
return proxy;
|
|
386
|
+
}
|
|
301
387
|
/**
|
|
302
|
-
* Schedules cleanup of units that weren't re-registered
|
|
388
|
+
* Schedules cleanup of units that weren't re-registered.
|
|
303
389
|
*/
|
|
304
390
|
scheduleCleanup() {
|
|
305
391
|
if (this.cleanupScheduled) return;
|
|
306
392
|
this.cleanupScheduled = true;
|
|
307
|
-
|
|
393
|
+
if (this.hmrDebounce) clearTimeout(this.hmrDebounce);
|
|
394
|
+
this.hmrDebounce = setTimeout(() => {
|
|
308
395
|
this.cleanupDeadUnits();
|
|
309
396
|
this.cleanupScheduled = false;
|
|
310
|
-
|
|
397
|
+
this.hmrDebounce = null;
|
|
398
|
+
}, 150);
|
|
311
399
|
}
|
|
312
|
-
/**
|
|
313
|
-
* Removes units that weren't re-registered in the current generation.
|
|
314
|
-
* Uses mark-and-sweep: units that were re-registered have current generation,
|
|
315
|
-
* units that weren't are from old generation and should be removed.
|
|
316
|
-
*/
|
|
317
400
|
cleanupDeadUnits() {
|
|
318
|
-
const toDelete = [];
|
|
319
|
-
this.units.forEach((unit, key) => {
|
|
320
|
-
const gen = unit._generation;
|
|
321
|
-
if (gen !== void 0 && gen < this.currentGeneration) {
|
|
322
|
-
toDelete.push(key);
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
toDelete.forEach((key) => this.units.delete(key));
|
|
326
|
-
if (toDelete.length > 0) {
|
|
327
|
-
console.log(`[Pulse] Cleaned up ${toDelete.length} deleted units after HMR`);
|
|
328
|
-
}
|
|
329
401
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
const existingUnit = this.units.get(name);
|
|
340
|
-
if (existingUnit) {
|
|
341
|
-
const existingGen = existingUnit?._generation;
|
|
342
|
-
if (existingGen === this.currentGeneration) {
|
|
343
|
-
unitWithMetadata._generation = this.currentGeneration;
|
|
344
|
-
this.units.set(name, unit);
|
|
345
|
-
this.listeners.forEach((l) => l(unit));
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
this.currentGeneration++;
|
|
349
|
-
this.scheduleCleanup();
|
|
350
|
-
}
|
|
351
|
-
unitWithMetadata._generation = this.currentGeneration;
|
|
352
|
-
this.units.set(name, unit);
|
|
353
|
-
this.listeners.forEach((l) => l(unit));
|
|
402
|
+
notifyListeners(unit, event) {
|
|
403
|
+
this.listeners.forEach((l) => l(unit, event));
|
|
404
|
+
}
|
|
405
|
+
get(nameOrUid) {
|
|
406
|
+
const proxy = this.proxies.get(nameOrUid);
|
|
407
|
+
if (proxy) return proxy;
|
|
408
|
+
const uid = generateUID(nameOrUid);
|
|
409
|
+
return this.proxies.get(uid);
|
|
354
410
|
}
|
|
355
411
|
getAll() {
|
|
356
|
-
return Array.from(this.
|
|
412
|
+
return Array.from(this.proxies.values());
|
|
413
|
+
}
|
|
414
|
+
getAllWithMeta() {
|
|
415
|
+
return Array.from(this.proxies.entries()).map(([uid, unit]) => ({
|
|
416
|
+
unit,
|
|
417
|
+
uid
|
|
418
|
+
}));
|
|
357
419
|
}
|
|
358
420
|
onRegister(listener) {
|
|
359
421
|
this.listeners.add(listener);
|
|
@@ -362,7 +424,8 @@ var Registry = class {
|
|
|
362
424
|
};
|
|
363
425
|
}
|
|
364
426
|
reset() {
|
|
365
|
-
this.
|
|
427
|
+
this.targets.clear();
|
|
428
|
+
this.proxies.clear();
|
|
366
429
|
this.currentGeneration = 0;
|
|
367
430
|
}
|
|
368
431
|
};
|
|
@@ -404,8 +467,192 @@ function source(initialValue, options = {}) {
|
|
|
404
467
|
return () => subscribers.delete(listener);
|
|
405
468
|
};
|
|
406
469
|
s._name = options.name;
|
|
407
|
-
PulseRegistry.register(s);
|
|
408
|
-
|
|
470
|
+
return PulseRegistry.register(s);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/signal.ts
|
|
474
|
+
var batchQueue = null;
|
|
475
|
+
var batchDepth = 0;
|
|
476
|
+
function batch(fn) {
|
|
477
|
+
batchDepth++;
|
|
478
|
+
if (!batchQueue) {
|
|
479
|
+
batchQueue = /* @__PURE__ */ new Set();
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
fn();
|
|
483
|
+
} finally {
|
|
484
|
+
batchDepth--;
|
|
485
|
+
if (batchDepth === 0 && batchQueue) {
|
|
486
|
+
const queue = batchQueue;
|
|
487
|
+
batchQueue = null;
|
|
488
|
+
queue.forEach((notify) => notify());
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function scheduleNotification(notify) {
|
|
493
|
+
if (batchQueue) {
|
|
494
|
+
batchQueue.add(notify);
|
|
495
|
+
} else {
|
|
496
|
+
notify();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function createSignal(initialValue, equals = (a, b) => a === b) {
|
|
500
|
+
let value = initialValue;
|
|
501
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
502
|
+
const dependents = /* @__PURE__ */ new Set();
|
|
503
|
+
const notify = () => {
|
|
504
|
+
subscribers.forEach((sub) => sub(value));
|
|
505
|
+
const deps = Array.from(dependents);
|
|
506
|
+
dependents.clear();
|
|
507
|
+
deps.forEach((dep) => dep.notify());
|
|
508
|
+
};
|
|
509
|
+
const signal = {
|
|
510
|
+
get() {
|
|
511
|
+
const activeGuard = getCurrentGuard();
|
|
512
|
+
if (activeGuard) {
|
|
513
|
+
dependents.add(activeGuard);
|
|
514
|
+
activeGuard.addDependency(signal);
|
|
515
|
+
}
|
|
516
|
+
return value;
|
|
517
|
+
},
|
|
518
|
+
peek() {
|
|
519
|
+
return value;
|
|
520
|
+
},
|
|
521
|
+
set(newValue) {
|
|
522
|
+
if (!equals(value, newValue)) {
|
|
523
|
+
value = newValue;
|
|
524
|
+
scheduleNotification(notify);
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
update(fn) {
|
|
528
|
+
signal.set(fn(value));
|
|
529
|
+
},
|
|
530
|
+
subscribe(listener) {
|
|
531
|
+
subscribers.add(listener);
|
|
532
|
+
return () => subscribers.delete(listener);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
return signal;
|
|
536
|
+
}
|
|
537
|
+
function effect(fn) {
|
|
538
|
+
let cleanup;
|
|
539
|
+
let disposed = false;
|
|
540
|
+
const run = () => {
|
|
541
|
+
if (disposed) return;
|
|
542
|
+
if (cleanup) cleanup();
|
|
543
|
+
cleanup = fn();
|
|
544
|
+
};
|
|
545
|
+
run();
|
|
546
|
+
return () => {
|
|
547
|
+
disposed = true;
|
|
548
|
+
if (cleanup) cleanup();
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/pulse.ts
|
|
553
|
+
var PULSE_META = /* @__PURE__ */ Symbol("PULSE_META");
|
|
554
|
+
function pulse(target, options = {}) {
|
|
555
|
+
const { name, deep = true } = options;
|
|
556
|
+
const signals = /* @__PURE__ */ new Map();
|
|
557
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
558
|
+
const dependents = /* @__PURE__ */ new Set();
|
|
559
|
+
const nestedCache = /* @__PURE__ */ new Map();
|
|
560
|
+
const meta = {
|
|
561
|
+
signals,
|
|
562
|
+
subscribers,
|
|
563
|
+
dependents,
|
|
564
|
+
name,
|
|
565
|
+
target
|
|
566
|
+
};
|
|
567
|
+
function getSignal(key) {
|
|
568
|
+
if (!signals.has(key)) {
|
|
569
|
+
const initialValue = target[key];
|
|
570
|
+
signals.set(key, createSignal(initialValue));
|
|
571
|
+
}
|
|
572
|
+
return signals.get(key);
|
|
573
|
+
}
|
|
574
|
+
function notify() {
|
|
575
|
+
subscribers.forEach((sub) => sub(proxy));
|
|
576
|
+
const deps = Array.from(dependents);
|
|
577
|
+
dependents.clear();
|
|
578
|
+
deps.forEach((dep) => dep.notify());
|
|
579
|
+
}
|
|
580
|
+
function trackAccess() {
|
|
581
|
+
const activeGuard = getCurrentGuard();
|
|
582
|
+
if (activeGuard) {
|
|
583
|
+
dependents.add(activeGuard);
|
|
584
|
+
activeGuard.addDependency(proxy);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const proxy = new Proxy(target, {
|
|
588
|
+
get(obj, prop) {
|
|
589
|
+
if (prop === PULSE_META) return meta;
|
|
590
|
+
if (prop === "$raw") return target;
|
|
591
|
+
if (prop === "$subscribe") {
|
|
592
|
+
return (listener) => {
|
|
593
|
+
subscribers.add(listener);
|
|
594
|
+
return () => subscribers.delete(listener);
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
if (prop === "$snapshot") {
|
|
598
|
+
return () => ({ ...target });
|
|
599
|
+
}
|
|
600
|
+
const value = obj[prop];
|
|
601
|
+
if (typeof value === "function") {
|
|
602
|
+
return value.bind(proxy);
|
|
603
|
+
}
|
|
604
|
+
trackAccess();
|
|
605
|
+
const signal = getSignal(prop);
|
|
606
|
+
const currentValue = signal.get();
|
|
607
|
+
if (deep && currentValue !== null && typeof currentValue === "object" && !isPulseObject(currentValue)) {
|
|
608
|
+
if (!nestedCache.has(prop)) {
|
|
609
|
+
nestedCache.set(prop, pulse(currentValue, { deep, name: name ? `${name}.${String(prop)}` : void 0 }));
|
|
610
|
+
}
|
|
611
|
+
return nestedCache.get(prop);
|
|
612
|
+
}
|
|
613
|
+
return currentValue;
|
|
614
|
+
},
|
|
615
|
+
set(obj, prop, value) {
|
|
616
|
+
const oldValue = obj[prop];
|
|
617
|
+
if (oldValue === value) return true;
|
|
618
|
+
obj[prop] = value;
|
|
619
|
+
getSignal(prop).set(value);
|
|
620
|
+
notify();
|
|
621
|
+
return true;
|
|
622
|
+
},
|
|
623
|
+
has(obj, prop) {
|
|
624
|
+
if (prop === PULSE_META) return true;
|
|
625
|
+
trackAccess();
|
|
626
|
+
return Reflect.has(obj, prop);
|
|
627
|
+
},
|
|
628
|
+
ownKeys(obj) {
|
|
629
|
+
trackAccess();
|
|
630
|
+
return Reflect.ownKeys(obj);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
return PulseRegistry.register(proxy);
|
|
634
|
+
}
|
|
635
|
+
function isPulseObject(value) {
|
|
636
|
+
return value !== null && typeof value === "object" && PULSE_META in value;
|
|
637
|
+
}
|
|
638
|
+
function toRaw(pulseObj) {
|
|
639
|
+
return pulseObj.$raw;
|
|
640
|
+
}
|
|
641
|
+
function readonly(pulseObj) {
|
|
642
|
+
return new Proxy(pulseObj, {
|
|
643
|
+
set() {
|
|
644
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
645
|
+
console.warn("[Pulse] Attempted to mutate a readonly pulse object");
|
|
646
|
+
}
|
|
647
|
+
return false;
|
|
648
|
+
},
|
|
649
|
+
deleteProperty() {
|
|
650
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
651
|
+
console.warn("[Pulse] Attempted to delete from a readonly pulse object");
|
|
652
|
+
}
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
});
|
|
409
656
|
}
|
|
410
657
|
|
|
411
658
|
// src/compute.ts
|
|
@@ -473,14 +720,21 @@ var guardExtensions = {
|
|
|
473
720
|
var extendedGuard = Object.assign(guard, guardExtensions);
|
|
474
721
|
export {
|
|
475
722
|
PulseRegistry,
|
|
723
|
+
batch,
|
|
476
724
|
compute,
|
|
725
|
+
createSignal,
|
|
726
|
+
effect,
|
|
477
727
|
evaluate,
|
|
478
728
|
getCurrentGuard,
|
|
479
729
|
extendedGuard as guard,
|
|
480
730
|
guardFail,
|
|
481
731
|
guardOk,
|
|
482
732
|
hydrate,
|
|
733
|
+
isPulseObject,
|
|
734
|
+
pulse,
|
|
735
|
+
readonly,
|
|
483
736
|
registerGuardForHydration,
|
|
484
737
|
runInContext,
|
|
485
|
-
source
|
|
738
|
+
source,
|
|
739
|
+
toRaw
|
|
486
740
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulse-js/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"module": "dist/index.js",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
"build": "bun x tsup src/index.ts --format esm,cjs --dts --clean",
|
|
46
46
|
"dev": "bun x tsup src/index.ts --format esm,cjs --watch --dts",
|
|
47
47
|
"lint": "bun x tsc --noEmit",
|
|
48
|
-
"test": "vitest run"
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:v2": "bun test tests/v2.test.ts tests/astro.test.ts tests/tanstack.test.ts",
|
|
50
|
+
"benchmark": "bun run tests/v2-benchmarks.ts"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
51
53
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|