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.
- package/dist/jsx-runtime.d.ts +1 -4
- package/dist/jsx-runtime.d.ts.map +1 -1
- package/dist/jsx-runtime.js +5 -7
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +21 -17
- package/dist/types.d.ts +3 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/swc-plugin/target/wasm32-wasip1/release/swc_plugin_rask_component.wasm +0 -0
- package/dist/batch.d.ts +0 -6
- package/dist/batch.d.ts.map +0 -1
- package/dist/batch.js +0 -84
- package/dist/compiler.d.ts +0 -4
- package/dist/compiler.d.ts.map +0 -1
- package/dist/compiler.js +0 -7
- package/dist/createAsync.d.ts +0 -39
- package/dist/createAsync.d.ts.map +0 -1
- package/dist/createAsync.js +0 -47
- package/dist/createComputed.d.ts +0 -4
- package/dist/createComputed.d.ts.map +0 -1
- package/dist/createComputed.js +0 -69
- package/dist/createEffect.d.ts +0 -2
- package/dist/createEffect.d.ts.map +0 -1
- package/dist/createEffect.js +0 -29
- package/dist/createMutation.d.ts +0 -43
- package/dist/createMutation.d.ts.map +0 -1
- package/dist/createMutation.js +0 -76
- package/dist/createQuery.d.ts +0 -42
- package/dist/createQuery.d.ts.map +0 -1
- package/dist/createQuery.js +0 -80
- package/dist/createRouter.d.ts +0 -8
- package/dist/createRouter.d.ts.map +0 -1
- package/dist/createRouter.js +0 -27
- package/dist/createState.d.ts +0 -28
- package/dist/createState.d.ts.map +0 -1
- package/dist/createState.js +0 -129
- package/dist/createTask.d.ts +0 -31
- package/dist/createTask.d.ts.map +0 -1
- package/dist/createTask.js +0 -79
- package/dist/createView.d.ts +0 -28
- package/dist/createView.d.ts.map +0 -1
- package/dist/createView.js +0 -77
- package/dist/error.d.ts +0 -5
- package/dist/error.d.ts.map +0 -1
- package/dist/error.js +0 -16
- package/dist/jsx.d.ts +0 -11
- package/dist/observation.d.ts +0 -93
- package/dist/observation.d.ts.map +0 -1
- package/dist/observation.js +0 -200
- package/dist/patchInferno.d.ts +0 -6
- package/dist/patchInferno.d.ts.map +0 -1
- package/dist/patchInferno.js +0 -53
- package/dist/tests/batch.test.d.ts +0 -2
- package/dist/tests/batch.test.d.ts.map +0 -1
- package/dist/tests/batch.test.js +0 -434
- package/dist/tests/createComputed.test.d.ts +0 -2
- package/dist/tests/createComputed.test.d.ts.map +0 -1
- package/dist/tests/createComputed.test.js +0 -257
- package/dist/tests/createContext.test.d.ts +0 -2
- package/dist/tests/createContext.test.d.ts.map +0 -1
- package/dist/tests/createContext.test.js +0 -149
- package/dist/tests/createEffect.test.d.ts +0 -2
- package/dist/tests/createEffect.test.d.ts.map +0 -1
- package/dist/tests/createEffect.test.js +0 -467
- package/dist/tests/createState.test.d.ts +0 -2
- package/dist/tests/createState.test.d.ts.map +0 -1
- package/dist/tests/createState.test.js +0 -144
- package/dist/tests/createTask.test.d.ts +0 -2
- package/dist/tests/createTask.test.d.ts.map +0 -1
- package/dist/tests/createTask.test.js +0 -322
- package/dist/tests/createView.test.d.ts +0 -2
- package/dist/tests/createView.test.d.ts.map +0 -1
- package/dist/tests/createView.test.js +0 -203
- package/dist/tests/error.test.d.ts +0 -2
- package/dist/tests/error.test.d.ts.map +0 -1
- package/dist/tests/error.test.js +0 -181
- package/dist/tests/observation.test.d.ts +0 -2
- package/dist/tests/observation.test.d.ts.map +0 -1
- package/dist/tests/observation.test.js +0 -341
- package/dist/tests/renderCount.test.d.ts +0 -2
- package/dist/tests/renderCount.test.d.ts.map +0 -1
- package/dist/tests/renderCount.test.js +0 -95
- package/dist/tests/scopeEnforcement.test.d.ts +0 -2
- package/dist/tests/scopeEnforcement.test.d.ts.map +0 -1
- package/dist/tests/scopeEnforcement.test.js +0 -157
- package/dist/tests/useAction.test.d.ts +0 -2
- package/dist/tests/useAction.test.d.ts.map +0 -1
- package/dist/tests/useAction.test.js +0 -132
- package/dist/tests/useAsync.test.d.ts +0 -2
- package/dist/tests/useAsync.test.d.ts.map +0 -1
- package/dist/tests/useAsync.test.js +0 -499
- package/dist/tests/useDerived.test.d.ts +0 -2
- package/dist/tests/useDerived.test.d.ts.map +0 -1
- package/dist/tests/useDerived.test.js +0 -407
- package/dist/tests/useEffect.test.d.ts +0 -2
- package/dist/tests/useEffect.test.d.ts.map +0 -1
- package/dist/tests/useEffect.test.js +0 -600
- package/dist/tests/useLookup.test.d.ts +0 -2
- package/dist/tests/useLookup.test.d.ts.map +0 -1
- package/dist/tests/useLookup.test.js +0 -299
- package/dist/tests/useRef.test.d.ts +0 -2
- package/dist/tests/useRef.test.d.ts.map +0 -1
- package/dist/tests/useRef.test.js +0 -189
- package/dist/tests/useState.test.d.ts +0 -2
- package/dist/tests/useState.test.d.ts.map +0 -1
- package/dist/tests/useState.test.js +0 -178
- package/dist/tests/useSuspend.test.d.ts +0 -2
- package/dist/tests/useSuspend.test.d.ts.map +0 -1
- package/dist/tests/useSuspend.test.js +0 -752
- package/dist/tests/useView.test.d.ts +0 -2
- package/dist/tests/useView.test.d.ts.map +0 -1
- package/dist/tests/useView.test.js +0 -305
- package/dist/useComputed.d.ts +0 -5
- package/dist/useComputed.d.ts.map +0 -1
- package/dist/useComputed.js +0 -69
- package/dist/useQuery.d.ts +0 -25
- package/dist/useQuery.d.ts.map +0 -1
- package/dist/useQuery.js +0 -25
- package/dist/useSuspendAsync.d.ts +0 -18
- package/dist/useSuspendAsync.d.ts.map +0 -1
- package/dist/useSuspendAsync.js +0 -37
- package/dist/useTask.d.ts +0 -25
- package/dist/useTask.d.ts.map +0 -1
- package/dist/useTask.js +0 -70
package/dist/observation.js
DELETED
|
@@ -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
|
-
}
|
package/dist/patchInferno.d.ts
DELETED
|
@@ -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"}
|
package/dist/patchInferno.js
DELETED
|
@@ -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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"batch.test.d.ts","sourceRoot":"","sources":["../../src/tests/batch.test.ts"],"names":[],"mappings":""}
|
package/dist/tests/batch.test.js
DELETED
|
@@ -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
|
-
});
|