rask-ui 0.21.0 → 0.22.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/asyncState.d.ts +16 -0
- package/dist/asyncState.d.ts.map +1 -0
- package/dist/asyncState.js +24 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +4 -1
- package/dist/context.d.ts +5 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +29 -0
- package/dist/createAsync.test.d.ts +2 -0
- package/dist/createAsync.test.d.ts.map +1 -0
- package/dist/createAsync.test.js +110 -0
- package/dist/createContext.d.ts +20 -21
- package/dist/createContext.d.ts.map +1 -1
- package/dist/createContext.js +29 -25
- package/dist/createMutation.test.d.ts +2 -0
- package/dist/createMutation.test.d.ts.map +1 -0
- package/dist/createMutation.test.js +168 -0
- package/dist/createQuery.test.d.ts +2 -0
- package/dist/createQuery.test.d.ts.map +1 -0
- package/dist/createQuery.test.js +156 -0
- package/dist/createRef.d.ts +6 -0
- package/dist/createRef.d.ts.map +1 -0
- package/dist/createRef.js +8 -0
- package/dist/createState.d.ts +0 -2
- package/dist/createState.d.ts.map +1 -1
- package/dist/createState.js +5 -40
- package/dist/createState.test.d.ts.map +1 -0
- package/dist/createState.test.js +111 -0
- package/dist/createView.d.ts +44 -18
- package/dist/createView.d.ts.map +1 -1
- package/dist/createView.js +48 -57
- package/dist/createView.test.d.ts.map +1 -0
- package/dist/{tests/createView.test.js → createView.test.js} +40 -40
- package/dist/error.d.ts +14 -3
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +15 -14
- package/dist/jsx.d.ts +256 -10
- package/dist/observation.test.d.ts.map +1 -0
- package/dist/observation.test.js +150 -0
- package/dist/suspense.d.ts +25 -0
- package/dist/suspense.d.ts.map +1 -0
- package/dist/suspense.js +97 -0
- package/dist/test-setup.d.ts +16 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +40 -0
- package/dist/test.d.ts +2 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +24 -0
- package/dist/useCatchError.d.ts +3 -1
- package/dist/useCatchError.d.ts.map +1 -1
- package/dist/useCatchError.js +4 -3
- package/package.json +2 -2
- package/swc-plugin/target/wasm32-wasip1/release/swc_plugin_rask_component.wasm +0 -0
- 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/createRouter.d.ts +0 -8
- package/dist/createRouter.d.ts.map +0 -1
- package/dist/createRouter.js +0 -27
- package/dist/createTask.d.ts +0 -31
- package/dist/createTask.d.ts.map +0 -1
- package/dist/createTask.js +0 -79
- package/dist/patchInferno.d.ts +0 -6
- package/dist/patchInferno.d.ts.map +0 -1
- package/dist/patchInferno.js +0 -53
- package/dist/scheduler.d.ts +0 -4
- package/dist/scheduler.d.ts.map +0 -1
- package/dist/scheduler.js +0 -107
- 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 -244
- 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 -136
- 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.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.map +0 -1
- 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 -168
- package/dist/tests/observation.test.d.ts.map +0 -1
- package/dist/tests/observation.test.js +0 -341
- 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/{tests/createState.test.d.ts → createState.test.d.ts} +0 -0
- /package/dist/{tests/createView.test.d.ts → createView.test.d.ts} +0 -0
- /package/dist/{tests/observation.test.d.ts → observation.test.d.ts} +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { createQuery } from './createQuery';
|
|
3
|
+
describe('createQuery', () => {
|
|
4
|
+
it('should start in pending state and fetch immediately', async () => {
|
|
5
|
+
const fetcher = vi.fn(() => Promise.resolve('data'));
|
|
6
|
+
const query = createQuery(fetcher);
|
|
7
|
+
expect(query.isPending).toBe(true);
|
|
8
|
+
expect(query.data).toBeNull();
|
|
9
|
+
expect(query.error).toBeNull();
|
|
10
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
11
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
12
|
+
expect(query.isPending).toBe(false);
|
|
13
|
+
expect(query.data).toBe('data');
|
|
14
|
+
});
|
|
15
|
+
it('should resolve to data state on success', async () => {
|
|
16
|
+
const fetcher = () => Promise.resolve({ id: 1, name: 'Test' });
|
|
17
|
+
const query = createQuery(fetcher);
|
|
18
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
19
|
+
expect(query.isPending).toBe(false);
|
|
20
|
+
expect(query.data).toEqual({ id: 1, name: 'Test' });
|
|
21
|
+
expect(query.error).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
it('should resolve to error state on failure', async () => {
|
|
24
|
+
const fetcher = () => Promise.reject(new Error('Network error'));
|
|
25
|
+
const query = createQuery(fetcher);
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
27
|
+
expect(query.isPending).toBe(false);
|
|
28
|
+
expect(query.data).toBeNull();
|
|
29
|
+
expect(query.error).toContain('Network error');
|
|
30
|
+
});
|
|
31
|
+
it('should allow manual refetch', async () => {
|
|
32
|
+
const fetcher = vi.fn(() => Promise.resolve('data'));
|
|
33
|
+
const query = createQuery(fetcher);
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
35
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
36
|
+
query.fetch();
|
|
37
|
+
expect(query.isPending).toBe(true);
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
39
|
+
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
40
|
+
expect(query.isPending).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
it('should retain old data during refetch by default', async () => {
|
|
43
|
+
const fetcher = vi
|
|
44
|
+
.fn()
|
|
45
|
+
.mockResolvedValueOnce('data1')
|
|
46
|
+
.mockResolvedValueOnce('data2');
|
|
47
|
+
const query = createQuery(fetcher);
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
49
|
+
expect(query.data).toBe('data1');
|
|
50
|
+
query.fetch();
|
|
51
|
+
expect(query.isPending).toBe(true);
|
|
52
|
+
expect(query.data).toBe('data1'); // Old data retained
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
54
|
+
expect(query.data).toBe('data2');
|
|
55
|
+
});
|
|
56
|
+
it('should clear old data when force refetch is used', async () => {
|
|
57
|
+
const fetcher = vi
|
|
58
|
+
.fn()
|
|
59
|
+
.mockResolvedValueOnce('data1')
|
|
60
|
+
.mockResolvedValueOnce('data2');
|
|
61
|
+
const query = createQuery(fetcher);
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
63
|
+
expect(query.data).toBe('data1');
|
|
64
|
+
query.fetch(true); // Force refresh
|
|
65
|
+
expect(query.isPending).toBe(true);
|
|
66
|
+
expect(query.data).toBeNull(); // Old data cleared
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
68
|
+
expect(query.data).toBe('data2');
|
|
69
|
+
});
|
|
70
|
+
it('should cancel previous request on new fetch', async () => {
|
|
71
|
+
let resolveFirst;
|
|
72
|
+
let resolveSecond;
|
|
73
|
+
const firstPromise = new Promise((resolve) => {
|
|
74
|
+
resolveFirst = resolve;
|
|
75
|
+
});
|
|
76
|
+
const secondPromise = new Promise((resolve) => {
|
|
77
|
+
resolveSecond = resolve;
|
|
78
|
+
});
|
|
79
|
+
const fetcher = vi
|
|
80
|
+
.fn()
|
|
81
|
+
.mockReturnValueOnce(firstPromise)
|
|
82
|
+
.mockReturnValueOnce(secondPromise);
|
|
83
|
+
const query = createQuery(fetcher);
|
|
84
|
+
expect(query.isPending).toBe(true);
|
|
85
|
+
// Trigger second fetch before first completes
|
|
86
|
+
query.fetch();
|
|
87
|
+
// Resolve first (should be ignored due to cancellation)
|
|
88
|
+
resolveFirst('first');
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
90
|
+
expect(query.data).toBeNull(); // First result ignored
|
|
91
|
+
// Resolve second
|
|
92
|
+
resolveSecond('second');
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
94
|
+
expect(query.data).toBe('second');
|
|
95
|
+
});
|
|
96
|
+
it('should handle rapid successive fetches', async () => {
|
|
97
|
+
let counter = 0;
|
|
98
|
+
const fetcher = vi.fn(() => Promise.resolve(`data-${++counter}`));
|
|
99
|
+
const query = createQuery(fetcher);
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
101
|
+
// Rapid fetches
|
|
102
|
+
query.fetch();
|
|
103
|
+
query.fetch();
|
|
104
|
+
query.fetch();
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
106
|
+
// Only the last fetch should matter
|
|
107
|
+
expect(fetcher).toHaveBeenCalledTimes(4); // Initial + 3 fetches
|
|
108
|
+
expect(query.data).toBe('data-4');
|
|
109
|
+
});
|
|
110
|
+
it('should cancel on error and allow refetch', async () => {
|
|
111
|
+
const fetcher = vi
|
|
112
|
+
.fn()
|
|
113
|
+
.mockRejectedValueOnce(new Error('First error'))
|
|
114
|
+
.mockResolvedValueOnce('success');
|
|
115
|
+
const query = createQuery(fetcher);
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
117
|
+
expect(query.error).toContain('First error');
|
|
118
|
+
expect(query.data).toBeNull();
|
|
119
|
+
query.fetch();
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
121
|
+
expect(query.error).toBeNull();
|
|
122
|
+
expect(query.data).toBe('success');
|
|
123
|
+
});
|
|
124
|
+
it('should handle AbortController cancellation correctly', async () => {
|
|
125
|
+
const abortedPromise = new Promise((_, reject) => {
|
|
126
|
+
const error = new Error('Aborted');
|
|
127
|
+
error.name = 'AbortError';
|
|
128
|
+
setTimeout(() => reject(error), 5);
|
|
129
|
+
});
|
|
130
|
+
const successPromise = Promise.resolve('success');
|
|
131
|
+
const fetcher = vi
|
|
132
|
+
.fn()
|
|
133
|
+
.mockReturnValueOnce(abortedPromise)
|
|
134
|
+
.mockReturnValueOnce(successPromise);
|
|
135
|
+
const query = createQuery(fetcher);
|
|
136
|
+
// Immediately trigger second fetch to abort first
|
|
137
|
+
query.fetch();
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
139
|
+
expect(query.data).toBe('success');
|
|
140
|
+
expect(query.error).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
it('should expose reactive getters', async () => {
|
|
143
|
+
const fetcher = () => Promise.resolve('data');
|
|
144
|
+
const query = createQuery(fetcher);
|
|
145
|
+
// Access getters
|
|
146
|
+
const pending1 = query.isPending;
|
|
147
|
+
const data1 = query.data;
|
|
148
|
+
const error1 = query.error;
|
|
149
|
+
expect(pending1).toBe(true);
|
|
150
|
+
expect(data1).toBeNull();
|
|
151
|
+
expect(error1).toBeNull();
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
153
|
+
expect(query.isPending).toBe(false);
|
|
154
|
+
expect(query.data).toBe('data');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createRef.d.ts","sourceRoot":"","sources":["../src/createRef.ts"],"names":[],"mappings":"AAAA,wBAAgB,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI;WAChD,CAAC,GAAG,IAAI;;mBADe,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI;EASpE"}
|
package/dist/createState.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export declare function assignState<T extends object>(state: T, newState: T): T;
|
|
2
1
|
/**
|
|
3
2
|
* Creates a reactive state object that tracks property access and notifies observers on changes.
|
|
4
3
|
*
|
|
@@ -24,5 +23,4 @@ export declare function assignState<T extends object>(state: T, newState: T): T;
|
|
|
24
23
|
* @returns A reactive proxy of the state object
|
|
25
24
|
*/
|
|
26
25
|
export declare function createState<T extends object>(state: T): T;
|
|
27
|
-
export declare const PROXY_MARKER: unique symbol;
|
|
28
26
|
//# sourceMappingURL=createState.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createState.d.ts","sourceRoot":"","sources":["../src/createState.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"createState.d.ts","sourceRoot":"","sources":["../src/createState.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAEzD"}
|
package/dist/createState.js
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import { getCurrentComponent } from "./component";
|
|
2
|
-
import { INSPECT_MARKER, INSPECTOR_ENABLED } from "./inspect";
|
|
3
1
|
import { getCurrentObserver, Signal } from "./observation";
|
|
4
|
-
export function assignState(state, newState) {
|
|
5
|
-
return Object.assign(state, newState);
|
|
6
|
-
}
|
|
7
2
|
/**
|
|
8
3
|
* Creates a reactive state object that tracks property access and notifies observers on changes.
|
|
9
4
|
*
|
|
@@ -29,14 +24,11 @@ export function assignState(state, newState) {
|
|
|
29
24
|
* @returns A reactive proxy of the state object
|
|
30
25
|
*/
|
|
31
26
|
export function createState(state) {
|
|
32
|
-
|
|
33
|
-
throw new Error("createState cannot be called during render. Call it in component setup or globally.");
|
|
34
|
-
}
|
|
35
|
-
return getProxy(state, {});
|
|
27
|
+
return getProxy(state);
|
|
36
28
|
}
|
|
37
29
|
const proxyCache = new WeakMap();
|
|
38
|
-
|
|
39
|
-
function getProxy(value
|
|
30
|
+
const PROXY_MARKER = Symbol("isProxy");
|
|
31
|
+
function getProxy(value) {
|
|
40
32
|
// Check if already a proxy to avoid double-wrapping
|
|
41
33
|
if (PROXY_MARKER in value) {
|
|
42
34
|
return value;
|
|
@@ -51,9 +43,6 @@ function getProxy(value, notifyInspectorRef) {
|
|
|
51
43
|
if (key === PROXY_MARKER) {
|
|
52
44
|
return true;
|
|
53
45
|
}
|
|
54
|
-
if (INSPECTOR_ENABLED && key === INSPECT_MARKER) {
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
46
|
return Reflect.has(target, key);
|
|
58
47
|
},
|
|
59
48
|
get(target, key) {
|
|
@@ -61,9 +50,6 @@ function getProxy(value, notifyInspectorRef) {
|
|
|
61
50
|
if (key === PROXY_MARKER) {
|
|
62
51
|
return true;
|
|
63
52
|
}
|
|
64
|
-
if (INSPECTOR_ENABLED && key === INSPECT_MARKER) {
|
|
65
|
-
return !notifyInspectorRef.current;
|
|
66
|
-
}
|
|
67
53
|
const value = Reflect.get(target, key);
|
|
68
54
|
if (typeof key === "symbol" || typeof value === "function") {
|
|
69
55
|
return value;
|
|
@@ -75,26 +61,11 @@ function getProxy(value, notifyInspectorRef) {
|
|
|
75
61
|
}
|
|
76
62
|
if (Array.isArray(value) ||
|
|
77
63
|
(typeof value === "object" && value !== null)) {
|
|
78
|
-
return getProxy(value
|
|
79
|
-
? {
|
|
80
|
-
current: {
|
|
81
|
-
notify: notifyInspectorRef.current.notify,
|
|
82
|
-
path: notifyInspectorRef.current.path.concat(key),
|
|
83
|
-
},
|
|
84
|
-
}
|
|
85
|
-
: notifyInspectorRef);
|
|
64
|
+
return getProxy(value);
|
|
86
65
|
}
|
|
87
66
|
return value;
|
|
88
67
|
},
|
|
89
68
|
set(target, key, newValue) {
|
|
90
|
-
if (INSPECTOR_ENABLED && key === INSPECT_MARKER) {
|
|
91
|
-
Object.defineProperty(notifyInspectorRef, "current", {
|
|
92
|
-
get() {
|
|
93
|
-
return newValue.current;
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
return Reflect.set(target, key, newValue);
|
|
97
|
-
}
|
|
98
69
|
if (typeof key === "symbol") {
|
|
99
70
|
return Reflect.set(target, key, newValue);
|
|
100
71
|
}
|
|
@@ -102,16 +73,10 @@ function getProxy(value, notifyInspectorRef) {
|
|
|
102
73
|
const setResult = Reflect.set(target, key, newValue);
|
|
103
74
|
// We only notify if actual change, though array length actually updates under the hood
|
|
104
75
|
if (newValue !== oldValue || (Array.isArray(value) && key === "length")) {
|
|
76
|
+
console.log("WTF", key, newValue);
|
|
105
77
|
const signal = signals[key];
|
|
106
78
|
signal?.notify();
|
|
107
79
|
}
|
|
108
|
-
if (INSPECTOR_ENABLED) {
|
|
109
|
-
notifyInspectorRef.current?.notify({
|
|
110
|
-
type: "mutation",
|
|
111
|
-
path: notifyInspectorRef.current.path,
|
|
112
|
-
value: newValue,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
80
|
return setResult;
|
|
116
81
|
},
|
|
117
82
|
deleteProperty(target, key) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createState.test.d.ts","sourceRoot":"","sources":["../src/createState.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createState } from './createState';
|
|
3
|
+
import { Observer } from './observation';
|
|
4
|
+
describe('createState', () => {
|
|
5
|
+
it('should create a reactive proxy from an object', () => {
|
|
6
|
+
const state = createState({ count: 0 });
|
|
7
|
+
expect(state.count).toBe(0);
|
|
8
|
+
});
|
|
9
|
+
it('should allow mutations', () => {
|
|
10
|
+
const state = createState({ count: 0 });
|
|
11
|
+
state.count = 5;
|
|
12
|
+
expect(state.count).toBe(5);
|
|
13
|
+
});
|
|
14
|
+
it('should return the same proxy for the same object', () => {
|
|
15
|
+
const obj = { count: 0 };
|
|
16
|
+
const proxy1 = createState(obj);
|
|
17
|
+
const proxy2 = createState(obj);
|
|
18
|
+
expect(proxy1).toBe(proxy2);
|
|
19
|
+
});
|
|
20
|
+
it('should create nested proxies for nested objects', () => {
|
|
21
|
+
const state = createState({ user: { name: 'Alice', age: 30 } });
|
|
22
|
+
state.user.name = 'Bob';
|
|
23
|
+
expect(state.user.name).toBe('Bob');
|
|
24
|
+
});
|
|
25
|
+
it('should handle arrays reactively', () => {
|
|
26
|
+
const state = createState({ items: [1, 2, 3] });
|
|
27
|
+
state.items.push(4);
|
|
28
|
+
expect(state.items).toEqual([1, 2, 3, 4]);
|
|
29
|
+
});
|
|
30
|
+
it('should track property access in observers', () => {
|
|
31
|
+
const state = createState({ count: 0 });
|
|
32
|
+
let renderCount = 0;
|
|
33
|
+
const observer = new Observer(() => {
|
|
34
|
+
renderCount++;
|
|
35
|
+
});
|
|
36
|
+
const dispose = observer.observe();
|
|
37
|
+
state.count; // Access property to track it
|
|
38
|
+
dispose();
|
|
39
|
+
expect(renderCount).toBe(0);
|
|
40
|
+
// Mutate after observation setup
|
|
41
|
+
const dispose2 = observer.observe();
|
|
42
|
+
const value = state.count; // Track
|
|
43
|
+
dispose2(); // Stop observing, subscriptions are now active
|
|
44
|
+
state.count = 1;
|
|
45
|
+
// Wait for microtask
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
queueMicrotask(() => {
|
|
48
|
+
expect(renderCount).toBeGreaterThan(0);
|
|
49
|
+
resolve(undefined);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
it('should handle property deletion', () => {
|
|
54
|
+
const state = createState({ count: 0, temp: 'value' });
|
|
55
|
+
delete state.temp;
|
|
56
|
+
expect(state.temp).toBeUndefined();
|
|
57
|
+
expect('temp' in state).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it('should not create proxies for functions', () => {
|
|
60
|
+
const fn = () => 'hello';
|
|
61
|
+
const state = createState({ method: fn });
|
|
62
|
+
expect(state.method).toBe(fn);
|
|
63
|
+
expect(state.method()).toBe('hello');
|
|
64
|
+
});
|
|
65
|
+
it('should handle symbol properties', () => {
|
|
66
|
+
const sym = Symbol('test');
|
|
67
|
+
const state = createState({ [sym]: 'value' });
|
|
68
|
+
expect(state[sym]).toBe('value');
|
|
69
|
+
});
|
|
70
|
+
it('should notify observers only on actual changes', () => {
|
|
71
|
+
const state = createState({ count: 0 });
|
|
72
|
+
let notifyCount = 0;
|
|
73
|
+
const observer = new Observer(() => {
|
|
74
|
+
notifyCount++;
|
|
75
|
+
});
|
|
76
|
+
const dispose = observer.observe();
|
|
77
|
+
state.count; // Track
|
|
78
|
+
dispose();
|
|
79
|
+
state.count = 0; // Same value - should still notify per current implementation
|
|
80
|
+
state.count = 0;
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
queueMicrotask(() => {
|
|
83
|
+
// The implementation notifies even for same value, except for optimization cases
|
|
84
|
+
observer.dispose();
|
|
85
|
+
resolve(undefined);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
it('should handle deeply nested objects', () => {
|
|
90
|
+
const state = createState({
|
|
91
|
+
level1: {
|
|
92
|
+
level2: {
|
|
93
|
+
level3: {
|
|
94
|
+
value: 'deep',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
state.level1.level2.level3.value = 'modified';
|
|
100
|
+
expect(state.level1.level2.level3.value).toBe('modified');
|
|
101
|
+
});
|
|
102
|
+
it('should handle array mutations correctly', () => {
|
|
103
|
+
const state = createState({ items: [1, 2, 3] });
|
|
104
|
+
state.items.pop();
|
|
105
|
+
expect(state.items).toEqual([1, 2]);
|
|
106
|
+
state.items.unshift(0);
|
|
107
|
+
expect(state.items).toEqual([0, 1, 2]);
|
|
108
|
+
state.items.splice(1, 1, 99);
|
|
109
|
+
expect(state.items).toEqual([0, 99, 2]);
|
|
110
|
+
});
|
|
111
|
+
});
|
package/dist/createView.d.ts
CHANGED
|
@@ -1,28 +1,54 @@
|
|
|
1
|
-
type Simplify<T> =
|
|
1
|
+
type Simplify<T> = {
|
|
2
2
|
[K in keyof T]: T[K];
|
|
3
|
-
}
|
|
4
|
-
type
|
|
5
|
-
[K in keyof T]-?: [T[K]] extends [undefined] ? K : never;
|
|
6
|
-
}[keyof T];
|
|
7
|
-
type MergeTwo<A extends object, B extends object> = A extends any ? Simplify<Omit<A, keyof B> & Omit<B, UndefinedKeys<B>>> : never;
|
|
3
|
+
} & {};
|
|
4
|
+
type MergeTwo<A extends object, B extends object> = Simplify<Omit<A, keyof B> & B>;
|
|
8
5
|
type MergeMany<T extends readonly object[]> = T extends [
|
|
9
6
|
infer H extends object,
|
|
10
7
|
...infer R extends object[]
|
|
11
|
-
] ?
|
|
12
|
-
type MergeManyAcc<Acc extends object, Rest extends object[]> = Rest extends [
|
|
13
|
-
infer H extends object,
|
|
14
|
-
...infer R extends object[]
|
|
15
|
-
] ? MergeManyAcc<MergeTwo<Acc, H>, R> : Acc;
|
|
8
|
+
] ? MergeTwo<H, MergeMany<R>> : {};
|
|
16
9
|
/**
|
|
17
|
-
* Creates a view that merges multiple objects (reactive or not) into a single
|
|
18
|
-
*
|
|
19
|
-
*
|
|
10
|
+
* Creates a view that merges multiple objects (reactive or not) into a single object while
|
|
11
|
+
* maintaining reactivity through getters. Properties from later arguments override earlier ones.
|
|
12
|
+
*
|
|
13
|
+
* @warning **Do not destructure the returned view object!** Destructuring breaks reactivity
|
|
14
|
+
* because it extracts plain values instead of maintaining getter access. This is the same rule
|
|
15
|
+
* as other reactive primitives.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // ❌ Bad - destructuring loses reactivity
|
|
19
|
+
* function Component() {
|
|
20
|
+
* const state = createState({ count: 0 });
|
|
21
|
+
* const helpers = { increment: () => state.count++ };
|
|
22
|
+
* const view = createView(state, helpers);
|
|
23
|
+
* const { count, increment } = view; // Don't do this!
|
|
24
|
+
* return () => <button onClick={increment}>{count}</button>; // Won't update!
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* // ✅ Good - access properties directly in render
|
|
28
|
+
* function Component() {
|
|
29
|
+
* const state = createState({ count: 0 });
|
|
30
|
+
* const helpers = { increment: () => state.count++ };
|
|
31
|
+
* const view = createView(state, helpers);
|
|
32
|
+
* return () => <button onClick={view.increment}>{view.count}</button>; // Reactive!
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Merge multiple reactive objects
|
|
37
|
+
* const state = createState({ count: 0 });
|
|
38
|
+
* const user = createState({ name: "Alice" });
|
|
39
|
+
* const view = createView(state, user);
|
|
40
|
+
* // view has both count and name properties, maintaining reactivity
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // Later arguments override earlier ones
|
|
44
|
+
* const a = { x: 1, y: 2 };
|
|
45
|
+
* const b = { y: 3, z: 4 };
|
|
46
|
+
* const view = createView(a, b);
|
|
47
|
+
* // view.x === 1, view.y === 3, view.z === 4
|
|
20
48
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
49
|
+
* @param args - Objects to merge (reactive or plain objects)
|
|
50
|
+
* @returns A view object with getters for all properties, maintaining reactivity
|
|
23
51
|
*/
|
|
24
|
-
export declare function createView<A extends object>(a: A): A;
|
|
25
|
-
export declare function createView<A extends object, B extends object>(a: A, b: B): MergeTwo<A, B>;
|
|
26
52
|
export declare function createView<T extends readonly object[]>(...args: T): MergeMany<T>;
|
|
27
53
|
export {};
|
|
28
54
|
//# sourceMappingURL=createView.d.ts.map
|
package/dist/createView.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createView.d.ts","sourceRoot":"","sources":["../src/createView.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"createView.d.ts","sourceRoot":"","sources":["../src/createView.ts"],"names":[],"mappings":"AAAA,KAAK,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,EAAE,CAAC;AAEjD,KAAK,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,IAAI,QAAQ,CAC1D,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CACrB,CAAC;AAEF,KAAK,SAAS,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,IAAI,CAAC,SAAS;IACtD,MAAM,CAAC,SAAS,MAAM;IACtB,GAAG,MAAM,CAAC,SAAS,MAAM,EAAE;CAC5B,GACG,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,GACzB,EAAE,CAAC;AAEP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,EACpD,GAAG,IAAI,EAAE,CAAC,GACT,SAAS,CAAC,CAAC,CAAC,CA0Bd"}
|
package/dist/createView.js
CHANGED
|
@@ -1,77 +1,68 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Creates a view that merges multiple objects (reactive or not) into a single object while
|
|
3
|
+
* maintaining reactivity through getters. Properties from later arguments override earlier ones.
|
|
4
|
+
*
|
|
5
|
+
* @warning **Do not destructure the returned view object!** Destructuring breaks reactivity
|
|
6
|
+
* because it extracts plain values instead of maintaining getter access. This is the same rule
|
|
7
|
+
* as other reactive primitives.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // ❌ Bad - destructuring loses reactivity
|
|
11
|
+
* function Component() {
|
|
12
|
+
* const state = createState({ count: 0 });
|
|
13
|
+
* const helpers = { increment: () => state.count++ };
|
|
14
|
+
* const view = createView(state, helpers);
|
|
15
|
+
* const { count, increment } = view; // Don't do this!
|
|
16
|
+
* return () => <button onClick={increment}>{count}</button>; // Won't update!
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* // ✅ Good - access properties directly in render
|
|
20
|
+
* function Component() {
|
|
21
|
+
* const state = createState({ count: 0 });
|
|
22
|
+
* const helpers = { increment: () => state.count++ };
|
|
23
|
+
* const view = createView(state, helpers);
|
|
24
|
+
* return () => <button onClick={view.increment}>{view.count}</button>; // Reactive!
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // Merge multiple reactive objects
|
|
29
|
+
* const state = createState({ count: 0 });
|
|
30
|
+
* const user = createState({ name: "Alice" });
|
|
31
|
+
* const view = createView(state, user);
|
|
32
|
+
* // view has both count and name properties, maintaining reactivity
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Later arguments override earlier ones
|
|
36
|
+
* const a = { x: 1, y: 2 };
|
|
37
|
+
* const b = { y: 3, z: 4 };
|
|
38
|
+
* const view = createView(a, b);
|
|
39
|
+
* // view.x === 1, view.y === 3, view.z === 4
|
|
40
|
+
*
|
|
41
|
+
* @param args - Objects to merge (reactive or plain objects)
|
|
42
|
+
* @returns A view object with getters for all properties, maintaining reactivity
|
|
43
|
+
*/
|
|
3
44
|
export function createView(...args) {
|
|
4
|
-
if (!getCurrentComponent()) {
|
|
5
|
-
throw new Error("Only use createView in component setup");
|
|
6
|
-
}
|
|
7
45
|
const result = {};
|
|
8
46
|
const seen = new Set();
|
|
9
|
-
let notifyInspectorRef = {};
|
|
10
47
|
for (let i = args.length - 1; i >= 0; i--) {
|
|
11
48
|
const src = args[i];
|
|
12
|
-
|
|
13
|
-
continue;
|
|
14
|
-
if (INSPECTOR_ENABLED && src[INSPECT_MARKER]) {
|
|
15
|
-
src[INSPECT_MARKER] = notifyInspectorRef;
|
|
16
|
-
}
|
|
17
|
-
// Mimic Object.assign: only enumerable own property keys
|
|
49
|
+
// mimic Object.assign: only enumerable own property keys
|
|
18
50
|
for (const key of Reflect.ownKeys(src)) {
|
|
19
51
|
if (seen.has(key))
|
|
20
52
|
continue;
|
|
21
53
|
const desc = Object.getOwnPropertyDescriptor(src, key);
|
|
22
54
|
if (!desc || !desc.enumerable)
|
|
23
55
|
continue;
|
|
56
|
+
// Capture the current source for this key (last write wins).
|
|
24
57
|
Object.defineProperty(result, key, {
|
|
25
58
|
enumerable: true,
|
|
26
59
|
configurable: true,
|
|
27
|
-
get: () =>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return value;
|
|
31
|
-
}
|
|
32
|
-
// Propagate inspector marker into nested observables
|
|
33
|
-
if (value?.[INSPECT_MARKER]) {
|
|
34
|
-
value[INSPECT_MARKER] = {
|
|
35
|
-
current: {
|
|
36
|
-
notify: notifyInspectorRef.current.notify,
|
|
37
|
-
path: notifyInspectorRef.current.path.concat(key),
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
else if (typeof value === "function") {
|
|
42
|
-
// Wrap actions to notify inspector
|
|
43
|
-
return (...params) => {
|
|
44
|
-
notifyInspectorRef.current.notify({
|
|
45
|
-
type: "action",
|
|
46
|
-
path: notifyInspectorRef.current.path.concat(key),
|
|
47
|
-
params,
|
|
48
|
-
});
|
|
49
|
-
return value(...params);
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
return value;
|
|
53
|
-
},
|
|
60
|
+
get: () => src[key],
|
|
61
|
+
// Optional write-through (commented out by default):
|
|
62
|
+
// set: (value) => { (src as any)[key as any] = value; },
|
|
54
63
|
});
|
|
55
64
|
seen.add(key);
|
|
56
65
|
}
|
|
57
66
|
}
|
|
58
|
-
if (INSPECTOR_ENABLED) {
|
|
59
|
-
Object.defineProperty(result, INSPECT_MARKER, {
|
|
60
|
-
enumerable: false,
|
|
61
|
-
configurable: false,
|
|
62
|
-
get() {
|
|
63
|
-
return !notifyInspectorRef.current;
|
|
64
|
-
},
|
|
65
|
-
set: (value) => {
|
|
66
|
-
Object.defineProperty(notifyInspectorRef, "current", {
|
|
67
|
-
configurable: true,
|
|
68
|
-
get() {
|
|
69
|
-
return value.current;
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
// The overload signatures expose a precise type; this is the shared impl.
|
|
76
67
|
return result;
|
|
77
68
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createView.test.d.ts","sourceRoot":"","sources":["../src/createView.test.ts"],"names":[],"mappings":""}
|