@silasdevs/transport 1.0.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,396 @@
1
+ /** Maps protocol field names to the actual wire field names. All fields are optional. */
2
+ interface ProtocolFields {
3
+ /**
4
+ * Wire field name for the channel on outgoing (request) messages.
5
+ * When omitted, outgoing messages carry no channel field and the
6
+ * wildcard `'*'` is used internally for handler routing.
7
+ */
8
+ requestChannel?: string;
9
+ /**
10
+ * Wire field name for the channel on incoming (response) messages.
11
+ * When omitted, incoming messages resolve their channel via
12
+ * `subscriptionChannel` or fall back to the wildcard `'*'`.
13
+ */
14
+ responseChannel?: string;
15
+ /**
16
+ * Wire field name for the channel on subscription/event messages.
17
+ * Used as a fallback when `responseChannel` yields no value.
18
+ * Useful when event messages arrive on a different field than responses
19
+ * (e.g. Binance uses `"e"` for event type).
20
+ */
21
+ subscriptionChannel?: string;
22
+ /** Field name for the unique message ID. When omitted, defaults to 0. */
23
+ messageId?: string;
24
+ /** Field name for the result code. When omitted, defaults to ''. */
25
+ code?: string;
26
+ /** Field name for the human-readable description. When omitted, defaults to ''. */
27
+ description?: string;
28
+ /**
29
+ * Wire field name for the error value in the raw response message.
30
+ * When omitted, `IncomingMessage.error` is `undefined`.
31
+ * Can be any shape — string, object, etc.
32
+ */
33
+ error?: string;
34
+ /** Wire field name for the data on outgoing (request) messages. When omitted, data is not nested. */
35
+ payload?: string;
36
+ /** Wire field name for the data on incoming (response) messages. When omitted, defaults to {}. */
37
+ body?: string;
38
+ /**
39
+ * Wire field name for the data payload in subscription/event messages.
40
+ * Falls back to `body` when omitted.
41
+ * Useful when event messages carry data in a different field than responses
42
+ * (e.g. WhiteBit uses `"result"` for responses and `"params"` for events).
43
+ */
44
+ eventBody?: string;
45
+ }
46
+ /**
47
+ * Result code values that have special meaning in the protocol.
48
+ *
49
+ * All fields are optional:
50
+ * - When `success` is undefined, every non-interim, non-error response is
51
+ * treated as success.
52
+ * - When `error` is undefined, no responses are treated as errors (unless
53
+ * they fail the success check when success IS defined).
54
+ * - When `interim` is undefined, no responses are treated as interim.
55
+ * - When the entire `codes` object is omitted from the schema, all responses
56
+ * resolve immediately.
57
+ */
58
+ interface ProtocolCodes {
59
+ /** Value indicating success. When undefined, all non-interim/non-error responses succeed. */
60
+ success?: string;
61
+ /** Value indicating an interim/partial response (keep listening). */
62
+ interim?: string;
63
+ /** Value(s) indicating an error. Multiple codes can be provided. */
64
+ error?: string[];
65
+ }
66
+ /**
67
+ * Structured error returned when `request()` rejects due to a non-success response.
68
+ * Allows consumers to programmatically branch on the error code.
69
+ */
70
+ interface TransportError<E = unknown> {
71
+ /** The protocol result code (e.g. the value from `ProtocolCodes.error`). */
72
+ code: string;
73
+ /** Error value extracted from the response via `ProtocolFields.error`. */
74
+ error: E;
75
+ /** Response data payload. */
76
+ data: Record<string, unknown>;
77
+ /** The full normalized incoming message for advanced inspection. */
78
+ response: IncomingMessage<Record<string, unknown>, E>;
79
+ }
80
+ /**
81
+ * Injectable protocol schema that describes the wire format.
82
+ * All properties are optional and have sensible defaults.
83
+ */
84
+ interface ProtocolSchema {
85
+ /** Maps canonical field names to wire field names. Defaults to `{}`. */
86
+ fields?: ProtocolFields;
87
+ /**
88
+ * Special result code values. When omitted, all responses are treated
89
+ * as successful (no interim or error classification).
90
+ */
91
+ codes?: ProtocolCodes;
92
+ /**
93
+ * Generate a unique message ID.
94
+ * Default: `crypto.getRandomValues` for a cryptographically random 32-bit unsigned integer.
95
+ */
96
+ generateId?: () => number;
97
+ /**
98
+ * Serialize a message for the wire.
99
+ * Default: `JSON.stringify`.
100
+ */
101
+ encode?: (message: Record<string, unknown>) => string;
102
+ /**
103
+ * Deserialize a raw wire message.
104
+ * Default: `JSON.parse` (returns `null` on failure).
105
+ */
106
+ decode?: (raw: string) => Record<string, unknown> | null;
107
+ /**
108
+ * Whether outgoing `data` fields are flattened onto the root message object.
109
+ * When true, `{ channel: 'x', data: { a: 1 } }` becomes `{ action: 'x', a: 1 }`.
110
+ * Default: `false`.
111
+ */
112
+ flattenOutgoing?: boolean;
113
+ /**
114
+ * Whether to include the generated message ID in outgoing request messages.
115
+ * When true, the `messageId` field is added to the wire message built by
116
+ * `buildOutgoing`. When false (default), the ID is used only internally for
117
+ * request/response linking and is not sent on the wire.
118
+ */
119
+ includeIdInRequest?: boolean;
120
+ }
121
+ /**
122
+ * Fully resolved protocol schema with all defaults applied.
123
+ * This is the internal type used throughout the library after calling `resolveSchema()`.
124
+ */
125
+ interface ResolvedProtocolSchema {
126
+ fields: ProtocolFields;
127
+ codes?: ProtocolCodes;
128
+ generateId: () => number;
129
+ encode: (message: Record<string, unknown>) => string;
130
+ decode: (raw: string) => Record<string, unknown> | null;
131
+ flattenOutgoing: boolean;
132
+ includeIdInRequest: boolean;
133
+ }
134
+ /** A normalized incoming message (protocol-agnostic shape).
135
+ */
136
+ interface IncomingMessage<BData = Record<string, unknown>, E = unknown> {
137
+ /** The API channel / operation name. */
138
+ channel: string;
139
+ /** The unique message ID (0 = spontaneous server push). */
140
+ messageId: number;
141
+ /** Result code (e.g. success, interim, or error codes). */
142
+ code: string;
143
+ /** Human-readable description. */
144
+ description: string;
145
+ /** Error value extracted from the response via `ProtocolFields.error`. */
146
+ error: E;
147
+ /** Response data payload. */
148
+ data: BData;
149
+ /** The original un-normalized wire message. */
150
+ raw: Record<string, unknown>;
151
+ }
152
+ /** Outgoing message to send over the transport. */
153
+ interface OutgoingMessage<PData = Record<string, unknown>> {
154
+ /**
155
+ * The API channel / operation name.
156
+ * Optional when the protocol has no `requestChannel` defined — in that
157
+ * case the wildcard `'*'` is used for internal handler routing.
158
+ */
159
+ channel?: string;
160
+ /** Optional data payload. */
161
+ data?: PData;
162
+ }
163
+ /** Callback for handling an incoming message. */
164
+ type HandlerCallback<BData = Record<string, unknown>, E = unknown> = (message: IncomingMessage<BData, E>) => boolean | void;
165
+ /**
166
+ * Unified handler that replaces the separate HANDLERS (persistent) and
167
+ * MANEJADORES (ephemeral) systems.
168
+ *
169
+ * - persistent: stays registered until explicitly removed. Used for
170
+ * spontaneous server pushes (like entity change notifications).
171
+ * - ephemeral: auto-removed after handling one definitive response.
172
+ * Returns false to stay alive (interim pattern).
173
+ */
174
+ interface Handler<BData = Record<string, unknown>, E = unknown> {
175
+ type: 'persistent' | 'ephemeral';
176
+ callback: HandlerCallback<BData, E>;
177
+ /** Name key for persistent handlers (for deduplication/removal). */
178
+ name?: string;
179
+ }
180
+ /** Reconnection configuration. */
181
+ interface ReconnectOptions {
182
+ /** Enable auto-reconnection (default: true). */
183
+ auto?: boolean;
184
+ /** Delay in ms before reconnecting (default: 10_000). */
185
+ delayMs?: number;
186
+ /** Maximum reconnection attempts (default: Infinity). */
187
+ maxAttempts?: number;
188
+ /** Backoff strategy (default: 'fixed'). */
189
+ backoff?: 'fixed' | 'exponential';
190
+ }
191
+ /** Options for request() — Promise-based send with response. */
192
+ interface RequestOptions {
193
+ /** Timeout in ms. 0 = no timeout (default: 30_000). */
194
+ timeout?: number;
195
+ }
196
+ /** Options for fire() — callback-based send. */
197
+ interface FireOptions {
198
+ }
199
+ /**
200
+ * Connection state of the transport.
201
+ *
202
+ * - disconnected: no active connection
203
+ * - connecting: WebSocket is opening
204
+ * - connected: WebSocket is open and ready
205
+ * - reconnecting: attempting to re-establish after a drop
206
+ */
207
+ type TransportState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
208
+ /** Configuration for createTransport(). */
209
+ interface TransportOptions {
210
+ /** WebSocket URL. Can be a string or a function for lazy evaluation. */
211
+ url: string | (() => string);
212
+ /** Protocol schema. All fields are optional with sensible defaults. */
213
+ protocol?: ProtocolSchema;
214
+ /** Reconnection config. Pass false to disable. */
215
+ reconnect?: ReconnectOptions | false;
216
+ /** Enable debug logging (default: false). */
217
+ debug?: boolean;
218
+ }
219
+ /** Map of transport lifecycle events and their payloads. */
220
+ interface TransportEvents {
221
+ /** WebSocket is opening. */
222
+ connecting: undefined;
223
+ /** WebSocket is open and ready. */
224
+ connected: Event;
225
+ /** WebSocket has closed. */
226
+ disconnected: {
227
+ code?: number;
228
+ reason?: string;
229
+ wasClean?: boolean;
230
+ };
231
+ /** Attempting auto-reconnection. */
232
+ reconnecting: {
233
+ attempt: number;
234
+ delayMs: number;
235
+ };
236
+ /** WebSocket error. */
237
+ error: Event;
238
+ /** Raw message received (before parsing). */
239
+ 'message:raw': {
240
+ data: string;
241
+ };
242
+ /** Parsed and normalized message. */
243
+ 'message:parsed': IncomingMessage;
244
+ /** No handler matched for this message. */
245
+ 'message:unhandled': IncomingMessage;
246
+ /** About to send a message. */
247
+ 'send:before': {
248
+ payload: Record<string, unknown>;
249
+ };
250
+ /** Message sent successfully. */
251
+ 'send:after': {
252
+ payload: Record<string, unknown>;
253
+ };
254
+ /** Failed to send (socket not open). */
255
+ 'send:error': {
256
+ payload: Record<string, unknown>;
257
+ reason: string;
258
+ };
259
+ }
260
+ /** The public API of a Transport instance created by createTransport(). */
261
+ interface Transport {
262
+ /** Open the WebSocket connection. Idempotent. */
263
+ connect(): void;
264
+ /** Close the WebSocket connection. */
265
+ disconnect(options?: {
266
+ clean?: boolean;
267
+ }): void;
268
+ /** Current connection state. */
269
+ readonly state: TransportState;
270
+ /**
271
+ * Fire-and-forget send.
272
+ * Use request() for Promise-based responses or fire() for callbacks.
273
+ */
274
+ send<PData = Record<string, unknown>>(msg: OutgoingMessage<PData>): void;
275
+ /**
276
+ * Promise-based send. Resolves on success, rejects on failure or timeout.
277
+ * Handles interim responses transparently.
278
+ */
279
+ request<BData = Record<string, unknown>, PData = Record<string, unknown>, E = unknown>(msg: OutgoingMessage<PData>, options?: RequestOptions): Promise<IncomingMessage<BData, E>>;
280
+ /**
281
+ * Callback-based send.
282
+ * The callback receives each response. Return false to keep listening (interim).
283
+ * Returns an unsubscribe function.
284
+ */
285
+ fire<BData = Record<string, unknown>, PData = Record<string, unknown>, E = unknown>(msg: OutgoingMessage<PData>, callback: HandlerCallback<BData, E>, options?: FireOptions): () => void;
286
+ /** Register a persistent handler. Returns unsubscribe function. */
287
+ addHandler<BData = Record<string, unknown>, E = unknown>(channel: string, name: string, callback: HandlerCallback<BData, E>): () => void;
288
+ /** Remove a persistent handler by name. */
289
+ removeHandler(channel: string, name: string): boolean;
290
+ /** Subscribe to lifecycle events. Returns unsubscribe function. */
291
+ on<K extends keyof TransportEvents>(event: K, callback: (data: TransportEvents[K]) => void): () => void;
292
+ /** Subscribe to a lifecycle event once. Returns unsubscribe function. */
293
+ once<K extends keyof TransportEvents>(event: K, callback: (data: TransportEvents[K]) => void): () => void;
294
+ /** The resolved protocol schema (readonly). */
295
+ readonly protocol: ResolvedProtocolSchema;
296
+ /** Toggle debug logging. */
297
+ debug(enabled: boolean): void;
298
+ /** Disconnect, clear all handlers, remove all listeners. */
299
+ destroy(): void;
300
+ }
301
+
302
+ /**
303
+ * Create a Transport instance.
304
+ *
305
+ * ```ts
306
+ * const transport = createTransport({
307
+ * url: 'wss://api.example.com/ws',
308
+ * protocol: myProtocol,
309
+ * });
310
+ *
311
+ * transport.connect();
312
+ * const res = await transport.request({ channel: 'getUser', data: { id: 5 } });
313
+ * ```
314
+ */
315
+ declare function createTransport(options: TransportOptions): Transport;
316
+
317
+ /**
318
+ * Apply defaults to a partial `ProtocolSchema`, producing a fully
319
+ * resolved schema ready for internal use.
320
+ */
321
+ declare function resolveSchema(input?: ProtocolSchema): ResolvedProtocolSchema;
322
+ /**
323
+ * Transform a raw wire message into the canonical IncomingMessage shape.
324
+ * Reads field names from the protocol schema so the rest of the library
325
+ * can work with a stable, protocol-agnostic structure.
326
+ */
327
+ declare function normalizeIncoming(raw: Record<string, unknown>, schema: ResolvedProtocolSchema): IncomingMessage;
328
+ /**
329
+ * Build a wire-format message object from an OutgoingMessage.
330
+ *
331
+ * If `flattenOutgoing` is true, data keys are spread onto the root
332
+ * alongside the channel and message ID fields.
333
+ *
334
+ * If false, data is nested under the data field name.
335
+ */
336
+ declare function buildOutgoing<PData = Record<string, unknown>>(msg: OutgoingMessage<PData>, messageId: number, schema: ResolvedProtocolSchema): Record<string, unknown>;
337
+
338
+ /** Generic typed event emitter. */
339
+ interface Emitter<TEvents extends object = Record<string, unknown>> {
340
+ on<K extends keyof TEvents>(event: K, callback: (data: TEvents[K]) => void): () => void;
341
+ once<K extends keyof TEvents>(event: K, callback: (data: TEvents[K]) => void): () => void;
342
+ emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void;
343
+ off<K extends keyof TEvents>(event: K, callback: (data: TEvents[K]) => void): void;
344
+ removeAll(event?: keyof TEvents): void;
345
+ }
346
+ /**
347
+ * Create a minimal typed event emitter.
348
+ *
349
+ * ```ts
350
+ * const ee = createEmitter<TransportEvents>();
351
+ * const unsub = ee.on('connected', (evt) => { ... });
352
+ * ee.emit('connected', evt);
353
+ * unsub(); // or ee.off('connected', callback)
354
+ * ```
355
+ */
356
+ declare function createEmitter<TEvents extends object = Record<string, unknown>>(): Emitter<TEvents>;
357
+
358
+ interface HandlerStore {
359
+ /**
360
+ * Register a handler.
361
+ * - Persistent: key is the handler name (string).
362
+ * - Ephemeral: key is the messageId (number).
363
+ * Returns an unsubscribe function.
364
+ */
365
+ add(channel: string, key: string | number, handler: Handler): () => void;
366
+ /** Remove a handler by channel + key. */
367
+ remove(channel: string, key: string | number): boolean;
368
+ /**
369
+ * Route an incoming message to the appropriate handler(s).
370
+ * Returns true if at least one handler processed the message.
371
+ */
372
+ execute(message: IncomingMessage): boolean;
373
+ /** Check if an ephemeral handler exists for (channel, messageId). */
374
+ hasEphemeral(channel: string, messageId: number): boolean;
375
+ /** Look up the channel for a given messageId via the secondary index. */
376
+ findChannelByMessageId(messageId: number): string | undefined;
377
+ /** Clear all handlers. */
378
+ clear(): void;
379
+ /**
380
+ * Clear stale ephemeral handlers for a given channel (or all channels).
381
+ */
382
+ clearStale(channel?: string): void;
383
+ }
384
+ /**
385
+ * Create a new handler store.
386
+ *
387
+ * Internal structure:
388
+ * ephemeral: Map< channel, Map< messageId (number), Handler > >
389
+ * persistent: Map< channel, Map< name (string), Handler > >
390
+ */
391
+ declare function createHandlerStore(): HandlerStore;
392
+
393
+ /** A pre-bound Emitter type for transport lifecycle events. */
394
+ type TransportEmitter = Emitter<TransportEvents>;
395
+
396
+ export { type Emitter, type FireOptions, type Handler, type HandlerCallback, type HandlerStore, type IncomingMessage, type OutgoingMessage, type ProtocolCodes, type ProtocolFields, type ProtocolSchema, type ReconnectOptions, type RequestOptions, type ResolvedProtocolSchema, type Transport, type TransportEmitter, type TransportError, type TransportEvents, type TransportOptions, type TransportState, buildOutgoing, createEmitter, createHandlerStore, createTransport, normalizeIncoming, resolveSchema };