@fitzzero/quickdraw-core 3.3.0 → 3.4.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 +52 -2
- package/dist/client/index.d.ts +39 -3
- package/dist/client/index.js +64 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/testing.d.ts +1 -1
- package/dist/server/index.d.ts +13 -5
- package/dist/server/index.js +9 -1
- package/dist/server/index.js.map +1 -1
- package/dist/{types-Cr8YC7Kg.d.ts → types-BJj94_XW.d.ts} +27 -1
- package/eslint-config/index.mjs +4 -0
- package/eslint-plugin-quickdraw/index.mjs +4 -0
- package/eslint-plugin-quickdraw/rules/no-raw-socket-emit.mjs +95 -0
- package/eslint-plugin-quickdraw/rules/no-raw-socket-on.mjs +79 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,10 +76,10 @@ export default function RootLayout({ children }) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// app/chat/page.tsx
|
|
79
|
-
import { useService, useSubscription } from '@fitzzero/quickdraw-core/client';
|
|
79
|
+
import { useService, useSubscription, useRoomEvents } from '@fitzzero/quickdraw-core/client';
|
|
80
80
|
|
|
81
81
|
function ChatPage({ chatId }: { chatId: string }) {
|
|
82
|
-
// Subscribe to real-time updates
|
|
82
|
+
// Subscribe to real-time entity updates
|
|
83
83
|
const { data: chat, isLoading } = useSubscription('chatService', chatId);
|
|
84
84
|
|
|
85
85
|
// Mutation hook
|
|
@@ -87,6 +87,14 @@ function ChatPage({ chatId }: { chatId: string }) {
|
|
|
87
87
|
onSuccess: () => console.log('Title updated!'),
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
// Listen for custom events broadcast to the chat room
|
|
91
|
+
const [typing, setTyping] = useState(false);
|
|
92
|
+
useRoomEvents({
|
|
93
|
+
'chat:message': (msg) => appendMessage(msg),
|
|
94
|
+
'agent_typing_start': () => setTyping(true),
|
|
95
|
+
'agent_typing_stop': () => setTyping(false),
|
|
96
|
+
});
|
|
97
|
+
|
|
90
98
|
if (isLoading) return <div>Loading...</div>;
|
|
91
99
|
|
|
92
100
|
return (
|
|
@@ -119,6 +127,46 @@ function ChatTitleEditor({ chat, updateChat }) {
|
|
|
119
127
|
}
|
|
120
128
|
```
|
|
121
129
|
|
|
130
|
+
### Custom Room Events
|
|
131
|
+
|
|
132
|
+
When your server broadcasts custom events to subscription rooms via `emitToRoom`, use `useRoomEvents` on the client to listen with proper lifecycle management:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { useSubscription, useRoomEvents } from '@fitzzero/quickdraw-core/client';
|
|
136
|
+
|
|
137
|
+
function ProjectBoard({ projectId }: { projectId: string }) {
|
|
138
|
+
const { data: project } = useSubscription('projectService', projectId);
|
|
139
|
+
const [tasks, setTasks] = useState<Task[]>([]);
|
|
140
|
+
|
|
141
|
+
// Lifecycle-managed event listeners — cleanup handled automatically
|
|
142
|
+
useRoomEvents({
|
|
143
|
+
'task:created': (task: Task) => setTasks(prev => [...prev, task]),
|
|
144
|
+
'task:deleted': ({ id }: { id: string }) => setTasks(prev => prev.filter(t => t.id !== id)),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return <Board tasks={tasks} />;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Auto-Invalidating Queries
|
|
152
|
+
|
|
153
|
+
For list queries that should refresh when related events fire, use `invalidateOn`:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
import { useServiceQuery } from '@fitzzero/quickdraw-core/client';
|
|
157
|
+
|
|
158
|
+
function TaskList({ projectId }: { projectId: string }) {
|
|
159
|
+
// Auto-refetches when tasks are created, deleted, or change status
|
|
160
|
+
const { data: tasks } = useServiceQuery('taskService', 'listTasks', { projectId }, {
|
|
161
|
+
invalidateOn: ['task:created', 'task:deleted', 'task:statusUpdate'],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return <ul>{tasks?.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Rapid-fire events within 100ms are debounced into a single refetch.
|
|
169
|
+
|
|
122
170
|
## Package Exports
|
|
123
171
|
|
|
124
172
|
```typescript
|
|
@@ -144,7 +192,9 @@ import {
|
|
|
144
192
|
QuickdrawProvider,
|
|
145
193
|
useQuickdrawSocket,
|
|
146
194
|
useService,
|
|
195
|
+
useServiceQuery,
|
|
147
196
|
useSubscription,
|
|
197
|
+
useRoomEvents,
|
|
148
198
|
SocketCheckbox,
|
|
149
199
|
SocketTextField,
|
|
150
200
|
SocketSelect,
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { a as QuickdrawProviderProps, Q as QuickdrawSocketContextValue, U as UseServiceOptions, b as UseServiceResult, c as UseServiceQueryOptions, d as UseServiceQueryResult, e as UseSubscriptionOptions, f as UseSubscriptionResult } from '../types-
|
|
3
|
-
export { C as ClientServiceMethodMap, S as SubscriptionDataMap } from '../types-
|
|
2
|
+
import { a as QuickdrawProviderProps, Q as QuickdrawSocketContextValue, U as UseServiceOptions, b as UseServiceResult, c as UseServiceQueryOptions, d as UseServiceQueryResult, e as UseSubscriptionOptions, f as UseSubscriptionResult, g as UseRoomEventsOptions } from '../types-BJj94_XW.js';
|
|
3
|
+
export { C as ClientServiceMethodMap, S as SubscriptionDataMap } from '../types-BJj94_XW.js';
|
|
4
4
|
import 'socket.io-client';
|
|
5
5
|
import '@tanstack/react-query';
|
|
6
6
|
import '../shared/index.js';
|
|
@@ -171,6 +171,42 @@ declare function useSubscription<TData extends {
|
|
|
171
171
|
id: string;
|
|
172
172
|
}>(serviceName: string, entryId: string | null, options?: UseSubscriptionOptions<TData>): UseSubscriptionResult<TData>;
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Hook for listening to custom socket events with managed lifecycle.
|
|
176
|
+
*
|
|
177
|
+
* Handles `socket.on`/`socket.off` cleanup, reconnection re-attachment,
|
|
178
|
+
* and connection-awareness. Each component gets its own listeners (no dedup needed).
|
|
179
|
+
* Room scoping is handled server-side via `emitToRoom`.
|
|
180
|
+
*
|
|
181
|
+
* Pair with `useSubscription` to ensure the socket is in the correct room:
|
|
182
|
+
* `useSubscription` manages room membership, `useRoomEvents` manages event listeners.
|
|
183
|
+
*
|
|
184
|
+
* @param handlers - Map of event names to handler functions. Event names should be
|
|
185
|
+
* stable (don't change between renders). Handler functions can change freely.
|
|
186
|
+
* @param options - Optional configuration
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```tsx
|
|
190
|
+
* function ChatView({ chatId }: { chatId: string }) {
|
|
191
|
+
* const [messages, setMessages] = useState<Message[]>([]);
|
|
192
|
+
* const [typing, setTyping] = useState(false);
|
|
193
|
+
*
|
|
194
|
+
* // Subscribe to the chat room (manages room membership)
|
|
195
|
+
* const { data: chat } = useSubscription('chatService', chatId);
|
|
196
|
+
*
|
|
197
|
+
* // Listen for custom events broadcast to the chat room
|
|
198
|
+
* useRoomEvents({
|
|
199
|
+
* 'chat:message': (msg: Message) => setMessages(prev => [...prev, msg]),
|
|
200
|
+
* 'agent_typing_start': () => setTyping(true),
|
|
201
|
+
* 'agent_typing_stop': () => setTyping(false),
|
|
202
|
+
* });
|
|
203
|
+
*
|
|
204
|
+
* return <div>{chat?.title}</div>;
|
|
205
|
+
* }
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
declare function useRoomEvents(handlers: Record<string, (data: never) => void>, options?: UseRoomEventsOptions): void;
|
|
209
|
+
|
|
174
210
|
type CommitMode = "change" | "blur" | "debounce";
|
|
175
211
|
interface UseSocketInputOptions<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate> {
|
|
176
212
|
/**
|
|
@@ -481,4 +517,4 @@ declare function buildBreadcrumbs(pathname: string, navigation: NavItem[]): Brea
|
|
|
481
517
|
*/
|
|
482
518
|
declare function routeRequiresAuth(pathname: string, navigation: NavItem[], userMenuItems?: NavItem[]): boolean;
|
|
483
519
|
|
|
484
|
-
export { type BreadcrumbItem, type CommitMode, type JWTPayload, type NavItem, QuickdrawProvider, QuickdrawProviderProps, QuickdrawSocketContextValue, SocketCheckbox, type SocketCheckboxProps, SocketSelect, type SocketSelectProps, SocketSlider, type SocketSliderProps, SocketSwitch, type SocketSwitchProps, SocketTextField, type SocketTextFieldProps, UseServiceOptions, UseServiceQueryOptions, UseServiceQueryResult, UseServiceResult, type UseSocketInputOptions, type UseSocketInputResult, UseSubscriptionOptions, UseSubscriptionResult, buildBreadcrumbs, clearAuthToken, findNavItemByHref, findParentNavItem, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, getAuthToken, getOAuthUrl, logout, logoutAllDevices, parseJWTPayload, routeRequiresAuth, setAuthToken, truncate, useQuickdrawSocket, useService, useServiceMethod, useServiceQuery, useSocketInput, useSubscription };
|
|
520
|
+
export { type BreadcrumbItem, type CommitMode, type JWTPayload, type NavItem, QuickdrawProvider, QuickdrawProviderProps, QuickdrawSocketContextValue, SocketCheckbox, type SocketCheckboxProps, SocketSelect, type SocketSelectProps, SocketSlider, type SocketSliderProps, SocketSwitch, type SocketSwitchProps, SocketTextField, type SocketTextFieldProps, UseRoomEventsOptions, UseServiceOptions, UseServiceQueryOptions, UseServiceQueryResult, UseServiceResult, type UseSocketInputOptions, type UseSocketInputResult, UseSubscriptionOptions, UseSubscriptionResult, buildBreadcrumbs, clearAuthToken, findNavItemByHref, findParentNavItem, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, getAuthToken, getOAuthUrl, logout, logoutAllDevices, parseJWTPayload, routeRequiresAuth, setAuthToken, truncate, useQuickdrawSocket, useRoomEvents, useService, useServiceMethod, useServiceQuery, useSocketInput, useSubscription };
|
package/dist/client/index.js
CHANGED
|
@@ -386,7 +386,8 @@ function useServiceQuery(serviceName, methodName, payload, options) {
|
|
|
386
386
|
skipCache = false,
|
|
387
387
|
timeout = 1e4,
|
|
388
388
|
retry = 1,
|
|
389
|
-
retryDelay
|
|
389
|
+
retryDelay,
|
|
390
|
+
invalidateOn
|
|
390
391
|
} = options ?? {};
|
|
391
392
|
const queryKey = React.useMemo(
|
|
392
393
|
() => [serviceName, methodName, JSON.stringify(payload)],
|
|
@@ -420,6 +421,34 @@ function useServiceQuery(serviceName, methodName, payload, options) {
|
|
|
420
421
|
queryClient.invalidateQueries({ queryKey });
|
|
421
422
|
}
|
|
422
423
|
}, [skipCache, enabled, queryClient, queryKey]);
|
|
424
|
+
const invalidateOnKey = invalidateOn?.join("\0");
|
|
425
|
+
const stableInvalidateOn = React.useMemo(
|
|
426
|
+
() => invalidateOn,
|
|
427
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
428
|
+
[invalidateOnKey]
|
|
429
|
+
);
|
|
430
|
+
React.useEffect(() => {
|
|
431
|
+
if (!socket || !isConnected || !stableInvalidateOn?.length || !enabled) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
let debounceTimer = null;
|
|
435
|
+
const handleInvalidate = () => {
|
|
436
|
+
if (debounceTimer) return;
|
|
437
|
+
debounceTimer = setTimeout(() => {
|
|
438
|
+
debounceTimer = null;
|
|
439
|
+
queryClient.invalidateQueries({ queryKey });
|
|
440
|
+
}, 100);
|
|
441
|
+
};
|
|
442
|
+
for (const eventName of stableInvalidateOn) {
|
|
443
|
+
socket.on(eventName, handleInvalidate);
|
|
444
|
+
}
|
|
445
|
+
return () => {
|
|
446
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
447
|
+
for (const eventName of stableInvalidateOn) {
|
|
448
|
+
socket.off(eventName, handleInvalidate);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}, [socket, isConnected, stableInvalidateOn, enabled, queryClient, queryKey]);
|
|
423
452
|
const query = useQuery({
|
|
424
453
|
queryKey,
|
|
425
454
|
queryFn,
|
|
@@ -607,6 +636,39 @@ function useSubscription(serviceName, entryId, options = {}) {
|
|
|
607
636
|
unsubscribe
|
|
608
637
|
};
|
|
609
638
|
}
|
|
639
|
+
function useRoomEvents(handlers, options) {
|
|
640
|
+
const { socket, isConnected } = useQuickdrawSocket();
|
|
641
|
+
const { enabled = true } = options ?? {};
|
|
642
|
+
const handlersRef = React.useRef(handlers);
|
|
643
|
+
React.useEffect(() => {
|
|
644
|
+
handlersRef.current = handlers;
|
|
645
|
+
}, [handlers]);
|
|
646
|
+
const eventNames = Object.keys(handlers);
|
|
647
|
+
const eventNamesKey = eventNames.join("\0");
|
|
648
|
+
const stableEventNames = React.useMemo(
|
|
649
|
+
() => eventNames,
|
|
650
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
651
|
+
[eventNamesKey]
|
|
652
|
+
);
|
|
653
|
+
React.useEffect(() => {
|
|
654
|
+
if (!socket || !isConnected || !enabled || stableEventNames.length === 0) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const wrappers = /* @__PURE__ */ new Map();
|
|
658
|
+
for (const eventName of stableEventNames) {
|
|
659
|
+
const wrapper = (...args) => {
|
|
660
|
+
handlersRef.current[eventName]?.(...args);
|
|
661
|
+
};
|
|
662
|
+
wrappers.set(eventName, wrapper);
|
|
663
|
+
socket.on(eventName, wrapper);
|
|
664
|
+
}
|
|
665
|
+
return () => {
|
|
666
|
+
for (const [eventName, wrapper] of wrappers) {
|
|
667
|
+
socket.off(eventName, wrapper);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}, [socket, isConnected, enabled, stableEventNames]);
|
|
671
|
+
}
|
|
610
672
|
function useSocketInput(options) {
|
|
611
673
|
const {
|
|
612
674
|
state,
|
|
@@ -1137,6 +1199,6 @@ function routeRequiresAuth(pathname, navigation, userMenuItems) {
|
|
|
1137
1199
|
return false;
|
|
1138
1200
|
}
|
|
1139
1201
|
|
|
1140
|
-
export { QuickdrawProvider, SocketCheckbox, SocketSelect, SocketSlider, SocketSwitch, SocketTextField, buildBreadcrumbs, clearAuthToken, findNavItemByHref, findParentNavItem, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, getAuthToken, getOAuthUrl, logout, logoutAllDevices, parseJWTPayload, routeRequiresAuth, setAuthToken, truncate, useQuickdrawSocket, useService, useServiceMethod, useServiceQuery, useSocketInput, useSubscription };
|
|
1202
|
+
export { QuickdrawProvider, SocketCheckbox, SocketSelect, SocketSlider, SocketSwitch, SocketTextField, buildBreadcrumbs, clearAuthToken, findNavItemByHref, findParentNavItem, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, getAuthToken, getOAuthUrl, logout, logoutAllDevices, parseJWTPayload, routeRequiresAuth, setAuthToken, truncate, useQuickdrawSocket, useRoomEvents, useService, useServiceMethod, useServiceQuery, useSocketInput, useSubscription };
|
|
1141
1203
|
//# sourceMappingURL=index.js.map
|
|
1142
1204
|
//# sourceMappingURL=index.js.map
|