@parsrun/realtime 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.
@@ -0,0 +1,740 @@
1
+ // src/types.ts
2
+ var RealtimeError = class extends Error {
3
+ constructor(message, code, cause) {
4
+ super(message);
5
+ this.code = code;
6
+ this.cause = cause;
7
+ this.name = "RealtimeError";
8
+ }
9
+ };
10
+ var RealtimeErrorCodes = {
11
+ CONNECTION_FAILED: "CONNECTION_FAILED",
12
+ CHANNEL_NOT_FOUND: "CHANNEL_NOT_FOUND",
13
+ UNAUTHORIZED: "UNAUTHORIZED",
14
+ MESSAGE_TOO_LARGE: "MESSAGE_TOO_LARGE",
15
+ RATE_LIMITED: "RATE_LIMITED",
16
+ ADAPTER_ERROR: "ADAPTER_ERROR",
17
+ INVALID_MESSAGE: "INVALID_MESSAGE"
18
+ };
19
+ function createMessage(options) {
20
+ return {
21
+ id: crypto.randomUUID(),
22
+ event: options.event,
23
+ channel: options.channel,
24
+ data: options.data,
25
+ senderId: options.senderId,
26
+ timestamp: Date.now(),
27
+ metadata: options.metadata
28
+ };
29
+ }
30
+ function formatSSEEvent(message) {
31
+ const lines = [];
32
+ lines.push(`id:${message.id}`);
33
+ lines.push(`event:${message.event}`);
34
+ lines.push(`data:${JSON.stringify(message)}`);
35
+ lines.push("");
36
+ return lines.join("\n") + "\n";
37
+ }
38
+
39
+ // src/adapters/sse.ts
40
+ var DEFAULT_OPTIONS = {
41
+ pingInterval: 3e4,
42
+ connectionTimeout: 0,
43
+ maxConnectionsPerChannel: 1e3,
44
+ retryDelay: 3e3
45
+ };
46
+ var SSEAdapter = class {
47
+ type = "sse";
48
+ options;
49
+ connections = /* @__PURE__ */ new Map();
50
+ channelSubscribers = /* @__PURE__ */ new Map();
51
+ presence = /* @__PURE__ */ new Map();
52
+ pingIntervalId = null;
53
+ constructor(options = {}) {
54
+ this.options = {
55
+ pingInterval: options.pingInterval ?? DEFAULT_OPTIONS.pingInterval,
56
+ connectionTimeout: options.connectionTimeout ?? DEFAULT_OPTIONS.connectionTimeout,
57
+ maxConnectionsPerChannel: options.maxConnectionsPerChannel ?? DEFAULT_OPTIONS.maxConnectionsPerChannel,
58
+ retryDelay: options.retryDelay ?? DEFAULT_OPTIONS.retryDelay
59
+ };
60
+ if (this.options.pingInterval > 0) {
61
+ this.startPingInterval();
62
+ }
63
+ }
64
+ /**
65
+ * Create SSE response for a new connection
66
+ */
67
+ createConnection(sessionId, userId) {
68
+ const { readable, writable } = new TransformStream();
69
+ const writer = writable.getWriter();
70
+ const connection = {
71
+ sessionId,
72
+ userId,
73
+ channels: /* @__PURE__ */ new Set(),
74
+ writer,
75
+ state: "open",
76
+ createdAt: Date.now(),
77
+ lastPingAt: Date.now()
78
+ };
79
+ this.connections.set(sessionId, connection);
80
+ const encoder = new TextEncoder();
81
+ writer.write(encoder.encode(`retry:${this.options.retryDelay}
82
+
83
+ `));
84
+ const response = new Response(readable, {
85
+ headers: {
86
+ "Content-Type": "text/event-stream",
87
+ "Cache-Control": "no-cache",
88
+ "Connection": "keep-alive",
89
+ "X-Accel-Buffering": "no"
90
+ // Disable nginx buffering
91
+ }
92
+ });
93
+ return { response, connection };
94
+ }
95
+ /**
96
+ * Close a connection
97
+ */
98
+ async closeConnection(sessionId) {
99
+ const connection = this.connections.get(sessionId);
100
+ if (!connection) return;
101
+ connection.state = "closed";
102
+ for (const channel of connection.channels) {
103
+ await this.unsubscribe(channel, sessionId);
104
+ await this.removePresence(channel, sessionId);
105
+ }
106
+ try {
107
+ await connection.writer.close();
108
+ } catch {
109
+ }
110
+ this.connections.delete(sessionId);
111
+ }
112
+ /**
113
+ * Get a connection by session ID
114
+ */
115
+ getConnection(sessionId) {
116
+ return this.connections.get(sessionId);
117
+ }
118
+ /**
119
+ * Get all active connections
120
+ */
121
+ getConnections() {
122
+ return this.connections;
123
+ }
124
+ // ============================================================================
125
+ // RealtimeAdapter Implementation
126
+ // ============================================================================
127
+ async subscribe(channel, sessionId, handler) {
128
+ const connection = this.connections.get(sessionId);
129
+ if (!connection) {
130
+ throw new RealtimeError(
131
+ "Connection not found",
132
+ RealtimeErrorCodes.CONNECTION_FAILED
133
+ );
134
+ }
135
+ const subscribers = this.channelSubscribers.get(channel);
136
+ if (subscribers && subscribers.size >= this.options.maxConnectionsPerChannel) {
137
+ throw new RealtimeError(
138
+ "Channel subscriber limit reached",
139
+ RealtimeErrorCodes.RATE_LIMITED
140
+ );
141
+ }
142
+ if (!this.channelSubscribers.has(channel)) {
143
+ this.channelSubscribers.set(channel, /* @__PURE__ */ new Map());
144
+ }
145
+ this.channelSubscribers.get(channel).set(sessionId, handler);
146
+ connection.channels.add(channel);
147
+ await this.sendToConnection(connection, {
148
+ id: crypto.randomUUID(),
149
+ event: "channel:subscribe",
150
+ channel,
151
+ data: { channel },
152
+ timestamp: Date.now()
153
+ });
154
+ }
155
+ async unsubscribe(channel, sessionId) {
156
+ const subscribers = this.channelSubscribers.get(channel);
157
+ if (subscribers) {
158
+ subscribers.delete(sessionId);
159
+ if (subscribers.size === 0) {
160
+ this.channelSubscribers.delete(channel);
161
+ }
162
+ }
163
+ const connection = this.connections.get(sessionId);
164
+ if (connection) {
165
+ connection.channels.delete(channel);
166
+ }
167
+ }
168
+ async publish(channel, message) {
169
+ const subscribers = this.channelSubscribers.get(channel);
170
+ if (!subscribers) return;
171
+ const promises = [];
172
+ for (const [sessionId, handler] of subscribers) {
173
+ const connection = this.connections.get(sessionId);
174
+ if (connection && connection.state === "open") {
175
+ promises.push(this.sendToConnection(connection, message));
176
+ try {
177
+ const result = handler(message);
178
+ if (result instanceof Promise) {
179
+ promises.push(result.then(() => {
180
+ }));
181
+ }
182
+ } catch {
183
+ }
184
+ }
185
+ }
186
+ await Promise.allSettled(promises);
187
+ }
188
+ async sendToSession(sessionId, message) {
189
+ const connection = this.connections.get(sessionId);
190
+ if (!connection || connection.state !== "open") {
191
+ return false;
192
+ }
193
+ try {
194
+ await this.sendToConnection(connection, message);
195
+ return true;
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+ async getSubscribers(channel) {
201
+ const subscribers = this.channelSubscribers.get(channel);
202
+ return subscribers ? Array.from(subscribers.keys()) : [];
203
+ }
204
+ async setPresence(channel, sessionId, userId, data) {
205
+ if (!this.presence.has(channel)) {
206
+ this.presence.set(channel, /* @__PURE__ */ new Map());
207
+ }
208
+ const now = Date.now();
209
+ const existing = this.presence.get(channel).get(sessionId);
210
+ const user = {
211
+ userId,
212
+ sessionId,
213
+ data,
214
+ joinedAt: existing?.joinedAt ?? now,
215
+ lastSeenAt: now
216
+ };
217
+ this.presence.get(channel).set(sessionId, user);
218
+ const eventType = existing ? "presence:update" : "presence:join";
219
+ await this.publish(channel, {
220
+ id: crypto.randomUUID(),
221
+ event: eventType,
222
+ channel,
223
+ data: {
224
+ type: existing ? "update" : "join",
225
+ user,
226
+ presence: await this.getPresence(channel)
227
+ },
228
+ timestamp: now
229
+ });
230
+ }
231
+ async removePresence(channel, sessionId) {
232
+ const channelPresence = this.presence.get(channel);
233
+ if (!channelPresence) return;
234
+ const user = channelPresence.get(sessionId);
235
+ if (!user) return;
236
+ channelPresence.delete(sessionId);
237
+ if (channelPresence.size === 0) {
238
+ this.presence.delete(channel);
239
+ }
240
+ await this.publish(channel, {
241
+ id: crypto.randomUUID(),
242
+ event: "presence:leave",
243
+ channel,
244
+ data: {
245
+ type: "leave",
246
+ user,
247
+ presence: await this.getPresence(channel)
248
+ },
249
+ timestamp: Date.now()
250
+ });
251
+ }
252
+ async getPresence(channel) {
253
+ const channelPresence = this.presence.get(channel);
254
+ if (!channelPresence) return [];
255
+ return Array.from(channelPresence.values());
256
+ }
257
+ async close() {
258
+ if (this.pingIntervalId) {
259
+ clearInterval(this.pingIntervalId);
260
+ this.pingIntervalId = null;
261
+ }
262
+ const closePromises = Array.from(this.connections.keys()).map(
263
+ (sessionId) => this.closeConnection(sessionId)
264
+ );
265
+ await Promise.allSettled(closePromises);
266
+ this.connections.clear();
267
+ this.channelSubscribers.clear();
268
+ this.presence.clear();
269
+ }
270
+ // ============================================================================
271
+ // Private Methods
272
+ // ============================================================================
273
+ async sendToConnection(connection, message) {
274
+ if (connection.state !== "open") return;
275
+ const encoder = new TextEncoder();
276
+ const sseEvent = formatSSEEvent(message);
277
+ try {
278
+ await connection.writer.write(encoder.encode(sseEvent));
279
+ } catch (err) {
280
+ connection.state = "closed";
281
+ await this.closeConnection(connection.sessionId);
282
+ }
283
+ }
284
+ startPingInterval() {
285
+ this.pingIntervalId = setInterval(() => {
286
+ this.sendPingToAll();
287
+ }, this.options.pingInterval);
288
+ }
289
+ async sendPingToAll() {
290
+ const now = Date.now();
291
+ const encoder = new TextEncoder();
292
+ const pingEvent = `event:ping
293
+ data:${now}
294
+
295
+ `;
296
+ for (const [sessionId, connection] of this.connections) {
297
+ if (connection.state !== "open") continue;
298
+ if (this.options.connectionTimeout > 0 && now - connection.lastPingAt > this.options.connectionTimeout) {
299
+ await this.closeConnection(sessionId);
300
+ continue;
301
+ }
302
+ try {
303
+ await connection.writer.write(encoder.encode(pingEvent));
304
+ connection.lastPingAt = now;
305
+ } catch {
306
+ await this.closeConnection(sessionId);
307
+ }
308
+ }
309
+ }
310
+ /**
311
+ * Update last ping time (call when receiving client ping)
312
+ */
313
+ updateLastPing(sessionId) {
314
+ const connection = this.connections.get(sessionId);
315
+ if (connection) {
316
+ connection.lastPingAt = Date.now();
317
+ }
318
+ }
319
+ /**
320
+ * Get statistics
321
+ */
322
+ getStats() {
323
+ const connectionsByChannel = {};
324
+ for (const [channel, subscribers] of this.channelSubscribers) {
325
+ connectionsByChannel[channel] = subscribers.size;
326
+ }
327
+ return {
328
+ totalConnections: this.connections.size,
329
+ totalChannels: this.channelSubscribers.size,
330
+ connectionsByChannel
331
+ };
332
+ }
333
+ };
334
+ function createSSEAdapter(options) {
335
+ return new SSEAdapter(options);
336
+ }
337
+
338
+ // src/adapters/durable-objects.ts
339
+ var RealtimeChannelDO = class {
340
+ sessions = /* @__PURE__ */ new Map();
341
+ presence = /* @__PURE__ */ new Map();
342
+ channelName = "";
343
+ state;
344
+ constructor(state, _env) {
345
+ this.state = state;
346
+ this.state.getWebSockets().forEach((ws) => {
347
+ const meta = ws.deserializeAttachment();
348
+ if (meta) {
349
+ this.sessions.set(meta.sessionId, {
350
+ sessionId: meta.sessionId,
351
+ userId: meta.userId,
352
+ webSocket: ws,
353
+ state: "open",
354
+ createdAt: Date.now()
355
+ });
356
+ }
357
+ });
358
+ }
359
+ async fetch(request) {
360
+ const url = new URL(request.url);
361
+ const pathParts = url.pathname.split("/").filter(Boolean);
362
+ const lastPart = pathParts[pathParts.length - 1];
363
+ if (lastPart) {
364
+ this.channelName = lastPart;
365
+ }
366
+ if (request.headers.get("Upgrade") === "websocket") {
367
+ return this.handleWebSocketUpgrade(request);
368
+ }
369
+ switch (url.pathname.split("/").pop()) {
370
+ case "broadcast":
371
+ return this.handleBroadcast(request);
372
+ case "presence":
373
+ return this.handleGetPresence();
374
+ case "info":
375
+ return this.handleGetInfo();
376
+ case "send":
377
+ return this.handleSendToUser(request);
378
+ default:
379
+ return new Response("Not found", { status: 404 });
380
+ }
381
+ }
382
+ // ============================================================================
383
+ // WebSocket Handling
384
+ // ============================================================================
385
+ handleWebSocketUpgrade(request) {
386
+ const url = new URL(request.url);
387
+ const sessionId = url.searchParams.get("sessionId") || crypto.randomUUID();
388
+ const userId = url.searchParams.get("userId") || void 0;
389
+ const pair = new WebSocketPair();
390
+ const client = pair[0];
391
+ const server = pair[1];
392
+ server.serializeAttachment({ sessionId, userId });
393
+ this.state.acceptWebSocket(server);
394
+ const session = {
395
+ sessionId,
396
+ userId,
397
+ webSocket: server,
398
+ state: "open",
399
+ createdAt: Date.now()
400
+ };
401
+ this.sessions.set(sessionId, session);
402
+ server.send(
403
+ JSON.stringify(
404
+ createMessage({
405
+ event: "connection:open",
406
+ channel: this.channelName,
407
+ data: { sessionId, userId }
408
+ })
409
+ )
410
+ );
411
+ return new Response(null, { status: 101, webSocket: client });
412
+ }
413
+ async webSocketMessage(ws, message) {
414
+ const session = this.getSessionByWebSocket(ws);
415
+ if (!session) return;
416
+ try {
417
+ const data = typeof message === "string" ? message : new TextDecoder().decode(message);
418
+ const parsed = JSON.parse(data);
419
+ await this.handleMessage(session, parsed);
420
+ } catch {
421
+ }
422
+ }
423
+ async webSocketClose(ws, code, reason, _wasClean) {
424
+ const session = this.getSessionByWebSocket(ws);
425
+ if (!session) return;
426
+ this.presence.delete(session.sessionId);
427
+ await this.broadcastPresenceUpdate();
428
+ this.sessions.delete(session.sessionId);
429
+ await this.broadcastToAll({
430
+ event: "connection:close",
431
+ channel: this.channelName,
432
+ data: {
433
+ sessionId: session.sessionId,
434
+ userId: session.userId,
435
+ code,
436
+ reason
437
+ }
438
+ });
439
+ }
440
+ async webSocketError(ws, _error) {
441
+ const session = this.getSessionByWebSocket(ws);
442
+ if (session) {
443
+ session.state = "closed";
444
+ this.sessions.delete(session.sessionId);
445
+ this.presence.delete(session.sessionId);
446
+ }
447
+ }
448
+ // ============================================================================
449
+ // Message Handling
450
+ // ============================================================================
451
+ async handleMessage(session, message) {
452
+ switch (message.event) {
453
+ case "ping":
454
+ session.webSocket.send(
455
+ JSON.stringify(
456
+ createMessage({
457
+ event: "pong",
458
+ channel: this.channelName,
459
+ data: { timestamp: Date.now() }
460
+ })
461
+ )
462
+ );
463
+ break;
464
+ case "presence:join":
465
+ await this.handlePresenceJoin(session, message.data);
466
+ break;
467
+ case "presence:update":
468
+ await this.handlePresenceUpdate(session, message.data);
469
+ break;
470
+ case "presence:leave":
471
+ await this.handlePresenceLeave(session);
472
+ break;
473
+ case "broadcast":
474
+ await this.broadcastToAll(
475
+ {
476
+ event: message.event,
477
+ channel: this.channelName,
478
+ data: message.data,
479
+ senderId: session.sessionId
480
+ },
481
+ session.sessionId
482
+ );
483
+ break;
484
+ default:
485
+ await this.broadcastToAll(
486
+ {
487
+ event: message.event,
488
+ channel: this.channelName,
489
+ data: message.data,
490
+ senderId: session.sessionId
491
+ },
492
+ session.sessionId
493
+ );
494
+ }
495
+ }
496
+ // ============================================================================
497
+ // Presence Handling
498
+ // ============================================================================
499
+ async handlePresenceJoin(session, data) {
500
+ const now = Date.now();
501
+ const user = {
502
+ userId: session.userId || session.sessionId,
503
+ sessionId: session.sessionId,
504
+ data,
505
+ joinedAt: now,
506
+ lastSeenAt: now
507
+ };
508
+ this.presence.set(session.sessionId, user);
509
+ session.presence = data;
510
+ await this.broadcastPresenceUpdate();
511
+ }
512
+ async handlePresenceUpdate(session, data) {
513
+ const existing = this.presence.get(session.sessionId);
514
+ if (existing) {
515
+ existing.data = data;
516
+ existing.lastSeenAt = Date.now();
517
+ session.presence = data;
518
+ await this.broadcastPresenceUpdate();
519
+ }
520
+ }
521
+ async handlePresenceLeave(session) {
522
+ this.presence.delete(session.sessionId);
523
+ session.presence = void 0;
524
+ await this.broadcastPresenceUpdate();
525
+ }
526
+ async broadcastPresenceUpdate() {
527
+ const presenceList = Array.from(this.presence.values());
528
+ await this.broadcastToAll({
529
+ event: "presence:sync",
530
+ channel: this.channelName,
531
+ data: presenceList
532
+ });
533
+ }
534
+ // ============================================================================
535
+ // REST API Handlers
536
+ // ============================================================================
537
+ async handleBroadcast(request) {
538
+ try {
539
+ const body = await request.json();
540
+ await this.broadcastToAll({
541
+ event: body.event,
542
+ channel: this.channelName,
543
+ data: body.data
544
+ });
545
+ return new Response(JSON.stringify({ success: true }), {
546
+ headers: { "Content-Type": "application/json" }
547
+ });
548
+ } catch (err) {
549
+ return new Response(
550
+ JSON.stringify({ success: false, error: String(err) }),
551
+ { status: 400, headers: { "Content-Type": "application/json" } }
552
+ );
553
+ }
554
+ }
555
+ handleGetPresence() {
556
+ const presenceList = Array.from(this.presence.values());
557
+ return new Response(JSON.stringify(presenceList), {
558
+ headers: { "Content-Type": "application/json" }
559
+ });
560
+ }
561
+ handleGetInfo() {
562
+ return new Response(
563
+ JSON.stringify({
564
+ channel: this.channelName,
565
+ connections: this.sessions.size,
566
+ presence: this.presence.size
567
+ }),
568
+ { headers: { "Content-Type": "application/json" } }
569
+ );
570
+ }
571
+ async handleSendToUser(request) {
572
+ try {
573
+ const body = await request.json();
574
+ let sent = false;
575
+ for (const session of this.sessions.values()) {
576
+ if (session.userId === body.userId && session.state === "open") {
577
+ session.webSocket.send(
578
+ JSON.stringify(
579
+ createMessage({
580
+ event: body.event,
581
+ channel: this.channelName,
582
+ data: body.data
583
+ })
584
+ )
585
+ );
586
+ sent = true;
587
+ }
588
+ }
589
+ return new Response(JSON.stringify({ success: true, sent }), {
590
+ headers: { "Content-Type": "application/json" }
591
+ });
592
+ } catch (err) {
593
+ return new Response(
594
+ JSON.stringify({ success: false, error: String(err) }),
595
+ { status: 400, headers: { "Content-Type": "application/json" } }
596
+ );
597
+ }
598
+ }
599
+ // ============================================================================
600
+ // Helpers
601
+ // ============================================================================
602
+ getSessionByWebSocket(ws) {
603
+ for (const session of this.sessions.values()) {
604
+ if (session.webSocket === ws) {
605
+ return session;
606
+ }
607
+ }
608
+ return void 0;
609
+ }
610
+ async broadcastToAll(messageData, excludeSessionId) {
611
+ const message = createMessage(messageData);
612
+ const payload = JSON.stringify(message);
613
+ for (const session of this.sessions.values()) {
614
+ if (session.sessionId === excludeSessionId) continue;
615
+ if (session.state !== "open") continue;
616
+ try {
617
+ session.webSocket.send(payload);
618
+ } catch {
619
+ session.state = "closed";
620
+ }
621
+ }
622
+ }
623
+ };
624
+ var DurableObjectsAdapter = class {
625
+ type = "durable-objects";
626
+ namespace;
627
+ channelPrefix;
628
+ localHandlers = /* @__PURE__ */ new Map();
629
+ constructor(options) {
630
+ this.namespace = options.namespace;
631
+ this.channelPrefix = options.channelPrefix ?? "channel:";
632
+ }
633
+ /**
634
+ * Get Durable Object stub for a channel
635
+ */
636
+ getChannelStub(channel) {
637
+ const id = this.namespace.idFromName(`${this.channelPrefix}${channel}`);
638
+ return this.namespace.get(id);
639
+ }
640
+ /**
641
+ * Get WebSocket URL for a channel
642
+ */
643
+ getWebSocketUrl(channel, sessionId, userId, baseUrl) {
644
+ const base = baseUrl || "wss://your-worker.your-subdomain.workers.dev";
645
+ const params = new URLSearchParams({ sessionId });
646
+ if (userId) params.set("userId", userId);
647
+ return `${base}/realtime/${channel}?${params}`;
648
+ }
649
+ // ============================================================================
650
+ // RealtimeAdapter Implementation
651
+ // ============================================================================
652
+ async subscribe(channel, sessionId, handler) {
653
+ if (!this.localHandlers.has(channel)) {
654
+ this.localHandlers.set(channel, /* @__PURE__ */ new Map());
655
+ }
656
+ this.localHandlers.get(channel).set(sessionId, handler);
657
+ }
658
+ async unsubscribe(channel, sessionId) {
659
+ const handlers = this.localHandlers.get(channel);
660
+ if (handlers) {
661
+ handlers.delete(sessionId);
662
+ if (handlers.size === 0) {
663
+ this.localHandlers.delete(channel);
664
+ }
665
+ }
666
+ }
667
+ async publish(channel, message) {
668
+ const stub = this.getChannelStub(channel);
669
+ await stub.fetch(new Request("https://do/broadcast", {
670
+ method: "POST",
671
+ headers: { "Content-Type": "application/json" },
672
+ body: JSON.stringify({
673
+ event: message.event,
674
+ data: message.data
675
+ })
676
+ }));
677
+ const handlers = this.localHandlers.get(channel);
678
+ if (handlers) {
679
+ for (const handler of handlers.values()) {
680
+ try {
681
+ await handler(message);
682
+ } catch {
683
+ }
684
+ }
685
+ }
686
+ }
687
+ async sendToSession(_sessionId, _message) {
688
+ return false;
689
+ }
690
+ async getSubscribers(channel) {
691
+ const handlers = this.localHandlers.get(channel);
692
+ return handlers ? Array.from(handlers.keys()) : [];
693
+ }
694
+ async setPresence(_channel, _sessionId, _userId, _data) {
695
+ }
696
+ async removePresence(_channel, _sessionId) {
697
+ }
698
+ async getPresence(channel) {
699
+ const stub = this.getChannelStub(channel);
700
+ const response = await stub.fetch(new Request("https://do/presence"));
701
+ return response.json();
702
+ }
703
+ async close() {
704
+ this.localHandlers.clear();
705
+ }
706
+ /**
707
+ * Get channel info from Durable Object
708
+ */
709
+ async getChannelInfo(channel) {
710
+ const stub = this.getChannelStub(channel);
711
+ const response = await stub.fetch(new Request("https://do/info"));
712
+ return response.json();
713
+ }
714
+ /**
715
+ * Send message to specific user in a channel
716
+ */
717
+ async sendToUser(channel, userId, event, data) {
718
+ const stub = this.getChannelStub(channel);
719
+ const response = await stub.fetch(
720
+ new Request("https://do/send", {
721
+ method: "POST",
722
+ headers: { "Content-Type": "application/json" },
723
+ body: JSON.stringify({ userId, event, data })
724
+ })
725
+ );
726
+ const result = await response.json();
727
+ return result.sent;
728
+ }
729
+ };
730
+ function createDurableObjectsAdapter(options) {
731
+ return new DurableObjectsAdapter(options);
732
+ }
733
+ export {
734
+ DurableObjectsAdapter,
735
+ RealtimeChannelDO,
736
+ SSEAdapter,
737
+ createDurableObjectsAdapter,
738
+ createSSEAdapter
739
+ };
740
+ //# sourceMappingURL=index.js.map