rask-ui 0.23.0 → 0.24.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/compiler.d.ts +3 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +7 -1
- package/dist/component.d.ts +4 -18
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +13 -65
- package/dist/createComputed.d.ts +4 -0
- package/dist/createComputed.d.ts.map +1 -0
- package/dist/createComputed.js +69 -0
- package/dist/createEffect.d.ts +2 -0
- package/dist/createEffect.d.ts.map +1 -0
- package/dist/createEffect.js +29 -0
- package/dist/createRouter.d.ts +8 -0
- package/dist/createRouter.d.ts.map +1 -0
- package/dist/createRouter.js +27 -0
- package/dist/createState.d.ts +2 -0
- package/dist/createState.d.ts.map +1 -1
- package/dist/createState.js +40 -5
- package/dist/createTask.d.ts +31 -0
- package/dist/createTask.d.ts.map +1 -0
- package/dist/createTask.js +79 -0
- package/dist/createView.d.ts +18 -44
- package/dist/createView.d.ts.map +1 -1
- package/dist/createView.js +57 -48
- package/dist/error.d.ts +3 -14
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +14 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/jsx.d.ts +10 -256
- package/dist/observation.d.ts +1 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/patchInferno.d.ts +6 -0
- package/dist/patchInferno.d.ts.map +1 -0
- package/dist/patchInferno.js +53 -0
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +14 -26
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +107 -0
- package/dist/tests/batch.test.d.ts +2 -0
- package/dist/tests/batch.test.d.ts.map +1 -0
- package/dist/tests/batch.test.js +244 -0
- package/dist/tests/createComputed.test.d.ts +2 -0
- package/dist/tests/createComputed.test.d.ts.map +1 -0
- package/dist/tests/createComputed.test.js +257 -0
- package/dist/tests/createContext.test.d.ts +2 -0
- package/dist/tests/createContext.test.d.ts.map +1 -0
- package/dist/tests/createContext.test.js +136 -0
- package/dist/tests/createEffect.test.d.ts +2 -0
- package/dist/tests/createEffect.test.d.ts.map +1 -0
- package/dist/tests/createEffect.test.js +467 -0
- package/dist/tests/createState.test.d.ts.map +1 -0
- package/dist/tests/createState.test.js +144 -0
- package/dist/tests/createTask.test.d.ts +2 -0
- package/dist/tests/createTask.test.d.ts.map +1 -0
- package/dist/tests/createTask.test.js +322 -0
- package/dist/tests/createView.test.d.ts.map +1 -0
- package/dist/{createView.test.js → tests/createView.test.js} +40 -40
- package/dist/tests/error.test.d.ts +2 -0
- package/dist/tests/error.test.d.ts.map +1 -0
- package/dist/tests/error.test.js +168 -0
- package/dist/tests/observation.test.d.ts.map +1 -0
- package/dist/tests/observation.test.js +341 -0
- package/dist/useComputed.d.ts +5 -0
- package/dist/useComputed.d.ts.map +1 -0
- package/dist/useComputed.js +69 -0
- package/dist/useQuery.d.ts +25 -0
- package/dist/useQuery.d.ts.map +1 -0
- package/dist/useQuery.js +25 -0
- package/dist/useSuspendAsync.d.ts +18 -0
- package/dist/useSuspendAsync.d.ts.map +1 -0
- package/dist/useSuspendAsync.js +37 -0
- package/dist/useTask.d.ts +25 -0
- package/dist/useTask.d.ts.map +1 -0
- package/dist/useTask.js +70 -0
- package/package.json +1 -1
- package/swc-plugin/target/wasm32-wasip1/release/swc_plugin_rask_component.wasm +0 -0
- package/dist/asyncState.d.ts +0 -16
- package/dist/asyncState.d.ts.map +0 -1
- package/dist/asyncState.js +0 -24
- package/dist/context.d.ts +0 -5
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -29
- package/dist/createAsync.test.d.ts +0 -2
- package/dist/createAsync.test.d.ts.map +0 -1
- package/dist/createAsync.test.js +0 -110
- package/dist/createMutation.test.d.ts +0 -2
- package/dist/createMutation.test.d.ts.map +0 -1
- package/dist/createMutation.test.js +0 -168
- package/dist/createQuery.test.d.ts +0 -2
- package/dist/createQuery.test.d.ts.map +0 -1
- package/dist/createQuery.test.js +0 -156
- package/dist/createRef.d.ts +0 -6
- package/dist/createRef.d.ts.map +0 -1
- package/dist/createRef.js +0 -8
- package/dist/createState.test.d.ts.map +0 -1
- package/dist/createState.test.js +0 -111
- package/dist/createView.test.d.ts.map +0 -1
- package/dist/observation.test.d.ts.map +0 -1
- package/dist/observation.test.js +0 -150
- package/dist/suspense.d.ts +0 -25
- package/dist/suspense.d.ts.map +0 -1
- package/dist/suspense.js +0 -97
- package/dist/test-setup.d.ts +0 -16
- package/dist/test-setup.d.ts.map +0 -1
- package/dist/test-setup.js +0 -40
- package/dist/test.d.ts +0 -2
- package/dist/test.d.ts.map +0 -1
- package/dist/test.js +0 -24
- /package/dist/{createState.test.d.ts → tests/createState.test.d.ts} +0 -0
- /package/dist/{createView.test.d.ts → tests/createView.test.d.ts} +0 -0
- /package/dist/{observation.test.d.ts → tests/observation.test.d.ts} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.test.d.ts","sourceRoot":"","sources":["../../src/tests/error.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "./jsx-runtime";
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { ErrorBoundary } from "../error";
|
|
4
|
+
import { render } from "../";
|
|
5
|
+
describe("ErrorBoundary", () => {
|
|
6
|
+
it("should render children when no error occurs", async () => {
|
|
7
|
+
function SafeChild() {
|
|
8
|
+
return () => _jsx("div", { children: "Safe content" });
|
|
9
|
+
}
|
|
10
|
+
function TestComponent() {
|
|
11
|
+
return () => (_jsx(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Error: ", String(error)] }), children: _jsx(SafeChild, {}) }));
|
|
12
|
+
}
|
|
13
|
+
const container = document.createElement("div");
|
|
14
|
+
document.body.appendChild(container);
|
|
15
|
+
render(_jsx(TestComponent, {}), container);
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
17
|
+
expect(container.textContent).toContain("Safe content");
|
|
18
|
+
expect(container.textContent).not.toContain("Error:");
|
|
19
|
+
document.body.removeChild(container);
|
|
20
|
+
});
|
|
21
|
+
it("should catch errors thrown in child components", async () => {
|
|
22
|
+
function ThrowingChild() {
|
|
23
|
+
return () => {
|
|
24
|
+
throw new Error("Child component error");
|
|
25
|
+
return _jsx("div", {});
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function TestComponent() {
|
|
29
|
+
return () => (_jsx(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Error: ", String(error)] }), children: _jsx(ThrowingChild, {}) }));
|
|
30
|
+
}
|
|
31
|
+
const container = document.createElement("div");
|
|
32
|
+
document.body.appendChild(container);
|
|
33
|
+
render(_jsx(TestComponent, {}), container);
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
35
|
+
expect(container.textContent).toContain("Error:");
|
|
36
|
+
expect(container.textContent).toContain("Child component error");
|
|
37
|
+
document.body.removeChild(container);
|
|
38
|
+
});
|
|
39
|
+
it("should render custom error UI", async () => {
|
|
40
|
+
function ThrowingChild() {
|
|
41
|
+
return () => {
|
|
42
|
+
throw new Error("Something went wrong");
|
|
43
|
+
return _jsx("div", {});
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function TestComponent() {
|
|
47
|
+
return () => (_jsx(ErrorBoundary, { error: (error) => (_jsxs("div", { class: "error-ui", children: [_jsx("h1", { children: "Oops!" }), _jsx("p", { children: String(error) })] })), children: _jsx(ThrowingChild, {}) }));
|
|
48
|
+
}
|
|
49
|
+
const container = document.createElement("div");
|
|
50
|
+
document.body.appendChild(container);
|
|
51
|
+
render(_jsx(TestComponent, {}), container);
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
53
|
+
const errorUI = document.querySelector(".error-ui");
|
|
54
|
+
expect(errorUI).not.toBeNull();
|
|
55
|
+
expect(errorUI?.querySelector("h1")?.textContent).toBe("Oops!");
|
|
56
|
+
expect(errorUI?.textContent).toContain("Something went wrong");
|
|
57
|
+
document.body.removeChild(container);
|
|
58
|
+
});
|
|
59
|
+
it("should handle multiple children", async () => {
|
|
60
|
+
function SafeChild1() {
|
|
61
|
+
return () => _jsx("div", { children: "Child 1" });
|
|
62
|
+
}
|
|
63
|
+
function SafeChild2() {
|
|
64
|
+
return () => _jsx("div", { children: "Child 2" });
|
|
65
|
+
}
|
|
66
|
+
function TestComponent() {
|
|
67
|
+
return () => (_jsxs(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Error: ", String(error)] }), children: [_jsx(SafeChild1, {}), _jsx(SafeChild2, {})] }));
|
|
68
|
+
}
|
|
69
|
+
const container = document.createElement("div");
|
|
70
|
+
document.body.appendChild(container);
|
|
71
|
+
render(_jsx(TestComponent, {}), container);
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
73
|
+
expect(container.textContent).toContain("Child 1");
|
|
74
|
+
expect(container.textContent).toContain("Child 2");
|
|
75
|
+
document.body.removeChild(container);
|
|
76
|
+
});
|
|
77
|
+
it("should catch errors from nested children", async () => {
|
|
78
|
+
function DeepChild() {
|
|
79
|
+
return () => {
|
|
80
|
+
throw new Error("Deep error");
|
|
81
|
+
return _jsx("div", {});
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function MiddleChild() {
|
|
85
|
+
return () => _jsx(DeepChild, {});
|
|
86
|
+
}
|
|
87
|
+
function TestComponent() {
|
|
88
|
+
return () => (_jsx(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Caught: ", String(error)] }), children: _jsx(MiddleChild, {}) }));
|
|
89
|
+
}
|
|
90
|
+
const container = document.createElement("div");
|
|
91
|
+
document.body.appendChild(container);
|
|
92
|
+
render(_jsx(TestComponent, {}), container);
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
94
|
+
expect(container.textContent).toContain("Caught:");
|
|
95
|
+
expect(container.textContent).toContain("Deep error");
|
|
96
|
+
document.body.removeChild(container);
|
|
97
|
+
});
|
|
98
|
+
it("should allow nested error boundaries", async () => {
|
|
99
|
+
function ThrowingChild() {
|
|
100
|
+
return () => {
|
|
101
|
+
throw new Error("Inner error");
|
|
102
|
+
return _jsx("div", {});
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function TestComponent() {
|
|
106
|
+
return () => (_jsx(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Outer: ", String(error)] }), children: _jsx(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Inner: ", String(error)] }), children: _jsx(ThrowingChild, {}) }) }));
|
|
107
|
+
}
|
|
108
|
+
const container = document.createElement("div");
|
|
109
|
+
document.body.appendChild(container);
|
|
110
|
+
render(_jsx(TestComponent, {}), container);
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
112
|
+
// Inner boundary should catch the error
|
|
113
|
+
expect(container.textContent).toContain("Inner:");
|
|
114
|
+
expect(container.textContent).not.toContain("Outer:");
|
|
115
|
+
document.body.removeChild(container);
|
|
116
|
+
});
|
|
117
|
+
it("should handle string errors", async () => {
|
|
118
|
+
function ThrowingChild() {
|
|
119
|
+
return () => {
|
|
120
|
+
throw "String error";
|
|
121
|
+
return _jsx("div", {});
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function TestComponent() {
|
|
125
|
+
return () => (_jsx(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Error: ", String(error)] }), children: _jsx(ThrowingChild, {}) }));
|
|
126
|
+
}
|
|
127
|
+
const container = document.createElement("div");
|
|
128
|
+
document.body.appendChild(container);
|
|
129
|
+
render(_jsx(TestComponent, {}), container);
|
|
130
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
131
|
+
expect(container.textContent).toContain("String error");
|
|
132
|
+
document.body.removeChild(container);
|
|
133
|
+
});
|
|
134
|
+
it("should handle object errors", async () => {
|
|
135
|
+
function ThrowingChild() {
|
|
136
|
+
return () => {
|
|
137
|
+
throw { message: "Custom error object", code: 500 };
|
|
138
|
+
return _jsx("div", {});
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function TestComponent() {
|
|
142
|
+
return () => (_jsx(ErrorBoundary, { error: (error) => (_jsxs("div", { children: ["Error: ", error.message, " (Code: ", error.code, ")"] })), children: _jsx(ThrowingChild, {}) }));
|
|
143
|
+
}
|
|
144
|
+
const container = document.createElement("div");
|
|
145
|
+
document.body.appendChild(container);
|
|
146
|
+
render(_jsx(TestComponent, {}), container);
|
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
148
|
+
expect(container.textContent).toContain("Custom error object");
|
|
149
|
+
expect(container.textContent).toContain("500");
|
|
150
|
+
document.body.removeChild(container);
|
|
151
|
+
});
|
|
152
|
+
it("should switch back to children if error is cleared", async () => {
|
|
153
|
+
// Note: This test demonstrates the current behavior
|
|
154
|
+
// In practice, error clearing would require additional implementation
|
|
155
|
+
function SafeChild() {
|
|
156
|
+
return () => _jsx("div", { children: "Safe content" });
|
|
157
|
+
}
|
|
158
|
+
function TestComponent() {
|
|
159
|
+
return () => (_jsx(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Error: ", String(error)] }), children: _jsx(SafeChild, {}) }));
|
|
160
|
+
}
|
|
161
|
+
const container = document.createElement("div");
|
|
162
|
+
document.body.appendChild(container);
|
|
163
|
+
render(_jsx(TestComponent, {}), container);
|
|
164
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
165
|
+
expect(container.textContent).toContain("Safe content");
|
|
166
|
+
document.body.removeChild(container);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observation.test.d.ts","sourceRoot":"","sources":["../../src/tests/observation.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { Signal, Observer, getCurrentObserver } from "../observation";
|
|
3
|
+
describe("Signal-Observer System (Optimized)", () => {
|
|
4
|
+
describe("Observer", () => {
|
|
5
|
+
it("should track signals during observation", () => {
|
|
6
|
+
const callback = vi.fn();
|
|
7
|
+
const observer = new Observer(callback);
|
|
8
|
+
const signal = new Signal();
|
|
9
|
+
const dispose = observer.observe();
|
|
10
|
+
observer.subscribeSignal(signal);
|
|
11
|
+
dispose();
|
|
12
|
+
signal.notify();
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
queueMicrotask(() => {
|
|
15
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
16
|
+
resolve(undefined);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
it("should handle multiple signals", async () => {
|
|
21
|
+
const callback = vi.fn();
|
|
22
|
+
const observer = new Observer(callback);
|
|
23
|
+
const signal1 = new Signal();
|
|
24
|
+
const signal2 = new Signal();
|
|
25
|
+
const dispose = observer.observe();
|
|
26
|
+
observer.subscribeSignal(signal1);
|
|
27
|
+
observer.subscribeSignal(signal2);
|
|
28
|
+
dispose();
|
|
29
|
+
signal1.notify();
|
|
30
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
31
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
32
|
+
signal2.notify();
|
|
33
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
34
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
35
|
+
});
|
|
36
|
+
it("should clear signals when observing again", async () => {
|
|
37
|
+
let callCount = 0;
|
|
38
|
+
const observer = new Observer(() => {
|
|
39
|
+
callCount++;
|
|
40
|
+
});
|
|
41
|
+
const signal1 = new Signal();
|
|
42
|
+
const signal2 = new Signal();
|
|
43
|
+
// First observation
|
|
44
|
+
let dispose = observer.observe();
|
|
45
|
+
observer.subscribeSignal(signal1);
|
|
46
|
+
dispose();
|
|
47
|
+
// Second observation - should clear previous signals
|
|
48
|
+
dispose = observer.observe();
|
|
49
|
+
observer.subscribeSignal(signal2);
|
|
50
|
+
dispose();
|
|
51
|
+
// Notify first signal - should not trigger observer
|
|
52
|
+
signal1.notify();
|
|
53
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
54
|
+
expect(callCount).toBe(0);
|
|
55
|
+
// Notify second signal - should trigger observer
|
|
56
|
+
signal2.notify();
|
|
57
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
58
|
+
expect(callCount).toBe(1);
|
|
59
|
+
});
|
|
60
|
+
it("should dispose of all signal subscriptions", async () => {
|
|
61
|
+
const callback = vi.fn();
|
|
62
|
+
const observer = new Observer(callback);
|
|
63
|
+
const signal = new Signal();
|
|
64
|
+
const dispose = observer.observe();
|
|
65
|
+
observer.subscribeSignal(signal);
|
|
66
|
+
dispose();
|
|
67
|
+
observer.dispose();
|
|
68
|
+
signal.notify();
|
|
69
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
70
|
+
expect(callback).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
it("should not notify after disposal", async () => {
|
|
73
|
+
const callback = vi.fn();
|
|
74
|
+
const observer = new Observer(callback);
|
|
75
|
+
const signal = new Signal();
|
|
76
|
+
const dispose = observer.observe();
|
|
77
|
+
observer.subscribeSignal(signal);
|
|
78
|
+
dispose();
|
|
79
|
+
observer.dispose();
|
|
80
|
+
signal.notify();
|
|
81
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
82
|
+
expect(callback).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
it("should set current observer during observation", () => {
|
|
85
|
+
const observer = new Observer(() => { });
|
|
86
|
+
expect(getCurrentObserver()).toBeUndefined();
|
|
87
|
+
const dispose = observer.observe();
|
|
88
|
+
expect(getCurrentObserver()).toBe(observer);
|
|
89
|
+
dispose();
|
|
90
|
+
expect(getCurrentObserver()).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
it("should handle nested observations with stack", () => {
|
|
93
|
+
const observer1 = new Observer(() => { });
|
|
94
|
+
const observer2 = new Observer(() => { });
|
|
95
|
+
const dispose1 = observer1.observe();
|
|
96
|
+
expect(getCurrentObserver()).toBe(observer1);
|
|
97
|
+
const dispose2 = observer2.observe();
|
|
98
|
+
expect(getCurrentObserver()).toBe(observer2);
|
|
99
|
+
dispose2();
|
|
100
|
+
expect(getCurrentObserver()).toBe(observer1);
|
|
101
|
+
dispose1();
|
|
102
|
+
expect(getCurrentObserver()).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe("Signal", () => {
|
|
106
|
+
it("should notify all subscribed observers", async () => {
|
|
107
|
+
const signal = new Signal();
|
|
108
|
+
const callback1 = vi.fn();
|
|
109
|
+
const callback2 = vi.fn();
|
|
110
|
+
const observer1 = new Observer(callback1);
|
|
111
|
+
const observer2 = new Observer(callback2);
|
|
112
|
+
const dispose1 = observer1.observe();
|
|
113
|
+
observer1.subscribeSignal(signal);
|
|
114
|
+
dispose1();
|
|
115
|
+
const dispose2 = observer2.observe();
|
|
116
|
+
observer2.subscribeSignal(signal);
|
|
117
|
+
dispose2();
|
|
118
|
+
signal.notify();
|
|
119
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
120
|
+
expect(callback1).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(callback2).toHaveBeenCalledTimes(1);
|
|
122
|
+
});
|
|
123
|
+
it("should handle empty signal (no subscribers)", () => {
|
|
124
|
+
const signal = new Signal();
|
|
125
|
+
// Should not throw
|
|
126
|
+
expect(() => signal.notify()).not.toThrow();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe("Optimization: Epoch Barrier", () => {
|
|
130
|
+
it("should NOT notify new subscriptions created during notify", async () => {
|
|
131
|
+
const signal = new Signal();
|
|
132
|
+
const callback1 = vi.fn();
|
|
133
|
+
const callback2 = vi.fn();
|
|
134
|
+
const observer1 = new Observer(callback1);
|
|
135
|
+
const observer2 = new Observer(() => {
|
|
136
|
+
callback2();
|
|
137
|
+
// Subscribe a new observer DURING notification
|
|
138
|
+
const dispose = observer3.observe();
|
|
139
|
+
observer3.subscribeSignal(signal);
|
|
140
|
+
dispose();
|
|
141
|
+
});
|
|
142
|
+
const observer3 = new Observer(vi.fn());
|
|
143
|
+
const dispose1 = observer1.observe();
|
|
144
|
+
observer1.subscribeSignal(signal);
|
|
145
|
+
dispose1();
|
|
146
|
+
const dispose2 = observer2.observe();
|
|
147
|
+
observer2.subscribeSignal(signal);
|
|
148
|
+
dispose2();
|
|
149
|
+
signal.notify();
|
|
150
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
151
|
+
// observer1 and observer2 should fire
|
|
152
|
+
expect(callback1).toHaveBeenCalledTimes(1);
|
|
153
|
+
expect(callback2).toHaveBeenCalledTimes(1);
|
|
154
|
+
// observer3 should NOT fire in the same notify pass (added during notify)
|
|
155
|
+
// This is tested implicitly - if it fired, we'd see 3 calls above
|
|
156
|
+
});
|
|
157
|
+
it("should notify new subscriptions on NEXT notify", async () => {
|
|
158
|
+
const signal = new Signal();
|
|
159
|
+
let observer3Created = false;
|
|
160
|
+
const callback3 = vi.fn();
|
|
161
|
+
const observer3 = new Observer(callback3);
|
|
162
|
+
const observer1 = new Observer(() => {
|
|
163
|
+
if (!observer3Created) {
|
|
164
|
+
observer3Created = true;
|
|
165
|
+
const dispose = observer3.observe();
|
|
166
|
+
observer3.subscribeSignal(signal);
|
|
167
|
+
dispose();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
const dispose1 = observer1.observe();
|
|
171
|
+
observer1.subscribeSignal(signal);
|
|
172
|
+
dispose1();
|
|
173
|
+
// First notify - observer3 gets created but should not fire
|
|
174
|
+
signal.notify();
|
|
175
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
176
|
+
expect(callback3).toHaveBeenCalledTimes(0);
|
|
177
|
+
// Second notify - observer3 should now fire
|
|
178
|
+
signal.notify();
|
|
179
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
180
|
+
expect(callback3).toHaveBeenCalledTimes(1);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
describe("Optimization: Safe Unsubscribe During Notify", () => {
|
|
184
|
+
it("should handle observer disposing itself during notification", async () => {
|
|
185
|
+
const signal = new Signal();
|
|
186
|
+
const callback1 = vi.fn();
|
|
187
|
+
const callback2 = vi.fn();
|
|
188
|
+
const observer1 = new Observer(() => {
|
|
189
|
+
callback1();
|
|
190
|
+
observer1.dispose(); // Dispose itself during notification
|
|
191
|
+
});
|
|
192
|
+
const observer2 = new Observer(callback2);
|
|
193
|
+
const dispose1 = observer1.observe();
|
|
194
|
+
observer1.subscribeSignal(signal);
|
|
195
|
+
dispose1();
|
|
196
|
+
const dispose2 = observer2.observe();
|
|
197
|
+
observer2.subscribeSignal(signal);
|
|
198
|
+
dispose2();
|
|
199
|
+
// First notify - both should fire, observer1 disposes itself
|
|
200
|
+
signal.notify();
|
|
201
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
202
|
+
expect(callback1).toHaveBeenCalledTimes(1);
|
|
203
|
+
expect(callback2).toHaveBeenCalledTimes(1);
|
|
204
|
+
// Second notify - only observer2 should fire
|
|
205
|
+
signal.notify();
|
|
206
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
207
|
+
expect(callback1).toHaveBeenCalledTimes(1); // Still 1
|
|
208
|
+
expect(callback2).toHaveBeenCalledTimes(2); // Incremented
|
|
209
|
+
});
|
|
210
|
+
it("should handle observer clearing signals during notification", async () => {
|
|
211
|
+
const signal1 = new Signal();
|
|
212
|
+
const signal2 = new Signal();
|
|
213
|
+
const callback = vi.fn();
|
|
214
|
+
const observer = new Observer(() => {
|
|
215
|
+
callback();
|
|
216
|
+
// Re-observe, which clears all signals
|
|
217
|
+
const dispose = observer.observe();
|
|
218
|
+
observer.subscribeSignal(signal2);
|
|
219
|
+
dispose();
|
|
220
|
+
});
|
|
221
|
+
const dispose = observer.observe();
|
|
222
|
+
observer.subscribeSignal(signal1);
|
|
223
|
+
dispose();
|
|
224
|
+
signal1.notify();
|
|
225
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
226
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
227
|
+
// Should not respond to signal1 anymore
|
|
228
|
+
signal1.notify();
|
|
229
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
230
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
231
|
+
// Should respond to signal2
|
|
232
|
+
signal2.notify();
|
|
233
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
234
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
describe("Optimization: Memory Efficiency", () => {
|
|
238
|
+
it("should clean up subscriptions when observer is disposed", async () => {
|
|
239
|
+
const signal = new Signal();
|
|
240
|
+
const observers = Array.from({ length: 100 }, () => new Observer(() => { }));
|
|
241
|
+
// Subscribe all observers
|
|
242
|
+
observers.forEach((observer) => {
|
|
243
|
+
const dispose = observer.observe();
|
|
244
|
+
observer.subscribeSignal(signal);
|
|
245
|
+
dispose();
|
|
246
|
+
});
|
|
247
|
+
// Dispose half of them
|
|
248
|
+
observers.slice(0, 50).forEach((observer) => observer.dispose());
|
|
249
|
+
// Notify - should not throw or have issues
|
|
250
|
+
expect(() => signal.notify()).not.toThrow();
|
|
251
|
+
// Cleanup
|
|
252
|
+
observers.slice(50).forEach((observer) => observer.dispose());
|
|
253
|
+
});
|
|
254
|
+
it("should handle multiple observe/clearSignals cycles", () => {
|
|
255
|
+
const observer = new Observer(() => { });
|
|
256
|
+
const signal1 = new Signal();
|
|
257
|
+
const signal2 = new Signal();
|
|
258
|
+
const signal3 = new Signal();
|
|
259
|
+
// First cycle
|
|
260
|
+
let dispose = observer.observe();
|
|
261
|
+
observer.subscribeSignal(signal1);
|
|
262
|
+
dispose();
|
|
263
|
+
// Second cycle - clears signal1
|
|
264
|
+
dispose = observer.observe();
|
|
265
|
+
observer.subscribeSignal(signal2);
|
|
266
|
+
dispose();
|
|
267
|
+
// Third cycle - clears signal2
|
|
268
|
+
dispose = observer.observe();
|
|
269
|
+
observer.subscribeSignal(signal3);
|
|
270
|
+
dispose();
|
|
271
|
+
// Should not throw
|
|
272
|
+
expect(() => {
|
|
273
|
+
signal1.notify();
|
|
274
|
+
signal2.notify();
|
|
275
|
+
signal3.notify();
|
|
276
|
+
}).not.toThrow();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
describe("Edge Cases", () => {
|
|
280
|
+
it("should handle rapid consecutive notifies", async () => {
|
|
281
|
+
const signal = new Signal();
|
|
282
|
+
const callback = vi.fn();
|
|
283
|
+
const observer = new Observer(callback);
|
|
284
|
+
const dispose = observer.observe();
|
|
285
|
+
observer.subscribeSignal(signal);
|
|
286
|
+
dispose();
|
|
287
|
+
// Rapid notifies
|
|
288
|
+
signal.notify();
|
|
289
|
+
signal.notify();
|
|
290
|
+
signal.notify();
|
|
291
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
292
|
+
// Due to batching, exact count depends on implementation
|
|
293
|
+
// But should have been called at least once
|
|
294
|
+
expect(callback).toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
it("should not notify after subscribeSignal on disposed observer", async () => {
|
|
297
|
+
const signal = new Signal();
|
|
298
|
+
const callback = vi.fn();
|
|
299
|
+
const observer = new Observer(callback);
|
|
300
|
+
observer.dispose();
|
|
301
|
+
const dispose = observer.observe();
|
|
302
|
+
observer.subscribeSignal(signal);
|
|
303
|
+
dispose();
|
|
304
|
+
signal.notify();
|
|
305
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
306
|
+
expect(callback).not.toHaveBeenCalled();
|
|
307
|
+
});
|
|
308
|
+
it("should handle complex subscription graph", async () => {
|
|
309
|
+
// Create a diamond dependency pattern
|
|
310
|
+
const signal1 = new Signal();
|
|
311
|
+
const signal2 = new Signal();
|
|
312
|
+
const signal3 = new Signal();
|
|
313
|
+
const calls = [];
|
|
314
|
+
const observer1 = new Observer(() => {
|
|
315
|
+
calls.push("o1");
|
|
316
|
+
signal2.notify();
|
|
317
|
+
signal3.notify();
|
|
318
|
+
});
|
|
319
|
+
const observer2 = new Observer(() => calls.push("o2"));
|
|
320
|
+
const observer3 = new Observer(() => calls.push("o3"));
|
|
321
|
+
// observer1 listens to signal1
|
|
322
|
+
let dispose = observer1.observe();
|
|
323
|
+
observer1.subscribeSignal(signal1);
|
|
324
|
+
dispose();
|
|
325
|
+
// observer2 listens to signal2
|
|
326
|
+
dispose = observer2.observe();
|
|
327
|
+
observer2.subscribeSignal(signal2);
|
|
328
|
+
dispose();
|
|
329
|
+
// observer3 listens to signal3
|
|
330
|
+
dispose = observer3.observe();
|
|
331
|
+
observer3.subscribeSignal(signal3);
|
|
332
|
+
dispose();
|
|
333
|
+
signal1.notify();
|
|
334
|
+
await new Promise((resolve) => queueMicrotask(() => resolve()));
|
|
335
|
+
// All should have been called
|
|
336
|
+
expect(calls).toContain("o1");
|
|
337
|
+
expect(calls).toContain("o2");
|
|
338
|
+
expect(calls).toContain("o3");
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useComputed.d.ts","sourceRoot":"","sources":["../src/useComputed.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,IAAI;KACzD,CAAC,IAAI,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACjC,CAAC;AAEF,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,EAC7D,QAAQ,EAAE,CAAC,GACV,QAAQ,CAAC,CAAC,CAAC,CA0Eb"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { getCurrentComponent, useCleanup } from "./component";
|
|
2
|
+
import { INSPECT_MARKER, INSPECTOR_ENABLED } from "./inspect";
|
|
3
|
+
import { getCurrentObserver, Observer, Signal } from "./observation";
|
|
4
|
+
export function useComputed(computed) {
|
|
5
|
+
const currentComponent = getCurrentComponent();
|
|
6
|
+
const proxy = {};
|
|
7
|
+
let notifyInspectorRef = {};
|
|
8
|
+
for (const prop in computed) {
|
|
9
|
+
let isDirty = true;
|
|
10
|
+
let value;
|
|
11
|
+
const signal = new Signal();
|
|
12
|
+
const computedObserver = new Observer(() => {
|
|
13
|
+
isDirty = true;
|
|
14
|
+
signal.notify();
|
|
15
|
+
if (INSPECTOR_ENABLED) {
|
|
16
|
+
notifyInspectorRef.current?.notify({
|
|
17
|
+
type: "computed",
|
|
18
|
+
path: notifyInspectorRef.current.path.concat(prop),
|
|
19
|
+
isDirty: true,
|
|
20
|
+
value,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
useCleanup(() => computedObserver.dispose());
|
|
25
|
+
Object.defineProperty(proxy, prop, {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
configurable: true,
|
|
28
|
+
get() {
|
|
29
|
+
const currentObserver = getCurrentObserver();
|
|
30
|
+
if (currentObserver) {
|
|
31
|
+
currentObserver.subscribeSignal(signal);
|
|
32
|
+
}
|
|
33
|
+
if (isDirty) {
|
|
34
|
+
const stopObserving = computedObserver.observe();
|
|
35
|
+
value = computed[prop]();
|
|
36
|
+
stopObserving();
|
|
37
|
+
isDirty = false;
|
|
38
|
+
if (INSPECTOR_ENABLED) {
|
|
39
|
+
notifyInspectorRef.current?.notify({
|
|
40
|
+
type: "computed",
|
|
41
|
+
path: notifyInspectorRef.current.path.concat(prop),
|
|
42
|
+
isDirty: false,
|
|
43
|
+
value,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (INSPECTOR_ENABLED) {
|
|
53
|
+
Object.defineProperty(proxy, INSPECT_MARKER, {
|
|
54
|
+
enumerable: false,
|
|
55
|
+
configurable: false,
|
|
56
|
+
get() {
|
|
57
|
+
return !notifyInspectorRef.current;
|
|
58
|
+
},
|
|
59
|
+
set: (value) => {
|
|
60
|
+
Object.defineProperty(notifyInspectorRef, "current", {
|
|
61
|
+
get() {
|
|
62
|
+
return value.current;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return proxy;
|
|
69
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type QueryState<T, I = null> = {
|
|
2
|
+
isPending: true;
|
|
3
|
+
error: null;
|
|
4
|
+
value: I;
|
|
5
|
+
} | {
|
|
6
|
+
isPending: false;
|
|
7
|
+
error: null;
|
|
8
|
+
value: T;
|
|
9
|
+
} | {
|
|
10
|
+
isPending: false;
|
|
11
|
+
error: string;
|
|
12
|
+
value: I | null;
|
|
13
|
+
};
|
|
14
|
+
export type Query<T, P, I = null> = [
|
|
15
|
+
QueryState<T, I>,
|
|
16
|
+
[
|
|
17
|
+
P
|
|
18
|
+
] extends [never] ? (params: P) => void : () => void
|
|
19
|
+
];
|
|
20
|
+
export declare function useQuery<T>(query: () => Promise<T>): Query<T, never>;
|
|
21
|
+
export declare function useQuery<T>(query: () => Promise<T>, initialValue: T): Query<T, never, T>;
|
|
22
|
+
export declare function useQuery<T, P>(query: (params: P) => Promise<T>): Query<T, never>;
|
|
23
|
+
export declare function useQuery<T, P>(query: (params: P) => Promise<T>, initialValue: T): Query<T, never, T>;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=useQuery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useQuery.d.ts","sourceRoot":"","sources":["../src/useQuery.ts"],"names":[],"mappings":"AAGA,KAAK,UAAU,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,IACvB;IACE,SAAS,EAAE,IAAI,CAAC;IAChB,KAAK,EAAE,IAAI,CAAC;IACZ,KAAK,EAAE,CAAC,CAAC;CACV,GACD;IACE,SAAS,EAAE,KAAK,CAAC;IACjB,KAAK,EAAE,IAAI,CAAC;IACZ,KAAK,EAAE,CAAC,CAAC;CACV,GACD;IACE,SAAS,EAAE,KAAK,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;CACjB,CAAC;AAEN,MAAM,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,IAAI,IAAI;IAClC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC;IAChB;QAAC,CAAC;KAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI;CACvD,CAAC;AAEF,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AACtE,wBAAgB,QAAQ,CAAC,CAAC,EACxB,KAAK,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACvB,YAAY,EAAE,CAAC,GACd,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;AACtB,wBAAgB,QAAQ,CAAC,CAAC,EAAE,CAAC,EAC3B,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAC/B,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AACnB,wBAAgB,QAAQ,CAAC,CAAC,EAAE,CAAC,EAC3B,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAChC,YAAY,EAAE,CAAC,GACd,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC"}
|
package/dist/useQuery.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect } from "./useEffect";
|
|
2
|
+
import { useTask } from "./useTask";
|
|
3
|
+
export function useQuery(query, initialValue) {
|
|
4
|
+
const [taskState, runTask] = useTask(query);
|
|
5
|
+
let lastValue = initialValue === undefined ? null : initialValue;
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (taskState.result) {
|
|
8
|
+
lastValue = taskState.result;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
get value() {
|
|
14
|
+
return taskState.result || lastValue;
|
|
15
|
+
},
|
|
16
|
+
get isPending() {
|
|
17
|
+
return taskState.isPending;
|
|
18
|
+
},
|
|
19
|
+
get error() {
|
|
20
|
+
return taskState.error;
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
runTask,
|
|
24
|
+
];
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AsyncState } from "./useAsync";
|
|
2
|
+
type SuspendAsyncState<T extends Record<string, AsyncState<any>>> = {
|
|
3
|
+
values: {
|
|
4
|
+
[K in keyof T]: T[K]["value"];
|
|
5
|
+
};
|
|
6
|
+
} & ({
|
|
7
|
+
isPending: true;
|
|
8
|
+
error: null;
|
|
9
|
+
} | {
|
|
10
|
+
isPending: false;
|
|
11
|
+
error: string;
|
|
12
|
+
} | {
|
|
13
|
+
isPending: false;
|
|
14
|
+
error: null;
|
|
15
|
+
});
|
|
16
|
+
export declare function useSuspendAsync<T extends Record<string, AsyncState<any>>>(asyncs: T): SuspendAsyncState<T>;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=useSuspendAsync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSuspendAsync.d.ts","sourceRoot":"","sources":["../src/useSuspendAsync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAIxC,KAAK,iBAAiB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI;IAClE,MAAM,EAAE;SACL,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;KAC9B,CAAC;CACH,GAAG,CACA;IACE,SAAS,EAAE,IAAI,CAAC;IAChB,KAAK,EAAE,IAAI,CAAC;CACb,GACD;IACE,SAAS,EAAE,KAAK,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IACE,SAAS,EAAE,KAAK,CAAC;IACjB,KAAK,EAAE,IAAI,CAAC;CACb,CACJ,CAAC;AAEF,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,EACvE,MAAM,EAAE,CAAC,wBAyCV"}
|