@praxisjs/core 2.0.0 → 2.2.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/__tests__/batch.test.js +52 -1
  3. package/dist/__tests__/batch.test.js.map +1 -1
  4. package/dist/__tests__/computed.test.js +37 -0
  5. package/dist/__tests__/computed.test.js.map +1 -1
  6. package/dist/__tests__/effect.test.js +11 -0
  7. package/dist/__tests__/effect.test.js.map +1 -1
  8. package/dist/__tests__/persisted-signal.test.js +97 -1
  9. package/dist/__tests__/persisted-signal.test.js.map +1 -1
  10. package/dist/__tests__/reactive.test.js +35 -0
  11. package/dist/__tests__/reactive.test.js.map +1 -1
  12. package/dist/__tests__/resource-server-pass.test.d.ts +2 -0
  13. package/dist/__tests__/resource-server-pass.test.d.ts.map +1 -0
  14. package/dist/__tests__/resource-server-pass.test.js +78 -0
  15. package/dist/__tests__/resource-server-pass.test.js.map +1 -0
  16. package/dist/__tests__/signal.test.js +68 -0
  17. package/dist/__tests__/signal.test.js.map +1 -1
  18. package/dist/__tests__/synced.test.js +6 -7
  19. package/dist/__tests__/synced.test.js.map +1 -1
  20. package/dist/async/resource-cache.d.ts +7 -0
  21. package/dist/async/resource-cache.d.ts.map +1 -1
  22. package/dist/async/resource-cache.js +18 -0
  23. package/dist/async/resource-cache.js.map +1 -1
  24. package/dist/async/resource.d.ts +7 -0
  25. package/dist/async/resource.d.ts.map +1 -1
  26. package/dist/async/resource.js +16 -1
  27. package/dist/async/resource.js.map +1 -1
  28. package/dist/async/server-mode.d.ts +10 -0
  29. package/dist/async/server-mode.d.ts.map +1 -0
  30. package/dist/async/server-mode.js +15 -0
  31. package/dist/async/server-mode.js.map +1 -0
  32. package/dist/internal.d.ts +4 -3
  33. package/dist/internal.d.ts.map +1 -1
  34. package/dist/internal.js +3 -2
  35. package/dist/internal.js.map +1 -1
  36. package/dist/reactive/history.d.ts +2 -0
  37. package/dist/reactive/history.d.ts.map +1 -1
  38. package/dist/reactive/history.js +2 -1
  39. package/dist/reactive/history.js.map +1 -1
  40. package/dist/signal/batch.d.ts.map +1 -1
  41. package/dist/signal/batch.js +22 -8
  42. package/dist/signal/batch.js.map +1 -1
  43. package/dist/signal/computed.d.ts.map +1 -1
  44. package/dist/signal/computed.js +11 -0
  45. package/dist/signal/computed.js.map +1 -1
  46. package/dist/signal/effect.d.ts +3 -1
  47. package/dist/signal/effect.d.ts.map +1 -1
  48. package/dist/signal/effect.js +11 -2
  49. package/dist/signal/effect.js.map +1 -1
  50. package/dist/signal/index.d.ts +1 -1
  51. package/dist/signal/index.d.ts.map +1 -1
  52. package/dist/signal/index.js.map +1 -1
  53. package/dist/signal/persisted.d.ts +11 -1
  54. package/dist/signal/persisted.d.ts.map +1 -1
  55. package/dist/signal/persisted.js +39 -5
  56. package/dist/signal/persisted.js.map +1 -1
  57. package/dist/signal/signal.d.ts +2 -2
  58. package/dist/signal/signal.d.ts.map +1 -1
  59. package/dist/signal/signal.js +9 -20
  60. package/dist/signal/signal.js.map +1 -1
  61. package/package.json +2 -2
  62. package/src/__tests__/batch.test.ts +51 -0
  63. package/src/__tests__/computed.test.ts +43 -0
  64. package/src/__tests__/effect.test.ts +15 -0
  65. package/src/__tests__/persisted-signal.test.ts +112 -1
  66. package/src/__tests__/reactive.test.ts +38 -0
  67. package/src/__tests__/resource-server-pass.test.ts +90 -0
  68. package/src/__tests__/signal.test.ts +78 -0
  69. package/src/async/resource-cache.ts +20 -0
  70. package/src/async/resource.ts +17 -0
  71. package/src/async/server-mode.ts +16 -0
  72. package/src/internal.ts +4 -1
  73. package/src/reactive/history.ts +4 -1
  74. package/src/signal/batch.ts +20 -6
  75. package/src/signal/computed.ts +11 -1
  76. package/src/signal/effect.ts +12 -3
  77. package/src/signal/index.ts +1 -1
  78. package/src/signal/persisted.ts +54 -5
  79. package/src/signal/signal.ts +8 -13
