@borealise/pipeline 1.0.0-alpha.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/dist/index.mjs ADDED
@@ -0,0 +1,481 @@
1
+ // src/constants/opcodes.ts
2
+ var Opcodes = {
3
+ IDENTIFY: 0,
4
+ HEARTBEAT: 1,
5
+ PRESENCE_UPDATE: 2,
6
+ SUBSCRIBE: 3,
7
+ UNSUBSCRIBE: 4,
8
+ REQUEST: 5,
9
+ CHAT_SEND: 6,
10
+ HELLO: 16,
11
+ HEARTBEAT_ACK: 17,
12
+ READY: 18,
13
+ INVALID_SESSION: 19,
14
+ RECONNECT: 20,
15
+ DISPATCH: 21,
16
+ ERROR: 255
17
+ };
18
+ var Events = {
19
+ USER_UPDATE: 0,
20
+ USER_PRESENCE_UPDATE: 1,
21
+ USER_TYPING: 2,
22
+ USER_LEVEL_UP: 3,
23
+ SESSION_CREATE: 16,
24
+ SESSION_DELETE: 17,
25
+ SESSION_UPDATE: 18,
26
+ NOTIFICATION_CREATE: 32,
27
+ NOTIFICATION_DELETE: 33,
28
+ ROOM_JOIN: 48,
29
+ ROOM_LEAVE: 49,
30
+ ROOM_UPDATE: 50,
31
+ ROOM_DELETE: 51,
32
+ ROOM_USER_JOIN: 64,
33
+ ROOM_USER_LEAVE: 65,
34
+ ROOM_USER_KICK: 66,
35
+ ROOM_USER_BAN: 67,
36
+ ROOM_USER_MUTE: 68,
37
+ ROOM_USER_UNMUTE: 69,
38
+ ROOM_USER_ROLE_UPDATE: 70,
39
+ ROOM_USER_AVATAR_UPDATE: 71,
40
+ ROOM_USER_SUBSCRIPTION_UPDATE: 72,
41
+ ROOM_CHAT_MESSAGE: 80,
42
+ ROOM_CHAT_DELETE: 81,
43
+ ROOM_DJ_ADVANCE: 96,
44
+ ROOM_DJ_UPDATE: 97,
45
+ ROOM_WAITLIST_JOIN: 98,
46
+ ROOM_WAITLIST_LEAVE: 99,
47
+ ROOM_WAITLIST_UPDATE: 100,
48
+ ROOM_WAITLIST_LOCK: 101,
49
+ ROOM_WAITLIST_CYCLE: 102,
50
+ ROOM_TIME_SYNC: 103,
51
+ ROOM_VOTE: 112,
52
+ ROOM_GRAB: 113,
53
+ FRIEND_REQUEST: 128,
54
+ FRIEND_REQUEST_CANCEL: 129,
55
+ FRIEND_ACCEPT: 130,
56
+ FRIEND_REMOVE: 131,
57
+ SYSTEM_MESSAGE: 240,
58
+ MAINTENANCE: 241,
59
+ RATE_LIMIT: 242
60
+ };
61
+ var Presence = {
62
+ ONLINE: 0,
63
+ IDLE: 1,
64
+ DND: 2,
65
+ INVISIBLE: 3,
66
+ OFFLINE: 4
67
+ };
68
+ var Activity = {
69
+ NONE: 0,
70
+ VIEWING: 1,
71
+ EDITING: 2,
72
+ IDLE: 3,
73
+ STREAMING: 4,
74
+ LISTENING: 5,
75
+ WATCHING: 6,
76
+ CUSTOM: 255
77
+ };
78
+ var Roles = {
79
+ USER: 0,
80
+ MODERATOR: 1,
81
+ ADMIN: 2,
82
+ OWNER: 255
83
+ };
84
+ var CloseCodes = {
85
+ NORMAL: 1e3,
86
+ GOING_AWAY: 1001,
87
+ PROTOCOL_ERROR: 1002,
88
+ UNKNOWN_ERROR: 4e3,
89
+ UNKNOWN_OPCODE: 4001,
90
+ DECODE_ERROR: 4002,
91
+ NOT_AUTHENTICATED: 4003,
92
+ AUTHENTICATION_FAILED: 4004,
93
+ ALREADY_AUTHENTICATED: 4005,
94
+ INVALID_SESSION: 4006,
95
+ RATE_LIMITED: 4008,
96
+ SESSION_TIMEOUT: 4009,
97
+ SERVER_SHUTDOWN: 4010
98
+ };
99
+ var PipelineErrors = {
100
+ CHAT_MESSAGE_EMPTY: 4100,
101
+ CHAT_MESSAGE_TOO_LONG: 4101,
102
+ CHAT_ROOM_NOT_FOUND: 4102,
103
+ CHAT_NOT_IN_ROOM: 4103,
104
+ CHAT_USER_MUTED: 4104,
105
+ ROOM_NOT_FOUND: 4200,
106
+ ROOM_NOT_ACTIVE: 4201,
107
+ ROOM_ALREADY_MEMBER: 4202,
108
+ ROOM_NOT_MEMBER: 4203,
109
+ ROOM_BANNED: 4204,
110
+ ROOM_FULL: 4205,
111
+ WAITLIST_LOCKED: 4300,
112
+ WAITLIST_FULL: 4301,
113
+ WAITLIST_ALREADY_IN: 4302,
114
+ WAITLIST_NOT_IN: 4303,
115
+ VOTE_INVALID: 4400,
116
+ VOTE_ALREADY_VOTED: 4401,
117
+ VOTE_NO_TRACK: 4402
118
+ };
119
+ function getPipelineErrorName(code) {
120
+ return Object.entries(PipelineErrors).find(([, value]) => value === code)?.[0] || "UNKNOWN_ERROR";
121
+ }
122
+ function getEventName(code) {
123
+ return Object.entries(Events).find(([, value]) => value === code)?.[0] || "UNKNOWN";
124
+ }
125
+
126
+ // src/logger.ts
127
+ var _Logger = class _Logger {
128
+ constructor(name, options = {}) {
129
+ this.name = name;
130
+ this.options = options;
131
+ }
132
+ static create(name, options = {}) {
133
+ const logger = new _Logger(name, options);
134
+ _Logger.loggers.set(name, logger);
135
+ return logger;
136
+ }
137
+ enabled(level) {
138
+ return level >= (this.options.minLevel ?? 0 /* DEBUG */);
139
+ }
140
+ debug(message, ...args) {
141
+ if (!this.enabled(0 /* DEBUG */)) return;
142
+ console.log(`[DEBUG] [${this.name}] ${message}`, ...args);
143
+ }
144
+ info(message, ...args) {
145
+ if (!this.enabled(1 /* INFO */)) return;
146
+ console.info(`[INFO] [${this.name}] ${message}`, ...args);
147
+ }
148
+ warn(message, ...args) {
149
+ if (!this.enabled(2 /* WARN */)) return;
150
+ console.warn(`[WARN] [${this.name}] ${message}`, ...args);
151
+ }
152
+ error(message, ...args) {
153
+ if (!this.enabled(3 /* ERROR */)) return;
154
+ console.error(`[ERROR] [${this.name}] ${message}`, ...args);
155
+ }
156
+ };
157
+ _Logger.loggers = /* @__PURE__ */ new Map();
158
+ var Logger = _Logger;
159
+
160
+ // src/PipelineClient.ts
161
+ var PipelineClient = class {
162
+ constructor(options) {
163
+ this.ws = null;
164
+ this.sessionId = null;
165
+ this.heartbeatInterval = null;
166
+ this.heartbeatTimer = null;
167
+ this.reconnectTimer = null;
168
+ this.reconnectAttempts = 0;
169
+ this.lastSequence = 0;
170
+ this.subscriptions = /* @__PURE__ */ new Set();
171
+ this._state = "disconnected";
172
+ this._user = null;
173
+ this.eventListeners = /* @__PURE__ */ new Map();
174
+ this.connectionListeners = /* @__PURE__ */ new Map();
175
+ this.dispatchHandler = null;
176
+ this.maxReconnectAttempts = 10;
177
+ this.reconnectBackoff = [1e3, 2e3, 5e3, 1e4, 3e4];
178
+ this.options = { ...options };
179
+ this.logger = Logger.create(options.loggerName || "Pipeline");
180
+ }
181
+ get state() {
182
+ return this._state;
183
+ }
184
+ get user() {
185
+ return this._user;
186
+ }
187
+ get isConnected() {
188
+ return this._state === "connected" || this._state === "identified";
189
+ }
190
+ get isIdentified() {
191
+ return this._state === "identified";
192
+ }
193
+ setDispatchHandler(handler) {
194
+ this.dispatchHandler = handler;
195
+ }
196
+ connect() {
197
+ if (!this.options.url) {
198
+ this.logger.error("Cannot connect: missing pipeline url");
199
+ return;
200
+ }
201
+ if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
202
+ this.logger.warn("Already connected or connecting");
203
+ return;
204
+ }
205
+ this.setState("connecting");
206
+ try {
207
+ const factory = this.options.webSocketFactory || ((url) => new WebSocket(url));
208
+ this.ws = factory(this.options.url);
209
+ this.ws.onopen = () => this.handleOpen();
210
+ this.ws.onmessage = (event) => this.handleMessage(event);
211
+ this.ws.onclose = (event) => this.handleClose(event);
212
+ this.ws.onerror = (event) => this.handleError(event);
213
+ } catch (error) {
214
+ this.logger.error("Connection failed", error);
215
+ this.scheduleReconnect();
216
+ }
217
+ }
218
+ disconnect() {
219
+ this.clearTimers();
220
+ this.reconnectAttempts = 0;
221
+ if (this.ws) {
222
+ this.ws.close(CloseCodes.NORMAL, "client disconnect");
223
+ this.ws = null;
224
+ }
225
+ this.sessionId = null;
226
+ this._user = null;
227
+ this.setState("disconnected");
228
+ }
229
+ identify(token) {
230
+ this.send(Opcodes.IDENTIFY, { token });
231
+ }
232
+ updatePresence(status, activity) {
233
+ this.send(Opcodes.PRESENCE_UPDATE, { status, activity });
234
+ }
235
+ subscribe(events) {
236
+ for (const event of events) {
237
+ this.subscriptions.add(event);
238
+ }
239
+ if (this.isIdentified) {
240
+ this.send(Opcodes.SUBSCRIBE, { events });
241
+ }
242
+ }
243
+ unsubscribe(events) {
244
+ for (const event of events) {
245
+ this.subscriptions.delete(event);
246
+ }
247
+ if (this.isIdentified) {
248
+ this.send(Opcodes.UNSUBSCRIBE, { events });
249
+ }
250
+ }
251
+ sendChatMessage(roomSlug, content) {
252
+ if (!this.isIdentified) {
253
+ this.logger.warn("Cannot send chat: not identified");
254
+ return false;
255
+ }
256
+ this.send(Opcodes.CHAT_SEND, {
257
+ room_slug: roomSlug,
258
+ content
259
+ });
260
+ return true;
261
+ }
262
+ on(event, listener) {
263
+ if (!this.eventListeners.has(event)) {
264
+ this.eventListeners.set(event, /* @__PURE__ */ new Set());
265
+ }
266
+ this.eventListeners.get(event).add(listener);
267
+ return () => this.off(event, listener);
268
+ }
269
+ off(event, listener) {
270
+ this.eventListeners.get(event)?.delete(listener);
271
+ }
272
+ onConnection(event, listener) {
273
+ if (!this.connectionListeners.has(event)) {
274
+ this.connectionListeners.set(event, /* @__PURE__ */ new Set());
275
+ }
276
+ this.connectionListeners.get(event).add(listener);
277
+ return () => this.offConnection(event, listener);
278
+ }
279
+ offConnection(event, listener) {
280
+ this.connectionListeners.get(event)?.delete(listener);
281
+ }
282
+ setState(state) {
283
+ if (this._state === state) return;
284
+ this._state = state;
285
+ this.emit("onStateChange", state);
286
+ this.dispatchHandler?.("pipeline/setConnectionState", state);
287
+ }
288
+ clearTimers() {
289
+ if (this.heartbeatTimer !== null) {
290
+ clearInterval(this.heartbeatTimer);
291
+ this.heartbeatTimer = null;
292
+ }
293
+ if (this.reconnectTimer !== null) {
294
+ clearTimeout(this.reconnectTimer);
295
+ this.reconnectTimer = null;
296
+ }
297
+ }
298
+ handleOpen() {
299
+ this.logger.info("Connected");
300
+ this.setState("connected");
301
+ this.reconnectAttempts = 0;
302
+ this.emit("onConnect");
303
+ }
304
+ handleMessage(event) {
305
+ try {
306
+ const message = JSON.parse(String(event.data));
307
+ switch (message.op) {
308
+ case Opcodes.HELLO:
309
+ this.handleHello(message.d);
310
+ break;
311
+ case Opcodes.HEARTBEAT_ACK:
312
+ break;
313
+ case Opcodes.READY:
314
+ this.handleReady(message.d);
315
+ break;
316
+ case Opcodes.INVALID_SESSION:
317
+ this.handleInvalidSession(message.d);
318
+ break;
319
+ case Opcodes.RECONNECT:
320
+ this.handleReconnect();
321
+ break;
322
+ case Opcodes.DISPATCH:
323
+ this.handleDispatch(message.t, message.d, message.s);
324
+ break;
325
+ case Opcodes.ERROR:
326
+ this.handleServerError(message.d);
327
+ break;
328
+ default:
329
+ this.logger.warn(`Unknown opcode: ${message.op}`);
330
+ }
331
+ } catch (error) {
332
+ this.logger.error("Failed to parse message", error);
333
+ }
334
+ }
335
+ handleClose(event) {
336
+ this.logger.info(`Disconnected: ${event.code} - ${event.reason}`);
337
+ this.clearTimers();
338
+ this.ws = null;
339
+ this.emit("onDisconnect", event.code, event.reason);
340
+ const noReconnectCodes = [
341
+ CloseCodes.AUTHENTICATION_FAILED,
342
+ CloseCodes.NOT_AUTHENTICATED,
343
+ CloseCodes.NORMAL
344
+ ];
345
+ if (!noReconnectCodes.includes(event.code)) {
346
+ this.scheduleReconnect();
347
+ return;
348
+ }
349
+ this.setState("disconnected");
350
+ }
351
+ handleError(_event) {
352
+ this.logger.error("WebSocket error");
353
+ }
354
+ handleHello(payload) {
355
+ this.sessionId = payload.session_id;
356
+ this.heartbeatInterval = payload.heartbeat_interval;
357
+ this.startHeartbeat();
358
+ const token = this.resolveToken();
359
+ if (token) {
360
+ this.identify(token);
361
+ }
362
+ }
363
+ handleReady(payload) {
364
+ this._user = payload.user;
365
+ this.setState("identified");
366
+ this.emit("onReady", payload);
367
+ this.dispatchHandler?.("pipeline/setReady", payload);
368
+ if (this.subscriptions.size > 0) {
369
+ this.subscribe(Array.from(this.subscriptions));
370
+ }
371
+ }
372
+ handleInvalidSession(payload) {
373
+ if (!payload.resumable) {
374
+ this._user = null;
375
+ this.setState("connected");
376
+ this.dispatchHandler?.("pipeline/setInvalidSession");
377
+ }
378
+ }
379
+ handleReconnect() {
380
+ this.logger.info("Server requested reconnect");
381
+ this.disconnect();
382
+ this.connect();
383
+ }
384
+ handleDispatch(event, data, sequence) {
385
+ if (typeof sequence === "number") {
386
+ this.lastSequence = sequence;
387
+ }
388
+ this.emitEvent(event, data);
389
+ this.emit("onDispatch", event, data);
390
+ this.dispatchHandler?.("pipeline/handleDispatch", { event, data });
391
+ }
392
+ handleServerError(payload) {
393
+ this.logger.error(`Server error: ${payload.code} - ${payload.message || "unknown"}`);
394
+ this.emit("onError", payload);
395
+ this.dispatchHandler?.("pipeline/handleServerError", payload);
396
+ }
397
+ startHeartbeat() {
398
+ if (this.heartbeatTimer !== null) {
399
+ clearInterval(this.heartbeatTimer);
400
+ }
401
+ if (!this.heartbeatInterval) return;
402
+ const jitter = this.heartbeatInterval * 0.1 * (Math.random() * 2 - 1);
403
+ const interval = this.heartbeatInterval + jitter;
404
+ this.heartbeatTimer = window.setInterval(() => {
405
+ this.sendHeartbeat();
406
+ }, interval);
407
+ this.sendHeartbeat();
408
+ }
409
+ sendHeartbeat() {
410
+ this.send(Opcodes.HEARTBEAT, {
411
+ seq: this.lastSequence || null
412
+ });
413
+ }
414
+ scheduleReconnect() {
415
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
416
+ this.logger.error("Max reconnect attempts reached");
417
+ this.setState("disconnected");
418
+ return;
419
+ }
420
+ this.setState("reconnecting");
421
+ const backoffIndex = Math.min(this.reconnectAttempts, this.reconnectBackoff.length - 1);
422
+ const delay = this.reconnectBackoff[backoffIndex];
423
+ this.reconnectTimer = window.setTimeout(() => {
424
+ this.reconnectAttempts += 1;
425
+ this.emit("onReconnect", this.reconnectAttempts);
426
+ this.connect();
427
+ }, delay);
428
+ }
429
+ send(op, data) {
430
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
431
+ this.logger.warn("Cannot send: not connected");
432
+ return;
433
+ }
434
+ const message = { op, d: data };
435
+ this.ws.send(JSON.stringify(message));
436
+ }
437
+ emitEvent(event, data) {
438
+ const listeners = this.eventListeners.get(event);
439
+ if (!listeners) return;
440
+ for (const listener of listeners) {
441
+ try {
442
+ listener(data);
443
+ } catch (error) {
444
+ this.logger.error(`Event listener error for ${event}`, error);
445
+ }
446
+ }
447
+ }
448
+ emit(event, ...args) {
449
+ const listeners = this.connectionListeners.get(event);
450
+ if (!listeners) return;
451
+ for (const listener of listeners) {
452
+ try {
453
+ ;
454
+ listener(...args);
455
+ } catch (error) {
456
+ this.logger.error(`Connection listener error for ${event}`, error);
457
+ }
458
+ }
459
+ }
460
+ resolveToken() {
461
+ const fromProvider = this.options.tokenProvider?.();
462
+ if (!fromProvider) return null;
463
+ return fromProvider;
464
+ }
465
+ };
466
+ function createPipeline(options) {
467
+ return new PipelineClient(options);
468
+ }
469
+ export {
470
+ Activity,
471
+ CloseCodes,
472
+ Events,
473
+ Opcodes,
474
+ PipelineClient,
475
+ PipelineErrors,
476
+ Presence,
477
+ Roles,
478
+ createPipeline,
479
+ getEventName,
480
+ getPipelineErrorName
481
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@borealise/pipeline",
3
+ "version": "1.0.0-alpha.0",
4
+ "description": "Official realtime pipeline client for Borealise",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
21
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
+ "lint": "tsc --noEmit",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "borealise",
27
+ "pipeline",
28
+ "websocket",
29
+ "realtime",
30
+ "client"
31
+ ],
32
+ "author": "Borealise",
33
+ "license": "MIT",
34
+ "homepage": "https://borealise.com",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/Borealise-Platform/pipeline"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/Borealise-Platform/pipeline/issues"
41
+ },
42
+ "devDependencies": {
43
+ "tsup": "^8.5.0",
44
+ "typescript": "^5.9.2"
45
+ }
46
+ }