rask-ui 0.29.3 → 0.30.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 (124) hide show
  1. package/dist/jsx-runtime.d.ts +1 -4
  2. package/dist/jsx-runtime.d.ts.map +1 -1
  3. package/dist/jsx-runtime.js +5 -7
  4. package/dist/render.d.ts.map +1 -1
  5. package/dist/render.js +21 -17
  6. package/dist/types.d.ts +3 -1
  7. package/dist/types.d.ts.map +1 -1
  8. package/package.json +1 -1
  9. package/swc-plugin/target/wasm32-wasip1/release/swc_plugin_rask_component.wasm +0 -0
  10. package/dist/batch.d.ts +0 -6
  11. package/dist/batch.d.ts.map +0 -1
  12. package/dist/batch.js +0 -84
  13. package/dist/compiler.d.ts +0 -4
  14. package/dist/compiler.d.ts.map +0 -1
  15. package/dist/compiler.js +0 -7
  16. package/dist/createAsync.d.ts +0 -39
  17. package/dist/createAsync.d.ts.map +0 -1
  18. package/dist/createAsync.js +0 -47
  19. package/dist/createComputed.d.ts +0 -4
  20. package/dist/createComputed.d.ts.map +0 -1
  21. package/dist/createComputed.js +0 -69
  22. package/dist/createEffect.d.ts +0 -2
  23. package/dist/createEffect.d.ts.map +0 -1
  24. package/dist/createEffect.js +0 -29
  25. package/dist/createMutation.d.ts +0 -43
  26. package/dist/createMutation.d.ts.map +0 -1
  27. package/dist/createMutation.js +0 -76
  28. package/dist/createQuery.d.ts +0 -42
  29. package/dist/createQuery.d.ts.map +0 -1
  30. package/dist/createQuery.js +0 -80
  31. package/dist/createRouter.d.ts +0 -8
  32. package/dist/createRouter.d.ts.map +0 -1
  33. package/dist/createRouter.js +0 -27
  34. package/dist/createState.d.ts +0 -28
  35. package/dist/createState.d.ts.map +0 -1
  36. package/dist/createState.js +0 -129
  37. package/dist/createTask.d.ts +0 -31
  38. package/dist/createTask.d.ts.map +0 -1
  39. package/dist/createTask.js +0 -79
  40. package/dist/createView.d.ts +0 -28
  41. package/dist/createView.d.ts.map +0 -1
  42. package/dist/createView.js +0 -77
  43. package/dist/error.d.ts +0 -5
  44. package/dist/error.d.ts.map +0 -1
  45. package/dist/error.js +0 -16
  46. package/dist/jsx.d.ts +0 -11
  47. package/dist/observation.d.ts +0 -93
  48. package/dist/observation.d.ts.map +0 -1
  49. package/dist/observation.js +0 -200
  50. package/dist/patchInferno.d.ts +0 -6
  51. package/dist/patchInferno.d.ts.map +0 -1
  52. package/dist/patchInferno.js +0 -53
  53. package/dist/tests/batch.test.d.ts +0 -2
  54. package/dist/tests/batch.test.d.ts.map +0 -1
  55. package/dist/tests/batch.test.js +0 -434
  56. package/dist/tests/createComputed.test.d.ts +0 -2
  57. package/dist/tests/createComputed.test.d.ts.map +0 -1
  58. package/dist/tests/createComputed.test.js +0 -257
  59. package/dist/tests/createContext.test.d.ts +0 -2
  60. package/dist/tests/createContext.test.d.ts.map +0 -1
  61. package/dist/tests/createContext.test.js +0 -149
  62. package/dist/tests/createEffect.test.d.ts +0 -2
  63. package/dist/tests/createEffect.test.d.ts.map +0 -1
  64. package/dist/tests/createEffect.test.js +0 -467
  65. package/dist/tests/createState.test.d.ts +0 -2
  66. package/dist/tests/createState.test.d.ts.map +0 -1
  67. package/dist/tests/createState.test.js +0 -144
  68. package/dist/tests/createTask.test.d.ts +0 -2
  69. package/dist/tests/createTask.test.d.ts.map +0 -1
  70. package/dist/tests/createTask.test.js +0 -322
  71. package/dist/tests/createView.test.d.ts +0 -2
  72. package/dist/tests/createView.test.d.ts.map +0 -1
  73. package/dist/tests/createView.test.js +0 -203
  74. package/dist/tests/error.test.d.ts +0 -2
  75. package/dist/tests/error.test.d.ts.map +0 -1
  76. package/dist/tests/error.test.js +0 -181
  77. package/dist/tests/observation.test.d.ts +0 -2
  78. package/dist/tests/observation.test.d.ts.map +0 -1
  79. package/dist/tests/observation.test.js +0 -341
  80. package/dist/tests/renderCount.test.d.ts +0 -2
  81. package/dist/tests/renderCount.test.d.ts.map +0 -1
  82. package/dist/tests/renderCount.test.js +0 -95
  83. package/dist/tests/scopeEnforcement.test.d.ts +0 -2
  84. package/dist/tests/scopeEnforcement.test.d.ts.map +0 -1
  85. package/dist/tests/scopeEnforcement.test.js +0 -157
  86. package/dist/tests/useAction.test.d.ts +0 -2
  87. package/dist/tests/useAction.test.d.ts.map +0 -1
  88. package/dist/tests/useAction.test.js +0 -132
  89. package/dist/tests/useAsync.test.d.ts +0 -2
  90. package/dist/tests/useAsync.test.d.ts.map +0 -1
  91. package/dist/tests/useAsync.test.js +0 -499
  92. package/dist/tests/useDerived.test.d.ts +0 -2
  93. package/dist/tests/useDerived.test.d.ts.map +0 -1
  94. package/dist/tests/useDerived.test.js +0 -407
  95. package/dist/tests/useEffect.test.d.ts +0 -2
  96. package/dist/tests/useEffect.test.d.ts.map +0 -1
  97. package/dist/tests/useEffect.test.js +0 -600
  98. package/dist/tests/useLookup.test.d.ts +0 -2
  99. package/dist/tests/useLookup.test.d.ts.map +0 -1
  100. package/dist/tests/useLookup.test.js +0 -299
  101. package/dist/tests/useRef.test.d.ts +0 -2
  102. package/dist/tests/useRef.test.d.ts.map +0 -1
  103. package/dist/tests/useRef.test.js +0 -189
  104. package/dist/tests/useState.test.d.ts +0 -2
  105. package/dist/tests/useState.test.d.ts.map +0 -1
  106. package/dist/tests/useState.test.js +0 -178
  107. package/dist/tests/useSuspend.test.d.ts +0 -2
  108. package/dist/tests/useSuspend.test.d.ts.map +0 -1
  109. package/dist/tests/useSuspend.test.js +0 -752
  110. package/dist/tests/useView.test.d.ts +0 -2
  111. package/dist/tests/useView.test.d.ts.map +0 -1
  112. package/dist/tests/useView.test.js +0 -305
  113. package/dist/useComputed.d.ts +0 -5
  114. package/dist/useComputed.d.ts.map +0 -1
  115. package/dist/useComputed.js +0 -69
  116. package/dist/useQuery.d.ts +0 -25
  117. package/dist/useQuery.d.ts.map +0 -1
  118. package/dist/useQuery.js +0 -25
  119. package/dist/useSuspendAsync.d.ts +0 -18
  120. package/dist/useSuspendAsync.d.ts.map +0 -1
  121. package/dist/useSuspendAsync.js +0 -37
  122. package/dist/useTask.d.ts +0 -25
  123. package/dist/useTask.d.ts.map +0 -1
  124. package/dist/useTask.js +0 -70