@@ -1,6 +1,9 @@
1
+ // Past this many subscribers an array makes unsubscribing O(n) per effect re-run, which turns one
2
+ // signal with thousands of bindings into a quadratic update; a Set keeps removal O(1).
3
+ export const SUBS_SET_THRESHOLD = 16;
1
4
  export let activeEffect = null;
2
5
  const effectStack = [];
3
- function removeTrackedSub(holder, eff) {
6
+ export function removeSub(holder, eff) {
4
7
  const subs = holder.subs;
5
8
  if (subs === null)
6
9
  return;
@@ -9,6 +12,12 @@ function removeTrackedSub(holder, eff) {
9
12
  holder.subs = null;
10
13
  return;
11
14
  }
15
+ if (subs instanceof Set) {
16
+ subs.delete(eff);
17
+ if (subs.size === 0)
18
+ holder.subs = null;
19
+ return;
20
+ }
12
21
  const idx = subs.indexOf(eff);
13
22
  if (idx >= 0)
14
23
  subs.splice(idx, 1);
@@ -26,7 +35,7 @@ export function cleanupEffectDeps(eff) {
26
35
  if (!deps)
27
36
  return;
28
37
  for (const holder of deps)
29
- removeTrackedSub(holder, eff);
38
+ removeSub(holder, eff);
30
39
  deps.clear();
31
40
  }
32
41
  export function track(effect) {
@@ -1 +1 @@
1
- {"version":3,"file":"effect.js","sourceRoot":"","sources":["../../src/signal/effect.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"effect.js","sourceRoot":"","sources":["../../src/signal/effect.ts"],"names":[],"mappings":"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"}
@@ -2,7 +2,7 @@ export { batch } from "./batch";
2
2
  export { peek } from "./peek";
3
3
  export { signal } from "./signal";
4
4
  export { computed, writableComputed } from "./computed";
5
- export { type PersistedSignalOptions, persistedSignal } from "./persisted";
5
+ export { type PersistedSignalOptions, type PersistedSignal, persistedSignal } from "./persisted";
6
6
  export { type SyncedSignal, syncedSignal } from "./synced";
7
7
  export { effect, untrack } from "./effect";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/signal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,KAAK,sBAAsB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,KAAK,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/signal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,KAAK,sBAAsB,EAAE,KAAK,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACjG,OAAO,EAAE,KAAK,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/signal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAA+B,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAqB,YAAY,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/signal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAqD,eAAe,EAAE,MAAM,aAAa,CAAC;AACjG,OAAO,EAAqB,YAAY,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
@@ -3,6 +3,16 @@ export interface PersistedSignalOptions<T> {
3
3
  serialize?: (value: T) => string;
4
4
  deserialize?: (value: string) => T;
5
5
  syncTabs?: boolean;
6
+ /**
7
+ * Milliseconds to hold back writes to `localStorage`. Writes within the window are coalesced
8
+ * into one, using the latest value; pending data is flushed on `pagehide` and `close()`.
9
+ * `0` (default) writes on every `set`.
10
+ */
11
+ writeDelay?: number;
6
12
  }
7
- export declare function persistedSignal<T>(key: string, initialValue: T, options?: PersistedSignalOptions<T>): Signal<T>;
13
+ export interface PersistedSignal<T> extends Signal<T> {
14
+ /** Stops listening for `storage` events from other tabs. The signal keeps working locally. */
15
+ close(): void;
16
+ }
17
+ export declare function persistedSignal<T>(key: string, initialValue: T, options?: PersistedSignalOptions<T>): PersistedSignal<T>;
8
18
  //# sourceMappingURL=persisted.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"persisted.d.ts","sourceRoot":"","sources":["../../src/signal/persisted.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAI/C,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IACjC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,CAAC,EAC/B,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,EACf,OAAO,GAAE,sBAAsB,CAAC,CAAC,CAAM,aAwExC"}
1
+ {"version":3,"file":"persisted.d.ts","sourceRoot":"","sources":["../../src/signal/persisted.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAI/C,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;IACjC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe,CAAC,CAAC,CAAE,SAAQ,MAAM,CAAC,CAAC,CAAC;IACnD,8FAA8F;IAC9F,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,eAAe,CAAC,CAAC,EAC/B,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,EACf,OAAO,GAAE,sBAAsB,CAAC,CAAC,CAAM,sBA8GxC"}
@@ -1,6 +1,6 @@
1
1
  import { signal } from "./signal";
2
2
  export function persistedSignal(key, initialValue, options = {}) {
3
- const { serialize = JSON.stringify, deserialize = JSON.parse, syncTabs = true, } = options;
3
+ const { serialize = JSON.stringify, deserialize = JSON.parse, syncTabs = true, writeDelay = 0, } = options;
4
4
  function getStoredValue() {
5
5
  try {
6
6
  const stored = localStorage.getItem(key);
@@ -24,23 +24,47 @@ export function persistedSignal(key, initialValue, options = {}) {
24
24
  console.warn(`Failed to serialize value for key "${key}":`, e);
25
25
  }
26
26
  }
27
+ let pendingWrite;
28
+ let writeTimer;
29
+ function flushWrite() {
30
+ clearTimeout(writeTimer);
31
+ writeTimer = undefined;
32
+ if (!pendingWrite)
33
+ return;
34
+ const { value } = pendingWrite;
35
+ pendingWrite = undefined;
36
+ setStoredValue(value);
37
+ }
38
+ // The timer starts at the first write of a burst (throttle, not debounce) so a value that keeps
39
+ // changing still reaches storage within `writeDelay`.
40
+ function scheduleWrite(value) {
41
+ if (writeDelay <= 0) {
42
+ setStoredValue(value);
43
+ return;
44
+ }
45
+ pendingWrite = { value };
46
+ writeTimer ??= setTimeout(flushWrite, writeDelay);
47
+ }
27
48
  const inner = signal(getStoredValue());
28
49
  function read() {
29
50
  return inner();
30
51
  }
31
52
  function set(value) {
32
- setStoredValue(value);
53
+ scheduleWrite(value);
33
54
  inner.set(value);
34
55
  }
35
56
  function update(fh) {
36
57
  const newValue = fh(inner());
37
- setStoredValue(newValue);
58
+ scheduleWrite(newValue);
38
59
  inner.set(newValue);
39
60
  }
61
+ let removeStorageListener;
40
62
  if (syncTabs) {
41
- window.addEventListener("storage", (event) => {
63
+ const onStorage = (event) => {
42
64
  if (event.key !== key || event.storageArea !== localStorage)
43
65
  return;
66
+ // Another tab's value wins; a still-pending local write would overwrite it later.
67
+ pendingWrite = undefined;
44
68
  try {
45
69
  const newValue = event.newValue
46
70
  ? deserialize(event.newValue)
@@ -51,13 +75,23 @@ export function persistedSignal(key, initialValue, options = {}) {
51
75
  console.warn(`Failed to deserialize value for key "${key}" from storage event:`, e);
52
76
  inner.set(initialValue);
53
77
  }
54
- });
78
+ };
79
+ window.addEventListener("storage", onStorage);
80
+ removeStorageListener = () => { window.removeEventListener("storage", onStorage); };
55
81
  }
82
+ if (writeDelay > 0)
83
+ window.addEventListener("pagehide", flushWrite);
56
84
  const source = read;
57
85
  source.set = set;
58
86
  source.update = update;
59
87
  source.subscribe = inner.subscribe.bind(inner);
60
88
  source.__isSignal = true;
89
+ source.close = () => {
90
+ removeStorageListener?.();
91
+ removeStorageListener = undefined;
92
+ window.removeEventListener("pagehide", flushWrite);
93
+ flushWrite();
94
+ };
61
95
  return source;
62
96
  }
63
97
  //# sourceMappingURL=persisted.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"persisted.js","sourceRoot":"","sources":["../../src/signal/persisted.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAQlC,MAAM,UAAU,eAAe,CAC7B,GAAW,EACX,YAAe,EACf,UAAqC,EAAE;IAEvC,MAAM,EACJ,SAAS,GAAG,IAAI,CAAC,SAAS,EAC1B,WAAW,GAAG,IAAI,CAAC,KAA6B,EAChD,QAAQ,GAAG,IAAI,GAChB,GAAG,OAAO,CAAC;IAEZ,SAAS,cAAc;QACrB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACzC,OAAO,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;QACrD,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,wCAAwC,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;YACjE,OAAO,YAAY,CAAC;QACtB,CAAC;IACH,CAAC;IAED,SAAS,cAAc,CAAC,KAAQ;QAC9B,IAAI,CAAC;YACH,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBAC1C,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YAC/B,CAAC;iBAAM,CAAC;gBACN,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YAC9C,CAAC;QACH,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,sCAAsC,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC;IAEvC,SAAS,IAAI;QACX,OAAO,KAAK,EAAE,CAAC;IACjB,CAAC;IAED,SAAS,GAAG,CAAC,KAAQ;QACnB,cAAc,CAAC,KAAK,CAAC,CAAC;QACtB,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC;IAED,SAAS,MAAM,CAAC,EAAkB;QAChC,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC;QAC7B,cAAc,CAAC,QAAQ,CAAC,CAAC;QACzB,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACtB,CAAC;IAED,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE;YAC3C,IAAI,KAAK,CAAC,GAAG,KAAK,GAAG,IAAI,KAAK,CAAC,WAAW,KAAK,YAAY;gBAAE,OAAO;YACpE,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ;oBAC7B,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC;oBAC7B,CAAC,CAAC,YAAY,CAAC;gBACjB,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACtB,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,OAAO,CAAC,IAAI,CACV,wCAAwC,GAAG,uBAAuB,EAClE,CAAC,CACF,CAAC;gBACF,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;YAC1B,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,MAAM,GAAG,IAAiB,CAAC;IACjC,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC;IAEzB,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"persisted.js","sourceRoot":"","sources":["../../src/signal/persisted.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  import type { Signal } from "@praxisjs/shared";
2
- import { type Effect, type SubList, type SubscriberHolder } from "./effect";
2
+ import { removeSub, type Effect, type SubList, type SubscriberHolder } from "./effect";
3
3
  export type { SubList } from "./effect";
4
4
  export declare function addSub(holder: SubscriberHolder, eff: Effect): void;
5
- export declare function removeSub(holder: SubscriberHolder, eff: Effect): void;
5
+ export { removeSub };
6
6
  export declare function notifySubs(subs: Exclude<SubList, null>, batching: boolean): void;
7
7
  export declare function signal<T>(initialValue: T): Signal<T>;
8
8
  //# sourceMappingURL=signal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,OAAO,EACZ,KAAK,gBAAgB,EACtB,MAAM,UAAU,CAAC;AAElB,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,wBAAgB,MAAM,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAUlE;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAUrE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAqBhF;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CA4BpD"}
1
+ {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,EAGL,SAAS,EAET,KAAK,MAAM,EACX,KAAK,OAAO,EACZ,KAAK,gBAAgB,EACtB,MAAM,UAAU,CAAC;AAElB,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,wBAAgB,MAAM,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAalE;AAED,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAqBhF;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CA4BpD"}
@@ -1,5 +1,5 @@
1
1
  import { isBatching, enqueueEffect } from "./batch";
2
- import { activeEffect, recordDependency, } from "./effect";
2
+ import { activeEffect, recordDependency, removeSub, SUBS_SET_THRESHOLD, } from "./effect";
3
3
  export function addSub(holder, eff) {
4
4
  const subs = holder.subs;
5
5
  if (subs === null) {
@@ -9,28 +9,17 @@ export function addSub(holder, eff) {
9
9
  if (subs !== eff)
10
10
  holder.subs = [subs, eff];
11
11
  }
12
- else {
13
- if (!subs.includes(eff))
14
- subs.push(eff);
12
+ else if (subs instanceof Set) {
13
+ subs.add(eff);
15
14
  }
16
- recordDependency(eff, holder);
17
- }
18
- export function removeSub(holder, eff) {
19
- const subs = holder.subs;
20
- if (subs === null)
21
- return;
22
- if (typeof subs === "function") {
23
- if (subs === eff)
24
- holder.subs = null;
25
- }
26
- else {
27
- const idx = subs.indexOf(eff);
28
- if (idx >= 0)
29
- subs.splice(idx, 1);
30
- if (subs.length === 0)
31
- holder.subs = null;
15
+ else if (!subs.includes(eff)) {
16
+ subs.push(eff);
17
+ if (subs.length > SUBS_SET_THRESHOLD)
18
+ holder.subs = new Set(subs);
32
19
  }
20
+ recordDependency(eff, holder);
33
21
  }
22
+ export { removeSub };
34
23
  export function notifySubs(subs, batching) {
35
24
  if (typeof subs === "function") {
36
25
  if (batching)
@@ -1 +1 @@
1
- {"version":3,"file":"signal.js","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"signal.js","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisjs/core",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -15,7 +15,7 @@
15
15
  }
16
16
  },
17
17
  "devDependencies": {
18
- "typescript": "^5.9.3"
18
+ "typescript": "^6.0.3"
19
19
  },
20
20
  "dependencies": {
21
21
  "@praxisjs/shared": "0.3.1"
@@ -84,4 +84,55 @@ describe("batch", () => {
84
84
  expect(seen).toContain(0);
85
85
  expect(seen).toContain(5);
86
86
  });
87
+
88
+ it("an effect fed by several signals runs once per batch", () => {
89
+ const a = signal(0);
90
+ const b = signal(0);
91
+ const runs = vi.fn();
92
+ effect(() => {
93
+ void a();
94
+ void b();
95
+ runs();
96
+ });
97
+ runs.mockClear();
98
+ batch(() => {
99
+ a.set(1);
100
+ b.set(1);
101
+ a.set(2);
102
+ });
103
+ expect(runs).toHaveBeenCalledOnce();
104
+ });
105
+
106
+ it("a throwing effect does not stop the others and the error is rethrown after the flush", () => {
107
+ const s = signal(0);
108
+ const after = vi.fn();
109
+ let armed = false;
110
+ effect(() => {
111
+ void s();
112
+ if (armed) throw new Error("effect boom");
113
+ });
114
+ effect(() => {
115
+ void s();
116
+ if (armed) after();
117
+ });
118
+ armed = true;
119
+ expect(() => { batch(() => { s.set(1); }); }).toThrow("effect boom");
120
+ expect(after).toHaveBeenCalledOnce();
121
+ });
122
+
123
+ it("a failed flush leaves no stale entries for the next batch", () => {
124
+ const s = signal(0);
125
+ const runs = vi.fn();
126
+ let throws = true;
127
+ effect(() => {
128
+ void s();
129
+ runs();
130
+ if (throws && s() === 1) throw new Error("once");
131
+ });
132
+ expect(() => { batch(() => { s.set(1); }); }).toThrow("once");
133
+ throws = false;
134
+ runs.mockClear();
135
+ batch(() => { s.set(2); });
136
+ expect(runs).toHaveBeenCalledOnce();
137
+ });
87
138
  });
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
2
 
3
3
  import { computed, writableComputed } from "../signal/computed";
4
+ import { effect } from "../signal/effect";
4
5
  import { signal } from "../signal/signal";
5
6
 
6
7
  describe("computed", () => {
@@ -288,3 +289,45 @@ describe("writableComputed", () => {
288
289
  expect(wc()).toBe(14);
289
290
  });
290
291
  });
292
+
293
+ describe("computed.dispose", () => {
294
+ type Disposable<T> = (() => T) & { dispose(): void };
295
+
296
+ it("does nothing while an effect still depends on it", async () => {
297
+ const s = signal(1);
298
+ const c = computed(() => s() * 2) as unknown as Disposable<number>;
299
+ let seen = 0;
300
+ effect(() => {
301
+ seen = c();
302
+ });
303
+ c.dispose();
304
+ s.set(2);
305
+ await Promise.resolve();
306
+ expect(seen).toBe(4);
307
+ });
308
+
309
+ it("does nothing while another computed depends on it", () => {
310
+ const s = signal(1);
311
+ const inner = computed(() => s() * 2) as unknown as Disposable<number>;
312
+ const outer = computed(() => inner() + 1);
313
+ expect(outer()).toBe(3);
314
+ inner.dispose();
315
+ s.set(2);
316
+ expect(outer()).toBe(5);
317
+ });
318
+
319
+ it("recomputes from scratch when read again after disposal", () => {
320
+ const getter = vi.fn(() => 42);
321
+ const c = computed(getter) as unknown as Disposable<number>;
322
+ c();
323
+ c.dispose();
324
+ expect(c()).toBe(42);
325
+ expect(getter).toHaveBeenCalledTimes(2);
326
+ });
327
+
328
+ it("is safe on a computed that was never read", () => {
329
+ const c = computed(() => 1) as unknown as Disposable<number>;
330
+ expect(() => { c.dispose(); }).not.toThrow();
331
+ expect(c()).toBe(1);
332
+ });
333
+ });
@@ -194,6 +194,21 @@ describe("effect", () => {
194
194
  expect(unmatchedArrayHolder.subs).toEqual([other]);
195
195
  });
196
196
 
197
+ it("cleanupEffectDeps handles Set dependency holders, emptying them to null", () => {
198
+ const tracked = vi.fn();
199
+ const other = vi.fn();
200
+ const sharedHolder: SubscriberHolder = { subs: new Set([tracked, other]) };
201
+ const soleHolder: SubscriberHolder = { subs: new Set([tracked]) };
202
+
203
+ recordDependency(tracked, sharedHolder);
204
+ recordDependency(tracked, soleHolder);
205
+
206
+ cleanupEffectDeps(tracked);
207
+
208
+ expect([...(sharedHolder.subs as Set<() => void>)]).toEqual([other]);
209
+ expect(soleHolder.subs).toBeNull();
210
+ });
211
+
197
212
  it("exception inside nested track() restores outer activeEffect", () => {
198
213
  const s = signal(0);
199
214
  const outer = vi.fn();
@@ -1,5 +1,5 @@
1
1
  // @vitest-environment jsdom
2
- import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
3
3
 
4
4
  import { persistedSignal } from "../signal/persisted";
5
5
 
@@ -191,4 +191,115 @@ describe("persistedSignal", () => {
191
191
  // Should stay at 20 since the event is from sessionStorage, not localStorage
192
192
  expect(s()).toBe(20);
193
193
  });
194
+
195
+ it("close() stops storage-event syncing but keeps the signal working locally", () => {
196
+ const s = persistedSignal("close-key", 0);
197
+ s.close();
198
+ window.dispatchEvent(
199
+ new StorageEvent("storage", { key: "close-key", newValue: "5", storageArea: localStorage }),
200
+ );
201
+ expect(s()).toBe(0);
202
+ s.set(3);
203
+ expect(s()).toBe(3);
204
+ expect(localStorage.getItem("close-key")).toBe("3");
205
+ });
206
+
207
+ it("close() removes the window listener and is safe to call twice", () => {
208
+ const remove = vi.spyOn(window, "removeEventListener");
209
+ const s = persistedSignal("close-twice", 0);
210
+ s.close();
211
+ s.close();
212
+ expect(remove.mock.calls.filter(([type]) => type === "storage")).toHaveLength(1);
213
+ remove.mockRestore();
214
+ });
215
+
216
+ it("close() is a no-op when syncTabs is false", () => {
217
+ const remove = vi.spyOn(window, "removeEventListener");
218
+ const s = persistedSignal("close-nosync", 0, { syncTabs: false });
219
+ expect(() => { s.close(); }).not.toThrow();
220
+ expect(remove.mock.calls.filter(([type]) => type === "storage")).toHaveLength(0);
221
+ remove.mockRestore();
222
+ });
223
+ });
224
+
225
+ describe("persistedSignal writeDelay", () => {
226
+ afterEach(() => {
227
+ vi.useRealTimers();
228
+ });
229
+
230
+ it("writes immediately by default", () => {
231
+ const s = persistedSignal("wd-default", 0);
232
+ s.set(1);
233
+ expect(localStorage.getItem("wd-default")).toBe("1");
234
+ });
235
+
236
+ it("holds writes back and flushes the latest value once the delay elapses", () => {
237
+ vi.useFakeTimers();
238
+ const setItem = vi.spyOn(Storage.prototype, "setItem");
239
+ const s = persistedSignal("wd-burst", 0, { writeDelay: 100 });
240
+ s.set(1);
241
+ s.set(2);
242
+ s.update((n) => n + 1);
243
+ expect(s()).toBe(3);
244
+ expect(localStorage.getItem("wd-burst")).toBeNull();
245
+ vi.advanceTimersByTime(100);
246
+ expect(localStorage.getItem("wd-burst")).toBe("3");
247
+ expect(setItem.mock.calls.filter(([k]) => k === "wd-burst")).toHaveLength(1);
248
+ setItem.mockRestore();
249
+ });
250
+
251
+ it("starts a new window for writes after a flush", () => {
252
+ vi.useFakeTimers();
253
+ const s = persistedSignal("wd-next", 0, { writeDelay: 50 });
254
+ s.set(1);
255
+ vi.advanceTimersByTime(50);
256
+ s.set(2);
257
+ expect(localStorage.getItem("wd-next")).toBe("1");
258
+ vi.advanceTimersByTime(50);
259
+ expect(localStorage.getItem("wd-next")).toBe("2");
260
+ });
261
+
262
+ it("flushes pending data on pagehide", () => {
263
+ vi.useFakeTimers();
264
+ const s = persistedSignal("wd-hide", 0, { writeDelay: 1000 });
265
+ s.set(5);
266
+ window.dispatchEvent(new Event("pagehide"));
267
+ expect(localStorage.getItem("wd-hide")).toBe("5");
268
+ vi.advanceTimersByTime(1000);
269
+ expect(localStorage.getItem("wd-hide")).toBe("5");
270
+ });
271
+
272
+ it("close() flushes pending data and stops listening for pagehide", () => {
273
+ vi.useFakeTimers();
274
+ const s = persistedSignal("wd-close", 0, { writeDelay: 1000 });
275
+ s.set(7);
276
+ s.close();
277
+ expect(localStorage.getItem("wd-close")).toBe("7");
278
+ s.set(8);
279
+ window.dispatchEvent(new Event("pagehide"));
280
+ expect(localStorage.getItem("wd-close")).toBe("7");
281
+ });
282
+
283
+ it("a value arriving from another tab discards the pending local write", () => {
284
+ vi.useFakeTimers();
285
+ const s = persistedSignal("wd-remote", 0, { writeDelay: 100 });
286
+ s.set(1);
287
+ localStorage.setItem("wd-remote", "9");
288
+ window.dispatchEvent(
289
+ new StorageEvent("storage", { key: "wd-remote", newValue: "9", storageArea: localStorage }),
290
+ );
291
+ vi.advanceTimersByTime(100);
292
+ expect(s()).toBe(9);
293
+ expect(localStorage.getItem("wd-remote")).toBe("9");
294
+ });
295
+
296
+ it("removes null and undefined values when the write is flushed", () => {
297
+ vi.useFakeTimers();
298
+ const s = persistedSignal<number | null>("wd-null", 1, { writeDelay: 10 });
299
+ s.set(2);
300
+ vi.advanceTimersByTime(10);
301
+ s.set(null);
302
+ vi.advanceTimersByTime(10);
303
+ expect(localStorage.getItem("wd-null")).toBeNull();
304
+ });
194
305
  });
@@ -303,3 +303,41 @@ describe("history", () => {
303
303
  expect(snapshot1).not.toBe(snapshot2);
304
304
  });
305
305
  });
306
+
307
+ describe("history.destroy", () => {
308
+ it("stops recording changes but keeps what was recorded", () => {
309
+ const s = signal(0);
310
+ const h = history(s);
311
+ s.set(1);
312
+ h.destroy();
313
+ s.set(2);
314
+ s.set(3);
315
+ expect(h.values()).toEqual([0, 1]);
316
+ expect(h.current()).toBe(1);
317
+ });
318
+
319
+ it("can still undo recorded entries after destroy", () => {
320
+ const s = signal(0);
321
+ const h = history(s);
322
+ s.set(1);
323
+ h.destroy();
324
+ h.undo();
325
+ expect(s()).toBe(0);
326
+ });
327
+ });
328
+
329
+ describe("history with a computed source", () => {
330
+ it("undo and redo move the tracked value without writing back to the read-only source", async () => {
331
+ const s = signal(0);
332
+ const source = computed(() => s());
333
+ const h = history(source);
334
+ s.set(1);
335
+ await Promise.resolve();
336
+ h.undo();
337
+ expect(h.current()).toBe(0);
338
+ expect(s()).toBe(1);
339
+ h.redo();
340
+ expect(h.current()).toBe(1);
341
+ expect(s()).toBe(1);
342
+ });
343
+ });