@covenant-rpc/server 0.1.3
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/CHANGELOG.md +17 -0
- package/lib/adapters/vanilla.ts +9 -0
- package/lib/index.ts +11 -0
- package/lib/interfaces/direct.ts +116 -0
- package/lib/interfaces/empty.ts +9 -0
- package/lib/interfaces/http.ts +111 -0
- package/lib/interfaces/mock.ts +32 -0
- package/lib/logger.ts +79 -0
- package/lib/server.ts +453 -0
- package/lib/sidekick/handlers.ts +173 -0
- package/lib/sidekick/index.ts +109 -0
- package/lib/sidekick/socket.ts +5 -0
- package/package.json +21 -0
- package/tests/channel-http.test.ts +481 -0
- package/tests/channel.test.ts +689 -0
- package/tests/procedure.test.ts +238 -0
- package/tests/sidekick.test.ts +23 -0
- package/tests/validation-types.test.ts +122 -0
- package/tests/validation.test.ts +144 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { test, expect } from "bun:test";
|
|
3
|
+
import { declareCovenant, channel } from "@covenant-rpc/core";
|
|
4
|
+
import { CovenantServer } from "../lib/server";
|
|
5
|
+
import { CovenantClient } from "@covenant-rpc/client";
|
|
6
|
+
import { directClientToServer } from "../lib/interfaces/direct";
|
|
7
|
+
import { InternalSidekick } from "../../sidekick/internal";
|
|
8
|
+
|
|
9
|
+
test("basic channel connection", async () => {
|
|
10
|
+
const sidekick = new InternalSidekick();
|
|
11
|
+
|
|
12
|
+
const covenant = declareCovenant({
|
|
13
|
+
procedures: {},
|
|
14
|
+
channels: {
|
|
15
|
+
chat: channel({
|
|
16
|
+
clientMessage: z.object({
|
|
17
|
+
text: z.string(),
|
|
18
|
+
}),
|
|
19
|
+
serverMessage: z.object({
|
|
20
|
+
text: z.string(),
|
|
21
|
+
timestamp: z.number(),
|
|
22
|
+
}),
|
|
23
|
+
connectionRequest: z.object({
|
|
24
|
+
username: z.string(),
|
|
25
|
+
}),
|
|
26
|
+
connectionContext: z.object({
|
|
27
|
+
userId: z.string(),
|
|
28
|
+
}),
|
|
29
|
+
params: ["roomId"],
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const server = new CovenantServer(covenant, {
|
|
35
|
+
contextGenerator: () => undefined,
|
|
36
|
+
derivation: () => {},
|
|
37
|
+
sidekickConnection: sidekick.getConnectionFromServer(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
sidekick.setServerCallback((channelName, params, data, context) =>
|
|
41
|
+
server.processChannelMessage(channelName, params, data, context)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
server.defineChannel("chat", {
|
|
45
|
+
onConnect: ({ inputs, params }) => {
|
|
46
|
+
expect(inputs.username).toBe("Alice");
|
|
47
|
+
expect(params.roomId).toBe("room1");
|
|
48
|
+
return {
|
|
49
|
+
userId: `user-${inputs.username}`,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
onMessage: ({ inputs, params, context }) => {
|
|
53
|
+
expect(inputs).toBeDefined();
|
|
54
|
+
expect(params.roomId).toBe("room1");
|
|
55
|
+
expect(context.userId).toBe("user-Alice");
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
server.assertAllDefined();
|
|
60
|
+
|
|
61
|
+
const client = new CovenantClient(covenant, {
|
|
62
|
+
sidekickConnection: sidekick.getConnectionFromClient(),
|
|
63
|
+
serverConnection: directClientToServer(server, {}),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Connect to the channel
|
|
67
|
+
const result = await client.connect("chat", { roomId: "room1" }, {
|
|
68
|
+
username: "Alice",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(result.success).toBe(true);
|
|
72
|
+
expect(result.error).toBe(null);
|
|
73
|
+
if (result.success) {
|
|
74
|
+
expect(result.token).toBeDefined();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("client sending messages through channel", async () => {
|
|
79
|
+
const sidekick = new InternalSidekick();
|
|
80
|
+
const receivedMessages: Array<{ text: string; userId: string }> = [];
|
|
81
|
+
|
|
82
|
+
const covenant = declareCovenant({
|
|
83
|
+
procedures: {},
|
|
84
|
+
channels: {
|
|
85
|
+
chat: channel({
|
|
86
|
+
clientMessage: z.object({
|
|
87
|
+
text: z.string(),
|
|
88
|
+
}),
|
|
89
|
+
serverMessage: z.object({
|
|
90
|
+
text: z.string(),
|
|
91
|
+
}),
|
|
92
|
+
connectionRequest: z.object({
|
|
93
|
+
username: z.string(),
|
|
94
|
+
}),
|
|
95
|
+
connectionContext: z.object({
|
|
96
|
+
userId: z.string(),
|
|
97
|
+
}),
|
|
98
|
+
params: ["roomId"],
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const server = new CovenantServer(covenant, {
|
|
104
|
+
contextGenerator: () => undefined,
|
|
105
|
+
derivation: () => {},
|
|
106
|
+
sidekickConnection: sidekick.getConnectionFromServer(),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
sidekick.setServerCallback((channelName, params, data, context) =>
|
|
110
|
+
server.processChannelMessage(channelName, params, data, context)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
server.defineChannel("chat", {
|
|
114
|
+
onConnect: ({ inputs }) => {
|
|
115
|
+
return {
|
|
116
|
+
userId: `user-${inputs.username}`,
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
onMessage: ({ inputs, context }) => {
|
|
120
|
+
receivedMessages.push({
|
|
121
|
+
text: inputs.text,
|
|
122
|
+
userId: context.userId,
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
server.assertAllDefined();
|
|
128
|
+
|
|
129
|
+
const client = new CovenantClient(covenant, {
|
|
130
|
+
sidekickConnection: sidekick.getConnectionFromClient(),
|
|
131
|
+
serverConnection: directClientToServer(server, {}),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const result = await client.connect("chat", { roomId: "room1" }, {
|
|
135
|
+
username: "Bob",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(result.success).toBe(true);
|
|
139
|
+
if (!result.success) return;
|
|
140
|
+
|
|
141
|
+
// Send a message
|
|
142
|
+
await client.send("chat", { roomId: "room1" }, result.token, {
|
|
143
|
+
text: "Hello, world!",
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Wait for message to be processed
|
|
147
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
148
|
+
|
|
149
|
+
expect(receivedMessages).toEqual([
|
|
150
|
+
{
|
|
151
|
+
text: "Hello, world!",
|
|
152
|
+
userId: "user-Bob",
|
|
153
|
+
},
|
|
154
|
+
]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("server sending messages to client", async () => {
|
|
158
|
+
const sidekick = new InternalSidekick();
|
|
159
|
+
const receivedMessages: Array<{ text: string }> = [];
|
|
160
|
+
|
|
161
|
+
const covenant = declareCovenant({
|
|
162
|
+
procedures: {},
|
|
163
|
+
channels: {
|
|
164
|
+
notifications: channel({
|
|
165
|
+
clientMessage: z.null(),
|
|
166
|
+
serverMessage: z.object({
|
|
167
|
+
text: z.string(),
|
|
168
|
+
}),
|
|
169
|
+
connectionRequest: z.object({
|
|
170
|
+
userId: z.string(),
|
|
171
|
+
}),
|
|
172
|
+
connectionContext: z.object({
|
|
173
|
+
userId: z.string(),
|
|
174
|
+
}),
|
|
175
|
+
params: [],
|
|
176
|
+
}),
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const server = new CovenantServer(covenant, {
|
|
181
|
+
contextGenerator: () => undefined,
|
|
182
|
+
derivation: () => {},
|
|
183
|
+
sidekickConnection: sidekick.getConnectionFromServer(),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
sidekick.setServerCallback((channelName, params, data, context) =>
|
|
187
|
+
server.processChannelMessage(channelName, params, data, context)
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
server.defineChannel("notifications", {
|
|
191
|
+
onConnect: ({ inputs }) => {
|
|
192
|
+
return {
|
|
193
|
+
userId: inputs.userId,
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
onMessage: () => {},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
server.assertAllDefined();
|
|
200
|
+
|
|
201
|
+
const client = new CovenantClient(covenant, {
|
|
202
|
+
sidekickConnection: sidekick.getConnectionFromClient(),
|
|
203
|
+
serverConnection: directClientToServer(server, {}),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const result = await client.connect("notifications", {}, {
|
|
207
|
+
userId: "user123",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(result.success).toBe(true);
|
|
211
|
+
if (!result.success) return;
|
|
212
|
+
|
|
213
|
+
// Subscribe to receive messages
|
|
214
|
+
const unsubscribe = await client.subscribe(
|
|
215
|
+
"notifications",
|
|
216
|
+
{},
|
|
217
|
+
result.token,
|
|
218
|
+
(message) => {
|
|
219
|
+
receivedMessages.push(message);
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Wait for subscription to be established
|
|
224
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
225
|
+
|
|
226
|
+
// Server sends a message
|
|
227
|
+
await server.postChannelMessage("notifications", {}, {
|
|
228
|
+
text: "New notification!",
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Wait for message to propagate
|
|
232
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
233
|
+
|
|
234
|
+
expect(receivedMessages).toEqual([
|
|
235
|
+
{
|
|
236
|
+
text: "New notification!",
|
|
237
|
+
},
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
unsubscribe();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("bidirectional channel communication", async () => {
|
|
244
|
+
const sidekick = new InternalSidekick();
|
|
245
|
+
const serverReceivedMessages: string[] = [];
|
|
246
|
+
const clientReceivedMessages: string[] = [];
|
|
247
|
+
|
|
248
|
+
const covenant = declareCovenant({
|
|
249
|
+
procedures: {},
|
|
250
|
+
channels: {
|
|
251
|
+
chat: channel({
|
|
252
|
+
clientMessage: z.object({
|
|
253
|
+
text: z.string(),
|
|
254
|
+
}),
|
|
255
|
+
serverMessage: z.object({
|
|
256
|
+
text: z.string(),
|
|
257
|
+
from: z.string(),
|
|
258
|
+
}),
|
|
259
|
+
connectionRequest: z.object({
|
|
260
|
+
username: z.string(),
|
|
261
|
+
}),
|
|
262
|
+
connectionContext: z.object({
|
|
263
|
+
username: z.string(),
|
|
264
|
+
}),
|
|
265
|
+
params: ["roomId"],
|
|
266
|
+
}),
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const server = new CovenantServer(covenant, {
|
|
271
|
+
contextGenerator: () => undefined,
|
|
272
|
+
derivation: () => {},
|
|
273
|
+
sidekickConnection: sidekick.getConnectionFromServer(),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
sidekick.setServerCallback((channelName, params, data, context) =>
|
|
277
|
+
server.processChannelMessage(channelName, params, data, context)
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
server.defineChannel("chat", {
|
|
281
|
+
onConnect: ({ inputs }) => {
|
|
282
|
+
return {
|
|
283
|
+
username: inputs.username,
|
|
284
|
+
};
|
|
285
|
+
},
|
|
286
|
+
onMessage: async ({ inputs, params, context }) => {
|
|
287
|
+
serverReceivedMessages.push(inputs.text);
|
|
288
|
+
|
|
289
|
+
// Echo the message back
|
|
290
|
+
await server.postChannelMessage("chat", params, {
|
|
291
|
+
text: inputs.text,
|
|
292
|
+
from: context.username,
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
server.assertAllDefined();
|
|
298
|
+
|
|
299
|
+
const client = new CovenantClient(covenant, {
|
|
300
|
+
sidekickConnection: sidekick.getConnectionFromClient(),
|
|
301
|
+
serverConnection: directClientToServer(server, {}),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const result = await client.connect("chat", { roomId: "general" }, {
|
|
305
|
+
username: "Charlie",
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(result.success).toBe(true);
|
|
309
|
+
if (!result.success) return;
|
|
310
|
+
|
|
311
|
+
// Subscribe to receive echoed messages
|
|
312
|
+
const unsubscribe = await client.subscribe(
|
|
313
|
+
"chat",
|
|
314
|
+
{ roomId: "general" },
|
|
315
|
+
result.token,
|
|
316
|
+
(message) => {
|
|
317
|
+
clientReceivedMessages.push(`${message.from}: ${message.text}`);
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
322
|
+
|
|
323
|
+
// Send a message
|
|
324
|
+
await client.send("chat", { roomId: "general" }, result.token, {
|
|
325
|
+
text: "Hello from client!",
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Wait for round trip
|
|
329
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
330
|
+
|
|
331
|
+
expect(serverReceivedMessages).toEqual(["Hello from client!"]);
|
|
332
|
+
expect(clientReceivedMessages).toEqual(["Charlie: Hello from client!"]);
|
|
333
|
+
|
|
334
|
+
unsubscribe();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("multiple clients on same channel", async () => {
|
|
338
|
+
const sidekick = new InternalSidekick();
|
|
339
|
+
|
|
340
|
+
const covenant = declareCovenant({
|
|
341
|
+
procedures: {},
|
|
342
|
+
channels: {
|
|
343
|
+
broadcast: channel({
|
|
344
|
+
clientMessage: z.null(),
|
|
345
|
+
serverMessage: z.object({
|
|
346
|
+
announcement: z.string(),
|
|
347
|
+
}),
|
|
348
|
+
connectionRequest: z.object({
|
|
349
|
+
name: z.string(),
|
|
350
|
+
}),
|
|
351
|
+
connectionContext: z.object({
|
|
352
|
+
name: z.string(),
|
|
353
|
+
}),
|
|
354
|
+
params: [],
|
|
355
|
+
}),
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const server = new CovenantServer(covenant, {
|
|
360
|
+
contextGenerator: () => undefined,
|
|
361
|
+
derivation: () => {},
|
|
362
|
+
sidekickConnection: sidekick.getConnectionFromServer(),
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
sidekick.setServerCallback((channelName, params, data, context) =>
|
|
366
|
+
server.processChannelMessage(channelName, params, data, context)
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
server.defineChannel("broadcast", {
|
|
370
|
+
onConnect: ({ inputs }) => {
|
|
371
|
+
return { name: inputs.name };
|
|
372
|
+
},
|
|
373
|
+
onMessage: () => {},
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
server.assertAllDefined();
|
|
377
|
+
|
|
378
|
+
// Create two clients
|
|
379
|
+
const client1 = new CovenantClient(covenant, {
|
|
380
|
+
sidekickConnection: sidekick.getConnectionFromClient(),
|
|
381
|
+
serverConnection: directClientToServer(server, {}),
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const client2 = new CovenantClient(covenant, {
|
|
385
|
+
sidekickConnection: sidekick.getConnectionFromClient(),
|
|
386
|
+
serverConnection: directClientToServer(server, {}),
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const result1 = await client1.connect("broadcast", {}, { name: "Client1" });
|
|
390
|
+
const result2 = await client2.connect("broadcast", {}, { name: "Client2" });
|
|
391
|
+
|
|
392
|
+
expect(result1.success).toBe(true);
|
|
393
|
+
expect(result2.success).toBe(true);
|
|
394
|
+
if (!result1.success || !result2.success) return;
|
|
395
|
+
|
|
396
|
+
const messages1: string[] = [];
|
|
397
|
+
const messages2: string[] = [];
|
|
398
|
+
|
|
399
|
+
const unsub1 = await client1.subscribe("broadcast", {}, result1.token, (msg) => {
|
|
400
|
+
messages1.push(msg.announcement);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const unsub2 = await client2.subscribe("broadcast", {}, result2.token, (msg) => {
|
|
404
|
+
messages2.push(msg.announcement);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
408
|
+
|
|
409
|
+
// Server broadcasts to all clients
|
|
410
|
+
await server.postChannelMessage("broadcast", {}, {
|
|
411
|
+
announcement: "System update!",
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
415
|
+
|
|
416
|
+
// Both clients should receive the message
|
|
417
|
+
expect(messages1).toEqual(["System update!"]);
|
|
418
|
+
expect(messages2).toEqual(["System update!"]);
|
|
419
|
+
|
|
420
|
+
unsub1();
|
|
421
|
+
unsub2();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("channel connection error handling", async () => {
|
|
425
|
+
const sidekick = new InternalSidekick();
|
|
426
|
+
|
|
427
|
+
const covenant = declareCovenant({
|
|
428
|
+
procedures: {},
|
|
429
|
+
channels: {
|
|
430
|
+
restricted: channel({
|
|
431
|
+
clientMessage: z.null(),
|
|
432
|
+
serverMessage: z.null(),
|
|
433
|
+
connectionRequest: z.object({
|
|
434
|
+
password: z.string(),
|
|
435
|
+
}),
|
|
436
|
+
connectionContext: z.object({
|
|
437
|
+
authenticated: z.boolean(),
|
|
438
|
+
}),
|
|
439
|
+
params: [],
|
|
440
|
+
}),
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const server = new CovenantServer(covenant, {
|
|
445
|
+
contextGenerator: () => undefined,
|
|
446
|
+
derivation: () => {},
|
|
447
|
+
sidekickConnection: sidekick.getConnectionFromServer(),
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
sidekick.setServerCallback((channelName, params, data, context) =>
|
|
451
|
+
server.processChannelMessage(channelName, params, data, context)
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
server.defineChannel("restricted", {
|
|
455
|
+
onConnect: ({ inputs, reject }) => {
|
|
456
|
+
if (inputs.password !== "secret") {
|
|
457
|
+
reject("Invalid password", "client");
|
|
458
|
+
}
|
|
459
|
+
return { authenticated: true };
|
|
460
|
+
},
|
|
461
|
+
onMessage: () => {},
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
server.assertAllDefined();
|
|
465
|
+
|
|
466
|
+
const client = new CovenantClient(covenant, {
|
|
467
|
+
sidekickConnection: sidekick.getConnectionFromClient(),
|
|
468
|
+
serverConnection: directClientToServer(server, {}),
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Try to connect with wrong password
|
|
472
|
+
const result = await client.connect("restricted", {}, {
|
|
473
|
+
password: "wrong",
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
expect(result.success).toBe(false);
|
|
477
|
+
expect(result.error).toBeDefined();
|
|
478
|
+
if (!result.success) {
|
|
479
|
+
expect(result.error.message).toBe("Invalid password");
|
|
480
|
+
expect(result.error.fault).toBe("client");
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("channel message error handling", async () => {
|
|
485
|
+
const sidekick = new InternalSidekick();
|
|
486
|
+
|
|
487
|
+
const covenant = declareCovenant({
|
|
488
|
+
procedures: {},
|
|
489
|
+
channels: {
|
|
490
|
+
moderated: channel({
|
|
491
|
+
clientMessage: z.object({
|
|
492
|
+
text: z.string(),
|
|
493
|
+
}),
|
|
494
|
+
serverMessage: z.null(),
|
|
495
|
+
connectionRequest: z.object({
|
|
496
|
+
username: z.string(),
|
|
497
|
+
}),
|
|
498
|
+
connectionContext: z.object({
|
|
499
|
+
username: z.string(),
|
|
500
|
+
}),
|
|
501
|
+
params: [],
|
|
502
|
+
}),
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const server = new CovenantServer(covenant, {
|
|
507
|
+
contextGenerator: () => undefined,
|
|
508
|
+
derivation: () => {},
|
|
509
|
+
sidekickConnection: sidekick.getConnectionFromServer(),
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
sidekick.setServerCallback((channelName, params, data, context) =>
|
|
513
|
+
server.processChannelMessage(channelName, params, data, context)
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
server.defineChannel("moderated", {
|
|
517
|
+
onConnect: ({ inputs }) => {
|
|
518
|
+
return { username: inputs.username };
|
|
519
|
+
},
|
|
520
|
+
onMessage: ({ inputs, error }) => {
|
|
521
|
+
if (inputs.text.includes("banned")) {
|
|
522
|
+
error("Message contains banned words", "client");
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
server.assertAllDefined();
|
|
528
|
+
|
|
529
|
+
const client = new CovenantClient(covenant, {
|
|
530
|
+
sidekickConnection: sidekick.getConnectionFromClient(),
|
|
531
|
+
serverConnection: directClientToServer(server, {}),
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
const result = await client.connect("moderated", {}, {
|
|
535
|
+
username: "User1",
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
expect(result.success).toBe(true);
|
|
539
|
+
if (!result.success) return;
|
|
540
|
+
|
|
541
|
+
// Try to send a message with banned content
|
|
542
|
+
try {
|
|
543
|
+
await client.send("moderated", {}, result.token, {
|
|
544
|
+
text: "This is banned content",
|
|
545
|
+
});
|
|
546
|
+
expect(true).toBe(false); // Should not reach here
|
|
547
|
+
} catch (error: any) {
|
|
548
|
+
expect(error.message).toContain("banned words");
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("channel with multiple params", async () => {
|
|
553
|
+
const sidekick = new InternalSidekick();
|
|
554
|
+
|
|
555
|
+
const covenant = declareCovenant({
|
|
556
|
+
procedures: {},
|
|
557
|
+
channels: {
|
|
558
|
+
thread: channel({
|
|
559
|
+
clientMessage: z.object({
|
|
560
|
+
text: z.string(),
|
|
561
|
+
}),
|
|
562
|
+
serverMessage: z.object({
|
|
563
|
+
text: z.string(),
|
|
564
|
+
}),
|
|
565
|
+
connectionRequest: z.null(),
|
|
566
|
+
connectionContext: z.object({
|
|
567
|
+
connected: z.boolean(),
|
|
568
|
+
}),
|
|
569
|
+
params: ["forumId", "threadId"],
|
|
570
|
+
}),
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
const server = new CovenantServer(covenant, {
|
|
575
|
+
contextGenerator: () => undefined,
|
|
576
|
+
derivation: () => {},
|
|
577
|
+
sidekickConnection: sidekick.getConnectionFromServer(),
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
sidekick.setServerCallback((channelName, params, data, context) =>
|
|
581
|
+
server.processChannelMessage(channelName, params, data, context)
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
server.defineChannel("thread", {
|
|
585
|
+
onConnect: ({ params }) => {
|
|
586
|
+
expect(params.forumId).toBe("tech");
|
|
587
|
+
expect(params.threadId).toBe("123");
|
|
588
|
+
return { connected: true };
|
|
589
|
+
},
|
|
590
|
+
onMessage: ({ params }) => {
|
|
591
|
+
expect(params.forumId).toBe("tech");
|
|
592
|
+
expect(params.threadId).toBe("123");
|
|
593
|
+
},
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
server.assertAllDefined();
|
|
597
|
+
|
|
598
|
+
const client = new CovenantClient(covenant, {
|
|
599
|
+
sidekickConnection: sidekick.getConnectionFromClient(),
|
|
600
|
+
serverConnection: directClientToServer(server, {}),
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
const result = await client.connect(
|
|
604
|
+
"thread",
|
|
605
|
+
{ forumId: "tech", threadId: "123" },
|
|
606
|
+
null
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
expect(result.success).toBe(true);
|
|
610
|
+
if (!result.success) return;
|
|
611
|
+
|
|
612
|
+
await client.send(
|
|
613
|
+
"thread",
|
|
614
|
+
{ forumId: "tech", threadId: "123" },
|
|
615
|
+
result.token,
|
|
616
|
+
{ text: "Test message" }
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("channel unsubscribe", async () => {
|
|
623
|
+
const sidekick = new InternalSidekick();
|
|
624
|
+
const receivedMessages: string[] = [];
|
|
625
|
+
|
|
626
|
+
const covenant = declareCovenant({
|
|
627
|
+
procedures: {},
|
|
628
|
+
channels: {
|
|
629
|
+
updates: channel({
|
|
630
|
+
clientMessage: z.null(),
|
|
631
|
+
serverMessage: z.object({
|
|
632
|
+
update: z.string(),
|
|
633
|
+
}),
|
|
634
|
+
connectionRequest: z.null(),
|
|
635
|
+
connectionContext: z.null(),
|
|
636
|
+
params: [],
|
|
637
|
+
}),
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
const server = new CovenantServer(covenant, {
|
|
642
|
+
contextGenerator: () => undefined,
|
|
643
|
+
derivation: () => {},
|
|
644
|
+
sidekickConnection: sidekick.getConnectionFromServer(),
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
sidekick.setServerCallback((channelName, params, data, context) =>
|
|
648
|
+
server.processChannelMessage(channelName, params, data, context)
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
server.defineChannel("updates", {
|
|
652
|
+
onConnect: () => null,
|
|
653
|
+
onMessage: () => {},
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
server.assertAllDefined();
|
|
657
|
+
|
|
658
|
+
const client = new CovenantClient(covenant, {
|
|
659
|
+
sidekickConnection: sidekick.getConnectionFromClient(),
|
|
660
|
+
serverConnection: directClientToServer(server, {}),
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
const result = await client.connect("updates", {}, null);
|
|
664
|
+
expect(result.success).toBe(true);
|
|
665
|
+
if (!result.success) return;
|
|
666
|
+
|
|
667
|
+
const unsubscribe = await client.subscribe("updates", {}, result.token, (msg) => {
|
|
668
|
+
receivedMessages.push(msg.update);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
672
|
+
|
|
673
|
+
// Send first message
|
|
674
|
+
await server.postChannelMessage("updates", {}, { update: "Update 1" });
|
|
675
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
676
|
+
|
|
677
|
+
expect(receivedMessages).toEqual(["Update 1"]);
|
|
678
|
+
|
|
679
|
+
// Unsubscribe
|
|
680
|
+
unsubscribe();
|
|
681
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
682
|
+
|
|
683
|
+
// Send second message - should not be received
|
|
684
|
+
await server.postChannelMessage("updates", {}, { update: "Update 2" });
|
|
685
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
686
|
+
|
|
687
|
+
// Should still only have the first message
|
|
688
|
+
expect(receivedMessages).toEqual(["Update 1"]);
|
|
689
|
+
});
|