@fitzzero/quickdraw-core 0.1.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/README.md +300 -0
- package/dist/client/index.d.ts +349 -0
- package/dist/client/index.js +690 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/testing.d.ts +73 -0
- package/dist/client/testing.js +78 -0
- package/dist/client/testing.js.map +1 -0
- package/dist/server/index.d.ts +336 -0
- package/dist/server/index.js +707 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/testing.d.ts +70 -0
- package/dist/server/testing.js +399 -0
- package/dist/server/testing.js.map +1 -0
- package/dist/shared/index.d.ts +158 -0
- package/dist/shared/index.js +12 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/types-CUExGF7F.d.ts +109 -0
- package/dist/types-OhRxDBID.d.ts +118 -0
- package/package.json +111 -0
package/README.md
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# @quickdraw/core
|
|
2
|
+
|
|
3
|
+
Fast fullstack patterns for real-time applications with Socket.io and TanStack Query.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Server Core**: BaseService class with typed CRUD, ACL-based access control, and real-time subscriptions
|
|
8
|
+
- **Client Core**: TanStack Query integration with Socket.io for real-time state management
|
|
9
|
+
- **Socket Inputs**: Pre-built form components that sync with server state
|
|
10
|
+
- **Custom OAuth**: JWT-based authentication with Discord and Google providers
|
|
11
|
+
- **Type Safety**: End-to-end TypeScript support with shared type definitions
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @quickdraw/core
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### Server Setup
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { createQuickdrawServer, BaseService } from '@quickdraw/core/server';
|
|
25
|
+
import { PrismaClient } from '@prisma/client';
|
|
26
|
+
|
|
27
|
+
const prisma = new PrismaClient();
|
|
28
|
+
|
|
29
|
+
// Define your service
|
|
30
|
+
class ChatService extends BaseService<Chat, Prisma.ChatCreateInput, Prisma.ChatUpdateInput, ChatServiceMethods> {
|
|
31
|
+
constructor() {
|
|
32
|
+
super({ serviceName: 'chatService', hasEntryACL: true });
|
|
33
|
+
this.setDelegate(prisma.chat);
|
|
34
|
+
|
|
35
|
+
// Define public methods
|
|
36
|
+
this.createChat = this.defineMethod('createChat', 'Read', async (payload, ctx) => {
|
|
37
|
+
const chat = await this.create({ title: payload.title, ownerId: ctx.userId });
|
|
38
|
+
return { id: chat.id };
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
createChat: ReturnType<typeof this.defineMethod<'createChat'>>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Start server
|
|
46
|
+
const { io, httpServer } = createQuickdrawServer({
|
|
47
|
+
port: 4000,
|
|
48
|
+
cors: { origin: 'http://localhost:3000' },
|
|
49
|
+
services: {
|
|
50
|
+
chatService: new ChatService(),
|
|
51
|
+
},
|
|
52
|
+
auth: {
|
|
53
|
+
authenticate: async (socket, auth) => {
|
|
54
|
+
const payload = await verifyJWT(auth.token, process.env.JWT_SECRET);
|
|
55
|
+
return payload?.userId;
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Client Setup
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// app/layout.tsx
|
|
65
|
+
import { QuickdrawProvider } from '@quickdraw/core/client';
|
|
66
|
+
|
|
67
|
+
export default function RootLayout({ children }) {
|
|
68
|
+
return (
|
|
69
|
+
<QuickdrawProvider
|
|
70
|
+
serverUrl="http://localhost:4000"
|
|
71
|
+
authToken={getAuthToken()}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
</QuickdrawProvider>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// app/chat/page.tsx
|
|
79
|
+
import { useService, useSubscription } from '@quickdraw/core/client';
|
|
80
|
+
|
|
81
|
+
function ChatPage({ chatId }: { chatId: string }) {
|
|
82
|
+
// Subscribe to real-time updates
|
|
83
|
+
const { data: chat, isLoading } = useSubscription('chatService', chatId);
|
|
84
|
+
|
|
85
|
+
// Mutation hook
|
|
86
|
+
const updateTitle = useService('chatService', 'updateTitle', {
|
|
87
|
+
onSuccess: () => console.log('Title updated!'),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (isLoading) return <div>Loading...</div>;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div>
|
|
94
|
+
<h1>{chat?.title}</h1>
|
|
95
|
+
<button onClick={() => updateTitle.mutate({ id: chatId, title: 'New Title' })}>
|
|
96
|
+
Update Title
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Socket Inputs
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { SocketTextField } from '@quickdraw/core/client';
|
|
107
|
+
|
|
108
|
+
function ChatTitleEditor({ chat, updateChat }) {
|
|
109
|
+
return (
|
|
110
|
+
<SocketTextField
|
|
111
|
+
state={chat}
|
|
112
|
+
update={(patch) => updateChat.mutateAsync({ id: chat.id, ...patch })}
|
|
113
|
+
property="title"
|
|
114
|
+
commitMode="debounce"
|
|
115
|
+
debounceMs={500}
|
|
116
|
+
placeholder="Chat title..."
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Package Exports
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Shared types (both server and client)
|
|
126
|
+
import { ServiceResponse, AccessLevel, ServiceMethodMap } from '@quickdraw/core';
|
|
127
|
+
|
|
128
|
+
// Server
|
|
129
|
+
import {
|
|
130
|
+
BaseService,
|
|
131
|
+
ServiceRegistry,
|
|
132
|
+
createQuickdrawServer,
|
|
133
|
+
createJWT,
|
|
134
|
+
verifyJWT,
|
|
135
|
+
discordProvider,
|
|
136
|
+
googleProvider,
|
|
137
|
+
} from '@quickdraw/core/server';
|
|
138
|
+
|
|
139
|
+
// Server testing
|
|
140
|
+
import { createTestServer, connectAsUser, emitWithAck } from '@quickdraw/core/server/testing';
|
|
141
|
+
|
|
142
|
+
// Client
|
|
143
|
+
import {
|
|
144
|
+
QuickdrawProvider,
|
|
145
|
+
useQuickdrawSocket,
|
|
146
|
+
useService,
|
|
147
|
+
useSubscription,
|
|
148
|
+
SocketCheckbox,
|
|
149
|
+
SocketTextField,
|
|
150
|
+
SocketSelect,
|
|
151
|
+
SocketSlider,
|
|
152
|
+
SocketSwitch,
|
|
153
|
+
} from '@quickdraw/core/client';
|
|
154
|
+
|
|
155
|
+
// Client testing
|
|
156
|
+
import { createMockSocket, createTestWrapper } from '@quickdraw/core/client/testing';
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Type Definitions
|
|
160
|
+
|
|
161
|
+
Define your service methods in a shared types file:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// shared/types.ts
|
|
165
|
+
import type { ServiceMethodMap } from '@quickdraw/core';
|
|
166
|
+
|
|
167
|
+
export type ChatServiceMethods = ServiceMethodMap<{
|
|
168
|
+
createChat: {
|
|
169
|
+
payload: { title: string };
|
|
170
|
+
response: { id: string };
|
|
171
|
+
};
|
|
172
|
+
updateTitle: {
|
|
173
|
+
payload: { id: string; title: string };
|
|
174
|
+
response: { id: string; title: string };
|
|
175
|
+
};
|
|
176
|
+
inviteUser: {
|
|
177
|
+
payload: { id: string; userId: string; level: 'Read' | 'Moderate' | 'Admin' };
|
|
178
|
+
response: { id: string };
|
|
179
|
+
};
|
|
180
|
+
}>;
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Architecture
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
187
|
+
│ Client (React) │
|
|
188
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
189
|
+
│ QuickdrawProvider │
|
|
190
|
+
│ ├── TanStack QueryClient │
|
|
191
|
+
│ └── Socket.io Connection │
|
|
192
|
+
│ │
|
|
193
|
+
│ useService() ──────────────────────────────────────────────┐ │
|
|
194
|
+
│ useSubscription() ─────────────────────────────────────────┤ │
|
|
195
|
+
│ SocketTextField, SocketCheckbox, ... ──────────────────────┤ │
|
|
196
|
+
│ │ │
|
|
197
|
+
└─────────────────────────────────────────────────────────────│───┘
|
|
198
|
+
│
|
|
199
|
+
Socket.io Events │
|
|
200
|
+
▼
|
|
201
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
202
|
+
│ Server (Node.js) │
|
|
203
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
204
|
+
│ createQuickdrawServer() │
|
|
205
|
+
│ └── ServiceRegistry │
|
|
206
|
+
│ ├── Auto-discovers public methods │
|
|
207
|
+
│ └── Wires methods to Socket.io events │
|
|
208
|
+
│ │
|
|
209
|
+
│ BaseService<Entity, CreateInput, UpdateInput, ServiceMethods> │
|
|
210
|
+
│ ├── defineMethod() - Type-safe method definition │
|
|
211
|
+
│ ├── subscribe() / unsubscribe() - Real-time subscriptions │
|
|
212
|
+
│ ├── create() / update() / delete() - CRUD with auto-emit │
|
|
213
|
+
│ └── checkAccess() - ACL enforcement │
|
|
214
|
+
│ │
|
|
215
|
+
│ Auth Utilities │
|
|
216
|
+
│ ├── createJWT() / verifyJWT() │
|
|
217
|
+
│ └── OAuth providers (Discord, Google) │
|
|
218
|
+
│ │
|
|
219
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Access Control
|
|
223
|
+
|
|
224
|
+
Quickdraw supports two levels of access control:
|
|
225
|
+
|
|
226
|
+
### Service-level ACL
|
|
227
|
+
|
|
228
|
+
Stored in `user.serviceAccess` JSON field:
|
|
229
|
+
```json
|
|
230
|
+
{ "chatService": "Admin", "userService": "Read" }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Entry-level ACL
|
|
234
|
+
|
|
235
|
+
Stored in entity's `acl` JSON field (when `hasEntryACL: true`):
|
|
236
|
+
```json
|
|
237
|
+
[
|
|
238
|
+
{ "userId": "user-1", "level": "Admin" },
|
|
239
|
+
{ "userId": "user-2", "level": "Read" }
|
|
240
|
+
]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Access Levels
|
|
244
|
+
|
|
245
|
+
- **Public**: No authentication required
|
|
246
|
+
- **Read**: Authenticated users (default for subscriptions)
|
|
247
|
+
- **Moderate**: Edit access (update, moderate content)
|
|
248
|
+
- **Admin**: Full access (delete, manage ACL)
|
|
249
|
+
|
|
250
|
+
## Testing
|
|
251
|
+
|
|
252
|
+
### Server Integration Tests
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { createTestServer, connectAsUser, emitWithAck } from '@quickdraw/core/server/testing';
|
|
256
|
+
|
|
257
|
+
describe('ChatService', () => {
|
|
258
|
+
let server;
|
|
259
|
+
|
|
260
|
+
beforeAll(async () => {
|
|
261
|
+
server = await createTestServer({
|
|
262
|
+
services: { chatService: new ChatService() },
|
|
263
|
+
seedDb: async () => { /* seed test data */ },
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
afterAll(() => server.stop());
|
|
268
|
+
|
|
269
|
+
it('creates chat', async () => {
|
|
270
|
+
const client = await server.connectAs('user-id');
|
|
271
|
+
const chat = await client.emit('chatService:createChat', { title: 'Test' });
|
|
272
|
+
expect(chat.id).toBeDefined();
|
|
273
|
+
client.close();
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Client Component Tests
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
import { createTestWrapper, createMockSocket, mockSuccessEmit } from '@quickdraw/core/client/testing';
|
|
282
|
+
|
|
283
|
+
test('renders chat', () => {
|
|
284
|
+
const mockSocket = createMockSocket();
|
|
285
|
+
mockSocket.emit.mockImplementation(mockSuccessEmit({ title: 'Test Chat' }));
|
|
286
|
+
|
|
287
|
+
const wrapper = createTestWrapper({ socketContext: { socket: mockSocket } });
|
|
288
|
+
render(<ChatView chatId="123" />, { wrapper });
|
|
289
|
+
|
|
290
|
+
expect(screen.getByText('Test Chat')).toBeInTheDocument();
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Contributing
|
|
295
|
+
|
|
296
|
+
Contributions are welcome! Please read our contributing guide for details.
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { a as QuickdrawProviderProps, Q as QuickdrawSocketContextValue, U as UseServiceOptions, b as UseServiceResult, c as UseSubscriptionOptions, d as UseSubscriptionResult } from '../types-CUExGF7F.js';
|
|
3
|
+
export { C as ClientServiceMethodMap, S as SubscriptionDataMap } from '../types-CUExGF7F.js';
|
|
4
|
+
import 'socket.io-client';
|
|
5
|
+
import '@tanstack/react-query';
|
|
6
|
+
import '../shared/index.js';
|
|
7
|
+
import 'zod';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hook to access the quickdraw socket context.
|
|
11
|
+
*/
|
|
12
|
+
declare function useQuickdrawSocket(): QuickdrawSocketContextValue;
|
|
13
|
+
/**
|
|
14
|
+
* Provider component that sets up TanStack Query and Socket.io connection.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* // In your app layout
|
|
19
|
+
* export default function RootLayout({ children }) {
|
|
20
|
+
* return (
|
|
21
|
+
* <QuickdrawProvider
|
|
22
|
+
* serverUrl="http://localhost:4000"
|
|
23
|
+
* authToken={token}
|
|
24
|
+
* >
|
|
25
|
+
* {children}
|
|
26
|
+
* </QuickdrawProvider>
|
|
27
|
+
* );
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
declare function QuickdrawProvider({ children, serverUrl, queryClient, authToken, autoConnect, }: QuickdrawProviderProps): React.ReactElement;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Hook for invoking service methods via Socket.io with TanStack Query integration.
|
|
35
|
+
*
|
|
36
|
+
* @typeParam TServiceMethods - Map of service method definitions
|
|
37
|
+
* @typeParam TService - Service name
|
|
38
|
+
* @typeParam TMethod - Method name
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* // Define your service methods type
|
|
43
|
+
* type ChatServiceMethods = {
|
|
44
|
+
* createChat: { payload: { title: string }; response: { id: string } };
|
|
45
|
+
* updateTitle: { payload: { id: string; title: string }; response: { id: string; title: string } };
|
|
46
|
+
* };
|
|
47
|
+
*
|
|
48
|
+
* // In your component
|
|
49
|
+
* function CreateChatButton() {
|
|
50
|
+
* const createChat = useService<ChatServiceMethods, 'chatService', 'createChat'>(
|
|
51
|
+
* 'chatService',
|
|
52
|
+
* 'createChat',
|
|
53
|
+
* {
|
|
54
|
+
* onSuccess: (data) => {
|
|
55
|
+
* router.push(`/chat/${data.id}`);
|
|
56
|
+
* },
|
|
57
|
+
* }
|
|
58
|
+
* );
|
|
59
|
+
*
|
|
60
|
+
* return (
|
|
61
|
+
* <button
|
|
62
|
+
* onClick={() => createChat.mutate({ title: 'New Chat' })}
|
|
63
|
+
* disabled={createChat.isPending}
|
|
64
|
+
* >
|
|
65
|
+
* {createChat.isPending ? 'Creating...' : 'Create Chat'}
|
|
66
|
+
* </button>
|
|
67
|
+
* );
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare function useService<TServiceMethods extends Record<string, Record<string, {
|
|
72
|
+
payload: unknown;
|
|
73
|
+
response: unknown;
|
|
74
|
+
}>>, TService extends keyof TServiceMethods & string, TMethod extends keyof TServiceMethods[TService] & string>(serviceName: TService, methodName: TMethod, options?: UseServiceOptions<TServiceMethods[TService][TMethod]["response"]>): UseServiceResult<TServiceMethods[TService][TMethod]["payload"], TServiceMethods[TService][TMethod]["response"]>;
|
|
75
|
+
/**
|
|
76
|
+
* Simplified hook for basic service method calls.
|
|
77
|
+
* Use this when you don't need the full TanStack Query mutation features.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```tsx
|
|
81
|
+
* const { execute, loading, error, data } = useServiceMethod('chatService', 'createChat');
|
|
82
|
+
*
|
|
83
|
+
* const handleCreate = async () => {
|
|
84
|
+
* const result = await execute({ title: 'New Chat' });
|
|
85
|
+
* if (result) {
|
|
86
|
+
* router.push(`/chat/${result.id}`);
|
|
87
|
+
* }
|
|
88
|
+
* };
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare function useServiceMethod<TPayload = unknown, TResponse = unknown>(serviceName: string, methodName: string, options?: UseServiceOptions<TResponse>): {
|
|
92
|
+
execute: (payload: TPayload) => Promise<TResponse | null>;
|
|
93
|
+
loading: boolean;
|
|
94
|
+
error: string | null;
|
|
95
|
+
data: TResponse | null;
|
|
96
|
+
isReady: boolean;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Hook for subscribing to real-time entity updates with TanStack Query integration.
|
|
101
|
+
*
|
|
102
|
+
* Features:
|
|
103
|
+
* - Automatic subscription management
|
|
104
|
+
* - TanStack Query cache integration for optimistic updates
|
|
105
|
+
* - Subscription deduplication across components
|
|
106
|
+
* - Automatic reconnection handling
|
|
107
|
+
*
|
|
108
|
+
* @typeParam TData - The entity type
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```tsx
|
|
112
|
+
* function ChatView({ chatId }: { chatId: string }) {
|
|
113
|
+
* const { data: chat, isLoading, error } = useSubscription<Chat>(
|
|
114
|
+
* 'chatService',
|
|
115
|
+
* chatId,
|
|
116
|
+
* {
|
|
117
|
+
* onData: (chat) => {
|
|
118
|
+
* console.log('Chat updated:', chat.title);
|
|
119
|
+
* },
|
|
120
|
+
* }
|
|
121
|
+
* );
|
|
122
|
+
*
|
|
123
|
+
* if (isLoading) return <div>Loading...</div>;
|
|
124
|
+
* if (error) return <div>Error: {error}</div>;
|
|
125
|
+
* if (!chat) return <div>Chat not found</div>;
|
|
126
|
+
*
|
|
127
|
+
* return <div>{chat.title}</div>;
|
|
128
|
+
* }
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
declare function useSubscription<TData extends {
|
|
132
|
+
id: string;
|
|
133
|
+
}>(serviceName: string, entryId: string | null, options?: UseSubscriptionOptions<TData>): UseSubscriptionResult<TData>;
|
|
134
|
+
|
|
135
|
+
type CommitMode = "change" | "blur" | "debounce";
|
|
136
|
+
interface UseSocketInputOptions<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate> {
|
|
137
|
+
/**
|
|
138
|
+
* Current state of the entity
|
|
139
|
+
*/
|
|
140
|
+
state: TEntry | null;
|
|
141
|
+
/**
|
|
142
|
+
* Property key to update
|
|
143
|
+
*/
|
|
144
|
+
property: TKey;
|
|
145
|
+
/**
|
|
146
|
+
* Update function that sends the patch to the server
|
|
147
|
+
*/
|
|
148
|
+
update: (patch: Partial<TAllowedUpdate>) => Promise<TEntry | undefined | null>;
|
|
149
|
+
/**
|
|
150
|
+
* When to commit changes to the server
|
|
151
|
+
* - "change": immediately on every change
|
|
152
|
+
* - "blur": when input loses focus
|
|
153
|
+
* - "debounce": after debounceMs of inactivity
|
|
154
|
+
*/
|
|
155
|
+
commitMode: CommitMode;
|
|
156
|
+
/**
|
|
157
|
+
* Debounce delay in milliseconds (only used when commitMode is "debounce")
|
|
158
|
+
*/
|
|
159
|
+
debounceMs?: number;
|
|
160
|
+
/**
|
|
161
|
+
* Format the value from state for display
|
|
162
|
+
*/
|
|
163
|
+
format?: (value: unknown) => unknown;
|
|
164
|
+
/**
|
|
165
|
+
* Parse the display value back to the state format
|
|
166
|
+
*/
|
|
167
|
+
parse?: (value: unknown) => unknown;
|
|
168
|
+
/**
|
|
169
|
+
* Callback on successful update
|
|
170
|
+
*/
|
|
171
|
+
onSuccess?: (entry: TEntry | undefined | null) => void;
|
|
172
|
+
/**
|
|
173
|
+
* Callback on error
|
|
174
|
+
*/
|
|
175
|
+
onError?: (error: string) => void;
|
|
176
|
+
}
|
|
177
|
+
interface UseSocketInputResult<T> {
|
|
178
|
+
/**
|
|
179
|
+
* Current local value (may differ from server while editing)
|
|
180
|
+
*/
|
|
181
|
+
value: T;
|
|
182
|
+
/**
|
|
183
|
+
* Update the local value
|
|
184
|
+
*/
|
|
185
|
+
onLocalChange: (newValue: T) => void;
|
|
186
|
+
/**
|
|
187
|
+
* Manually commit the current value to the server
|
|
188
|
+
*/
|
|
189
|
+
commit: () => void;
|
|
190
|
+
/**
|
|
191
|
+
* Handle blur event (commits if commitMode is "blur")
|
|
192
|
+
*/
|
|
193
|
+
onBlur: () => void;
|
|
194
|
+
/**
|
|
195
|
+
* Whether an update is currently in flight
|
|
196
|
+
*/
|
|
197
|
+
inFlight: boolean;
|
|
198
|
+
/**
|
|
199
|
+
* Whether the local value differs from the server value
|
|
200
|
+
*/
|
|
201
|
+
isDirty: boolean;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Hook for managing input state that syncs with the server.
|
|
205
|
+
* Handles optimistic updates, debouncing, and conflict resolution.
|
|
206
|
+
*/
|
|
207
|
+
declare function useSocketInput<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate>(options: UseSocketInputOptions<TEntry, TAllowedUpdate, TKey>): UseSocketInputResult<unknown>;
|
|
208
|
+
|
|
209
|
+
interface SocketCheckboxProps<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "checked" | "onChange" | "type" | "onError" | "property"> {
|
|
210
|
+
state: TEntry | null;
|
|
211
|
+
update: (patch: Partial<TAllowedUpdate>) => Promise<TEntry | undefined | null>;
|
|
212
|
+
property: TKey;
|
|
213
|
+
commitMode?: CommitMode;
|
|
214
|
+
debounceMs?: number;
|
|
215
|
+
format?: (v: unknown) => unknown;
|
|
216
|
+
parse?: (v: unknown) => unknown;
|
|
217
|
+
onSuccess?: (entry: TEntry | undefined | null) => void;
|
|
218
|
+
onError?: (error: string) => void;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* A checkbox input that syncs its checked state with the server via socket.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```tsx
|
|
225
|
+
* <SocketCheckbox
|
|
226
|
+
* state={chat}
|
|
227
|
+
* update={(patch) => updateChat.mutateAsync({ id: chat.id, ...patch })}
|
|
228
|
+
* property="isArchived"
|
|
229
|
+
* commitMode="change"
|
|
230
|
+
* />
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
declare function SocketCheckbox<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate>(props: SocketCheckboxProps<TEntry, TAllowedUpdate, TKey>): React.ReactElement;
|
|
234
|
+
|
|
235
|
+
interface SocketTextFieldProps<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "value" | "onChange" | "onError" | "property"> {
|
|
236
|
+
state: TEntry | null;
|
|
237
|
+
update: (patch: Partial<TAllowedUpdate>) => Promise<TEntry | undefined | null>;
|
|
238
|
+
property: TKey;
|
|
239
|
+
commitMode?: CommitMode;
|
|
240
|
+
debounceMs?: number;
|
|
241
|
+
format?: (v: unknown) => unknown;
|
|
242
|
+
parse?: (v: unknown) => unknown;
|
|
243
|
+
onSuccess?: (entry: TEntry | undefined | null) => void;
|
|
244
|
+
onError?: (error: string) => void;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* A text input that syncs its value with the server via socket.
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```tsx
|
|
251
|
+
* <SocketTextField
|
|
252
|
+
* state={chat}
|
|
253
|
+
* update={(patch) => updateChat.mutateAsync({ id: chat.id, ...patch })}
|
|
254
|
+
* property="title"
|
|
255
|
+
* commitMode="debounce"
|
|
256
|
+
* debounceMs={500}
|
|
257
|
+
* placeholder="Chat title..."
|
|
258
|
+
* />
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
declare function SocketTextField<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate>(props: SocketTextFieldProps<TEntry, TAllowedUpdate, TKey>): React.ReactElement;
|
|
262
|
+
|
|
263
|
+
interface SocketSelectProps<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate> extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "value" | "onChange" | "onError" | "property"> {
|
|
264
|
+
state: TEntry | null;
|
|
265
|
+
update: (patch: Partial<TAllowedUpdate>) => Promise<TEntry | undefined | null>;
|
|
266
|
+
property: TKey;
|
|
267
|
+
children: React.ReactNode;
|
|
268
|
+
commitMode?: CommitMode;
|
|
269
|
+
debounceMs?: number;
|
|
270
|
+
format?: (v: unknown) => unknown;
|
|
271
|
+
parse?: (v: unknown) => unknown;
|
|
272
|
+
onSuccess?: (entry: TEntry | undefined | null) => void;
|
|
273
|
+
onError?: (error: string) => void;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* A select input that syncs its value with the server via socket.
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* ```tsx
|
|
280
|
+
* <SocketSelect
|
|
281
|
+
* state={chat}
|
|
282
|
+
* update={(patch) => updateChat.mutateAsync({ id: chat.id, ...patch })}
|
|
283
|
+
* property="status"
|
|
284
|
+
* commitMode="change"
|
|
285
|
+
* >
|
|
286
|
+
* <option value="active">Active</option>
|
|
287
|
+
* <option value="archived">Archived</option>
|
|
288
|
+
* </SocketSelect>
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
declare function SocketSelect<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate>(props: SocketSelectProps<TEntry, TAllowedUpdate, TKey>): React.ReactElement;
|
|
292
|
+
|
|
293
|
+
interface SocketSliderProps<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "value" | "onChange" | "type" | "onError" | "property"> {
|
|
294
|
+
state: TEntry | null;
|
|
295
|
+
update: (patch: Partial<TAllowedUpdate>) => Promise<TEntry | undefined | null>;
|
|
296
|
+
property: TKey;
|
|
297
|
+
commitMode?: CommitMode;
|
|
298
|
+
debounceMs?: number;
|
|
299
|
+
format?: (v: unknown) => unknown;
|
|
300
|
+
parse?: (v: unknown) => unknown;
|
|
301
|
+
onSuccess?: (entry: TEntry | undefined | null) => void;
|
|
302
|
+
onError?: (error: string) => void;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* A range slider input that syncs its value with the server via socket.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```tsx
|
|
309
|
+
* <SocketSlider
|
|
310
|
+
* state={settings}
|
|
311
|
+
* update={(patch) => updateSettings.mutateAsync({ id: settings.id, ...patch })}
|
|
312
|
+
* property="volume"
|
|
313
|
+
* min={0}
|
|
314
|
+
* max={100}
|
|
315
|
+
* commitMode="debounce"
|
|
316
|
+
* debounceMs={200}
|
|
317
|
+
* />
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
declare function SocketSlider<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate>(props: SocketSliderProps<TEntry, TAllowedUpdate, TKey>): React.ReactElement;
|
|
321
|
+
|
|
322
|
+
interface SocketSwitchProps<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "checked" | "onChange" | "type" | "onError" | "property"> {
|
|
323
|
+
state: TEntry | null;
|
|
324
|
+
update: (patch: Partial<TAllowedUpdate>) => Promise<TEntry | undefined | null>;
|
|
325
|
+
property: TKey;
|
|
326
|
+
commitMode?: CommitMode;
|
|
327
|
+
debounceMs?: number;
|
|
328
|
+
format?: (v: unknown) => unknown;
|
|
329
|
+
parse?: (v: unknown) => unknown;
|
|
330
|
+
onSuccess?: (entry: TEntry | undefined | null) => void;
|
|
331
|
+
onError?: (error: string) => void;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* A switch/toggle input that syncs its state with the server via socket.
|
|
335
|
+
* Styled as a switch by default using CSS.
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```tsx
|
|
339
|
+
* <SocketSwitch
|
|
340
|
+
* state={settings}
|
|
341
|
+
* update={(patch) => updateSettings.mutateAsync({ id: settings.id, ...patch })}
|
|
342
|
+
* property="darkMode"
|
|
343
|
+
* commitMode="change"
|
|
344
|
+
* />
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
declare function SocketSwitch<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate>(props: SocketSwitchProps<TEntry, TAllowedUpdate, TKey>): React.ReactElement;
|
|
348
|
+
|
|
349
|
+
export { type CommitMode, QuickdrawProvider, QuickdrawProviderProps, QuickdrawSocketContextValue, SocketCheckbox, type SocketCheckboxProps, SocketSelect, type SocketSelectProps, SocketSlider, type SocketSliderProps, SocketSwitch, type SocketSwitchProps, SocketTextField, type SocketTextFieldProps, UseServiceOptions, UseServiceResult, type UseSocketInputOptions, type UseSocketInputResult, UseSubscriptionOptions, UseSubscriptionResult, useQuickdrawSocket, useService, useServiceMethod, useSocketInput, useSubscription };
|