openart-realtime-sdk 1.0.1 → 1.0.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.
@@ -1,2 +1,610 @@
1
- export * from './handler.js';
2
- export * from './realtime.js';
1
+ import z from 'zod/v4';
2
+ import * as z2 from 'zod/v4/core';
3
+
4
+ // src/shared/types.ts
5
+ z.discriminatedUnion("type", [
6
+ z.object({
7
+ type: z.literal("connected"),
8
+ channel: z.string(),
9
+ cursor: z.string().optional()
10
+ }),
11
+ z.object({ type: z.literal("reconnect"), timestamp: z.number() }),
12
+ z.object({ type: z.literal("error"), error: z.string() }),
13
+ z.object({ type: z.literal("disconnected"), channels: z.array(z.string()) }),
14
+ z.object({ type: z.literal("ping"), timestamp: z.number() })
15
+ ]);
16
+ var userEvent = z.object({
17
+ id: z.string(),
18
+ data: z.unknown(),
19
+ event: z.string(),
20
+ channel: z.string()
21
+ });
22
+
23
+ // src/server/utils.ts
24
+ function compareStreamIds(a, b) {
25
+ const [aTime = 0, aSeq = 0] = a.split("-").map(Number);
26
+ const [bTime = 0, bSeq = 0] = b.split("-").map(Number);
27
+ if (aTime !== bTime) return aTime - bTime;
28
+ return aSeq - bSeq;
29
+ }
30
+ function parseStreamResponse(response) {
31
+ if (!Array.isArray(response)) return [];
32
+ return response.map((item) => {
33
+ const id = item[0];
34
+ const fields = item[1];
35
+ const data = {};
36
+ if (Array.isArray(fields)) {
37
+ for (let i = 0; i < fields.length; i += 2) {
38
+ const key = fields[i];
39
+ if (typeof key === "string") {
40
+ data[key] = fields[i + 1];
41
+ }
42
+ }
43
+ }
44
+ if (typeof data.data === "string") {
45
+ try {
46
+ data.data = JSON.parse(data.data);
47
+ } catch {
48
+ }
49
+ }
50
+ return { ...data, id };
51
+ });
52
+ }
53
+
54
+ // src/server/handler.ts
55
+ function handle(config) {
56
+ return async (request) => {
57
+ const requestStartTime = Date.now();
58
+ const { searchParams } = new URL(request.url);
59
+ const rawChannels = searchParams.getAll("channel").length > 0 ? searchParams.getAll("channel") : ["default"];
60
+ const channels = [...new Set(rawChannels)];
61
+ const redis = config.realtime._redis;
62
+ const logger = config.realtime._logger;
63
+ const subscriptionManager = config.realtime._subscriptionManager;
64
+ const maxRecoveryLimit = config.maxRecoveryLimit ?? 2e3;
65
+ if (config.middleware) {
66
+ const result = await config.middleware({ request, channels });
67
+ if (result) return result;
68
+ }
69
+ if (!redis || !subscriptionManager) {
70
+ logger.error("No Redis instance provided to Realtime");
71
+ return new Response(JSON.stringify({ error: "Redis not configured" }), {
72
+ status: 500,
73
+ headers: { "Content-Type": "application/json" }
74
+ });
75
+ }
76
+ let cleanup;
77
+ const unsubs = [];
78
+ let reconnectTimeout;
79
+ let keepaliveInterval;
80
+ let isClosed = false;
81
+ let handleAbort;
82
+ const stream = new ReadableStream({
83
+ async start(controller) {
84
+ if (request.signal.aborted) {
85
+ controller.close();
86
+ return;
87
+ }
88
+ cleanup = async () => {
89
+ if (isClosed) return;
90
+ isClosed = true;
91
+ clearTimeout(reconnectTimeout);
92
+ clearInterval(keepaliveInterval);
93
+ if (handleAbort) {
94
+ request.signal.removeEventListener("abort", handleAbort);
95
+ }
96
+ unsubs.forEach((unsub) => unsub());
97
+ try {
98
+ if (!request.signal.aborted) controller.close();
99
+ logger.info("\u2705 Connection closed successfully.");
100
+ } catch (err) {
101
+ logger.error("\u26A0\uFE0F Error closing controller:", err);
102
+ }
103
+ };
104
+ handleAbort = async () => {
105
+ await cleanup?.();
106
+ };
107
+ request.signal.addEventListener("abort", handleAbort);
108
+ const safeEnqueue = (data) => {
109
+ if (isClosed) return;
110
+ if (controller.desiredSize && controller.desiredSize <= 0) {
111
+ logger.warn?.("\u26A0\uFE0F Client too slow, dropping message to prevent OOM.");
112
+ return;
113
+ }
114
+ try {
115
+ controller.enqueue(data);
116
+ } catch (err) {
117
+ logger.error("\u26A0\uFE0F Error closing controller:", err);
118
+ }
119
+ };
120
+ const elapsedMs = Date.now() - requestStartTime;
121
+ const remainingMs = config.realtime._maxDurationSecs * 1e3 - elapsedMs;
122
+ const streamDurationMs = Math.max(remainingMs - 2e3, 1e3);
123
+ reconnectTimeout = setTimeout(async () => {
124
+ const reconnectEvent = {
125
+ type: "reconnect",
126
+ timestamp: Date.now()
127
+ };
128
+ safeEnqueue(json(reconnectEvent));
129
+ await cleanup?.();
130
+ }, streamDurationMs);
131
+ let buffer = [];
132
+ let isHistoryReplayed = false;
133
+ const lastHistoryIds = /* @__PURE__ */ new Map();
134
+ const onManagerMessage = (message, encodedMessage) => {
135
+ logger.debug?.("\u2B07\uFE0F Received event:", message);
136
+ if (!isHistoryReplayed) {
137
+ buffer.push(message);
138
+ } else {
139
+ safeEnqueue(encodedMessage);
140
+ }
141
+ };
142
+ const executeHistoryPipeline = async () => {
143
+ const pipeline = redis.pipeline();
144
+ const channelAcks = /* @__PURE__ */ new Map();
145
+ for (const channel of channels) {
146
+ const connectedEvent = {
147
+ type: "connected",
148
+ channel
149
+ };
150
+ safeEnqueue(json(connectedEvent));
151
+ const lastAck = searchParams.get(`last_ack_${channel}`) ?? String(Date.now());
152
+ channelAcks.set(channel, lastAck);
153
+ pipeline.xrange(
154
+ channel,
155
+ `(${lastAck}`,
156
+ "+",
157
+ "COUNT",
158
+ maxRecoveryLimit
159
+ );
160
+ }
161
+ try {
162
+ return await pipeline.exec();
163
+ } catch (error) {
164
+ logger.error("Error executing history pipeline:", error);
165
+ return null;
166
+ }
167
+ };
168
+ const processHistoryResults = (results) => {
169
+ if (!results) return;
170
+ results.forEach((result, index) => {
171
+ const [err, rawMissing] = result;
172
+ const channel = channels[index];
173
+ if (!channel) return;
174
+ if (err) {
175
+ logger.error(
176
+ `Error fetching history for channel ${channel}:`,
177
+ err
178
+ );
179
+ return;
180
+ }
181
+ const missingMessages = parseStreamResponse(rawMissing);
182
+ if (missingMessages.length > 0) {
183
+ missingMessages.forEach((value) => {
184
+ const eventWithId = value;
185
+ const event = userEvent.safeParse(eventWithId);
186
+ if (event.success) safeEnqueue(json(event.data));
187
+ });
188
+ lastHistoryIds.set(
189
+ channel,
190
+ missingMessages[missingMessages.length - 1]?.id ?? ""
191
+ );
192
+ }
193
+ });
194
+ };
195
+ const flushBuffer = () => {
196
+ for (const msg of buffer) {
197
+ const channelLastId = lastHistoryIds.get(msg.channel);
198
+ if (channelLastId && compareStreamIds(msg.id, channelLastId) <= 0)
199
+ continue;
200
+ safeEnqueue(json(msg));
201
+ }
202
+ buffer = [];
203
+ isHistoryReplayed = true;
204
+ logger.info("\u2705 Subscription established:", { channels });
205
+ };
206
+ try {
207
+ const [_, historyResults] = await Promise.all([
208
+ Promise.all(
209
+ channels.map(async (channel) => {
210
+ const unsub = await subscriptionManager.subscribe(
211
+ channel,
212
+ onManagerMessage
213
+ );
214
+ unsubs.push(unsub);
215
+ })
216
+ ),
217
+ executeHistoryPipeline()
218
+ ]);
219
+ processHistoryResults(historyResults);
220
+ flushBuffer();
221
+ } catch (err) {
222
+ const errorMessage = err instanceof Error ? err.message : "Unknown error";
223
+ logger.error("\u26A0\uFE0F Redis subscriber error:", errorMessage);
224
+ const errorEvent = {
225
+ type: "error",
226
+ error: errorMessage
227
+ };
228
+ safeEnqueue(json(errorEvent));
229
+ }
230
+ keepaliveInterval = setInterval(() => {
231
+ const pingEvent = {
232
+ type: "ping",
233
+ timestamp: Date.now()
234
+ };
235
+ safeEnqueue(json(pingEvent));
236
+ }, 6e4);
237
+ },
238
+ async cancel() {
239
+ if (isClosed) return;
240
+ await cleanup?.();
241
+ }
242
+ });
243
+ return new StreamingResponse(stream);
244
+ };
245
+ }
246
+ var encoder = new TextEncoder();
247
+ function json(data) {
248
+ return encoder.encode(`data: ${JSON.stringify(data)}
249
+
250
+ `);
251
+ }
252
+ var StreamingResponse = class extends Response {
253
+ constructor(res, init) {
254
+ super(res, {
255
+ ...init,
256
+ status: 200,
257
+ headers: {
258
+ "Content-Type": "text/event-stream",
259
+ "Cache-Control": "no-cache",
260
+ Connection: "keep-alive",
261
+ "Content-Encoding": "none",
262
+ "Access-Control-Allow-Origin": "*",
263
+ "Access-Control-Allow-Headers": "Cache-Control",
264
+ ...init?.headers
265
+ }
266
+ });
267
+ }
268
+ };
269
+
270
+ // src/server/subscription-manager.ts
271
+ var encoder2 = new TextEncoder();
272
+ var SubscriptionManager = class {
273
+ redis;
274
+ subRedis;
275
+ // Map<ChannelName, Set<Listener>>
276
+ listeners = /* @__PURE__ */ new Map();
277
+ unsubscribeTimers = /* @__PURE__ */ new Map();
278
+ logger;
279
+ constructor(redis, logger) {
280
+ this.redis = redis;
281
+ this.subRedis = redis.duplicate();
282
+ this.logger = logger;
283
+ this.setupMessageListener();
284
+ }
285
+ setupMessageListener() {
286
+ this.subRedis.on("message", (channel, messageStr) => {
287
+ const handlers = this.listeners.get(channel);
288
+ if (!handlers || handlers.size === 0) return;
289
+ try {
290
+ let payload;
291
+ if (messageStr.startsWith("{") || messageStr.startsWith("[")) {
292
+ try {
293
+ payload = JSON.parse(messageStr);
294
+ } catch {
295
+ payload = { data: messageStr };
296
+ }
297
+ } else {
298
+ payload = { data: messageStr };
299
+ }
300
+ const result = userEvent.safeParse(payload);
301
+ if (result.success) {
302
+ const encodedMessage = encoder2.encode(
303
+ `data: ${JSON.stringify(result.data)}
304
+
305
+ `
306
+ );
307
+ this.logger.debug?.(
308
+ `[SubscriptionManager] Dispatching message to ${handlers.size} listeners on ${channel}`
309
+ );
310
+ handlers.forEach((listener) => {
311
+ try {
312
+ listener(result.data, encodedMessage);
313
+ } catch (listenerErr) {
314
+ this.logger.error(
315
+ `[SubscriptionManager] Error in listener for ${channel}:`,
316
+ listenerErr
317
+ );
318
+ }
319
+ });
320
+ }
321
+ } catch (err) {
322
+ this.logger.error(
323
+ `[SubscriptionManager] Error processing message on ${channel}:`,
324
+ err
325
+ );
326
+ }
327
+ });
328
+ this.subRedis.on("error", (err) => {
329
+ this.logger.error("[SubscriptionManager] Redis subscription error:", err);
330
+ });
331
+ }
332
+ async subscribe(channel, listener) {
333
+ if (this.unsubscribeTimers.has(channel)) {
334
+ clearTimeout(this.unsubscribeTimers.get(channel));
335
+ this.unsubscribeTimers.delete(channel);
336
+ this.logger.debug?.(
337
+ `[SubscriptionManager] Cancelled pending unsubscribe for: ${channel}`
338
+ );
339
+ }
340
+ if (!this.listeners.has(channel)) {
341
+ this.listeners.set(channel, /* @__PURE__ */ new Set());
342
+ this.logger.debug?.(
343
+ `[SubscriptionManager] Subscribing to Redis channel: ${channel}`
344
+ );
345
+ await this.subRedis.subscribe(channel);
346
+ }
347
+ const channelListeners = this.listeners.get(channel);
348
+ channelListeners.add(listener);
349
+ return () => {
350
+ const currentListeners = this.listeners.get(channel);
351
+ if (currentListeners) {
352
+ currentListeners.delete(listener);
353
+ if (currentListeners.size === 0) {
354
+ if (this.unsubscribeTimers.has(channel)) {
355
+ clearTimeout(this.unsubscribeTimers.get(channel));
356
+ }
357
+ const timer = setTimeout(() => {
358
+ this.listeners.delete(channel);
359
+ this.unsubscribeTimers.delete(channel);
360
+ this.logger.debug?.(
361
+ `[SubscriptionManager] Unsubscribing from Redis channel: ${channel}`
362
+ );
363
+ this.subRedis.unsubscribe(channel).catch((err) => {
364
+ this.logger.error(
365
+ `[SubscriptionManager] Error unsubscribing from ${channel}:`,
366
+ err
367
+ );
368
+ });
369
+ }, 2e3);
370
+ this.unsubscribeTimers.set(channel, timer);
371
+ }
372
+ }
373
+ };
374
+ }
375
+ async disconnect() {
376
+ await this.subRedis.quit().catch(() => this.subRedis.disconnect());
377
+ }
378
+ };
379
+
380
+ // src/server/realtime.ts
381
+ var DEFAULT_VERCEL_FLUID_TIMEOUT = 300;
382
+ var RealtimeBase = class {
383
+ channels = {};
384
+ _schema;
385
+ _verbose;
386
+ _history;
387
+ /** @internal */
388
+ _redis;
389
+ /** @internal */
390
+ _subscriptionManager;
391
+ /** @internal */
392
+ _maxDurationSecs;
393
+ /** @internal */
394
+ _logger;
395
+ constructor(data) {
396
+ Object.assign(this, data);
397
+ this._schema = data.schema || {};
398
+ this._redis = data.redis;
399
+ this._maxDurationSecs = data.maxDurationSecs ?? DEFAULT_VERCEL_FLUID_TIMEOUT;
400
+ this._verbose = data.verbose ?? false;
401
+ this._history = typeof data.history === "boolean" ? {} : data.history ?? {};
402
+ this._logger = data.logger ?? {
403
+ info: (...args) => {
404
+ if (this._verbose) console.log(...args);
405
+ },
406
+ warn: (...args) => {
407
+ console.warn(...args);
408
+ },
409
+ error: (...args) => {
410
+ console.error(...args);
411
+ },
412
+ debug: (...args) => {
413
+ if (this._verbose) console.debug(...args);
414
+ }
415
+ };
416
+ if (this._redis) {
417
+ this._subscriptionManager = new SubscriptionManager(
418
+ this._redis,
419
+ this._logger
420
+ );
421
+ }
422
+ Object.assign(this, this.createEventHandlers("default"));
423
+ }
424
+ createEventHandlers(channel, historyOverride) {
425
+ const historyConfig = {
426
+ ...this._history,
427
+ ...typeof historyOverride === "boolean" ? {} : historyOverride ?? {}
428
+ };
429
+ let unsubscribe = void 0;
430
+ let pingInterval = void 0;
431
+ const startPingInterval = () => {
432
+ pingInterval = setInterval(() => {
433
+ this._redis?.publish(
434
+ channel,
435
+ JSON.stringify({ type: "ping", timestamp: Date.now() })
436
+ );
437
+ }, 6e4);
438
+ };
439
+ const stopPingInterval = () => {
440
+ if (pingInterval) clearInterval(pingInterval);
441
+ };
442
+ const historyFunc = async (args) => {
443
+ const redis = this._redis;
444
+ if (!redis) throw new Error("Redis not configured.");
445
+ const start = args?.start ? String(args.start) : "-";
446
+ const end = args?.end ? String(args.end) : "+";
447
+ const limit = args?.limit ?? 1e3;
448
+ const rawHistory = await redis.xrange(
449
+ channel,
450
+ start,
451
+ end,
452
+ "COUNT",
453
+ limit
454
+ );
455
+ const historyMessages = parseStreamResponse(rawHistory);
456
+ return historyMessages.map((value) => {
457
+ if (typeof value === "object" && value !== null) {
458
+ const { id, channel: channel2, event, data } = value;
459
+ return { data, event, id, channel: channel2 };
460
+ }
461
+ return null;
462
+ }).filter((item) => item !== null);
463
+ };
464
+ const unsubscribeFunc = () => {
465
+ if (unsubscribe) {
466
+ unsubscribe();
467
+ this._logger.info("\u2705 Connection closed successfully.");
468
+ }
469
+ };
470
+ const subscribeFunc = async ({
471
+ events,
472
+ onData,
473
+ history
474
+ }) => {
475
+ const redis = this._redis;
476
+ if (!redis) throw new Error("Redis not configured.");
477
+ const subManager = this._subscriptionManager;
478
+ if (!subManager) throw new Error("SubscriptionManager not initialized.");
479
+ const buffer = [];
480
+ let isHistoryReplayed = false;
481
+ let lastHistoryId = null;
482
+ const onMessage = (message) => {
483
+ if (events && !events.includes(message.event)) return;
484
+ if (!isHistoryReplayed) {
485
+ buffer.push(message);
486
+ } else {
487
+ onData(message);
488
+ }
489
+ };
490
+ const unsubFromManager = await subManager.subscribe(channel, onMessage);
491
+ try {
492
+ if (history) {
493
+ const start = typeof history === "object" && history.start ? String(history.start) : "-";
494
+ const end = typeof history === "object" && history.end ? String(history.end) : "+";
495
+ const limit = typeof history === "object" ? history.limit : void 0;
496
+ let rawMessages = [];
497
+ if (limit) {
498
+ rawMessages = await redis.xrange(
499
+ channel,
500
+ start,
501
+ end,
502
+ "COUNT",
503
+ limit
504
+ );
505
+ } else {
506
+ rawMessages = await redis.xrange(channel, start, end);
507
+ }
508
+ const messages = parseStreamResponse(rawMessages);
509
+ for (const message of messages) {
510
+ const typedMessage = message;
511
+ if (!typedMessage.event || events && !events.includes(typedMessage.event))
512
+ continue;
513
+ const result = userEvent.safeParse(message);
514
+ if (result.success) {
515
+ onData(
516
+ result.data
517
+ );
518
+ }
519
+ }
520
+ if (messages.length > 0) {
521
+ lastHistoryId = messages[messages.length - 1]?.id ?? null;
522
+ }
523
+ }
524
+ for (const message of buffer) {
525
+ if (lastHistoryId && compareStreamIds(message.id, lastHistoryId) <= 0)
526
+ continue;
527
+ onData(message);
528
+ }
529
+ buffer.length = 0;
530
+ isHistoryReplayed = true;
531
+ startPingInterval();
532
+ } catch (err) {
533
+ unsubFromManager();
534
+ throw err;
535
+ }
536
+ unsubscribe = () => {
537
+ stopPingInterval();
538
+ unsubFromManager();
539
+ };
540
+ return unsubscribe;
541
+ };
542
+ const findSchema = (path) => {
543
+ let current = this._schema;
544
+ for (const key of path) {
545
+ if (!current || typeof current !== "object") return void 0;
546
+ current = current[key];
547
+ }
548
+ const typedCurrent = current;
549
+ return typedCurrent?._zod || typedCurrent?._def ? current : void 0;
550
+ };
551
+ const emitFunc = async (event, data, opts) => {
552
+ const pathParts = event.split(".");
553
+ const schema = findSchema(pathParts);
554
+ if (schema) {
555
+ z2.parse(schema, data);
556
+ }
557
+ if (!this._redis) {
558
+ this._logger.warn("No Redis instance provided to Realtime.");
559
+ return;
560
+ }
561
+ const currentHistoryConfig = {
562
+ ...historyConfig,
563
+ ...typeof opts?.history === "boolean" ? {} : opts?.history ?? {}
564
+ };
565
+ const xaddArgs = [channel];
566
+ if (currentHistoryConfig.maxLength) {
567
+ xaddArgs.push("MAXLEN", "~", currentHistoryConfig.maxLength);
568
+ }
569
+ xaddArgs.push("*");
570
+ const dataStr = typeof data === "object" ? JSON.stringify(data) : String(data);
571
+ xaddArgs.push("data", dataStr);
572
+ xaddArgs.push("event", event);
573
+ xaddArgs.push("channel", channel);
574
+ const id = await this._redis.xadd(...xaddArgs);
575
+ const payload = {
576
+ data,
577
+ event,
578
+ channel,
579
+ id
580
+ };
581
+ const pipeline = this._redis.pipeline();
582
+ if (currentHistoryConfig.expireAfterSecs) {
583
+ pipeline.expire(channel, currentHistoryConfig.expireAfterSecs);
584
+ }
585
+ pipeline.publish(channel, JSON.stringify(payload));
586
+ await pipeline.exec();
587
+ this._logger.info(`\u2B06\uFE0F Emitted event:`, {
588
+ id,
589
+ data,
590
+ event,
591
+ channel
592
+ });
593
+ };
594
+ return {
595
+ history: historyFunc,
596
+ unsubscribe: unsubscribeFunc,
597
+ subscribe: subscribeFunc,
598
+ emit: emitFunc
599
+ };
600
+ }
601
+ channel(channel, history) {
602
+ if (!this.channels[channel]) {
603
+ this.channels[channel] = this.createEventHandlers(channel, history);
604
+ }
605
+ return this.channels[channel];
606
+ }
607
+ };
608
+ var Realtime = RealtimeBase;
609
+
610
+ export { Realtime, StreamingResponse, handle, json };
@@ -1,12 +1,29 @@
1
1
  'use strict';
