@sylphx/lens-solid 1.0.2
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/context.d.ts +50 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +765 -0
- package/dist/primitives.d.ts +151 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.test.d.ts +5 -0
- package/dist/primitives.test.d.ts.map +1 -0
- package/package.json +44 -0
- package/src/context.tsx +86 -0
- package/src/index.ts +30 -0
- package/src/primitives.test.tsx +279 -0
- package/src/primitives.ts +315 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sylphx/lens-solid - Primitives
|
|
3
|
+
*
|
|
4
|
+
* SolidJS reactive primitives for Lens queries and mutations.
|
|
5
|
+
* Uses SolidJS fine-grained reactivity for optimal performance.
|
|
6
|
+
*/
|
|
7
|
+
import type { MutationResult, QueryResult } from "@sylphx/lens-client";
|
|
8
|
+
import { type Accessor } from "solid-js";
|
|
9
|
+
/** Query result with reactive signals */
|
|
10
|
+
export interface CreateQueryResult<T> {
|
|
11
|
+
/** Reactive data accessor */
|
|
12
|
+
data: Accessor<T | null>;
|
|
13
|
+
/** Reactive loading state */
|
|
14
|
+
loading: Accessor<boolean>;
|
|
15
|
+
/** Reactive error state */
|
|
16
|
+
error: Accessor<Error | null>;
|
|
17
|
+
/** Refetch the query */
|
|
18
|
+
refetch: () => void;
|
|
19
|
+
}
|
|
20
|
+
/** Mutation result with reactive signals */
|
|
21
|
+
export interface CreateMutationResult<TInput, TOutput> {
|
|
22
|
+
/** Reactive data accessor */
|
|
23
|
+
data: Accessor<TOutput | null>;
|
|
24
|
+
/** Reactive loading state */
|
|
25
|
+
loading: Accessor<boolean>;
|
|
26
|
+
/** Reactive error state */
|
|
27
|
+
error: Accessor<Error | null>;
|
|
28
|
+
/** Execute the mutation */
|
|
29
|
+
mutate: (input: TInput) => Promise<MutationResult<TOutput>>;
|
|
30
|
+
/** Reset state */
|
|
31
|
+
reset: () => void;
|
|
32
|
+
}
|
|
33
|
+
/** Lazy query result */
|
|
34
|
+
export interface CreateLazyQueryResult<T> {
|
|
35
|
+
/** Reactive data accessor */
|
|
36
|
+
data: Accessor<T | null>;
|
|
37
|
+
/** Reactive loading state */
|
|
38
|
+
loading: Accessor<boolean>;
|
|
39
|
+
/** Reactive error state */
|
|
40
|
+
error: Accessor<Error | null>;
|
|
41
|
+
/** Execute the query */
|
|
42
|
+
execute: () => Promise<T>;
|
|
43
|
+
/** Reset state */
|
|
44
|
+
reset: () => void;
|
|
45
|
+
}
|
|
46
|
+
/** Query options */
|
|
47
|
+
export interface CreateQueryOptions {
|
|
48
|
+
/** Skip the query (don't execute) */
|
|
49
|
+
skip?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** Mutation function type */
|
|
52
|
+
export type MutationFn<TInput, TOutput> = (input: TInput) => Promise<MutationResult<TOutput>>;
|
|
53
|
+
/**
|
|
54
|
+
* Create a reactive query from a QueryResult.
|
|
55
|
+
* Automatically subscribes to updates and manages cleanup.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```tsx
|
|
59
|
+
* import { createQuery } from '@sylphx/lens-solid';
|
|
60
|
+
*
|
|
61
|
+
* function UserProfile(props: { userId: string }) {
|
|
62
|
+
* const user = createQuery(() => client.queries.getUser({ id: props.userId }));
|
|
63
|
+
*
|
|
64
|
+
* return (
|
|
65
|
+
* <Show when={!user.loading()} fallback={<Spinner />}>
|
|
66
|
+
* <Show when={user.data()} fallback={<NotFound />}>
|
|
67
|
+
* {(data) => <h1>{data().name}</h1>}
|
|
68
|
+
* </Show>
|
|
69
|
+
* </Show>
|
|
70
|
+
* );
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function createQuery<T>(queryFn: () => QueryResult<T>, options?: CreateQueryOptions): CreateQueryResult<T>;
|
|
75
|
+
/**
|
|
76
|
+
* Create a reactive mutation with loading/error state.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```tsx
|
|
80
|
+
* import { createMutation } from '@sylphx/lens-solid';
|
|
81
|
+
*
|
|
82
|
+
* function CreatePostForm() {
|
|
83
|
+
* const createPost = createMutation(client.mutations.createPost);
|
|
84
|
+
*
|
|
85
|
+
* const handleSubmit = async (e: Event) => {
|
|
86
|
+
* e.preventDefault();
|
|
87
|
+
* try {
|
|
88
|
+
* const result = await createPost.mutate({ title: 'Hello World' });
|
|
89
|
+
* console.log('Created:', result.data);
|
|
90
|
+
* } catch (err) {
|
|
91
|
+
* console.error('Failed:', err);
|
|
92
|
+
* }
|
|
93
|
+
* };
|
|
94
|
+
*
|
|
95
|
+
* return (
|
|
96
|
+
* <form onSubmit={handleSubmit}>
|
|
97
|
+
* <button type="submit" disabled={createPost.loading()}>
|
|
98
|
+
* {createPost.loading() ? 'Creating...' : 'Create'}
|
|
99
|
+
* </button>
|
|
100
|
+
* <Show when={createPost.error()}>
|
|
101
|
+
* {(err) => <p class="error">{err().message}</p>}
|
|
102
|
+
* </Show>
|
|
103
|
+
* </form>
|
|
104
|
+
* );
|
|
105
|
+
* }
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export declare function createMutation<TInput, TOutput>(mutationFn: MutationFn<TInput, TOutput>): CreateMutationResult<TInput, TOutput>;
|
|
109
|
+
/**
|
|
110
|
+
* Create a lazy query that executes on demand.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```tsx
|
|
114
|
+
* import { createLazyQuery } from '@sylphx/lens-solid';
|
|
115
|
+
*
|
|
116
|
+
* function SearchUsers() {
|
|
117
|
+
* const [searchTerm, setSearchTerm] = createSignal('');
|
|
118
|
+
* const search = createLazyQuery(() =>
|
|
119
|
+
* client.queries.searchUsers({ query: searchTerm() })
|
|
120
|
+
* );
|
|
121
|
+
*
|
|
122
|
+
* const handleSearch = async () => {
|
|
123
|
+
* const results = await search.execute();
|
|
124
|
+
* console.log('Found:', results);
|
|
125
|
+
* };
|
|
126
|
+
*
|
|
127
|
+
* return (
|
|
128
|
+
* <div>
|
|
129
|
+
* <input
|
|
130
|
+
* value={searchTerm()}
|
|
131
|
+
* onInput={(e) => setSearchTerm(e.currentTarget.value)}
|
|
132
|
+
* />
|
|
133
|
+
* <button onClick={handleSearch} disabled={search.loading()}>
|
|
134
|
+
* Search
|
|
135
|
+
* </button>
|
|
136
|
+
* <Show when={search.data()}>
|
|
137
|
+
* {(users) => (
|
|
138
|
+
* <ul>
|
|
139
|
+
* <For each={users()}>
|
|
140
|
+
* {(user) => <li>{user.name}</li>}
|
|
141
|
+
* </For>
|
|
142
|
+
* </ul>
|
|
143
|
+
* )}
|
|
144
|
+
* </Show>
|
|
145
|
+
* </div>
|
|
146
|
+
* );
|
|
147
|
+
* }
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export declare function createLazyQuery<T>(queryFn: () => QueryResult<T>): CreateLazyQueryResult<T>;
|
|
151
|
+
//# sourceMappingURL=primitives.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../src/primitives.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,KAAK,QAAQ,EAA2B,MAAM,UAAU,CAAC;AAMlE,yCAAyC;AACzC,MAAM,WAAW,iBAAiB,CAAC,CAAC;IACnC,6BAA6B;IAC7B,IAAI,EAAE,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACzB,6BAA6B;IAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,2BAA2B;IAC3B,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9B,wBAAwB;IACxB,OAAO,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB,CAAC,MAAM,EAAE,OAAO;IACpD,6BAA6B;IAC7B,IAAI,EAAE,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IAC/B,6BAA6B;IAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,2BAA2B;IAC3B,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9B,2BAA2B;IAC3B,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5D,kBAAkB;IAClB,KAAK,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAwB;AACxB,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACvC,6BAA6B;IAC7B,IAAI,EAAE,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACzB,6BAA6B;IAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,2BAA2B;IAC3B,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9B,wBAAwB;IACxB,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1B,kBAAkB;IAClB,KAAK,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,oBAAoB;AACpB,MAAM,WAAW,kBAAkB;IAClC,qCAAqC;IACrC,IAAI,CAAC,EAAE,OAAO,CAAC;CACf;AAED,6BAA6B;AAC7B,MAAM,MAAM,UAAU,CAAC,MAAM,EAAE,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;AAM9F;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC5B,OAAO,EAAE,MAAM,WAAW,CAAC,CAAC,CAAC,EAC7B,OAAO,CAAC,EAAE,kBAAkB,GAC1B,iBAAiB,CAAC,CAAC,CAAC,CAgEtB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAC7C,UAAU,EAAE,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,oBAAoB,CAAC,MAAM,EAAE,OAAO,CAAC,CAmCvC;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC,CAAC,CAAC,GAAG,qBAAqB,CAAC,CAAC,CAAC,CAoC1F"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"primitives.test.d.ts","sourceRoot":"","sources":["../src/primitives.test.tsx"],"names":[],"mappings":"AAAA;;GAEG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sylphx/lens-solid",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "SolidJS bindings for Lens API framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target browser && bun run build:types",
|
|
16
|
+
"build:types": "tsc --emitDeclarationOnly --outDir ./dist",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"test": "bun test"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"lens",
|
|
26
|
+
"solid",
|
|
27
|
+
"solidjs",
|
|
28
|
+
"reactive",
|
|
29
|
+
"signals"
|
|
30
|
+
],
|
|
31
|
+
"author": "SylphxAI",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@sylphx/lens-client": "workspace:*"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"solid-js": ">=1.8.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@solidjs/testing-library": "^0.8.10",
|
|
41
|
+
"solid-js": "^1.9.5",
|
|
42
|
+
"typescript": "^5.9.3"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/context.tsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sylphx/lens-solid - Context
|
|
3
|
+
*
|
|
4
|
+
* SolidJS context for Lens client injection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { LensClient } from "@sylphx/lens-client";
|
|
8
|
+
import { type ParentComponent, createContext, useContext } from "solid-js";
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Context
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
const LensClientContext = createContext<LensClient<any, any>>();
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Provider
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
export interface LensProviderProps {
|
|
22
|
+
/** Lens client instance */
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
client: LensClient<any, any>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Provider for Lens client in SolidJS.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* import { createClient, httpLink } from '@sylphx/lens-client';
|
|
33
|
+
* import { LensProvider } from '@sylphx/lens-solid';
|
|
34
|
+
* import type { AppRouter } from './server';
|
|
35
|
+
*
|
|
36
|
+
* const client = createClient<AppRouter>({
|
|
37
|
+
* links: [httpLink({ url: '/api' })],
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* function App() {
|
|
41
|
+
* return (
|
|
42
|
+
* <LensProvider client={client}>
|
|
43
|
+
* <UserProfile />
|
|
44
|
+
* </LensProvider>
|
|
45
|
+
* );
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export const LensProvider: ParentComponent<LensProviderProps> = (props) => {
|
|
50
|
+
return (
|
|
51
|
+
<LensClientContext.Provider value={props.client}>{props.children}</LensClientContext.Provider>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// Hook
|
|
57
|
+
// =============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get Lens client from context.
|
|
61
|
+
*
|
|
62
|
+
* @throws Error if used outside LensProvider
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* function UserProfile() {
|
|
67
|
+
* const client = useLensClient<AppRouter>();
|
|
68
|
+
* const user = createQuery(() => client.queries.getUser({ id: '123' }));
|
|
69
|
+
* return <h1>{user.data?.name}</h1>;
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
+
export function useLensClient<TRouter = any>(): LensClient<any, any> & TRouter {
|
|
75
|
+
const client = useContext(LensClientContext);
|
|
76
|
+
|
|
77
|
+
if (!client) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"useLensClient must be used within a <LensProvider>. " +
|
|
80
|
+
"Make sure to wrap your app with <LensProvider client={client}>.",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
+
return client as LensClient<any, any> & TRouter;
|
|
86
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sylphx/lens-solid
|
|
3
|
+
*
|
|
4
|
+
* SolidJS bindings for Lens API framework.
|
|
5
|
+
* Reactive primitives that integrate with SolidJS fine-grained reactivity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Context & Provider
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
export { LensProvider, useLensClient, type LensProviderProps } from "./context";
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Reactive Primitives
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
// Query primitives
|
|
20
|
+
createQuery,
|
|
21
|
+
createLazyQuery,
|
|
22
|
+
// Mutation primitive
|
|
23
|
+
createMutation,
|
|
24
|
+
// Types
|
|
25
|
+
type CreateQueryResult,
|
|
26
|
+
type CreateMutationResult,
|
|
27
|
+
type CreateLazyQueryResult,
|
|
28
|
+
type CreateQueryOptions,
|
|
29
|
+
type MutationFn,
|
|
30
|
+
} from "./primitives";
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for SolidJS Primitives
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import type { MutationResult, QueryResult } from "@sylphx/lens-client";
|
|
7
|
+
import { createRoot } from "solid-js";
|
|
8
|
+
import { createLazyQuery, createMutation, createQuery } from "./primitives";
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Mock QueryResult
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
function createMockQueryResult<T>(initialValue: T | null = null): QueryResult<T> & {
|
|
15
|
+
_setValue: (value: T) => void;
|
|
16
|
+
_setError: (error: Error) => void;
|
|
17
|
+
} {
|
|
18
|
+
let currentValue = initialValue;
|
|
19
|
+
const subscribers: Array<(value: T) => void> = [];
|
|
20
|
+
let resolved = false;
|
|
21
|
+
let resolvePromise: ((value: T) => void) | null = null;
|
|
22
|
+
let rejectPromise: ((error: Error) => void) | null = null;
|
|
23
|
+
|
|
24
|
+
const promise = new Promise<T>((resolve, reject) => {
|
|
25
|
+
resolvePromise = resolve;
|
|
26
|
+
rejectPromise = reject;
|
|
27
|
+
if (initialValue !== null) {
|
|
28
|
+
resolved = true;
|
|
29
|
+
resolve(initialValue);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const result = {
|
|
34
|
+
get value() {
|
|
35
|
+
return currentValue;
|
|
36
|
+
},
|
|
37
|
+
signal: { value: currentValue } as any,
|
|
38
|
+
loading: { value: initialValue === null } as any,
|
|
39
|
+
error: { value: null } as any,
|
|
40
|
+
subscribe(callback?: (data: T) => void): () => void {
|
|
41
|
+
if (callback) {
|
|
42
|
+
subscribers.push(callback);
|
|
43
|
+
if (currentValue !== null) {
|
|
44
|
+
callback(currentValue);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return () => {
|
|
48
|
+
const idx = subscribers.indexOf(callback!);
|
|
49
|
+
if (idx >= 0) subscribers.splice(idx, 1);
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
select() {
|
|
53
|
+
return result as unknown as QueryResult<T>;
|
|
54
|
+
},
|
|
55
|
+
then<TResult1 = T, TResult2 = never>(
|
|
56
|
+
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
|
57
|
+
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
|
58
|
+
): Promise<TResult1 | TResult2> {
|
|
59
|
+
return promise.then(onfulfilled, onrejected);
|
|
60
|
+
},
|
|
61
|
+
// Test helpers
|
|
62
|
+
_setValue(value: T) {
|
|
63
|
+
currentValue = value;
|
|
64
|
+
subscribers.forEach((cb) => cb(value));
|
|
65
|
+
if (!resolved && resolvePromise) {
|
|
66
|
+
resolved = true;
|
|
67
|
+
resolvePromise(value);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
_setError(error: Error) {
|
|
71
|
+
if (!resolved && rejectPromise) {
|
|
72
|
+
resolved = true;
|
|
73
|
+
rejectPromise(error);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return result as QueryResult<T> & {
|
|
79
|
+
_setValue: (value: T) => void;
|
|
80
|
+
_setError: (error: Error) => void;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// =============================================================================
|
|
85
|
+
// Tests: createQuery
|
|
86
|
+
// =============================================================================
|
|
87
|
+
|
|
88
|
+
describe("createQuery", () => {
|
|
89
|
+
test("returns loading state initially", () => {
|
|
90
|
+
createRoot((dispose) => {
|
|
91
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
92
|
+
const query = createQuery(() => mockQuery);
|
|
93
|
+
|
|
94
|
+
expect(query.loading()).toBe(true);
|
|
95
|
+
expect(query.data()).toBe(null);
|
|
96
|
+
expect(query.error()).toBe(null);
|
|
97
|
+
|
|
98
|
+
dispose();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("returns data when query resolves", async () => {
|
|
103
|
+
await new Promise<void>((resolve) => {
|
|
104
|
+
createRoot(async (dispose) => {
|
|
105
|
+
// Create mock with initial value so promise resolves immediately
|
|
106
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>({
|
|
107
|
+
id: "123",
|
|
108
|
+
name: "John",
|
|
109
|
+
});
|
|
110
|
+
const query = createQuery(() => mockQuery);
|
|
111
|
+
|
|
112
|
+
// Wait for promise to resolve
|
|
113
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
114
|
+
|
|
115
|
+
expect(query.data()).toEqual({ id: "123", name: "John" });
|
|
116
|
+
expect(query.loading()).toBe(false);
|
|
117
|
+
expect(query.error()).toBe(null);
|
|
118
|
+
|
|
119
|
+
dispose();
|
|
120
|
+
resolve();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("skips query when skip option is true", () => {
|
|
126
|
+
createRoot((dispose) => {
|
|
127
|
+
const mockQuery = createMockQueryResult<{ id: string }>();
|
|
128
|
+
const query = createQuery(() => mockQuery, { skip: true });
|
|
129
|
+
|
|
130
|
+
expect(query.loading()).toBe(false);
|
|
131
|
+
expect(query.data()).toBe(null);
|
|
132
|
+
|
|
133
|
+
dispose();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// =============================================================================
|
|
139
|
+
// Tests: createMutation
|
|
140
|
+
// =============================================================================
|
|
141
|
+
|
|
142
|
+
describe("createMutation", () => {
|
|
143
|
+
test("executes mutation and returns result", async () => {
|
|
144
|
+
await new Promise<void>((resolve) => {
|
|
145
|
+
createRoot(async (dispose) => {
|
|
146
|
+
const mutationFn = async (input: { name: string }): Promise<
|
|
147
|
+
MutationResult<{ id: string; name: string }>
|
|
148
|
+
> => {
|
|
149
|
+
return { data: { id: "new-id", name: input.name } };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const mutation = createMutation(mutationFn);
|
|
153
|
+
|
|
154
|
+
expect(mutation.loading()).toBe(false);
|
|
155
|
+
expect(mutation.data()).toBe(null);
|
|
156
|
+
|
|
157
|
+
const result = await mutation.mutate({ name: "New User" });
|
|
158
|
+
|
|
159
|
+
expect(result.data).toEqual({ id: "new-id", name: "New User" });
|
|
160
|
+
expect(mutation.data()).toEqual({ id: "new-id", name: "New User" });
|
|
161
|
+
expect(mutation.loading()).toBe(false);
|
|
162
|
+
|
|
163
|
+
dispose();
|
|
164
|
+
resolve();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("handles mutation error", async () => {
|
|
170
|
+
await new Promise<void>((resolve) => {
|
|
171
|
+
createRoot(async (dispose) => {
|
|
172
|
+
const mutationFn = async (_input: { name: string }): Promise<
|
|
173
|
+
MutationResult<{ id: string }>
|
|
174
|
+
> => {
|
|
175
|
+
throw new Error("Mutation failed");
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const mutation = createMutation(mutationFn);
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
await mutation.mutate({ name: "New User" });
|
|
182
|
+
} catch {
|
|
183
|
+
// Expected error
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
expect(mutation.error()?.message).toBe("Mutation failed");
|
|
187
|
+
expect(mutation.loading()).toBe(false);
|
|
188
|
+
|
|
189
|
+
dispose();
|
|
190
|
+
resolve();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("reset clears mutation state", async () => {
|
|
196
|
+
await new Promise<void>((resolve) => {
|
|
197
|
+
createRoot(async (dispose) => {
|
|
198
|
+
const mutationFn = async (input: { name: string }): Promise<
|
|
199
|
+
MutationResult<{ id: string; name: string }>
|
|
200
|
+
> => {
|
|
201
|
+
return { data: { id: "new-id", name: input.name } };
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const mutation = createMutation(mutationFn);
|
|
205
|
+
await mutation.mutate({ name: "New User" });
|
|
206
|
+
|
|
207
|
+
expect(mutation.data()).not.toBe(null);
|
|
208
|
+
|
|
209
|
+
mutation.reset();
|
|
210
|
+
|
|
211
|
+
expect(mutation.data()).toBe(null);
|
|
212
|
+
expect(mutation.error()).toBe(null);
|
|
213
|
+
expect(mutation.loading()).toBe(false);
|
|
214
|
+
|
|
215
|
+
dispose();
|
|
216
|
+
resolve();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// =============================================================================
|
|
223
|
+
// Tests: createLazyQuery
|
|
224
|
+
// =============================================================================
|
|
225
|
+
|
|
226
|
+
describe("createLazyQuery", () => {
|
|
227
|
+
test("does not execute query on creation", () => {
|
|
228
|
+
createRoot((dispose) => {
|
|
229
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "123" });
|
|
230
|
+
const query = createLazyQuery(() => mockQuery);
|
|
231
|
+
|
|
232
|
+
expect(query.loading()).toBe(false);
|
|
233
|
+
expect(query.data()).toBe(null);
|
|
234
|
+
|
|
235
|
+
dispose();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("executes query when execute is called", async () => {
|
|
240
|
+
await new Promise<void>((resolve) => {
|
|
241
|
+
createRoot(async (dispose) => {
|
|
242
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>({
|
|
243
|
+
id: "123",
|
|
244
|
+
name: "John",
|
|
245
|
+
});
|
|
246
|
+
const query = createLazyQuery(() => mockQuery);
|
|
247
|
+
|
|
248
|
+
const result = await query.execute();
|
|
249
|
+
|
|
250
|
+
expect(result).toEqual({ id: "123", name: "John" });
|
|
251
|
+
expect(query.data()).toEqual({ id: "123", name: "John" });
|
|
252
|
+
|
|
253
|
+
dispose();
|
|
254
|
+
resolve();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("reset clears query state", async () => {
|
|
260
|
+
await new Promise<void>((resolve) => {
|
|
261
|
+
createRoot(async (dispose) => {
|
|
262
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "123" });
|
|
263
|
+
const query = createLazyQuery(() => mockQuery);
|
|
264
|
+
|
|
265
|
+
await query.execute();
|
|
266
|
+
expect(query.data()).not.toBe(null);
|
|
267
|
+
|
|
268
|
+
query.reset();
|
|
269
|
+
|
|
270
|
+
expect(query.data()).toBe(null);
|
|
271
|
+
expect(query.error()).toBe(null);
|
|
272
|
+
expect(query.loading()).toBe(false);
|
|
273
|
+
|
|
274
|
+
dispose();
|
|
275
|
+
resolve();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
});
|