@fitzzero/quickdraw-core 3.6.0 → 3.8.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 +218 -64
- package/dist/client/index.d.ts +34 -4
- package/dist/client/index.js +57 -65
- package/dist/client/index.js.map +1 -1
- package/dist/client/testing.d.ts +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 +330 -5
- package/dist/server/index.js +593 -144
- 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.d.ts +1 -1
- package/dist/server/testing.js +87 -22
- package/dist/server/testing.js.map +1 -1
- package/dist/shared/index.d.ts +77 -1
- package/dist/shared/index.js +5 -1
- package/dist/shared/index.js.map +1 -1
- package/dist/{types-DniMxkQs.d.ts → types-ClgSr90l.d.ts} +5 -1
- package/dist/{types-B1z5aXdR.d.ts → types-DUHPIy_5.d.ts} +20 -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,39 +155,183 @@ 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
|
|
|
168
181
|
Rapid-fire events within 100ms are debounced into a single refetch.
|
|
169
182
|
|
|
183
|
+
### Channels (High-Frequency Traffic)
|
|
184
|
+
|
|
185
|
+
Methods are request/response: ack'd, ACL-checked against the database, and
|
|
186
|
+
counted by the global rate limiter. **Channels** are their fire-and-forget
|
|
187
|
+
counterpart for traffic where per-message overhead matters and losing a
|
|
188
|
+
message is fine — game input, cursor positions, typing indicators, telemetry.
|
|
189
|
+
|
|
190
|
+
Channel messages have no ack and no response. Each message is validated
|
|
191
|
+
(zod schema required), access-checked entirely in memory (zero DB reads on
|
|
192
|
+
the hot path), and governed by a per-socket, per-channel token bucket instead
|
|
193
|
+
of the global limiter. Excess messages are silently dropped; sustained extreme
|
|
194
|
+
flooding disconnects the socket.
|
|
195
|
+
|
|
196
|
+
**Server** — define channels next to methods; broadcast tick data back with
|
|
197
|
+
`emitToRoomVolatile` (backpressured clients drop frames instead of queueing):
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
type GameServiceChannels = ServiceChannelMap<{
|
|
201
|
+
input: { seq: number; dx: number; dy: number; boost: boolean };
|
|
202
|
+
}>;
|
|
203
|
+
|
|
204
|
+
class GameService extends BaseService<
|
|
205
|
+
GameWorld,
|
|
206
|
+
Prisma.GameWorldCreateInput,
|
|
207
|
+
Prisma.GameWorldUpdateInput,
|
|
208
|
+
GameServiceMethods,
|
|
209
|
+
GameServiceChannels // 5th type param
|
|
210
|
+
> {
|
|
211
|
+
constructor(prisma: PrismaClient) {
|
|
212
|
+
super({ serviceName: "gameService" });
|
|
213
|
+
this.setDelegate(prisma.gameWorld);
|
|
214
|
+
|
|
215
|
+
this.defineChannel(
|
|
216
|
+
"input",
|
|
217
|
+
"Read",
|
|
218
|
+
(payload, ctx) => this.sim.applyInput(ctx.userId, payload),
|
|
219
|
+
{
|
|
220
|
+
schema: gameInputSchema,
|
|
221
|
+
ratePerSecond: 30, // default 30
|
|
222
|
+
burst: 60, // default 2x rate
|
|
223
|
+
requireRoom: () => this.getRoomName(WORLD_ID), // entry-level gate
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// In a 20Hz tick loop:
|
|
229
|
+
broadcastSnapshot(snapshot: WorldSnapshot): void {
|
|
230
|
+
this.emitToRoomVolatile(this.getRoomName(WORLD_ID), "game:snapshot", snapshot);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Exempt channel traffic from the global rate limiter (channels self-limit):
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { CHANNEL_EVENT_PREFIX } from "@fitzzero/quickdraw-core";
|
|
239
|
+
|
|
240
|
+
const rateLimiter = createRateLimiter({
|
|
241
|
+
maxRequests: 100,
|
|
242
|
+
excludePrefixes: [CHANNEL_EVENT_PREFIX],
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Client** — send with `useChannelSend`; receive broadcasts with the existing
|
|
247
|
+
`useRoomEvents` (volatile room emits arrive as ordinary events):
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
import { useSubscription, useRoomEvents, useChannelSend } from "@fitzzero/quickdraw-core/client";
|
|
251
|
+
|
|
252
|
+
function GameView({ worldId }: { worldId: string }) {
|
|
253
|
+
useSubscription("gameService", worldId); // room membership gates the channel
|
|
254
|
+
const { send, isReady } = useChannelSend<GameInput>("gameService", "input");
|
|
255
|
+
|
|
256
|
+
useRoomEvents({
|
|
257
|
+
"game:snapshot": (snap: WorldSnapshot) => applySnapshot(snap),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// e.g. called from a fixed-timestep loop
|
|
261
|
+
const onTick = (input: GameInput) => send(input);
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Access model** (all synchronous, in-memory):
|
|
266
|
+
|
|
267
|
+
| Check | Behavior |
|
|
268
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
269
|
+
| Authentication | Always required — anonymous messages dropped, even at `"Public"` access |
|
|
270
|
+
| `"Public"` / `"Read"` | Any authenticated user passes the service gate |
|
|
271
|
+
| `"Moderate"` / `"Admin"` | Requires that level in the socket's `serviceAccess` |
|
|
272
|
+
| `requireRoom` | Socket must already be in the resolved room — membership was ACL-checked at subscribe time, so this inherits entry ACL semantics without a DB read per message |
|
|
273
|
+
|
|
274
|
+
Channels route as the Socket.io event `channel:<serviceName>:<channelName>`
|
|
275
|
+
(helper: `channelEventName(serviceName, channelName)`), which also makes them
|
|
276
|
+
easy to speak from non-JS clients (game engines, native apps).
|
|
277
|
+
|
|
278
|
+
When to use which:
|
|
279
|
+
|
|
280
|
+
| | Method | Channel |
|
|
281
|
+
| -------------- | ------------------------- | ------------------------ |
|
|
282
|
+
| Response | ack with data/error | none (fire-and-forget) |
|
|
283
|
+
| Frequency | occasional (user actions) | tick rate (10-60Hz) |
|
|
284
|
+
| Loss tolerance | must not lose | next message supersedes |
|
|
285
|
+
| ACL | full async check incl. DB | in-memory only |
|
|
286
|
+
| Rate limit | global limiter | per-channel token bucket |
|
|
287
|
+
|
|
170
288
|
## Package Exports
|
|
171
289
|
|
|
172
290
|
```typescript
|
|
173
291
|
// Shared types (both server and client)
|
|
174
|
-
import { ServiceResponse, AccessLevel, ServiceMethodMap } from
|
|
292
|
+
import { ServiceResponse, AccessLevel, ServiceMethodMap } from "@fitzzero/quickdraw-core";
|
|
175
293
|
|
|
176
294
|
// Server
|
|
177
|
-
import {
|
|
178
|
-
BaseService,
|
|
179
|
-
ServiceRegistry,
|
|
295
|
+
import {
|
|
296
|
+
BaseService,
|
|
297
|
+
ServiceRegistry,
|
|
180
298
|
createQuickdrawServer,
|
|
181
299
|
createJWT,
|
|
182
300
|
verifyJWT,
|
|
183
301
|
discordProvider,
|
|
184
302
|
googleProvider,
|
|
185
|
-
|
|
303
|
+
// Auth & security (3.7+)
|
|
304
|
+
createMockOAuthProvider,
|
|
305
|
+
registerMockOAuthProvider,
|
|
306
|
+
isMockOAuthEnabled,
|
|
307
|
+
validateRedirectOrigin,
|
|
308
|
+
setSessionCookie,
|
|
309
|
+
clearSessionCookie,
|
|
310
|
+
createRequireAuth,
|
|
311
|
+
encrypt,
|
|
312
|
+
decrypt,
|
|
313
|
+
} from "@fitzzero/quickdraw-core/server";
|
|
314
|
+
|
|
315
|
+
// Express rate-limit presets (3.7+, requires the optional express-rate-limit peer)
|
|
316
|
+
import {
|
|
317
|
+
createAuthLimiter,
|
|
318
|
+
createWebhookLimiter,
|
|
319
|
+
createPublicApiLimiter,
|
|
320
|
+
} from "@fitzzero/quickdraw-core/server/express";
|
|
186
321
|
|
|
187
322
|
// Server testing
|
|
188
|
-
import {
|
|
323
|
+
import {
|
|
324
|
+
createTestServer,
|
|
325
|
+
connectAsUser,
|
|
326
|
+
emitWithAck,
|
|
327
|
+
} from "@fitzzero/quickdraw-core/server/testing";
|
|
328
|
+
|
|
329
|
+
// Dual-mode Prisma test databases (3.7+, optional peers: @electric-sql/pglite, pg)
|
|
330
|
+
import {
|
|
331
|
+
createPrismaTestGlobalSetup,
|
|
332
|
+
resetDatabase,
|
|
333
|
+
workerDatabaseUrl,
|
|
334
|
+
} from "@fitzzero/quickdraw-core/server/testing/prisma";
|
|
189
335
|
|
|
190
336
|
// Client
|
|
191
337
|
import {
|
|
@@ -200,10 +346,10 @@ import {
|
|
|
200
346
|
SocketSelect,
|
|
201
347
|
SocketSlider,
|
|
202
348
|
SocketSwitch,
|
|
203
|
-
} from
|
|
349
|
+
} from "@fitzzero/quickdraw-core/client";
|
|
204
350
|
|
|
205
351
|
// Client testing
|
|
206
|
-
import { createMockSocket, createTestWrapper } from
|
|
352
|
+
import { createMockSocket, createTestWrapper } from "@fitzzero/quickdraw-core/client/testing";
|
|
207
353
|
```
|
|
208
354
|
|
|
209
355
|
## Local Development
|
|
@@ -226,7 +372,7 @@ Define your service methods in a shared types file:
|
|
|
226
372
|
|
|
227
373
|
```typescript
|
|
228
374
|
// shared/types.ts
|
|
229
|
-
import type { ServiceMethodMap } from
|
|
375
|
+
import type { ServiceMethodMap } from "@fitzzero/quickdraw-core";
|
|
230
376
|
|
|
231
377
|
export type ChatServiceMethods = ServiceMethodMap<{
|
|
232
378
|
createChat: {
|
|
@@ -238,7 +384,7 @@ export type ChatServiceMethods = ServiceMethodMap<{
|
|
|
238
384
|
response: { id: string; title: string };
|
|
239
385
|
};
|
|
240
386
|
inviteUser: {
|
|
241
|
-
payload: { id: string; userId: string; level:
|
|
387
|
+
payload: { id: string; userId: string; level: "Read" | "Moderate" | "Admin" };
|
|
242
388
|
response: { id: string };
|
|
243
389
|
};
|
|
244
390
|
}>;
|
|
@@ -309,6 +455,7 @@ Per-entity permissions. Quickdraw supports two patterns:
|
|
|
309
455
|
#### Pattern 1: JSON ACL (Simple)
|
|
310
456
|
|
|
311
457
|
Store ACL directly on the entity. Best for:
|
|
458
|
+
|
|
312
459
|
- Simple ownership models (owner + collaborators)
|
|
313
460
|
- When you don't need to query "all entities user X can access" efficiently
|
|
314
461
|
- Minimal schema complexity
|
|
@@ -338,6 +485,7 @@ class DocumentService extends BaseService<Document, ...> {
|
|
|
338
485
|
#### Pattern 2: Membership Table (Complex)
|
|
339
486
|
|
|
340
487
|
Separate table for memberships. Best for:
|
|
488
|
+
|
|
341
489
|
- Querying "all entities user X can access" efficiently
|
|
342
490
|
- Complex role hierarchies
|
|
343
491
|
- Additional membership metadata (join date, invited by, etc.)
|
|
@@ -353,7 +501,7 @@ model ChatMember {
|
|
|
353
501
|
chatId String
|
|
354
502
|
userId String
|
|
355
503
|
level String // "Read" | "Moderate" | "Admin"
|
|
356
|
-
|
|
504
|
+
|
|
357
505
|
@@unique([chatId, userId])
|
|
358
506
|
}
|
|
359
507
|
|
|
@@ -384,35 +532,41 @@ When a method is called, `ensureAccessForMethod` checks in this order:
|
|
|
384
532
|
|
|
385
533
|
### Access Levels
|
|
386
534
|
|
|
387
|
-
| Level
|
|
388
|
-
|
|
389
|
-
| Public
|
|
390
|
-
| Read
|
|
391
|
-
| Moderate | 2
|
|
392
|
-
| Admin
|
|
535
|
+
| Level | Value | Typical Use |
|
|
536
|
+
| -------- | ----- | -------------------------------- |
|
|
537
|
+
| Public | 0 | No authentication required |
|
|
538
|
+
| Read | 1 | View data, subscribe to updates |
|
|
539
|
+
| Moderate | 2 | Edit content, manage members |
|
|
540
|
+
| Admin | 3 | Delete, manage ACL, full control |
|
|
393
541
|
|
|
394
542
|
## Testing
|
|
395
543
|
|
|
396
544
|
### Server Integration Tests
|
|
397
545
|
|
|
398
546
|
```typescript
|
|
399
|
-
import {
|
|
547
|
+
import {
|
|
548
|
+
createTestServer,
|
|
549
|
+
connectAsUser,
|
|
550
|
+
emitWithAck,
|
|
551
|
+
} from "@fitzzero/quickdraw-core/server/testing";
|
|
400
552
|
|
|
401
|
-
describe(
|
|
553
|
+
describe("ChatService", () => {
|
|
402
554
|
let server;
|
|
403
|
-
|
|
555
|
+
|
|
404
556
|
beforeAll(async () => {
|
|
405
557
|
server = await createTestServer({
|
|
406
558
|
services: { chatService: new ChatService() },
|
|
407
|
-
seedDb: async () => {
|
|
559
|
+
seedDb: async () => {
|
|
560
|
+
/* seed test data */
|
|
561
|
+
},
|
|
408
562
|
});
|
|
409
563
|
});
|
|
410
|
-
|
|
564
|
+
|
|
411
565
|
afterAll(() => server.stop());
|
|
412
|
-
|
|
413
|
-
it(
|
|
414
|
-
const client = await server.connectAs(
|
|
415
|
-
const chat = await client.emit(
|
|
566
|
+
|
|
567
|
+
it("creates chat", async () => {
|
|
568
|
+
const client = await server.connectAs("user-id");
|
|
569
|
+
const chat = await client.emit("chatService:createChat", { title: "Test" });
|
|
416
570
|
expect(chat.id).toBeDefined();
|
|
417
571
|
client.close();
|
|
418
572
|
});
|
|
@@ -427,10 +581,10 @@ import { createTestWrapper, createMockSocket, mockSuccessEmit } from '@fitzzero/
|
|
|
427
581
|
test('renders chat', () => {
|
|
428
582
|
const mockSocket = createMockSocket();
|
|
429
583
|
mockSocket.emit.mockImplementation(mockSuccessEmit({ title: 'Test Chat' }));
|
|
430
|
-
|
|
584
|
+
|
|
431
585
|
const wrapper = createTestWrapper({ socketContext: { socket: mockSocket } });
|
|
432
586
|
render(<ChatView chatId="123" />, { wrapper });
|
|
433
|
-
|
|
587
|
+
|
|
434
588
|
expect(screen.getByText('Test Chat')).toBeInTheDocument();
|
|
435
589
|
});
|
|
436
590
|
```
|
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, g as UseRoomEventsOptions } 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, h as UseChannelSendResult } from '../types-DUHPIy_5.js';
|
|
3
|
+
export { C as ClientServiceMethodMap, S as SubscriptionDataMap } from '../types-DUHPIy_5.js';
|
|
4
4
|
import 'socket.io-client';
|
|
5
5
|
import '@tanstack/react-query';
|
|
6
6
|
import '../shared/index.js';
|
|
@@ -28,7 +28,7 @@ declare function useQuickdrawSocket(): QuickdrawSocketContextValue;
|
|
|
28
28
|
* }
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
|
-
declare function QuickdrawProvider({ children, serverUrl, queryClient, authToken, autoConnect, withCredentials, }: QuickdrawProviderProps): React.ReactElement;
|
|
31
|
+
declare function QuickdrawProvider({ children, serverUrl, queryClient, authToken, autoConnect, withCredentials, socketPath, }: QuickdrawProviderProps): React.ReactElement;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Hook for invoking service methods via Socket.io with TanStack Query integration.
|
|
@@ -207,6 +207,36 @@ declare function useSubscription<TData extends {
|
|
|
207
207
|
*/
|
|
208
208
|
declare function useRoomEvents(handlers: Record<string, (data: never) => void>, options?: UseRoomEventsOptions): void;
|
|
209
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Hook for sending fire-and-forget channel messages (the high-frequency
|
|
212
|
+
* counterpart to useService).
|
|
213
|
+
*
|
|
214
|
+
* Channel messages are emitted volatile: if the connection is backpressured,
|
|
215
|
+
* the message is dropped instead of queued — the right behavior for input
|
|
216
|
+
* streams where the next message supersedes the last. There is no ack and no
|
|
217
|
+
* response; the server validates, rate-limits (per-channel token bucket), and
|
|
218
|
+
* access-checks each message, silently dropping any that fail.
|
|
219
|
+
*
|
|
220
|
+
* Receiving server broadcasts needs nothing new: volatile room emits arrive
|
|
221
|
+
* as ordinary events, so pair this with `useRoomEvents` (and `useSubscription`
|
|
222
|
+
* for room membership).
|
|
223
|
+
*
|
|
224
|
+
* @typeParam TPayload - The channel payload type (from the service's channel map)
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```tsx
|
|
228
|
+
* function GameInput({ worldId }: { worldId: string }) {
|
|
229
|
+
* // Room membership (ACL-checked server-side) gates the channel
|
|
230
|
+
* useSubscription("gameService", worldId);
|
|
231
|
+
* const { send, isReady } = useChannelSend<GameInput>("gameService", "input");
|
|
232
|
+
*
|
|
233
|
+
* // e.g. called from a 20Hz game loop
|
|
234
|
+
* const onTick = (input: GameInput) => send(input);
|
|
235
|
+
* }
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
declare function useChannelSend<TPayload = unknown>(serviceName: string, channelName: string): UseChannelSendResult<TPayload>;
|
|
239
|
+
|
|
210
240
|
type CommitMode = "change" | "blur" | "debounce";
|
|
211
241
|
interface UseSocketInputOptions<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate> {
|
|
212
242
|
/**
|
|
@@ -517,4 +547,4 @@ declare function buildBreadcrumbs(pathname: string, navigation: NavItem[]): Brea
|
|
|
517
547
|
*/
|
|
518
548
|
declare function routeRequiresAuth(pathname: string, navigation: NavItem[], userMenuItems?: NavItem[]): boolean;
|
|
519
549
|
|
|
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 };
|
|
550
|
+
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, UseChannelSendResult, 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, useChannelSend, useQuickdrawSocket, useRoomEvents, useService, useServiceMethod, useServiceQuery, useSocketInput, useSubscription };
|