2
2
 
3
- var types = require('./types');
3
+ var z = require('zod/v4');
4
4
 
5
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
5
6
 
7
+ var z__default = /*#__PURE__*/_interopDefault(z);
6
8
 
7
- Object.keys(types).forEach(function (k) {
8
- if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
9
- enumerable: true,
10
- get: function () { return types[k]; }
11
- });
9
+ // src/shared/types.ts
10
+ var systemEvent = z__default.default.discriminatedUnion("type", [
11
+ z__default.default.object({
12
+ type: z__default.default.literal("connected"),
13
+ channel: z__default.default.string(),
14
+ cursor: z__default.default.string().optional()
15
+ }),
16
+ z__default.default.object({ type: z__default.default.literal("reconnect"), timestamp: z__default.default.number() }),
17
+ z__default.default.object({ type: z__default.default.literal("error"), error: z__default.default.string() }),
18
+ z__default.default.object({ type: z__default.default.literal("disconnected"), channels: z__default.default.array(z__default.default.string()) }),
19
+ z__default.default.object({ type: z__default.default.literal("ping"), timestamp: z__default.default.number() })
20
+ ]);
21
+ var userEvent = z__default.default.object({
22
+ id: z__default.default.string(),
23
+ data: z__default.default.unknown(),
24
+ event: z__default.default.string(),
25
+ channel: z__default.default.string()
12
26
  });
27
+
28
+ exports.systemEvent = systemEvent;
29
+ exports.userEvent = userEvent;
@@ -1 +1,22 @@
1
- export * from './types';
1
+ import z from 'zod/v4';
2
+
3
+ // src/shared/types.ts
4
+ var systemEvent = z.discriminatedUnion("type", [
5
+ z.object({
6
+ type: z.literal("connected"),
7
+ channel: z.string(),
8
+ cursor: z.string().optional()
9
+ }),
10
+ z.object({ type: z.literal("reconnect"), timestamp: z.number() }),
11
+ z.object({ type: z.literal("error"), error: z.string() }),
12
+ z.object({ type: z.literal("disconnected"), channels: z.array(z.string()) }),
13
+ z.object({ type: z.literal("ping"), timestamp: z.number() })
14
+ ]);
15
+ var userEvent = z.object({
16
+ id: z.string(),
17
+ data: z.unknown(),
18
+ event: z.string(),
19
+ channel: z.string()
20
+ });
21
+
22
+ export { systemEvent, userEvent };