openart-realtime-sdk 1.0.1 → 1.0.2

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