@fitzzero/quickdraw-core 3.6.0 → 3.7.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 +113 -64
- package/dist/client/index.js +32 -61
- package/dist/client/index.js.map +1 -1
- package/dist/client/testing.js.map +1 -1
- package/dist/server/express/index.d.ts +37 -0
- package/dist/server/express/index.js +60 -0
- package/dist/server/express/index.js.map +1 -0
- package/dist/server/index.d.ts +227 -1
- package/dist/server/index.js +400 -142
- package/dist/server/index.js.map +1 -1
- package/dist/server/testing-prisma.d.ts +101 -0
- package/dist/server/testing-prisma.js +172 -0
- package/dist/server/testing-prisma.js.map +1 -0
- package/dist/server/testing.js +5 -22
- package/dist/server/testing.js.map +1 -1
- package/dist/shared/index.js.map +1 -1
- package/eslint-config/index.mjs +1 -2
- package/eslint-plugin-quickdraw/index.mjs +15 -7
- package/eslint-plugin-quickdraw/rules/no-cross-service-mutations.mjs +31 -32
- package/eslint-plugin-quickdraw/rules/no-direct-prisma-mutations.mjs +17 -18
- package/eslint-plugin-quickdraw/rules/no-raw-button-strings.mjs +52 -0
- package/eslint-plugin-quickdraw/rules/no-raw-service-room-string.mjs +140 -0
- package/eslint-plugin-quickdraw/rules/no-raw-socket-emit.mjs +20 -22
- package/eslint-plugin-quickdraw/rules/no-raw-socket-on.mjs +17 -17
- package/eslint-plugin-quickdraw/rules/no-raw-tooltip-strings.mjs +59 -0
- package/eslint-plugin-quickdraw/rules/no-raw-typography-strings.mjs +52 -0
- package/eslint-plugin-quickdraw/rules/no-service-method-record.mjs +3 -3
- package/eslint-plugin-quickdraw/rules/no-unsafe-payload-cast.mjs +8 -8
- package/eslint-plugin-quickdraw/rules/require-zod-schema.mjs +15 -20
- package/package.json +74 -50
package/README.md
CHANGED
|
@@ -21,31 +21,36 @@ pnpm add @fitzzero/quickdraw-core
|
|
|
21
21
|
### Server Setup
|
|
22
22
|
|
|
23
23
|
```typescript
|
|
24
|
-
import { createQuickdrawServer, BaseService } from
|
|
25
|
-
import { PrismaClient } from
|
|
24
|
+
import { createQuickdrawServer, BaseService } from "@fitzzero/quickdraw-core/server";
|
|
25
|
+
import { PrismaClient } from "@prisma/client";
|
|
26
26
|
|
|
27
27
|
const prisma = new PrismaClient();
|
|
28
28
|
|
|
29
29
|
// Define your service
|
|
30
|
-
class ChatService extends BaseService<
|
|
30
|
+
class ChatService extends BaseService<
|
|
31
|
+
Chat,
|
|
32
|
+
Prisma.ChatCreateInput,
|
|
33
|
+
Prisma.ChatUpdateInput,
|
|
34
|
+
ChatServiceMethods
|
|
35
|
+
> {
|
|
31
36
|
constructor() {
|
|
32
|
-
super({ serviceName:
|
|
37
|
+
super({ serviceName: "chatService", hasEntryACL: true });
|
|
33
38
|
this.setDelegate(prisma.chat);
|
|
34
|
-
|
|
39
|
+
|
|
35
40
|
// Define public methods
|
|
36
|
-
this.createChat = this.defineMethod(
|
|
41
|
+
this.createChat = this.defineMethod("createChat", "Read", async (payload, ctx) => {
|
|
37
42
|
const chat = await this.create({ title: payload.title, ownerId: ctx.userId });
|
|
38
43
|
return { id: chat.id };
|
|
39
44
|
});
|
|
40
45
|
}
|
|
41
|
-
|
|
42
|
-
createChat: ReturnType<typeof this.defineMethod<
|
|
46
|
+
|
|
47
|
+
createChat: ReturnType<typeof this.defineMethod<"createChat">>;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
// Start server
|
|
46
51
|
const { io, httpServer } = createQuickdrawServer({
|
|
47
52
|
port: 4000,
|
|
48
|
-
cors: { origin:
|
|
53
|
+
cors: { origin: "http://localhost:3000" },
|
|
49
54
|
services: {
|
|
50
55
|
chatService: new ChatService(),
|
|
51
56
|
},
|
|
@@ -62,45 +67,42 @@ const { io, httpServer } = createQuickdrawServer({
|
|
|
62
67
|
|
|
63
68
|
```tsx
|
|
64
69
|
// app/layout.tsx
|
|
65
|
-
import { QuickdrawProvider } from
|
|
70
|
+
import { QuickdrawProvider } from "@fitzzero/quickdraw-core/client";
|
|
66
71
|
|
|
67
72
|
export default function RootLayout({ children }) {
|
|
68
73
|
return (
|
|
69
|
-
<QuickdrawProvider
|
|
70
|
-
serverUrl="http://localhost:4000"
|
|
71
|
-
authToken={getAuthToken()}
|
|
72
|
-
>
|
|
74
|
+
<QuickdrawProvider serverUrl="http://localhost:4000" authToken={getAuthToken()}>
|
|
73
75
|
{children}
|
|
74
76
|
</QuickdrawProvider>
|
|
75
77
|
);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
// app/chat/page.tsx
|
|
79
|
-
import { useService, useSubscription, useRoomEvents } from
|
|
81
|
+
import { useService, useSubscription, useRoomEvents } from "@fitzzero/quickdraw-core/client";
|
|
80
82
|
|
|
81
83
|
function ChatPage({ chatId }: { chatId: string }) {
|
|
82
84
|
// Subscribe to real-time entity updates
|
|
83
|
-
const { data: chat, isLoading } = useSubscription(
|
|
84
|
-
|
|
85
|
+
const { data: chat, isLoading } = useSubscription("chatService", chatId);
|
|
86
|
+
|
|
85
87
|
// Mutation hook
|
|
86
|
-
const updateTitle = useService(
|
|
87
|
-
onSuccess: () => console.log(
|
|
88
|
+
const updateTitle = useService("chatService", "updateTitle", {
|
|
89
|
+
onSuccess: () => console.log("Title updated!"),
|
|
88
90
|
});
|
|
89
|
-
|
|
91
|
+
|
|
90
92
|
// Listen for custom events broadcast to the chat room
|
|
91
93
|
const [typing, setTyping] = useState(false);
|
|
92
94
|
useRoomEvents({
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
"chat:message": (msg) => appendMessage(msg),
|
|
96
|
+
agent_typing_start: () => setTyping(true),
|
|
97
|
+
agent_typing_stop: () => setTyping(false),
|
|
96
98
|
});
|
|
97
|
-
|
|
99
|
+
|
|
98
100
|
if (isLoading) return <div>Loading...</div>;
|
|
99
|
-
|
|
101
|
+
|
|
100
102
|
return (
|
|
101
103
|
<div>
|
|
102
104
|
<h1>{chat?.title}</h1>
|
|
103
|
-
<button onClick={() => updateTitle.mutate({ id: chatId, title:
|
|
105
|
+
<button onClick={() => updateTitle.mutate({ id: chatId, title: "New Title" })}>
|
|
104
106
|
Update Title
|
|
105
107
|
</button>
|
|
106
108
|
</div>
|
|
@@ -111,7 +113,7 @@ function ChatPage({ chatId }: { chatId: string }) {
|
|
|
111
113
|
### Socket Inputs
|
|
112
114
|
|
|
113
115
|
```tsx
|
|
114
|
-
import { SocketTextField } from
|
|
116
|
+
import { SocketTextField } from "@fitzzero/quickdraw-core/client";
|
|
115
117
|
|
|
116
118
|
function ChatTitleEditor({ chat, updateChat }) {
|
|
117
119
|
return (
|
|
@@ -132,16 +134,16 @@ function ChatTitleEditor({ chat, updateChat }) {
|
|
|
132
134
|
When your server broadcasts custom events to subscription rooms via `emitToRoom`, use `useRoomEvents` on the client to listen with proper lifecycle management:
|
|
133
135
|
|
|
134
136
|
```tsx
|
|
135
|
-
import { useSubscription, useRoomEvents } from
|
|
137
|
+
import { useSubscription, useRoomEvents } from "@fitzzero/quickdraw-core/client";
|
|
136
138
|
|
|
137
139
|
function ProjectBoard({ projectId }: { projectId: string }) {
|
|
138
|
-
const { data: project } = useSubscription(
|
|
140
|
+
const { data: project } = useSubscription("projectService", projectId);
|
|
139
141
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
140
142
|
|
|
141
143
|
// Lifecycle-managed event listeners — cleanup handled automatically
|
|
142
144
|
useRoomEvents({
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
"task:created": (task: Task) => setTasks((prev) => [...prev, task]),
|
|
146
|
+
"task:deleted": ({ id }: { id: string }) => setTasks((prev) => prev.filter((t) => t.id !== id)),
|
|
145
147
|
});
|
|
146
148
|
|
|
147
149
|
return <Board tasks={tasks} />;
|
|
@@ -153,15 +155,26 @@ function ProjectBoard({ projectId }: { projectId: string }) {
|
|
|
153
155
|
For list queries that should refresh when related events fire, use `invalidateOn`:
|
|
154
156
|
|
|
155
157
|
```tsx
|
|
156
|
-
import { useServiceQuery } from
|
|
158
|
+
import { useServiceQuery } from "@fitzzero/quickdraw-core/client";
|
|
157
159
|
|
|
158
160
|
function TaskList({ projectId }: { projectId: string }) {
|
|
159
161
|
// Auto-refetches when tasks are created, deleted, or change status
|
|
160
|
-
const { data: tasks } = useServiceQuery(
|
|
161
|
-
|
|
162
|
-
|
|
162
|
+
const { data: tasks } = useServiceQuery(
|
|
163
|
+
"taskService",
|
|
164
|
+
"listTasks",
|
|
165
|
+
{ projectId },
|
|
166
|
+
{
|
|
167
|
+
invalidateOn: ["task:created", "task:deleted", "task:statusUpdate"],
|
|
168
|
+
},
|
|
169
|
+
);
|
|
163
170
|
|
|
164
|
-
return
|
|
171
|
+
return (
|
|
172
|
+
<ul>
|
|
173
|
+
{tasks?.map((t) => (
|
|
174
|
+
<li key={t.id}>{t.title}</li>
|
|
175
|
+
))}
|
|
176
|
+
</ul>
|
|
177
|
+
);
|
|
165
178
|
}
|
|
166
179
|
```
|
|
167
180
|
|
|
@@ -171,21 +184,49 @@ Rapid-fire events within 100ms are debounced into a single refetch.
|
|
|
171
184
|
|
|
172
185
|
```typescript
|
|
173
186
|
// Shared types (both server and client)
|
|
174
|
-
import { ServiceResponse, AccessLevel, ServiceMethodMap } from
|
|
187
|
+
import { ServiceResponse, AccessLevel, ServiceMethodMap } from "@fitzzero/quickdraw-core";
|
|
175
188
|
|
|
176
189
|
// Server
|
|
177
|
-
import {
|
|
178
|
-
BaseService,
|
|
179
|
-
ServiceRegistry,
|
|
190
|
+
import {
|
|
191
|
+
BaseService,
|
|
192
|
+
ServiceRegistry,
|
|
180
193
|
createQuickdrawServer,
|
|
181
194
|
createJWT,
|
|
182
195
|
verifyJWT,
|
|
183
196
|
discordProvider,
|
|
184
197
|
googleProvider,
|
|
185
|
-
|
|
198
|
+
// Auth & security (3.7+)
|
|
199
|
+
createMockOAuthProvider,
|
|
200
|
+
registerMockOAuthProvider,
|
|
201
|
+
isMockOAuthEnabled,
|
|
202
|
+
validateRedirectOrigin,
|
|
203
|
+
setSessionCookie,
|
|
204
|
+
clearSessionCookie,
|
|
205
|
+
createRequireAuth,
|
|
206
|
+
encrypt,
|
|
207
|
+
decrypt,
|
|
208
|
+
} from "@fitzzero/quickdraw-core/server";
|
|
209
|
+
|
|
210
|
+
// Express rate-limit presets (3.7+, requires the optional express-rate-limit peer)
|
|
211
|
+
import {
|
|
212
|
+
createAuthLimiter,
|
|
213
|
+
createWebhookLimiter,
|
|
214
|
+
createPublicApiLimiter,
|
|
215
|
+
} from "@fitzzero/quickdraw-core/server/express";
|
|
186
216
|
|
|
187
217
|
// Server testing
|
|
188
|
-
import {
|
|
218
|
+
import {
|
|
219
|
+
createTestServer,
|
|
220
|
+
connectAsUser,
|
|
221
|
+
emitWithAck,
|
|
222
|
+
} from "@fitzzero/quickdraw-core/server/testing";
|
|
223
|
+
|
|
224
|
+
// Dual-mode Prisma test databases (3.7+, optional peers: @electric-sql/pglite, pg)
|
|
225
|
+
import {
|
|
226
|
+
createPrismaTestGlobalSetup,
|
|
227
|
+
resetDatabase,
|
|
228
|
+
workerDatabaseUrl,
|
|
229
|
+
} from "@fitzzero/quickdraw-core/server/testing/prisma";
|
|
189
230
|
|
|
190
231
|
// Client
|
|
191
232
|
import {
|
|
@@ -200,10 +241,10 @@ import {
|
|
|
200
241
|
SocketSelect,
|
|
201
242
|
SocketSlider,
|
|
202
243
|
SocketSwitch,
|
|
203
|
-
} from
|
|
244
|
+
} from "@fitzzero/quickdraw-core/client";
|
|
204
245
|
|
|
205
246
|
// Client testing
|
|
206
|
-
import { createMockSocket, createTestWrapper } from
|
|
247
|
+
import { createMockSocket, createTestWrapper } from "@fitzzero/quickdraw-core/client/testing";
|
|
207
248
|
```
|
|
208
249
|
|
|
209
250
|
## Local Development
|
|
@@ -226,7 +267,7 @@ Define your service methods in a shared types file:
|
|
|
226
267
|
|
|
227
268
|
```typescript
|
|
228
269
|
// shared/types.ts
|
|
229
|
-
import type { ServiceMethodMap } from
|
|
270
|
+
import type { ServiceMethodMap } from "@fitzzero/quickdraw-core";
|
|
230
271
|
|
|
231
272
|
export type ChatServiceMethods = ServiceMethodMap<{
|
|
232
273
|
createChat: {
|
|
@@ -238,7 +279,7 @@ export type ChatServiceMethods = ServiceMethodMap<{
|
|
|
238
279
|
response: { id: string; title: string };
|
|
239
280
|
};
|
|
240
281
|
inviteUser: {
|
|
241
|
-
payload: { id: string; userId: string; level:
|
|
282
|
+
payload: { id: string; userId: string; level: "Read" | "Moderate" | "Admin" };
|
|
242
283
|
response: { id: string };
|
|
243
284
|
};
|
|
244
285
|
}>;
|
|
@@ -309,6 +350,7 @@ Per-entity permissions. Quickdraw supports two patterns:
|
|
|
309
350
|
#### Pattern 1: JSON ACL (Simple)
|
|
310
351
|
|
|
311
352
|
Store ACL directly on the entity. Best for:
|
|
353
|
+
|
|
312
354
|
- Simple ownership models (owner + collaborators)
|
|
313
355
|
- When you don't need to query "all entities user X can access" efficiently
|
|
314
356
|
- Minimal schema complexity
|
|
@@ -338,6 +380,7 @@ class DocumentService extends BaseService<Document, ...> {
|
|
|
338
380
|
#### Pattern 2: Membership Table (Complex)
|
|
339
381
|
|
|
340
382
|
Separate table for memberships. Best for:
|
|
383
|
+
|
|
341
384
|
- Querying "all entities user X can access" efficiently
|
|
342
385
|
- Complex role hierarchies
|
|
343
386
|
- Additional membership metadata (join date, invited by, etc.)
|
|
@@ -353,7 +396,7 @@ model ChatMember {
|
|
|
353
396
|
chatId String
|
|
354
397
|
userId String
|
|
355
398
|
level String // "Read" | "Moderate" | "Admin"
|
|
356
|
-
|
|
399
|
+
|
|
357
400
|
@@unique([chatId, userId])
|
|
358
401
|
}
|
|
359
402
|
|
|
@@ -384,35 +427,41 @@ When a method is called, `ensureAccessForMethod` checks in this order:
|
|
|
384
427
|
|
|
385
428
|
### Access Levels
|
|
386
429
|
|
|
387
|
-
| Level
|
|
388
|
-
|
|
389
|
-
| Public
|
|
390
|
-
| Read
|
|
391
|
-
| Moderate | 2
|
|
392
|
-
| Admin
|
|
430
|
+
| Level | Value | Typical Use |
|
|
431
|
+
| -------- | ----- | -------------------------------- |
|
|
432
|
+
| Public | 0 | No authentication required |
|
|
433
|
+
| Read | 1 | View data, subscribe to updates |
|
|
434
|
+
| Moderate | 2 | Edit content, manage members |
|
|
435
|
+
| Admin | 3 | Delete, manage ACL, full control |
|
|
393
436
|
|
|
394
437
|
## Testing
|
|
395
438
|
|
|
396
439
|
### Server Integration Tests
|
|
397
440
|
|
|
398
441
|
```typescript
|
|
399
|
-
import {
|
|
442
|
+
import {
|
|
443
|
+
createTestServer,
|
|
444
|
+
connectAsUser,
|
|
445
|
+
emitWithAck,
|
|
446
|
+
} from "@fitzzero/quickdraw-core/server/testing";
|
|
400
447
|
|
|
401
|
-
describe(
|
|
448
|
+
describe("ChatService", () => {
|
|
402
449
|
let server;
|
|
403
|
-
|
|
450
|
+
|
|
404
451
|
beforeAll(async () => {
|
|
405
452
|
server = await createTestServer({
|
|
406
453
|
services: { chatService: new ChatService() },
|
|
407
|
-
seedDb: async () => {
|
|
454
|
+
seedDb: async () => {
|
|
455
|
+
/* seed test data */
|
|
456
|
+
},
|
|
408
457
|
});
|
|
409
458
|
});
|
|
410
|
-
|
|
459
|
+
|
|
411
460
|
afterAll(() => server.stop());
|
|
412
|
-
|
|
413
|
-
it(
|
|
414
|
-
const client = await server.connectAs(
|
|
415
|
-
const chat = await client.emit(
|
|
461
|
+
|
|
462
|
+
it("creates chat", async () => {
|
|
463
|
+
const client = await server.connectAs("user-id");
|
|
464
|
+
const chat = await client.emit("chatService:createChat", { title: "Test" });
|
|
416
465
|
expect(chat.id).toBeDefined();
|
|
417
466
|
client.close();
|
|
418
467
|
});
|
|
@@ -427,10 +476,10 @@ import { createTestWrapper, createMockSocket, mockSuccessEmit } from '@fitzzero/
|
|
|
427
476
|
test('renders chat', () => {
|
|
428
477
|
const mockSocket = createMockSocket();
|
|
429
478
|
mockSocket.emit.mockImplementation(mockSuccessEmit({ title: 'Test Chat' }));
|
|
430
|
-
|
|
479
|
+
|
|
431
480
|
const wrapper = createTestWrapper({ socketContext: { socket: mockSocket } });
|
|
432
481
|
render(<ChatView chatId="123" />, { wrapper });
|
|
433
|
-
|
|
482
|
+
|
|
434
483
|
expect(screen.getByText('Test Chat')).toBeInTheDocument();
|
|
435
484
|
});
|
|
436
485
|
```
|
package/dist/client/index.js
CHANGED
|
@@ -115,9 +115,7 @@ var QuickdrawSocketContext = React.createContext(null);
|
|
|
115
115
|
function useQuickdrawSocket() {
|
|
116
116
|
const context = React.useContext(QuickdrawSocketContext);
|
|
117
117
|
if (!context) {
|
|
118
|
-
throw new Error(
|
|
119
|
-
"useQuickdrawSocket must be used within a QuickdrawProvider"
|
|
120
|
-
);
|
|
118
|
+
throw new Error("useQuickdrawSocket must be used within a QuickdrawProvider");
|
|
121
119
|
}
|
|
122
120
|
return context;
|
|
123
121
|
}
|
|
@@ -149,9 +147,7 @@ function QuickdrawProvider({
|
|
|
149
147
|
const [isConnected, setIsConnected] = React.useState(false);
|
|
150
148
|
const [userId, setUserId] = React.useState(null);
|
|
151
149
|
const [serviceAccess, setServiceAccess] = React.useState({});
|
|
152
|
-
const subscriptionRegistryRef = React.useRef(
|
|
153
|
-
createSubscriptionRegistry()
|
|
154
|
-
);
|
|
150
|
+
const subscriptionRegistryRef = React.useRef(createSubscriptionRegistry());
|
|
155
151
|
const prevAuthTokenRef = React.useRef(authToken);
|
|
156
152
|
const socketRef = React.useRef(null);
|
|
157
153
|
const subscriptionBatcherRef = React.useRef(
|
|
@@ -259,18 +255,14 @@ function useService(serviceName, methodName, options) {
|
|
|
259
255
|
const timeout = setTimeout(() => {
|
|
260
256
|
reject(new Error("Request timeout"));
|
|
261
257
|
}, options?.timeout ?? 1e4);
|
|
262
|
-
socket.emit(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
resolve(response.data);
|
|
269
|
-
} else {
|
|
270
|
-
reject(new Error(response.error));
|
|
271
|
-
}
|
|
258
|
+
socket.emit(eventName, payload, (response) => {
|
|
259
|
+
clearTimeout(timeout);
|
|
260
|
+
if (response.success) {
|
|
261
|
+
resolve(response.data);
|
|
262
|
+
} else {
|
|
263
|
+
reject(new Error(response.error));
|
|
272
264
|
}
|
|
273
|
-
);
|
|
265
|
+
});
|
|
274
266
|
});
|
|
275
267
|
},
|
|
276
268
|
onSuccess: (data) => {
|
|
@@ -334,24 +326,20 @@ function useServiceMethod(serviceName, methodName, options) {
|
|
|
334
326
|
optionsRef.current?.onError?.(errorMsg);
|
|
335
327
|
resolve(null);
|
|
336
328
|
}, options?.timeout ?? 1e4);
|
|
337
|
-
socket.emit(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
(response)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
setError(response.error);
|
|
350
|
-
optionsRef.current?.onError?.(response.error);
|
|
351
|
-
resolve(null);
|
|
352
|
-
}
|
|
329
|
+
socket.emit(eventName, payload, (response) => {
|
|
330
|
+
clearTimeout(timeout);
|
|
331
|
+
setLoading(false);
|
|
332
|
+
if (response.success) {
|
|
333
|
+
setError(null);
|
|
334
|
+
setData(response.data);
|
|
335
|
+
optionsRef.current?.onSuccess?.(response.data);
|
|
336
|
+
resolve(response.data);
|
|
337
|
+
} else {
|
|
338
|
+
setError(response.error);
|
|
339
|
+
optionsRef.current?.onError?.(response.error);
|
|
340
|
+
resolve(null);
|
|
353
341
|
}
|
|
354
|
-
);
|
|
342
|
+
});
|
|
355
343
|
});
|
|
356
344
|
},
|
|
357
345
|
[socket, isConnected, serviceName, methodName, options?.timeout]
|
|
@@ -402,18 +390,14 @@ function useServiceQuery(serviceName, methodName, payload, options) {
|
|
|
402
390
|
const timeoutId = setTimeout(() => {
|
|
403
391
|
reject(new Error("Request timeout"));
|
|
404
392
|
}, timeout);
|
|
405
|
-
socket.emit(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
resolve(response.data);
|
|
412
|
-
} else {
|
|
413
|
-
reject(new Error(response.error));
|
|
414
|
-
}
|
|
393
|
+
socket.emit(eventName, payload, (response) => {
|
|
394
|
+
clearTimeout(timeoutId);
|
|
395
|
+
if (response.success) {
|
|
396
|
+
resolve(response.data);
|
|
397
|
+
} else {
|
|
398
|
+
reject(new Error(response.error));
|
|
415
399
|
}
|
|
416
|
-
);
|
|
400
|
+
});
|
|
417
401
|
});
|
|
418
402
|
}, [socket, isConnected, serviceName, methodName, payload, timeout]);
|
|
419
403
|
React.useEffect(() => {
|
|
@@ -564,10 +548,7 @@ function useSubscription(serviceName, entryId, options = {}) {
|
|
|
564
548
|
socket.on(updateEvent, handleUpdate);
|
|
565
549
|
const handleBatchResponse = (response) => {
|
|
566
550
|
if (response.success && response.data) {
|
|
567
|
-
queryClient.setQueryData(
|
|
568
|
-
queryKey,
|
|
569
|
-
response.data
|
|
570
|
-
);
|
|
551
|
+
queryClient.setQueryData(queryKey, response.data);
|
|
571
552
|
setIsSubscribed(true);
|
|
572
553
|
setError(null);
|
|
573
554
|
onDataRef.current?.(response.data);
|
|
@@ -577,24 +558,14 @@ function useSubscription(serviceName, entryId, options = {}) {
|
|
|
577
558
|
onErrorRef.current?.(response.error ?? "Subscription failed");
|
|
578
559
|
}
|
|
579
560
|
};
|
|
580
|
-
subscriptionBatcher.enqueue(
|
|
581
|
-
serviceName,
|
|
582
|
-
entryId,
|
|
583
|
-
requiredLevel,
|
|
584
|
-
handleBatchResponse
|
|
585
|
-
);
|
|
561
|
+
subscriptionBatcher.enqueue(serviceName, entryId, requiredLevel, handleBatchResponse);
|
|
586
562
|
let visibilityDebounceTimer = null;
|
|
587
563
|
const handleVisibilityChange = () => {
|
|
588
564
|
if (document.visibilityState !== "visible") return;
|
|
589
565
|
if (visibilityDebounceTimer) clearTimeout(visibilityDebounceTimer);
|
|
590
566
|
visibilityDebounceTimer = setTimeout(() => {
|
|
591
567
|
visibilityDebounceTimer = null;
|
|
592
|
-
subscriptionBatcher.enqueue(
|
|
593
|
-
serviceName,
|
|
594
|
-
entryId,
|
|
595
|
-
requiredLevel,
|
|
596
|
-
handleBatchResponse
|
|
597
|
-
);
|
|
568
|
+
subscriptionBatcher.enqueue(serviceName, entryId, requiredLevel, handleBatchResponse);
|
|
598
569
|
}, 200);
|
|
599
570
|
};
|
|
600
571
|
if (refetchOnWindowFocus) {
|