Skip to content

Commit bb74903

Browse files
committed
skills: Add protocol specific setup; more docs references
1 parent ad501b6 commit bb74903

File tree

23 files changed

+890
-11
lines changed

23 files changed

+890
-11
lines changed
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
---
2+
name: rdc-endpoint-setup
3+
description: Set up @data-client/endpoint for custom async operations. Wraps existing async functions with Endpoint for use with Data Client hooks. Use after rdc-setup detects non-REST/GraphQL async patterns.
4+
disable-model-invocation: true
5+
---
6+
7+
# Custom Endpoint Setup
8+
9+
This skill configures `@data-client/endpoint` for wrapping existing async functions. It should be applied after `rdc-setup` detects custom async patterns that aren't REST or GraphQL.
10+
11+
## Installation
12+
13+
Install the endpoint package alongside the core package:
14+
15+
```bash
16+
# npm
17+
npm install @data-client/endpoint
18+
19+
# yarn
20+
yarn add @data-client/endpoint
21+
22+
# pnpm
23+
pnpm add @data-client/endpoint
24+
```
25+
26+
## When to Use
27+
28+
Use `@data-client/endpoint` when:
29+
- Working with third-party SDK clients (Firebase, Supabase, AWS SDK, etc.)
30+
- Using WebSocket connections for data fetching
31+
- Accessing local async storage (IndexedDB, AsyncStorage)
32+
- Any async function that doesn't fit REST or GraphQL patterns
33+
34+
## Wrapping Async Functions
35+
36+
See [Endpoint](references/Endpoint.md) for full API documentation.
37+
38+
### Detection
39+
40+
Scan for existing async functions that fetch data:
41+
- Functions returning `Promise<T>`
42+
- SDK client methods
43+
- WebSocket message handlers
44+
- IndexedDB operations
45+
46+
### Basic Wrapping Pattern
47+
48+
**Before (existing code):**
49+
```ts
50+
// src/api/users.ts
51+
export async function getUser(id: string): Promise<User> {
52+
const response = await sdk.users.get(id);
53+
return response.data;
54+
}
55+
56+
export async function listUsers(filters: UserFilters): Promise<User[]> {
57+
const response = await sdk.users.list(filters);
58+
return response.data;
59+
}
60+
```
61+
62+
**After (with Endpoint wrapper):**
63+
```ts
64+
// src/api/users.ts
65+
import { Endpoint } from '@data-client/endpoint';
66+
import { User } from '../schemas/User';
67+
68+
// Original functions (keep for reference or direct use)
69+
async function fetchUser(id: string): Promise<User> {
70+
const response = await sdk.users.get(id);
71+
return response.data;
72+
}
73+
74+
async function fetchUsers(filters: UserFilters): Promise<User[]> {
75+
const response = await sdk.users.list(filters);
76+
return response.data;
77+
}
78+
79+
// Wrapped as Endpoints for use with Data Client hooks
80+
export const getUser = new Endpoint(fetchUser, {
81+
schema: User,
82+
name: 'getUser',
83+
});
84+
85+
export const listUsers = new Endpoint(fetchUsers, {
86+
schema: [User],
87+
name: 'listUsers',
88+
});
89+
```
90+
91+
## Endpoint Options
92+
93+
Configure based on the function's behavior:
94+
95+
```ts
96+
export const getUser = new Endpoint(fetchUser, {
97+
// Required for normalization
98+
schema: User,
99+
100+
// Unique name (important if function names get mangled in production)
101+
name: 'getUser',
102+
103+
// Mark as side-effect if it modifies data
104+
sideEffect: true, // for mutations
105+
106+
// Cache configuration
107+
dataExpiryLength: 60000, // 1 minute
108+
errorExpiryLength: 5000, // 5 seconds
109+
110+
// Enable polling
111+
pollFrequency: 30000, // poll every 30 seconds
112+
113+
// Optimistic updates
114+
getOptimisticResponse(snap, id) {
115+
return snap.get(User, { id });
116+
},
117+
});
118+
```
119+
120+
## Custom Key Function
121+
122+
If the default key function doesn't work for your use case:
123+
124+
```ts
125+
export const searchUsers = new Endpoint(fetchSearchUsers, {
126+
schema: [User],
127+
name: 'searchUsers',
128+
key({ query, page }) {
129+
// Custom key for complex parameters
130+
return `searchUsers:${query}:${page}`;
131+
},
132+
});
133+
```
134+
135+
## Common Patterns
136+
137+
### Firebase/Firestore
138+
139+
```ts
140+
import { Endpoint } from '@data-client/endpoint';
141+
import { doc, getDoc, collection, getDocs } from 'firebase/firestore';
142+
import { db } from './firebase';
143+
import { User } from '../schemas/User';
144+
145+
async function fetchUser(id: string): Promise<User> {
146+
const docRef = doc(db, 'users', id);
147+
const docSnap = await getDoc(docRef);
148+
return { id: docSnap.id, ...docSnap.data() } as User;
149+
}
150+
151+
async function fetchUsers(): Promise<User[]> {
152+
const querySnapshot = await getDocs(collection(db, 'users'));
153+
return querySnapshot.docs.map(doc => ({
154+
id: doc.id,
155+
...doc.data(),
156+
})) as User[];
157+
}
158+
159+
export const getUser = new Endpoint(fetchUser, {
160+
schema: User,
161+
name: 'getUser',
162+
});
163+
164+
export const listUsers = new Endpoint(fetchUsers, {
165+
schema: [User],
166+
name: 'listUsers',
167+
});
168+
```
169+
170+
### Supabase
171+
172+
```ts
173+
import { Endpoint } from '@data-client/endpoint';
174+
import { supabase } from './supabase';
175+
import { User } from '../schemas/User';
176+
177+
async function fetchUser(id: string): Promise<User> {
178+
const { data, error } = await supabase
179+
.from('users')
180+
.select('*')
181+
.eq('id', id)
182+
.single();
183+
if (error) throw error;
184+
return data;
185+
}
186+
187+
async function fetchUsers(filters?: { role?: string }): Promise<User[]> {
188+
let query = supabase.from('users').select('*');
189+
if (filters?.role) {
190+
query = query.eq('role', filters.role);
191+
}
192+
const { data, error } = await query;
193+
if (error) throw error;
194+
return data;
195+
}
196+
197+
export const getUser = new Endpoint(fetchUser, {
198+
schema: User,
199+
name: 'getUser',
200+
});
201+
202+
export const listUsers = new Endpoint(fetchUsers, {
203+
schema: [User],
204+
name: 'listUsers',
205+
});
206+
```
207+
208+
### IndexedDB
209+
210+
```ts
211+
import { Endpoint } from '@data-client/endpoint';
212+
import { User } from '../schemas/User';
213+
214+
async function fetchUserFromCache(id: string): Promise<User | undefined> {
215+
const db = await openDB('myapp', 1);
216+
return db.get('users', id);
217+
}
218+
219+
async function fetchUsersFromCache(): Promise<User[]> {
220+
const db = await openDB('myapp', 1);
221+
return db.getAll('users');
222+
}
223+
224+
export const getCachedUser = new Endpoint(fetchUserFromCache, {
225+
schema: User,
226+
name: 'getCachedUser',
227+
dataExpiryLength: Infinity, // Never expires
228+
});
229+
230+
export const listCachedUsers = new Endpoint(fetchUsersFromCache, {
231+
schema: [User],
232+
name: 'listCachedUsers',
233+
dataExpiryLength: Infinity,
234+
});
235+
```
236+
237+
### WebSocket Fetch
238+
239+
```ts
240+
import { Endpoint } from '@data-client/endpoint';
241+
import { socket } from './socket';
242+
import { Message } from '../schemas/Message';
243+
244+
async function fetchMessages(roomId: string): Promise<Message[]> {
245+
return new Promise((resolve, reject) => {
246+
socket.emit('getMessages', { roomId }, (response: any) => {
247+
if (response.error) reject(response.error);
248+
else resolve(response.data);
249+
});
250+
});
251+
}
252+
253+
export const getMessages = new Endpoint(fetchMessages, {
254+
schema: [Message],
255+
name: 'getMessages',
256+
});
257+
```
258+
259+
## Mutations with Side Effects
260+
261+
```ts
262+
export const createUser = new Endpoint(
263+
async (userData: Omit<User, 'id'>): Promise<User> => {
264+
const { data, error } = await supabase
265+
.from('users')
266+
.insert(userData)
267+
.select()
268+
.single();
269+
if (error) throw error;
270+
return data;
271+
},
272+
{
273+
schema: User,
274+
name: 'createUser',
275+
sideEffect: true,
276+
},
277+
);
278+
279+
export const deleteUser = new Endpoint(
280+
async (id: string): Promise<{ id: string }> => {
281+
const { error } = await supabase.from('users').delete().eq('id', id);
282+
if (error) throw error;
283+
return { id };
284+
},
285+
{
286+
name: 'deleteUser',
287+
sideEffect: true,
288+
},
289+
);
290+
```
291+
292+
## Using extend() for Variations
293+
294+
```ts
295+
const baseUserEndpoint = new Endpoint(fetchUser, {
296+
schema: User,
297+
name: 'getUser',
298+
});
299+
300+
// With different cache settings
301+
export const getUserFresh = baseUserEndpoint.extend({
302+
dataExpiryLength: 0, // Always refetch
303+
});
304+
305+
// With polling
306+
export const getUserLive = baseUserEndpoint.extend({
307+
pollFrequency: 5000, // Poll every 5 seconds
308+
});
309+
```
310+
311+
## Important: Function Name Mangling
312+
313+
In production builds, function names may be mangled. **Always provide explicit `name` option**:
314+
315+
```ts
316+
// Bad - name may become 'a' or similar in production
317+
const getUser = new Endpoint(fetchUser);
318+
319+
// Good - explicit name survives minification
320+
const getUser = new Endpoint(fetchUser, { name: 'getUser' });
321+
```
322+
323+
## Usage in Components
324+
325+
```tsx
326+
import { useSuspense, useController } from '@data-client/react';
327+
import { getUser, createUser } from './api/users';
328+
329+
function UserProfile({ id }: { id: string }) {
330+
const user = useSuspense(getUser, id);
331+
const ctrl = useController();
332+
333+
const handleCreate = async (userData: UserData) => {
334+
await ctrl.fetch(createUser, userData);
335+
};
336+
337+
return <div>{user.name}</div>;
338+
}
339+
```
340+
341+
## Next Steps
342+
343+
1. Apply skill "rdc-schema" to define Entity classes
344+
2. Apply skill "rdc-react" for hook usage
345+
346+
## References
347+
348+
- [Endpoint](references/Endpoint.md) - Full Endpoint API
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../docs/rest/api/Endpoint.md

0 commit comments

Comments
 (0)