@@ -1,200 +0,0 @@
1
- /**
2
- * ---------------------------------------------------------------------------
3
- * OBSERVER–SIGNAL SYSTEM (OPTIMIZED / LOW MEMORY / ZERO PENDING ARRAYS)
4
- * ---------------------------------------------------------------------------
5
- *
6
- * STRATEGY OVERVIEW
7
- * -----------------
8
- * Instead of storing subscriber callbacks and disposer closures,
9
- * we model the connection between an Observer and a Signal using a
10
- * lightweight Subscription node.
11
- *
12
- * Each Subscription is owned *by both sides*:
13
- * - Signal keeps a doubly-linked list of all Subscriptions.
14
- * - Observer keeps a doubly-linked list of all Subscriptions it has made.
15
- *
16
- * This gives several important advantages:
17
- *
18
- * ✔ NO per-subscription closures (disposers)
19
- * ✔ NO Set allocations in Signal or Observer
20
- * ✔ NO Array copies during notify()
21
- * ✔ Unsubscribing while iterating is safe (linked-list + cached "next")
22
- * ✔ New subscriptions inside notify() DO NOT fire in the same notify pass
23
- * (using an epoch barrier)
24
- * ✔ Observer.clearSignals() is O(n) with real O(1) unlink
25
- * ✔ Memory overhead is extremely small (one node with 4 pointers)
26
- *
27
- * NOTIFY BARRIER
28
- * --------------
29
- * A global integer `epoch` increments for each notify().
30
- * New subscriptions created during notify() store `createdAtEpoch = epoch+1`.
31
- * During traversal, we only fire subscriptions where:
32
- *
33
- * sub.createdAtEpoch <= epoch
34
- *
35
- * This guarantees consistency and prevents "late" subscribers from firing too early.
36
- *
37
- * ---------------------------------------------------------------------------
38
- */
39
- import { queue } from "./batch";
40
- // GLOBAL OBSERVER STACK (for dependency tracking)
41
- const observerStack = [];
42
- let stackTop = -1;
43
- // Get the active observer during a render/compute
44
- export function getCurrentObserver() {
45
- return stackTop >= 0 ? observerStack[stackTop] : undefined;
46
- }
47
- /**
48
- * A lightweight link connecting ONE observer ↔ ONE signal.
49
- * Stored in a linked list on both sides.
50
- */
51
- class Subscription {
52
- signal;
53
- observer;
54
- // Linked list pointers within the signal
55
- prevInSignal = null;
56
- nextInSignal = null;
57
- // Linked list pointers within the observer
58
- prevInObserver = null;
59
- nextInObserver = null;
60
- // Whether this subscription is active
61
- active = true;
62
- // Used for the notify barrier
63
- createdAtEpoch;
64
- constructor(signal, observer, epoch) {
65
- this.signal = signal;
66
- this.observer = observer;
67
- this.createdAtEpoch = epoch;
68
- }
69
- }
70
- /**
71
- * SIGNAL — Notifies subscribed observers.
72
- */
73
- export class Signal {
74
- head = null;
75
- tail = null;
76
- // Incremented for every notify() call
77
- epoch = 0;
78
- /** INTERNAL: Create a subscription from observer → signal. */
79
- _subscribe(observer) {
80
- const sub = new Subscription(this, observer, this.epoch + 1);
81
- // Attach to signal's linked list
82
- if (this.tail) {
83
- this.tail.nextInSignal = sub;
84
- sub.prevInSignal = this.tail;
85
- this.tail = sub;
86
- }
87
- else {
88
- this.head = this.tail = sub;
89
- }
90
- return sub;
91
- }
92
- /** INTERNAL: Unlink a subscription from this signal. */
93
- _unsubscribe(sub) {
94
- if (!sub.active)
95
- return;
96
- sub.active = false;
97
- const { prevInSignal, nextInSignal } = sub;
98
- if (prevInSignal)
99
- prevInSignal.nextInSignal = nextInSignal;
100
- else
101
- this.head = nextInSignal;
102
- if (nextInSignal)
103
- nextInSignal.prevInSignal = prevInSignal;
104
- else
105
- this.tail = prevInSignal;
106
- sub.prevInSignal = sub.nextInSignal = null;
107
- }
108
- /** Notify all observers.
109
- * Safe even if observers unsubscribe themselves or subscribe new ones mid-run.
110
- */
111
- notify() {
112
- if (!this.head)
113
- return;
114
- const barrier = ++this.epoch; // new subs won't fire now
115
- let sub = this.head;
116
- while (sub) {
117
- const next = sub.nextInSignal; // cache next → safe if sub unlinks itself
118
- if (sub.active && sub.createdAtEpoch <= barrier) {
119
- sub.observer._notify();
120
- }
121
- sub = next;
122
- }
123
- }
124
- }
125
- /**
126
- * OBSERVER — Reacts to signal changes.
127
- * Typically wraps a computation or component.
128
- */
129
- export class Observer {
130
- isDisposed = false;
131
- // Doubly-linked list of all subscriptions from this observer
132
- subsHead = null;
133
- subsTail = null;
134
- // Only ONE notify callback closure per observer
135
- onNotify;
136
- constructor(onNotify, shouldQueue = true) {
137
- const onNotifyQueued = onNotify;
138
- onNotifyQueued.__queued = false;
139
- if (shouldQueue) {
140
- this.onNotify = () => {
141
- if (onNotifyQueued.__queued)
142
- return;
143
- queue(onNotify);
144
- };
145
- }
146
- else {
147
- this.onNotify = onNotify;
148
- }
149
- }
150
- /** Called from Signal.notify() */
151
- _notify() {
152
- if (!this.isDisposed) {
153
- this.onNotify();
154
- }
155
- }
156
- /** Subscribe this observer to a signal */
157
- subscribeSignal(signal) {
158
- if (this.isDisposed)
159
- return;
160
- const sub = signal._subscribe(this);
161
- // Add to observer's linked list
162
- if (this.subsTail) {
163
- this.subsTail.nextInObserver = sub;
164
- sub.prevInObserver = this.subsTail;
165
- this.subsTail = sub;
166
- }
167
- else {
168
- this.subsHead = this.subsTail = sub;
169
- }
170
- }
171
- /** Remove all signal subscriptions (fast + safe) */
172
- clearSignals() {
173
- let sub = this.subsHead;
174
- this.subsHead = this.subsTail = null;
175
- while (sub) {
176
- const next = sub.nextInObserver;
177
- // Unlink from the signal
178
- sub.signal._unsubscribe(sub);
179
- // Clean up observer-side pointers
180
- sub.prevInObserver = sub.nextInObserver = null;
181
- sub = next;
182
- }
183
- }
184
- /** Begin dependency collection */
185
- observe() {
186
- this.clearSignals();
187
- observerStack[++stackTop] = this;
188
- // Return a disposer for this observation frame
189
- return () => {
190
- stackTop--;
191
- };
192
- }
193
- /** Dispose the observer completely */
194
- dispose() {
195
- if (this.isDisposed)
196
- return;
197
- this.isDisposed = true;
198
- this.clearSignals();
199
- }
200
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Temporarily patches document.addEventListener during render to capture
3
- * and wrap Inferno's delegated event listeners with syncBatch
4
- */
5
- export declare function patchInfernoEventHandling(renderFn: () => void): void;
6
- //# sourceMappingURL=patchInferno.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"patchInferno.d.ts","sourceRoot":"","sources":["../src/patchInferno.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,IAAI,QA0D7D"}
@@ -1,53 +0,0 @@
1
- import { syncBatch } from "./batch";
2
- /**
3
- * Temporarily patches document.addEventListener during render to capture
4
- * and wrap Inferno's delegated event listeners with syncBatch
5
- */
6
- export function patchInfernoEventHandling(renderFn) {
7
- const originalAddEventListener = document.addEventListener.bind(document);
8
- const patchedEvents = new Set();
9
- // Inferno's delegated events
10
- const INFERNO_EVENTS = [
11
- "click",
12
- "dblclick",
13
- "focusin",
14
- "focusout",
15
- "keydown",
16
- "keypress",
17
- "keyup",
18
- "mousedown",
19
- "mousemove",
20
- "mouseup",
21
- "touchend",
22
- "touchmove",
23
- "touchstart",
24
- "change",
25
- "input",
26
- "submit",
27
- ];
28
- // Temporarily replace addEventListener
29
- document.addEventListener = function (type, listener, options) {
30
- // Only wrap Inferno's delegated event listeners
31
- if (INFERNO_EVENTS.includes(type) &&
32
- typeof listener === "function" &&
33
- !patchedEvents.has(type)) {
34
- patchedEvents.add(type);
35
- const wrappedListener = function (event) {
36
- syncBatch(() => {
37
- listener.call(this, event);
38
- });
39
- };
40
- return originalAddEventListener(type, wrappedListener, options);
41
- }
42
- // @ts-ignore
43
- return originalAddEventListener(type, listener, options);
44
- };
45
- try {
46
- // Call render - Inferno will synchronously attach its listeners
47
- renderFn();
48
- }
49
- finally {
50
- // Restore original addEventListener
51
- document.addEventListener = originalAddEventListener;
52
- }
53
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=batch.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"batch.test.d.ts","sourceRoot":"","sources":["../../src/tests/batch.test.ts"],"names":[],"mappings":""}
@@ -1,434 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { syncBatch } from "../batch";
3
- import { useState } from "../useState";
4
- import { Observer } from "../observation";
5
- describe("syncBatch", () => {
6
- it("should batch multiple state changes into a single notification", () => {
7
- const state = useState({ count: 0, name: "Alice" });
8
- let notifyCount = 0;
9
- const observer = new Observer(() => {
10
- notifyCount++;
11
- });
12
- const dispose = observer.observe();
13
- state.count; // Track count
14
- state.name; // Track name
15
- dispose();
16
- // Make multiple changes in a batch
17
- syncBatch(() => {
18
- state.count = 1;
19
- state.name = "Bob";
20
- state.count = 2;
21
- });
22
- // Should only notify once despite multiple changes, and synchronously
23
- expect(notifyCount).toBe(1);
24
- expect(state.count).toBe(2);
25
- expect(state.name).toBe("Bob");
26
- observer.dispose();
27
- });
28
- it("should handle nested batches correctly", () => {
29
- const state = useState({ count: 0 });
30
- let notifyCount = 0;
31
- const observer = new Observer(() => {
32
- notifyCount++;
33
- });
34
- const dispose = observer.observe();
35
- state.count; // Track
36
- dispose();
37
- syncBatch(() => {
38
- state.count = 1;
39
- syncBatch(() => {
40
- state.count = 2;
41
- });
42
- state.count = 3;
43
- });
44
- // Should still only notify once for nested batches
45
- expect(notifyCount).toBe(1);
46
- expect(state.count).toBe(3);
47
- observer.dispose();
48
- });
49
- it("should handle multiple observers with syncBatch", () => {
50
- const state = useState({ count: 0 });
51
- let notifyCount1 = 0;
52
- let notifyCount2 = 0;
53
- const observer1 = new Observer(() => {
54
- notifyCount1++;
55
- });
56
- const observer2 = new Observer(() => {
57
- notifyCount2++;
58
- });
59
- const dispose1 = observer1.observe();
60
- state.count; // Track in observer1
61
- dispose1();
62
- const dispose2 = observer2.observe();
63
- state.count; // Track in observer2
64
- dispose2();
65
- syncBatch(() => {
66
- state.count = 1;
67
- state.count = 2;
68
- state.count = 3;
69
- });
70
- // Both observers should be notified exactly once
71
- expect(notifyCount1).toBe(1);
72
- expect(notifyCount2).toBe(1);
73
- observer1.dispose();
74
- observer2.dispose();
75
- });
76
- it("should maintain correct state values after syncBatch", () => {
77
- const state = useState({
78
- count: 0,
79
- name: "Alice",
80
- items: [1, 2, 3],
81
- });
82
- syncBatch(() => {
83
- state.count = 10;
84
- state.name = "Bob";
85
- state.items.push(4);
86
- state.items[0] = 100;
87
- });
88
- expect(state.count).toBe(10);
89
- expect(state.name).toBe("Bob");
90
- expect(state.items).toEqual([100, 2, 3, 4]);
91
- });
92
- it("should not flush if exception thrown within syncBatch", () => {
93
- const state = useState({ count: 0 });
94
- let notifyCount = 0;
95
- const observer = new Observer(() => {
96
- notifyCount++;
97
- });
98
- const dispose = observer.observe();
99
- state.count; // Track
100
- dispose();
101
- try {
102
- syncBatch(() => {
103
- state.count = 1;
104
- throw new Error("Test error");
105
- });
106
- }
107
- catch (e) {
108
- // Expected error
109
- }
110
- // Should NOT have flushed since the batch was interrupted
111
- expect(notifyCount).toBe(0);
112
- // But state change still occurred
113
- expect(state.count).toBe(1);
114
- observer.dispose();
115
- });
116
- it("should deduplicate notifications for the same observer", () => {
117
- const state = useState({ count: 0, name: "Alice" });
118
- let notifyCount = 0;
119
- const observer = new Observer(() => {
120
- notifyCount++;
121
- });
122
- const dispose = observer.observe();
123
- state.count; // Track
124
- state.name; // Track
125
- dispose();
126
- syncBatch(() => {
127
- state.count = 1; // Triggers observer
128
- state.name = "Bob"; // Triggers same observer again
129
- state.count = 2; // Triggers observer yet again
130
- });
131
- // Should deduplicate and only notify once
132
- expect(notifyCount).toBe(1);
133
- observer.dispose();
134
- });
135
- });
136
- describe("queue (async batching)", () => {
137
- it("should queue updates and flush on microtask", async () => {
138
- const state = useState({ count: 0 });
139
- let notifyCount = 0;
140
- const observer = new Observer(() => {
141
- notifyCount++;
142
- });
143
- const dispose = observer.observe();
144
- state.count; // Track
145
- dispose();
146
- // Make changes that will be queued
147
- state.count = 1;
148
- state.count = 2;
149
- state.count = 3;
150
- // Not yet notified (queued)
151
- expect(notifyCount).toBe(0);
152
- // Wait for microtask flush
153
- await new Promise((resolve) => setTimeout(resolve, 0));
154
- // Should have notified once after flush
155
- expect(notifyCount).toBe(1);
156
- expect(state.count).toBe(3);
157
- observer.dispose();
158
- });
159
- it("should batch multiple async updates into one notification", async () => {
160
- const state = useState({ count: 0, name: "Alice" });
161
- let notifyCount = 0;
162
- const observer = new Observer(() => {
163
- notifyCount++;
164
- });
165
- const dispose = observer.observe();
166
- state.count;
167
- state.name;
168
- dispose();
169
- state.count = 1;
170
- state.name = "Bob";
171
- state.count = 2;
172
- await new Promise((resolve) => setTimeout(resolve, 0));
173
- // Should batch all updates into single notification
174
- expect(notifyCount).toBe(1);
175
- observer.dispose();
176
- });
177
- it("should handle separate async batches", async () => {
178
- const state = useState({ count: 0 });
179
- let notifyCount = 0;
180
- const observer = new Observer(() => {
181
- notifyCount++;
182
- });
183
- const dispose = observer.observe();
184
- state.count;
185
- dispose();
186
- state.count = 1;
187
- await new Promise((resolve) => setTimeout(resolve, 0));
188
- const afterFirst = notifyCount;
189
- state.count = 2;
190
- await new Promise((resolve) => setTimeout(resolve, 0));
191
- const afterSecond = notifyCount;
192
- expect(afterFirst).toBe(1);
193
- expect(afterSecond).toBe(2);
194
- observer.dispose();
195
- });
196
- });
197
- describe("syncBatch with nested async updates", () => {
198
- it("should handle syncBatch inside async context", async () => {
199
- const state = useState({ count: 0 });
200
- let notifyCount = 0;
201
- const observer = new Observer(() => {
202
- notifyCount++;
203
- });
204
- const dispose = observer.observe();
205
- state.count;
206
- dispose();
207
- // Async update
208
- state.count = 1;
209
- await new Promise((resolve) => setTimeout(resolve, 0));
210
- expect(notifyCount).toBe(1);
211
- // Sync batch after async
212
- syncBatch(() => {
213
- state.count = 2;
214
- state.count = 3;
215
- });
216
- expect(notifyCount).toBe(2); // +1 from sync batch
217
- observer.dispose();
218
- });
219
- it("should handle async updates inside syncBatch callback", async () => {
220
- const state = useState({ count: 0 });
221
- let notifyCount = 0;
222
- const observer = new Observer(() => {
223
- notifyCount++;
224
- });
225
- const dispose = observer.observe();
226
- state.count;
227
- dispose();
228
- syncBatch(() => {
229
- state.count = 1;
230
- // Trigger an async update from within syncBatch
231
- setTimeout(() => {
232
- state.count = 2;
233
- }, 0);
234
- });
235
- // Sync batch should flush immediately
236
- expect(notifyCount).toBe(1);
237
- expect(state.count).toBe(1);
238
- // Wait for async update
239
- await new Promise((resolve) => setTimeout(resolve, 10));
240
- expect(notifyCount).toBe(2);
241
- expect(state.count).toBe(2);
242
- observer.dispose();
243
- });
244
- });
245
- describe("syncBatch with cascading updates", () => {
246
- it("should handle cascading observer notifications within the same batch", () => {
247
- const state = useState({ count: 0 });
248
- const derived = useState({ doubled: 0 });
249
- let stateNotifyCount = 0;
250
- let derivedNotifyCount = 0;
251
- let componentNotifyCount = 0;
252
- // Observer 1: Watches state, updates derived (simulates useDerived)
253
- const derivedObserver = new Observer(() => {
254
- stateNotifyCount++;
255
- // When state changes, update derived synchronously
256
- derived.doubled = state.count * 2;
257
- });
258
- const dispose1 = derivedObserver.observe();
259
- state.count; // Track state
260
- dispose1();
261
- // Observer 2: Watches derived (simulates component)
262
- const componentObserver = new Observer(() => {
263
- derivedNotifyCount++;
264
- });
265
- const dispose2 = componentObserver.observe();
266
- derived.doubled; // Track derived
267
- dispose2();
268
- // Observer 3: Also watches derived (another component)
269
- const component2Observer = new Observer(() => {
270
- componentNotifyCount++;
271
- });
272
- const dispose3 = component2Observer.observe();
273
- derived.doubled; // Track derived
274
- dispose3();
275
- // Make a change in a batch
276
- syncBatch(() => {
277
- state.count = 5;
278
- });
279
- // All observers should have been notified exactly once
280
- expect(stateNotifyCount).toBe(1);
281
- expect(derivedNotifyCount).toBe(1);
282
- expect(componentNotifyCount).toBe(1);
283
- expect(state.count).toBe(5);
284
- expect(derived.doubled).toBe(10);
285
- derivedObserver.dispose();
286
- componentObserver.dispose();
287
- component2Observer.dispose();
288
- });
289
- it("should handle multi-level cascading updates", () => {
290
- const state = useState({ value: 0 });
291
- const derived1 = useState({ level1: 0 });
292
- const derived2 = useState({ level2: 0 });
293
- const derived3 = useState({ level3: 0 });
294
- const notifyCounts = [0, 0, 0, 0];
295
- // Level 1: state -> derived1
296
- const observer1 = new Observer(() => {
297
- notifyCounts[0]++;
298
- derived1.level1 = state.value + 1;
299
- });
300
- const dispose1 = observer1.observe();
301
- state.value;
302
- dispose1();
303
- // Level 2: derived1 -> derived2
304
- const observer2 = new Observer(() => {
305
- notifyCounts[1]++;
306
- derived2.level2 = derived1.level1 + 1;
307
- });
308
- const dispose2 = observer2.observe();
309
- derived1.level1;
310
- dispose2();
311
- // Level 3: derived2 -> derived3
312
- const observer3 = new Observer(() => {
313
- notifyCounts[2]++;
314
- derived3.level3 = derived2.level2 + 1;
315
- });
316
- const dispose3 = observer3.observe();
317
- derived2.level2;
318
- dispose3();
319
- // Final observer: watches derived3
320
- const observer4 = new Observer(() => {
321
- notifyCounts[3]++;
322
- });
323
- const dispose4 = observer4.observe();
324
- derived3.level3;
325
- dispose4();
326
- // Update state in a batch
327
- syncBatch(() => {
328
- state.value = 10;
329
- });
330
- // All levels should have cascaded and each observer notified exactly once
331
- expect(notifyCounts).toEqual([1, 1, 1, 1]);
332
- expect(state.value).toBe(10);
333
- expect(derived1.level1).toBe(11);
334
- expect(derived2.level2).toBe(12);
335
- expect(derived3.level3).toBe(13);
336
- observer1.dispose();
337
- observer2.dispose();
338
- observer3.dispose();
339
- observer4.dispose();
340
- });
341
- it("should handle diamond dependency pattern", () => {
342
- // Diamond: state -> [derived1, derived2] -> derived3
343
- const state = useState({ value: 0 });
344
- const derived1 = useState({ path1: 0 });
345
- const derived2 = useState({ path2: 0 });
346
- const derived3 = useState({ combined: 0 });
347
- let derived3NotifyCount = 0;
348
- // State -> derived1
349
- const obs1 = new Observer(() => {
350
- derived1.path1 = state.value * 2;
351
- });
352
- const d1 = obs1.observe();
353
- state.value;
354
- d1();
355
- // State -> derived2
356
- const obs2 = new Observer(() => {
357
- derived2.path2 = state.value * 3;
358
- });
359
- const d2 = obs2.observe();
360
- state.value;
361
- d2();
362
- // [derived1, derived2] -> derived3
363
- const obs3 = new Observer(() => {
364
- derived3.combined = derived1.path1 + derived2.path2;
365
- });
366
- const d3 = obs3.observe();
367
- derived1.path1;
368
- derived2.path2;
369
- d3();
370
- // Watch derived3
371
- const obs4 = new Observer(() => {
372
- derived3NotifyCount++;
373
- });
374
- const d4 = obs4.observe();
375
- derived3.combined;
376
- d4();
377
- syncBatch(() => {
378
- state.value = 5;
379
- });
380
- // derived3 should only be notified once despite two paths updating
381
- expect(derived3NotifyCount).toBe(1);
382
- expect(derived1.path1).toBe(10);
383
- expect(derived2.path2).toBe(15);
384
- expect(derived3.combined).toBe(25);
385
- obs1.dispose();
386
- obs2.dispose();
387
- obs3.dispose();
388
- obs4.dispose();
389
- });
390
- it("should not create infinite loops with circular dependencies", () => {
391
- const state1 = useState({ value: 0 });
392
- const state2 = useState({ value: 0 });
393
- let notify1Count = 0;
394
- let notify2Count = 0;
395
- // Observer 1: watches state1, updates state2
396
- const obs1 = new Observer(() => {
397
- notify1Count++;
398
- if (notify1Count > 10) {
399
- throw new Error("Infinite loop detected");
400
- }
401
- // Only update if different to break the cycle
402
- if (state2.value !== state1.value + 1) {
403
- state2.value = state1.value + 1;
404
- }
405
- });
406
- const d1 = obs1.observe();
407
- state1.value;
408
- d1();
409
- // Observer 2: watches state2, updates state1
410
- const obs2 = new Observer(() => {
411
- notify2Count++;
412
- if (notify2Count > 10) {
413
- throw new Error("Infinite loop detected");
414
- }
415
- // Only update if different to break the cycle
416
- if (state1.value !== state2.value - 1) {
417
- state1.value = state2.value - 1;
418
- }
419
- });
420
- const d2 = obs2.observe();
421
- state2.value;
422
- d2();
423
- syncBatch(() => {
424
- state1.value = 5;
425
- });
426
- // Should stabilize without infinite loop
427
- expect(notify1Count).toBeLessThan(10);
428
- expect(notify2Count).toBeLessThan(10);
429
- expect(state1.value).toBe(5);
430
- expect(state2.value).toBe(6);
431
- obs1.dispose();
432
- obs2.dispose();
433
- });
434
- });