@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/README.md ADDED
@@ -0,0 +1,252 @@
1
+ # @borealise/pipeline
2
+
3
+ Official Borealise realtime pipeline client.
4
+
5
+ This package provides a typed WebSocket client for Borealise pipeline events, including:
6
+
7
+ - identify and session bootstrap
8
+ - heartbeat management
9
+ - dispatch event subscriptions
10
+ - reconnect with backoff
11
+ - strongly typed opcodes and payloads
12
+
13
+ ## Runtime support
14
+
15
+ - Browser: works out of the box using native WebSocket.
16
+ - Node.js: you must provide a WebSocket implementation, usually ws.
17
+
18
+ ## Installation
19
+
20
+ Install the package:
21
+
22
+ ```bash
23
+ npm install @borealise/pipeline
24
+ ```
25
+
26
+ If you run this in Node.js, install ws too:
27
+
28
+ ```bash
29
+ npm install ws
30
+ ```
31
+
32
+ ## Quick start (browser)
33
+
34
+ ```ts
35
+ import { createPipeline, Events } from '@borealise/pipeline'
36
+
37
+ const pipeline = createPipeline({
38
+ url: 'wss://prod.borealise.com/ws',
39
+ tokenProvider: () => localStorage.getItem('token'),
40
+ loggerName: 'Pipeline',
41
+ })
42
+
43
+ pipeline.onConnection('onReady', (ready) => {
44
+ console.log('ready session', ready.session_id)
45
+ pipeline.subscribe([Events.USER_UPDATE, Events.ROOM_CHAT_MESSAGE])
46
+ })
47
+
48
+ pipeline.onConnection('onDisconnect', (code, reason) => {
49
+ console.log('disconnected', code, reason)
50
+ })
51
+
52
+ pipeline.connect()
53
+ ```
54
+
55
+ ## Quick start (Node.js with ws)
56
+
57
+ Node has no global WebSocket by default. Inject one via webSocketFactory.
58
+
59
+ ```ts
60
+ import WebSocket from 'ws'
61
+ import { createPipeline, Events } from '@borealise/pipeline'
62
+
63
+ const pipeline = createPipeline({
64
+ url: 'wss://prod.borealise.com/ws',
65
+ tokenProvider: () => process.env.BOREALISE_TOKEN,
66
+ webSocketFactory: (url) => new WebSocket(url) as unknown as WebSocket,
67
+ })
68
+
69
+ pipeline.on(Events.USER_UPDATE, (payload) => {
70
+ console.log('user update', payload)
71
+ })
72
+
73
+ pipeline.connect()
74
+ ```
75
+
76
+ ## How the pipeline works
77
+
78
+ ### 1. Connect
79
+
80
+ When you call connect(), the client opens the WebSocket and moves state to connecting.
81
+
82
+ ### 2. HELLO handshake
83
+
84
+ Server sends HELLO with:
85
+
86
+ - session_id
87
+ - heartbeat_interval
88
+
89
+ Client starts heartbeat timer with jitter and resolves auth token from tokenProvider.
90
+
91
+ ### 3. Identify
92
+
93
+ If tokenProvider returns a token, the client sends IDENTIFY.
94
+
95
+ ### 4. READY
96
+
97
+ On successful auth, server sends READY.
98
+
99
+ Client:
100
+
101
+ - moves state to identified
102
+ - stores user/session metadata
103
+ - resubscribes previous event subscriptions
104
+
105
+ ### 5. Dispatch
106
+
107
+ Server emits DISPATCH frames with event code and payload.
108
+
109
+ Client fan-outs events to:
110
+
111
+ - event listeners registered via on(event, listener)
112
+ - connection listener onDispatch
113
+ - optional dispatch bridge set via setDispatchHandler
114
+
115
+ ### 6. Reconnect behavior
116
+
117
+ On non-terminal disconnects, client reconnects automatically with backoff:
118
+
119
+ - 1000ms
120
+ - 2000ms
121
+ - 5000ms
122
+ - 10000ms
123
+ - 30000ms
124
+
125
+ Max attempts: 10.
126
+
127
+ No reconnect for normal/explicit auth close codes.
128
+
129
+ ## State model
130
+
131
+ Possible connection states:
132
+
133
+ - disconnected
134
+ - connecting
135
+ - connected
136
+ - reconnecting
137
+ - identified
138
+
139
+ Useful getters:
140
+
141
+ - pipeline.state
142
+ - pipeline.user
143
+ - pipeline.isConnected
144
+ - pipeline.isIdentified
145
+
146
+ ## API surface
147
+
148
+ ### Connection
149
+
150
+ - connect()
151
+ - disconnect()
152
+
153
+ ### Authentication and presence
154
+
155
+ - identify(token)
156
+ - updatePresence(status, activity?)
157
+
158
+ ### Subscriptions
159
+
160
+ - subscribe(eventCodes)
161
+ - unsubscribe(eventCodes)
162
+
163
+ ### Chat
164
+
165
+ - sendChatMessage(roomSlug, content)
166
+
167
+ ### Event listeners
168
+
169
+ - on(eventCode, listener)
170
+ - off(eventCode, listener)
171
+ - onConnection(eventName, listener)
172
+ - offConnection(eventName, listener)
173
+
174
+ Connection event names:
175
+
176
+ - onConnect
177
+ - onDisconnect
178
+ - onReconnect
179
+ - onStateChange
180
+ - onReady
181
+ - onError
182
+ - onDispatch
183
+
184
+ ## Important options
185
+
186
+ createPipeline options:
187
+
188
+ - url: required pipeline endpoint
189
+ - tokenProvider: optional callback returning auth token
190
+ - loggerName: optional logger scope
191
+ - webSocketFactory: required for Node.js and custom environments
192
+
193
+ ## Typed constants and helpers
194
+
195
+ The package exports protocol constants and helper functions:
196
+
197
+ - Opcodes
198
+ - Events
199
+ - Presence
200
+ - Activity
201
+ - Roles
202
+ - CloseCodes
203
+ - PipelineErrors
204
+ - getEventName(code)
205
+ - getPipelineErrorName(code)
206
+
207
+ ## Dispatch bridge integration
208
+
209
+ For frameworks with centralized stores, use setDispatchHandler to bridge incoming lifecycle and event actions:
210
+
211
+ ```ts
212
+ pipeline.setDispatchHandler((action, payload) => {
213
+ // Example: forward to your store dispatcher
214
+ store.dispatch(action, payload)
215
+ })
216
+ ```
217
+
218
+ Actions emitted by the client:
219
+
220
+ - pipeline/setConnectionState
221
+ - pipeline/setReady
222
+ - pipeline/setInvalidSession
223
+ - pipeline/handleDispatch
224
+ - pipeline/handleServerError
225
+
226
+ ## Error handling notes
227
+
228
+ - Server-side protocol errors are emitted through onError with numeric code/message.
229
+ - Client parse/listener failures are logged, not thrown, to keep the connection alive.
230
+ - send methods are safe no-ops if socket is not open.
231
+
232
+ ## Production recommendations
233
+
234
+ - Always use wss in production.
235
+ - Keep tokenProvider fast and side-effect free.
236
+ - Subscribe only to events your UI or worker actually consumes.
237
+ - Register listeners before connect() when you need first-event guarantees.
238
+ - Call disconnect() on app shutdown or teardown.
239
+
240
+ ## Local development
241
+
242
+ Build package:
243
+
244
+ ```bash
245
+ npm run build
246
+ ```
247
+
248
+ Watch mode:
249
+
250
+ ```bash
251
+ npm run dev
252
+ ```
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Pipeline Opcodes and event codes.
3
+ */
4
+ declare const Opcodes: {
5
+ readonly IDENTIFY: 0;
6
+ readonly HEARTBEAT: 1;
7
+ readonly PRESENCE_UPDATE: 2;
8
+ readonly SUBSCRIBE: 3;
9
+ readonly UNSUBSCRIBE: 4;
10
+ readonly REQUEST: 5;
11
+ readonly CHAT_SEND: 6;
12
+ readonly HELLO: 16;
13
+ readonly HEARTBEAT_ACK: 17;
14
+ readonly READY: 18;
15
+ readonly INVALID_SESSION: 19;
16
+ readonly RECONNECT: 20;
17
+ readonly DISPATCH: 21;
18
+ readonly ERROR: 255;
19
+ };
20
+ type Opcode = typeof Opcodes[keyof typeof Opcodes];
21
+ declare const Events: {
22
+ readonly USER_UPDATE: 0;
23
+ readonly USER_PRESENCE_UPDATE: 1;
24
+ readonly USER_TYPING: 2;
25
+ readonly USER_LEVEL_UP: 3;
26
+ readonly SESSION_CREATE: 16;
27
+ readonly SESSION_DELETE: 17;
28
+ readonly SESSION_UPDATE: 18;
29
+ readonly NOTIFICATION_CREATE: 32;
30
+ readonly NOTIFICATION_DELETE: 33;
31
+ readonly ROOM_JOIN: 48;
32
+ readonly ROOM_LEAVE: 49;
33
+ readonly ROOM_UPDATE: 50;
34
+ readonly ROOM_DELETE: 51;
35
+ readonly ROOM_USER_JOIN: 64;
36
+ readonly ROOM_USER_LEAVE: 65;
37
+ readonly ROOM_USER_KICK: 66;
38
+ readonly ROOM_USER_BAN: 67;
39
+ readonly ROOM_USER_MUTE: 68;
40
+ readonly ROOM_USER_UNMUTE: 69;
41
+ readonly ROOM_USER_ROLE_UPDATE: 70;
42
+ readonly ROOM_USER_AVATAR_UPDATE: 71;
43
+ readonly ROOM_USER_SUBSCRIPTION_UPDATE: 72;
44
+ readonly ROOM_CHAT_MESSAGE: 80;
45
+ readonly ROOM_CHAT_DELETE: 81;
46
+ readonly ROOM_DJ_ADVANCE: 96;
47
+ readonly ROOM_DJ_UPDATE: 97;
48
+ readonly ROOM_WAITLIST_JOIN: 98;
49
+ readonly ROOM_WAITLIST_LEAVE: 99;
50
+ readonly ROOM_WAITLIST_UPDATE: 100;
51
+ readonly ROOM_WAITLIST_LOCK: 101;
52
+ readonly ROOM_WAITLIST_CYCLE: 102;
53
+ readonly ROOM_TIME_SYNC: 103;
54
+ readonly ROOM_VOTE: 112;
55
+ readonly ROOM_GRAB: 113;
56
+ readonly FRIEND_REQUEST: 128;
57
+ readonly FRIEND_REQUEST_CANCEL: 129;
58
+ readonly FRIEND_ACCEPT: 130;
59
+ readonly FRIEND_REMOVE: 131;
60
+ readonly SYSTEM_MESSAGE: 240;
61
+ readonly MAINTENANCE: 241;
62
+ readonly RATE_LIMIT: 242;
63
+ };
64
+ type EventCode = typeof Events[keyof typeof Events];
65
+ declare const Presence: {
66
+ readonly ONLINE: 0;
67
+ readonly IDLE: 1;
68
+ readonly DND: 2;
69
+ readonly INVISIBLE: 3;
70
+ readonly OFFLINE: 4;
71
+ };
72
+ type PresenceCode = typeof Presence[keyof typeof Presence];
73
+ declare const Activity: {
74
+ readonly NONE: 0;
75
+ readonly VIEWING: 1;
76
+ readonly EDITING: 2;
77
+ readonly IDLE: 3;
78
+ readonly STREAMING: 4;
79
+ readonly LISTENING: 5;
80
+ readonly WATCHING: 6;
81
+ readonly CUSTOM: 255;
82
+ };
83
+ type ActivityCode = typeof Activity[keyof typeof Activity];
84
+ declare const Roles: {
85
+ readonly USER: 0;
86
+ readonly MODERATOR: 1;
87
+ readonly ADMIN: 2;
88
+ readonly OWNER: 255;
89
+ };
90
+ type RoleCode = typeof Roles[keyof typeof Roles];
91
+ declare const CloseCodes: {
92
+ readonly NORMAL: 1000;
93
+ readonly GOING_AWAY: 1001;
94
+ readonly PROTOCOL_ERROR: 1002;
95
+ readonly UNKNOWN_ERROR: 4000;
96
+ readonly UNKNOWN_OPCODE: 4001;
97
+ readonly DECODE_ERROR: 4002;
98
+ readonly NOT_AUTHENTICATED: 4003;
99
+ readonly AUTHENTICATION_FAILED: 4004;
100
+ readonly ALREADY_AUTHENTICATED: 4005;
101
+ readonly INVALID_SESSION: 4006;
102
+ readonly RATE_LIMITED: 4008;
103
+ readonly SESSION_TIMEOUT: 4009;
104
+ readonly SERVER_SHUTDOWN: 4010;
105
+ };
106
+ type CloseCode = typeof CloseCodes[keyof typeof CloseCodes];
107
+ declare const PipelineErrors: {
108
+ readonly CHAT_MESSAGE_EMPTY: 4100;
109
+ readonly CHAT_MESSAGE_TOO_LONG: 4101;
110
+ readonly CHAT_ROOM_NOT_FOUND: 4102;
111
+ readonly CHAT_NOT_IN_ROOM: 4103;
112
+ readonly CHAT_USER_MUTED: 4104;
113
+ readonly ROOM_NOT_FOUND: 4200;
114
+ readonly ROOM_NOT_ACTIVE: 4201;
115
+ readonly ROOM_ALREADY_MEMBER: 4202;
116
+ readonly ROOM_NOT_MEMBER: 4203;
117
+ readonly ROOM_BANNED: 4204;
118
+ readonly ROOM_FULL: 4205;
119
+ readonly WAITLIST_LOCKED: 4300;
120
+ readonly WAITLIST_FULL: 4301;
121
+ readonly WAITLIST_ALREADY_IN: 4302;
122
+ readonly WAITLIST_NOT_IN: 4303;
123
+ readonly VOTE_INVALID: 4400;
124
+ readonly VOTE_ALREADY_VOTED: 4401;
125
+ readonly VOTE_NO_TRACK: 4402;
126
+ };
127
+ type PipelineError = typeof PipelineErrors[keyof typeof PipelineErrors];
128
+ declare function getPipelineErrorName(code: PipelineError): string;
129
+ declare function getEventName(code: EventCode): string;
130
+
131
+ interface PipelineMessage<T = unknown> {
132
+ op: Opcode;
133
+ d: T;
134
+ t?: EventCode;
135
+ s?: number;
136
+ }
137
+ interface IdentifyPayload {
138
+ token: string;
139
+ properties?: {
140
+ os?: number;
141
+ browser?: number;
142
+ device?: number;
143
+ };
144
+ }
145
+ interface HeartbeatPayload {
146
+ seq: number | null;
147
+ }
148
+ interface PresenceUpdatePayload {
149
+ status: PresenceCode;
150
+ activity?: {
151
+ type: ActivityCode;
152
+ name: string;
153
+ details?: string;
154
+ started_at?: number;
155
+ };
156
+ }
157
+ interface SubscribePayload {
158
+ events: EventCode[];
159
+ }
160
+ interface UnsubscribePayload {
161
+ events: EventCode[];
162
+ }
163
+ interface ChatSendPayload {
164
+ room_slug: string;
165
+ content: string;
166
+ }
167
+ interface HelloPayload {
168
+ heartbeat_interval: number;
169
+ session_id: string;
170
+ }
171
+ interface ReadyPayload {
172
+ session_id: string;
173
+ user: {
174
+ id: number;
175
+ username: string;
176
+ display_name: string | null;
177
+ role: RoleCode;
178
+ flags: number;
179
+ };
180
+ resume_url?: string;
181
+ }
182
+ interface InvalidSessionPayload {
183
+ resumable: boolean;
184
+ code: number;
185
+ }
186
+ interface ErrorPayload {
187
+ code: number;
188
+ message?: string;
189
+ }
190
+ type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'identified';
191
+ type EventListener<T = unknown> = (data: T) => void;
192
+ interface PipelineEvents {
193
+ onConnect: () => void;
194
+ onDisconnect: (code: number, reason: string) => void;
195
+ onReconnect: (attempt: number) => void;
196
+ onStateChange: (state: ConnectionState) => void;
197
+ onReady: (payload: ReadyPayload) => void;
198
+ onError: (payload: ErrorPayload) => void;
199
+ onDispatch: (event: EventCode, data: unknown) => void;
200
+ }
201
+ type DispatchHandler = (action: string, payload?: unknown) => void;
202
+ interface PipelineClientOptions {
203
+ url: string;
204
+ tokenProvider?: () => string | null | undefined;
205
+ loggerName?: string;
206
+ webSocketFactory?: (url: string) => WebSocket;
207
+ }
208
+
209
+ declare class PipelineClient {
210
+ private readonly logger;
211
+ private readonly options;
212
+ private ws;
213
+ private sessionId;
214
+ private heartbeatInterval;
215
+ private heartbeatTimer;
216
+ private reconnectTimer;
217
+ private reconnectAttempts;
218
+ private lastSequence;
219
+ private subscriptions;
220
+ private _state;
221
+ private _user;
222
+ private eventListeners;
223
+ private connectionListeners;
224
+ private dispatchHandler;
225
+ private readonly maxReconnectAttempts;
226
+ private readonly reconnectBackoff;
227
+ constructor(options: PipelineClientOptions);
228
+ get state(): ConnectionState;
229
+ get user(): ReadyPayload['user'] | null;
230
+ get isConnected(): boolean;
231
+ get isIdentified(): boolean;
232
+ setDispatchHandler(handler: DispatchHandler): void;
233
+ connect(): void;
234
+ disconnect(): void;
235
+ identify(token: string): void;
236
+ updatePresence(status: PresenceCode, activity?: PresenceUpdatePayload['activity']): void;
237
+ subscribe(events: EventCode[]): void;
238
+ unsubscribe(events: EventCode[]): void;
239
+ sendChatMessage(roomSlug: string, content: string): boolean;
240
+ on<T = unknown>(event: EventCode, listener: EventListener<T>): () => void;
241
+ off<T = unknown>(event: EventCode, listener: EventListener<T>): void;
242
+ onConnection<K extends keyof PipelineEvents>(event: K, listener: PipelineEvents[K]): () => void;
243
+ offConnection<K extends keyof PipelineEvents>(event: K, listener: PipelineEvents[K]): void;
244
+ private setState;
245
+ private clearTimers;
246
+ private handleOpen;
247
+ private handleMessage;
248
+ private handleClose;
249
+ private handleError;
250
+ private handleHello;
251
+ private handleReady;
252
+ private handleInvalidSession;
253
+ private handleReconnect;
254
+ private handleDispatch;
255
+ private handleServerError;
256
+ private startHeartbeat;
257
+ private sendHeartbeat;
258
+ private scheduleReconnect;
259
+ private send;
260
+ private emitEvent;
261
+ private emit;
262
+ private resolveToken;
263
+ }
264
+ declare function createPipeline(options: PipelineClientOptions): PipelineClient;
265
+
266
+ export { Activity, type ActivityCode, type ChatSendPayload, type CloseCode, CloseCodes, type ConnectionState, type DispatchHandler, type ErrorPayload, type EventCode, type EventListener, Events, type HeartbeatPayload, type HelloPayload, type IdentifyPayload, type InvalidSessionPayload, type Opcode, Opcodes, PipelineClient, type PipelineClientOptions, type PipelineError, PipelineErrors, type PipelineEvents, type PipelineMessage, Presence, type PresenceCode, type PresenceUpdatePayload, type ReadyPayload, type RoleCode, Roles, type SubscribePayload, type UnsubscribePayload, createPipeline, getEventName, getPipelineErrorName };