@pulse-js/core 0.2.1 → 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/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
 
@@ -138,7 +145,12 @@ function guard(nameOrFn, fn) {
138
145
  if (currentId === evaluationId) {
139
146
  persistDependencies();
140
147
  if (resolved === false) {
141
- const reason = name ? `${name} failed` : "condition failed";
148
+ const message = name ? `${name} failed` : "condition failed";
149
+ const reason = {
150
+ code: "GUARD_FAIL",
151
+ message,
152
+ toString: () => message
153
+ };
142
154
  state = { status: "fail", reason, lastReason: reason, updatedAt: Date.now() };
143
155
  } else if (resolved === void 0) {
144
156
  state = { ...state, status: "pending", updatedAt: Date.now() };
@@ -150,13 +162,22 @@ function guard(nameOrFn, fn) {
150
162
  }).catch((err) => {
151
163
  if (currentId === evaluationId) {
152
164
  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;
165
+ let reason;
166
+ if (err && err._pulseFail) {
167
+ reason = err._reason;
168
+ } else {
169
+ const message = err instanceof Error ? err.message : String(err);
170
+ reason = err && err.meta ? {
171
+ code: err.code || "ERROR",
172
+ message,
173
+ meta: err.meta,
174
+ toString: () => message
175
+ } : {
176
+ code: "ERROR",
177
+ message,
178
+ toString: () => message
179
+ };
180
+ }
160
181
  state = {
161
182
  status: "fail",
162
183
  reason,
@@ -169,7 +190,12 @@ function guard(nameOrFn, fn) {
169
190
  } else {
170
191
  persistDependencies();
171
192
  if (result === false) {
172
- const reason = name ? `${name} failed` : "condition failed";
193
+ const message = name ? `${name} failed` : "condition failed";
194
+ const reason = {
195
+ code: "GUARD_FAIL",
196
+ message,
197
+ toString: () => message
198
+ };
173
199
  state = { status: "fail", reason, lastReason: reason, updatedAt: Date.now() };
174
200
  } else if (result === void 0) {
175
201
  state = { ...state, status: "pending", updatedAt: Date.now() };
@@ -198,7 +224,11 @@ function guard(nameOrFn, fn) {
198
224
  message,
199
225
  meta: err.meta,
200
226
  toString: () => message
201
- } : message;
227
+ } : {
228
+ code: "ERROR",
229
+ message,
230
+ toString: () => message
231
+ };
202
232
  state = {
203
233
  status: "fail",
204
234
  reason,
@@ -293,9 +323,8 @@ function guard(nameOrFn, fn) {
293
323
  if (name) {
294
324
  registerGuardForHydration(name, g);
295
325
  }
296
- PulseRegistry.register(g);
297
326
  evaluate2();
298
- return g;
327
+ return PulseRegistry.register(g);
299
328
  }
300
329
  guard.map = function(source2, mapper, name) {
301
330
  const guardName = name || `map-${source2._name || "source"}`;
@@ -304,69 +333,132 @@ guard.map = function(source2, mapper, name) {
304
333
  return mapper(value);
305
334
  });
306
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
+ };
307
359
 
308
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
+ }
309
367
  var Registry = class {
310
- units = /* @__PURE__ */ new Map();
368
+ targets = /* @__PURE__ */ new Map();
369
+ proxies = /* @__PURE__ */ new Map();
311
370
  listeners = /* @__PURE__ */ new Set();
312
371
  currentGeneration = 0;
313
372
  cleanupScheduled = false;
373
+ hmrDebounce = null;
314
374
  /**
315
- * Schedules cleanup of units that weren't re-registered (deleted from code).
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
+ }
430
+ /**
431
+ * Schedules cleanup of units that weren't re-registered.
316
432
  */
317
433
  scheduleCleanup() {
318
434
  if (this.cleanupScheduled) return;
319
435
  this.cleanupScheduled = true;
320
- setTimeout(() => {
436
+ if (this.hmrDebounce) clearTimeout(this.hmrDebounce);
437
+ this.hmrDebounce = setTimeout(() => {
321
438
  this.cleanupDeadUnits();
322
439
  this.cleanupScheduled = false;
323
- }, 100);
440
+ this.hmrDebounce = null;
441
+ }, 150);
324
442
  }
325
- /**
326
- * Removes units that weren't re-registered in the current generation.
327
- * Uses mark-and-sweep: units that were re-registered have current generation,
328
- * units that weren't are from old generation and should be removed.
329
- */
330
443
  cleanupDeadUnits() {
331
- const toDelete = [];
332
- this.units.forEach((unit, key) => {
333
- const gen = unit._generation;
334
- if (gen !== void 0 && gen < this.currentGeneration) {
335
- toDelete.push(key);
336
- }
337
- });
338
- toDelete.forEach((key) => this.units.delete(key));
339
- if (toDelete.length > 0) {
340
- console.log(`[Pulse] Cleaned up ${toDelete.length} deleted units after HMR`);
341
- }
342
444
  }
343
- /**
344
- * Registers a unit (only if it has an explicit name).
345
- */
346
- register(unit) {
347
- const unitWithMetadata = unit;
348
- const name = unitWithMetadata._name;
349
- if (!name) {
350
- return;
351
- }
352
- const existingUnit = this.units.get(name);
353
- if (existingUnit) {
354
- const existingGen = existingUnit?._generation;
355
- if (existingGen === this.currentGeneration) {
356
- unitWithMetadata._generation = this.currentGeneration;
357
- this.units.set(name, unit);
358
- this.listeners.forEach((l) => l(unit));
359
- return;
360
- }
361
- this.currentGeneration++;
362
- this.scheduleCleanup();
363
- }
364
- unitWithMetadata._generation = this.currentGeneration;
365
- this.units.set(name, unit);
366
- 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);
367
453
  }
368
454
  getAll() {
369
- return Array.from(this.units.values());
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
+ }));
370
462
  }
371
463
  onRegister(listener) {
372
464
  this.listeners.add(listener);
@@ -375,7 +467,8 @@ var Registry = class {
375
467
  };
376
468
  }
377
469
  reset() {
378
- this.units.clear();
470
+ this.targets.clear();
471
+ this.proxies.clear();
379
472
  this.currentGeneration = 0;
380
473
  }
381
474
  };
@@ -417,8 +510,192 @@ function source(initialValue, options = {}) {
417
510
  return () => subscribers.delete(listener);
418
511
  };
419
512
  s._name = options.name;
420
- PulseRegistry.register(s);
421
- return s;
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
+ });
422
699
  }
423
700
 
424
701
  // src/compute.ts
@@ -487,14 +764,21 @@ var extendedGuard = Object.assign(guard, guardExtensions);
487
764
  // Annotate the CommonJS export names for ESM import in node:
488
765
  0 && (module.exports = {
489
766
  PulseRegistry,
767
+ batch,
490
768
  compute,
769
+ createSignal,
770
+ effect,
491
771
  evaluate,
492
772
  getCurrentGuard,
493
773
  guard,
494
774
  guardFail,
495
775
  guardOk,
496
776
  hydrate,
777
+ isPulseObject,
778
+ pulse,
779
+ readonly,
497
780
  registerGuardForHydration,
498
781
  runInContext,
499
- source
782
+ source,
783
+ toRaw
500
784
  });