@inf-minds/streams-protocol 0.0.1

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,53 @@
1
+ /**
2
+ * Header name for the next offset to read from.
3
+ */
4
+ export declare const HEADER_NEXT_OFFSET = "Stream-Next-Offset";
5
+ /**
6
+ * Header name indicating stream is caught up to current tail.
7
+ */
8
+ export declare const HEADER_UP_TO_DATE = "Stream-Up-To-Date";
9
+ /**
10
+ * Header name indicating stream is permanently closed.
11
+ */
12
+ export declare const HEADER_CLOSED = "Stream-Closed";
13
+ /**
14
+ * Header name for opaque cursor (CDN collapsing support).
15
+ */
16
+ export declare const HEADER_CURSOR = "Stream-Cursor";
17
+ /**
18
+ * Default polling interval in milliseconds for catch-up mode.
19
+ */
20
+ export declare const DEFAULT_POLL_INTERVAL = 500;
21
+ /**
22
+ * Parsed stream response headers.
23
+ */
24
+ export interface StreamResponseHeaders {
25
+ /** Next offset to read from */
26
+ nextOffset: string;
27
+ /** True when caught up to stream tail */
28
+ upToDate?: boolean;
29
+ /** True when stream is permanently closed */
30
+ closed?: boolean;
31
+ /** Opaque cursor for CDN request collapsing */
32
+ cursor?: string;
33
+ /** ETag for caching */
34
+ etag?: string;
35
+ }
36
+ /**
37
+ * Parses stream headers from an HTTP response.
38
+ *
39
+ * @example
40
+ * const headers = response.headers;
41
+ * const stream = parseStreamHeaders(headers);
42
+ * console.log(stream.nextOffset); // '210'
43
+ */
44
+ export declare function parseStreamHeaders(headers: Headers): StreamResponseHeaders;
45
+ /**
46
+ * Formats stream headers for an HTTP response.
47
+ *
48
+ * @example
49
+ * const headers = formatStreamHeaders({ nextOffset: '210', upToDate: true });
50
+ * response.headers = headers;
51
+ */
52
+ export declare function formatStreamHeaders(stream: StreamResponseHeaders): Headers;
53
+ //# sourceMappingURL=headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD;;GAEG;AACH,eAAO,MAAM,iBAAiB,sBAAsB,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAC;AAE7C;;GAEG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAC;AAE7C;;GAEG;AACH,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,+BAA+B;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,qBAAqB,CA4B1E;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAsB1E"}
@@ -0,0 +1,78 @@
1
+ // ABOUTME: HTTP header parsing and formatting utilities for Durable Streams
2
+ // ABOUTME: Handles Stream-* headers per protocol specification
3
+ /**
4
+ * Header name for the next offset to read from.
5
+ */
6
+ export const HEADER_NEXT_OFFSET = 'Stream-Next-Offset';
7
+ /**
8
+ * Header name indicating stream is caught up to current tail.
9
+ */
10
+ export const HEADER_UP_TO_DATE = 'Stream-Up-To-Date';
11
+ /**
12
+ * Header name indicating stream is permanently closed.
13
+ */
14
+ export const HEADER_CLOSED = 'Stream-Closed';
15
+ /**
16
+ * Header name for opaque cursor (CDN collapsing support).
17
+ */
18
+ export const HEADER_CURSOR = 'Stream-Cursor';
19
+ /**
20
+ * Default polling interval in milliseconds for catch-up mode.
21
+ */
22
+ export const DEFAULT_POLL_INTERVAL = 500;
23
+ /**
24
+ * Parses stream headers from an HTTP response.
25
+ *
26
+ * @example
27
+ * const headers = response.headers;
28
+ * const stream = parseStreamHeaders(headers);
29
+ * console.log(stream.nextOffset); // '210'
30
+ */
31
+ export function parseStreamHeaders(headers) {
32
+ const nextOffset = headers.get(HEADER_NEXT_OFFSET) ?? '';
33
+ const upToDateStr = headers.get(HEADER_UP_TO_DATE);
34
+ const closedStr = headers.get(HEADER_CLOSED);
35
+ const cursor = headers.get(HEADER_CURSOR) ?? undefined;
36
+ const etag = headers.get('ETag') ?? undefined;
37
+ const result = {
38
+ nextOffset,
39
+ };
40
+ if (upToDateStr !== null) {
41
+ result.upToDate = upToDateStr === 'true';
42
+ }
43
+ if (closedStr !== null) {
44
+ result.closed = closedStr === 'true';
45
+ }
46
+ if (cursor) {
47
+ result.cursor = cursor;
48
+ }
49
+ if (etag) {
50
+ result.etag = etag;
51
+ }
52
+ return result;
53
+ }
54
+ /**
55
+ * Formats stream headers for an HTTP response.
56
+ *
57
+ * @example
58
+ * const headers = formatStreamHeaders({ nextOffset: '210', upToDate: true });
59
+ * response.headers = headers;
60
+ */
61
+ export function formatStreamHeaders(stream) {
62
+ const headers = new Headers();
63
+ headers.set(HEADER_NEXT_OFFSET, stream.nextOffset);
64
+ if (stream.upToDate !== undefined) {
65
+ headers.set(HEADER_UP_TO_DATE, String(stream.upToDate));
66
+ }
67
+ if (stream.closed !== undefined) {
68
+ headers.set(HEADER_CLOSED, String(stream.closed));
69
+ }
70
+ if (stream.cursor !== undefined) {
71
+ headers.set(HEADER_CURSOR, stream.cursor);
72
+ }
73
+ if (stream.etag !== undefined) {
74
+ headers.set('ETag', stream.etag);
75
+ }
76
+ return headers;
77
+ }
78
+ //# sourceMappingURL=headers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headers.js","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,+DAA+D;AAE/D;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AAEvD;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,mBAAmB,CAAC;AAErD;;GAEG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAE7C;;GAEG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAE7C;;GAEG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAkBzC;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAgB;IACjD,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,EAAE,CAAC;IACzD,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,SAAS,CAAC;IACvD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC;IAE9C,MAAM,MAAM,GAA0B;QACpC,UAAU;KACX,CAAC;IAEF,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QACzB,MAAM,CAAC,QAAQ,GAAG,WAAW,KAAK,MAAM,CAAC;IAC3C,CAAC;IAED,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QACvB,MAAM,CAAC,MAAM,GAAG,SAAS,KAAK,MAAM,CAAC;IACvC,CAAC;IAED,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;IAED,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAA6B;IAC/D,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IAEnD,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAClC,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5C,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { encodeOffset, decodeOffset, OFFSET_BEGINNING, OFFSET_NOW, } from './offset.js';
2
+ export { formatSSEDataEvent, formatSSEControlEvent, parseSSEEvent, SSE_RETRY_MS, type SSEDataEvent, type SSEControlEvent, type SSEEvent, type SSEControlPayload, } from './sse.js';
3
+ export { parseStreamHeaders, formatStreamHeaders, HEADER_NEXT_OFFSET, HEADER_UP_TO_DATE, HEADER_CLOSED, HEADER_CURSOR, DEFAULT_POLL_INTERVAL, type StreamResponseHeaders, } from './headers.js';
4
+ export { isWSClientMessage, isWSServerMessage, parseWSClientMessage, parseWSServerMessage, serializeWSMessage, type WSClientMessage, type WSServerMessage, type WSConnectedMessage, type WSSubscribeMessage, type WSUnsubscribeMessage, type WSPingMessage, type WSSubscribedMessage, type WSUnsubscribedMessage, type WSEventMessage, type WSStatusMessage, type WSErrorMessage, type WSPongMessage, } from './websocket.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,aAAa,EACb,YAAY,EACZ,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,QAAQ,EACb,KAAK,iBAAiB,GACvB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,aAAa,GACnB,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ // ABOUTME: Durable Streams protocol types and utilities
2
+ // ABOUTME: Zero-dependency package for protocol compliance across clients and servers
3
+ // Offset encoding/decoding
4
+ export { encodeOffset, decodeOffset, OFFSET_BEGINNING, OFFSET_NOW, } from './offset.js';
5
+ // SSE formatting and parsing
6
+ export { formatSSEDataEvent, formatSSEControlEvent, parseSSEEvent, SSE_RETRY_MS, } from './sse.js';
7
+ // HTTP header utilities
8
+ export { parseStreamHeaders, formatStreamHeaders, HEADER_NEXT_OFFSET, HEADER_UP_TO_DATE, HEADER_CLOSED, HEADER_CURSOR, DEFAULT_POLL_INTERVAL, } from './headers.js';
9
+ // WebSocket protocol types
10
+ export { isWSClientMessage, isWSServerMessage, parseWSClientMessage, parseWSServerMessage, serializeWSMessage, } from './websocket.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,sFAAsF;AAEtF,2BAA2B;AAC3B,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,6BAA6B;AAC7B,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,aAAa,EACb,YAAY,GAKb,MAAM,UAAU,CAAC;AAElB,wBAAwB;AACxB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,qBAAqB,GAEtB,MAAM,cAAc,CAAC;AAEtB,2BAA2B;AAC3B,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,GAanB,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Special offset value indicating "from beginning" of stream.
3
+ */
4
+ export declare const OFFSET_BEGINNING = "-1";
5
+ /**
6
+ * Special offset value indicating "from current tail" (live mode).
7
+ */
8
+ export declare const OFFSET_NOW = "now";
9
+ /**
10
+ * Encodes a bigint event ID to a lexicographically sortable offset string.
11
+ *
12
+ * Uses length-prefixed base-36 encoding:
13
+ * - First character indicates the length of the base-36 representation
14
+ * - Remaining characters are the base-36 encoded value
15
+ *
16
+ * This ensures that smaller numeric values always sort before larger ones
17
+ * when using string comparison.
18
+ *
19
+ * @example
20
+ * encodeOffset(0n) // '10' (length 1, value '0')
21
+ * encodeOffset(35n) // '1z' (length 1, value 'z')
22
+ * encodeOffset(36n) // '210' (length 2, value '10')
23
+ * encodeOffset(1295n) // '2zz' (length 2, value 'zz')
24
+ */
25
+ export declare function encodeOffset(id: bigint): string;
26
+ /**
27
+ * Decodes a lexicographically sortable offset string back to a bigint.
28
+ *
29
+ * Inverse of encodeOffset.
30
+ *
31
+ * @example
32
+ * decodeOffset('10') // 0n
33
+ * decodeOffset('1z') // 35n
34
+ * decodeOffset('210') // 36n
35
+ */
36
+ export declare function decodeOffset(offset: string): bigint;
37
+ //# sourceMappingURL=offset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offset.d.ts","sourceRoot":"","sources":["../src/offset.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC;;GAEG;AACH,eAAO,MAAM,UAAU,QAAQ,CAAC;AAEhC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAgB/C;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAsBnD"}
package/dist/offset.js ADDED
@@ -0,0 +1,69 @@
1
+ // ABOUTME: Offset encoding/decoding utilities for Durable Streams protocol
2
+ // ABOUTME: Uses length-prefixed base-36 encoding for lexicographic ordering
3
+ /**
4
+ * Special offset value indicating "from beginning" of stream.
5
+ */
6
+ export const OFFSET_BEGINNING = '-1';
7
+ /**
8
+ * Special offset value indicating "from current tail" (live mode).
9
+ */
10
+ export const OFFSET_NOW = 'now';
11
+ /**
12
+ * Encodes a bigint event ID to a lexicographically sortable offset string.
13
+ *
14
+ * Uses length-prefixed base-36 encoding:
15
+ * - First character indicates the length of the base-36 representation
16
+ * - Remaining characters are the base-36 encoded value
17
+ *
18
+ * This ensures that smaller numeric values always sort before larger ones
19
+ * when using string comparison.
20
+ *
21
+ * @example
22
+ * encodeOffset(0n) // '10' (length 1, value '0')
23
+ * encodeOffset(35n) // '1z' (length 1, value 'z')
24
+ * encodeOffset(36n) // '210' (length 2, value '10')
25
+ * encodeOffset(1295n) // '2zz' (length 2, value 'zz')
26
+ */
27
+ export function encodeOffset(id) {
28
+ if (id < 0n) {
29
+ throw new Error('Offset ID must be non-negative');
30
+ }
31
+ const base36 = id.toString(36);
32
+ const length = base36.length;
33
+ // Use base-36 character for length prefix (supports up to 35 digits)
34
+ // 35 base-36 digits can represent numbers up to 36^35 - 1, which is enormous
35
+ if (length > 35) {
36
+ throw new Error('Offset ID too large');
37
+ }
38
+ const lengthPrefix = length.toString(36);
39
+ return lengthPrefix + base36;
40
+ }
41
+ /**
42
+ * Decodes a lexicographically sortable offset string back to a bigint.
43
+ *
44
+ * Inverse of encodeOffset.
45
+ *
46
+ * @example
47
+ * decodeOffset('10') // 0n
48
+ * decodeOffset('1z') // 35n
49
+ * decodeOffset('210') // 36n
50
+ */
51
+ export function decodeOffset(offset) {
52
+ if (offset.length < 2) {
53
+ throw new Error('Invalid offset format');
54
+ }
55
+ const lengthPrefix = offset[0];
56
+ const expectedLength = parseInt(lengthPrefix, 36);
57
+ const base36Value = offset.slice(1);
58
+ if (base36Value.length !== expectedLength) {
59
+ throw new Error(`Invalid offset format: expected ${expectedLength} digits, got ${base36Value.length}`);
60
+ }
61
+ // Parse base-36 string to bigint
62
+ // BigInt doesn't have native base-36 parsing, so we do it manually
63
+ let result = 0n;
64
+ for (const char of base36Value) {
65
+ result = result * 36n + BigInt(parseInt(char, 36));
66
+ }
67
+ return result;
68
+ }
69
+ //# sourceMappingURL=offset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offset.js","sourceRoot":"","sources":["../src/offset.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,4EAA4E;AAE5E;;GAEG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAErC;;GAEG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,CAAC;AAEhC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY,CAAC,EAAU;IACrC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACpD,CAAC;IAED,MAAM,MAAM,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAE7B,qEAAqE;IACrE,6EAA6E;IAC7E,IAAI,MAAM,GAAG,EAAE,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;IACzC,CAAC;IAED,MAAM,YAAY,GAAG,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACzC,OAAO,YAAY,GAAG,MAAM,CAAC;AAC/B,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAC3C,CAAC;IAED,MAAM,YAAY,GAAG,MAAM,CAAC,CAAC,CAAE,CAAC;IAChC,MAAM,cAAc,GAAG,QAAQ,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IAClD,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAEpC,IAAI,WAAW,CAAC,MAAM,KAAK,cAAc,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CACb,mCAAmC,cAAc,gBAAgB,WAAW,CAAC,MAAM,EAAE,CACtF,CAAC;IACJ,CAAC;IAED,iCAAiC;IACjC,mEAAmE;IACnE,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,MAAM,GAAG,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/dist/sse.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Default retry interval in milliseconds for SSE connections.
3
+ */
4
+ export declare const SSE_RETRY_MS = 3000;
5
+ /**
6
+ * SSE data event containing actual stream payload.
7
+ */
8
+ export interface SSEDataEvent<T = unknown> {
9
+ event: 'data';
10
+ data: T;
11
+ }
12
+ /**
13
+ * SSE control event containing stream metadata.
14
+ */
15
+ export interface SSEControlEvent {
16
+ event: 'control';
17
+ streamNextOffset: string;
18
+ streamCursor?: string;
19
+ upToDate?: boolean;
20
+ streamClosed?: boolean;
21
+ }
22
+ /**
23
+ * Union type for all SSE events in the Durable Streams protocol.
24
+ */
25
+ export type SSEEvent<T = unknown> = SSEDataEvent<T> | SSEControlEvent;
26
+ /**
27
+ * Control event payload (without the 'event' discriminator).
28
+ */
29
+ export interface SSEControlPayload {
30
+ streamNextOffset: string;
31
+ streamCursor?: string;
32
+ upToDate?: boolean;
33
+ streamClosed?: boolean;
34
+ }
35
+ /**
36
+ * Formats a data payload as an SSE data event string.
37
+ *
38
+ * @example
39
+ * formatSSEDataEvent({ type: 'message' })
40
+ * // Returns: 'event: data\ndata: {"type":"message"}\n\n'
41
+ */
42
+ export declare function formatSSEDataEvent(data: unknown): string;
43
+ /**
44
+ * Formats control metadata as an SSE control event string.
45
+ *
46
+ * @example
47
+ * formatSSEControlEvent({ streamNextOffset: '210', upToDate: true })
48
+ * // Returns: 'event: control\ndata: {"streamNextOffset":"210","upToDate":true}\n\n'
49
+ */
50
+ export declare function formatSSEControlEvent(control: SSEControlPayload): string;
51
+ /**
52
+ * Parses an SSE event block (lines between double newlines).
53
+ *
54
+ * Returns null for empty input, comments, or non-data/control events.
55
+ *
56
+ * @example
57
+ * parseSSEEvent('event: data\ndata: {"type":"message"}')
58
+ * // Returns: { event: 'data', data: { type: 'message' } }
59
+ */
60
+ export declare function parseSSEEvent(block: string): SSEEvent | null;
61
+ //# sourceMappingURL=sse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,CAAC,CAAC;CACT;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,SAAS,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,GAAG,OAAO,IAAI,YAAY,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC;AAEtE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAGxD;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAkBxE;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAyE5D"}
package/dist/sse.js ADDED
@@ -0,0 +1,114 @@
1
+ // ABOUTME: SSE (Server-Sent Events) formatting and parsing utilities
2
+ // ABOUTME: Implements two-event model (data + control) per Durable Streams spec
3
+ /**
4
+ * Default retry interval in milliseconds for SSE connections.
5
+ */
6
+ export const SSE_RETRY_MS = 3000;
7
+ /**
8
+ * Formats a data payload as an SSE data event string.
9
+ *
10
+ * @example
11
+ * formatSSEDataEvent({ type: 'message' })
12
+ * // Returns: 'event: data\ndata: {"type":"message"}\n\n'
13
+ */
14
+ export function formatSSEDataEvent(data) {
15
+ const json = JSON.stringify(data);
16
+ return `event: data\ndata: ${json}\n\n`;
17
+ }
18
+ /**
19
+ * Formats control metadata as an SSE control event string.
20
+ *
21
+ * @example
22
+ * formatSSEControlEvent({ streamNextOffset: '210', upToDate: true })
23
+ * // Returns: 'event: control\ndata: {"streamNextOffset":"210","upToDate":true}\n\n'
24
+ */
25
+ export function formatSSEControlEvent(control) {
26
+ // Only include defined fields
27
+ const payload = {
28
+ streamNextOffset: control.streamNextOffset,
29
+ };
30
+ if (control.streamCursor !== undefined) {
31
+ payload.streamCursor = control.streamCursor;
32
+ }
33
+ if (control.upToDate !== undefined) {
34
+ payload.upToDate = control.upToDate;
35
+ }
36
+ if (control.streamClosed !== undefined) {
37
+ payload.streamClosed = control.streamClosed;
38
+ }
39
+ const json = JSON.stringify(payload);
40
+ return `event: control\ndata: ${json}\n\n`;
41
+ }
42
+ /**
43
+ * Parses an SSE event block (lines between double newlines).
44
+ *
45
+ * Returns null for empty input, comments, or non-data/control events.
46
+ *
47
+ * @example
48
+ * parseSSEEvent('event: data\ndata: {"type":"message"}')
49
+ * // Returns: { event: 'data', data: { type: 'message' } }
50
+ */
51
+ export function parseSSEEvent(block) {
52
+ if (!block || block.trim() === '') {
53
+ return null;
54
+ }
55
+ const lines = block.split('\n');
56
+ let eventType = null;
57
+ const dataLines = [];
58
+ for (const line of lines) {
59
+ // Skip comments
60
+ if (line.startsWith(':')) {
61
+ continue;
62
+ }
63
+ // Parse event type
64
+ if (line.startsWith('event:')) {
65
+ eventType = line.slice(6).trim();
66
+ continue;
67
+ }
68
+ // Parse data lines
69
+ if (line.startsWith('data:')) {
70
+ dataLines.push(line.slice(5).trim());
71
+ continue;
72
+ }
73
+ // Handle "data:" without space (per SSE spec)
74
+ if (line.startsWith('data')) {
75
+ const colonIndex = line.indexOf(':');
76
+ if (colonIndex !== -1) {
77
+ dataLines.push(line.slice(colonIndex + 1).trim());
78
+ }
79
+ }
80
+ }
81
+ // Only process data and control events
82
+ if (eventType !== 'data' && eventType !== 'control') {
83
+ return null;
84
+ }
85
+ if (dataLines.length === 0) {
86
+ return null;
87
+ }
88
+ // Concatenate data lines (per SSE spec, they should be joined with newlines)
89
+ const dataString = dataLines.join('\n');
90
+ try {
91
+ const parsed = JSON.parse(dataString);
92
+ if (eventType === 'data') {
93
+ return {
94
+ event: 'data',
95
+ data: parsed,
96
+ };
97
+ }
98
+ if (eventType === 'control') {
99
+ return {
100
+ event: 'control',
101
+ streamNextOffset: parsed.streamNextOffset,
102
+ streamCursor: parsed.streamCursor,
103
+ upToDate: parsed.upToDate,
104
+ streamClosed: parsed.streamClosed,
105
+ };
106
+ }
107
+ }
108
+ catch {
109
+ // Invalid JSON - return null
110
+ return null;
111
+ }
112
+ return null;
113
+ }
114
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.js","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,gFAAgF;AAEhF;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC;AAoCjC;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAa;IAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAClC,OAAO,sBAAsB,IAAI,MAAM,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAA0B;IAC9D,8BAA8B;IAC9B,MAAM,OAAO,GAA4B;QACvC,gBAAgB,EAAE,OAAO,CAAC,gBAAgB;KAC3C,CAAC;IAEF,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACvC,OAAO,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IAC9C,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IACtC,CAAC;IACD,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACvC,OAAO,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IAC9C,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACrC,OAAO,yBAAyB,IAAI,MAAM,CAAC;AAC7C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,SAAS,GAAkB,IAAI,CAAC;IACpC,MAAM,SAAS,GAAa,EAAE,CAAC;IAE/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,gBAAgB;QAChB,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,SAAS;QACX,CAAC;QAED,mBAAmB;QACnB,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9B,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACjC,SAAS;QACX,CAAC;QAED,mBAAmB;QACnB,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC7B,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YACrC,SAAS;QACX,CAAC;QAED,8CAA8C;QAC9C,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5B,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACrC,IAAI,UAAU,KAAK,CAAC,CAAC,EAAE,CAAC;gBACtB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YACpD,CAAC;QACH,CAAC;IACH,CAAC;IAED,uCAAuC;IACvC,IAAI,SAAS,KAAK,MAAM,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QACpD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,6EAA6E;IAC7E,MAAM,UAAU,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAExC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAEtC,IAAI,SAAS,KAAK,MAAM,EAAE,CAAC;YACzB,OAAO;gBACL,KAAK,EAAE,MAAM;gBACb,IAAI,EAAE,MAAM;aACb,CAAC;QACJ,CAAC;QAED,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO;gBACL,KAAK,EAAE,SAAS;gBAChB,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;gBACzC,YAAY,EAAE,MAAM,CAAC,YAAY;gBACjC,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,YAAY,EAAE,MAAM,CAAC,YAAY;aAClC,CAAC;QACJ,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,6BAA6B;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Messages sent from client to server over WebSocket.
3
+ */
4
+ export type WSClientMessage = WSSubscribeMessage | WSUnsubscribeMessage | WSPingMessage;
5
+ /**
6
+ * Subscribe to a stream from a given offset.
7
+ */
8
+ export interface WSSubscribeMessage {
9
+ type: 'subscribe';
10
+ /** Stream ID to subscribe to */
11
+ streamId: string;
12
+ /** Starting offset, defaults to '-1' (beginning) */
13
+ offset?: string;
14
+ }
15
+ /**
16
+ * Unsubscribe from a stream.
17
+ */
18
+ export interface WSUnsubscribeMessage {
19
+ type: 'unsubscribe';
20
+ /** Stream ID to unsubscribe from */
21
+ streamId: string;
22
+ }
23
+ /**
24
+ * Ping message for keepalive.
25
+ */
26
+ export interface WSPingMessage {
27
+ type: 'ping';
28
+ }
29
+ /**
30
+ * Messages sent from server to client over WebSocket.
31
+ */
32
+ export type WSServerMessage = WSConnectedMessage | WSSubscribedMessage | WSUnsubscribedMessage | WSEventMessage | WSStatusMessage | WSErrorMessage | WSPongMessage;
33
+ /**
34
+ * Connection acknowledgment with assigned client ID.
35
+ */
36
+ export interface WSConnectedMessage {
37
+ type: 'connected';
38
+ /** Client ID for this connection */
39
+ clientId: string;
40
+ }
41
+ /**
42
+ * Confirmation of successful subscription.
43
+ */
44
+ export interface WSSubscribedMessage {
45
+ type: 'subscribed';
46
+ /** Stream ID subscribed to */
47
+ streamId: string;
48
+ /** Current offset position */
49
+ offset: string;
50
+ }
51
+ /**
52
+ * Confirmation of unsubscription.
53
+ */
54
+ export interface WSUnsubscribedMessage {
55
+ type: 'unsubscribed';
56
+ /** Stream ID unsubscribed from */
57
+ streamId: string;
58
+ }
59
+ /**
60
+ * Stream event delivery.
61
+ */
62
+ export interface WSEventMessage {
63
+ type: 'event';
64
+ /** Stream ID this event belongs to */
65
+ streamId: string;
66
+ /** Event payload */
67
+ event: unknown;
68
+ /** Offset after this event */
69
+ nextOffset: string;
70
+ }
71
+ /**
72
+ * Stream status update.
73
+ */
74
+ export interface WSStatusMessage {
75
+ type: 'status';
76
+ /** Stream ID */
77
+ streamId: string;
78
+ /** True when caught up to tail */
79
+ upToDate: boolean;
80
+ /** True when stream is permanently closed */
81
+ closed?: boolean;
82
+ }
83
+ /**
84
+ * Error message.
85
+ */
86
+ export interface WSErrorMessage {
87
+ type: 'error';
88
+ /** Human-readable error message */
89
+ message: string;
90
+ /** Machine-readable error code */
91
+ code?: string;
92
+ /** Stream ID if error is stream-specific */
93
+ streamId?: string;
94
+ }
95
+ /**
96
+ * Pong response to ping.
97
+ */
98
+ export interface WSPongMessage {
99
+ type: 'pong';
100
+ }
101
+ /**
102
+ * Type guard for client messages.
103
+ */
104
+ export declare function isWSClientMessage(msg: unknown): msg is WSClientMessage;
105
+ /**
106
+ * Type guard for server messages.
107
+ */
108
+ export declare function isWSServerMessage(msg: unknown): msg is WSServerMessage;
109
+ /**
110
+ * Parse a JSON string as a client message.
111
+ */
112
+ export declare function parseWSClientMessage(json: string): WSClientMessage | null;
113
+ /**
114
+ * Parse a JSON string as a server message.
115
+ */
116
+ export declare function parseWSServerMessage(json: string): WSServerMessage | null;
117
+ /**
118
+ * Serialize a message to JSON string.
119
+ */
120
+ export declare function serializeWSMessage(msg: WSClientMessage | WSServerMessage): string;
121
+ //# sourceMappingURL=websocket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB,kBAAkB,GAClB,oBAAoB,GACpB,aAAa,CAAC;AAElB;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,WAAW,CAAC;IAClB,gCAAgC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,aAAa,CAAC;IACpB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB,kBAAkB,GAClB,mBAAmB,GACnB,qBAAqB,GACrB,cAAc,GACd,eAAe,GACf,cAAc,GACd,aAAa,CAAC;AAElB;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,WAAW,CAAC;IAClB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,YAAY,CAAC;IACnB,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,cAAc,CAAC;IACrB,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,8BAA8B;IAC9B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,QAAQ,CAAC;IACf,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,QAAQ,EAAE,OAAO,CAAC;IAClB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,eAAe,CAItE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,eAAe,CAYtE;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAUzE;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAUzE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,GAAG,eAAe,GAAG,MAAM,CAEjF"}
@@ -0,0 +1,63 @@
1
+ // ABOUTME: WebSocket protocol types for Durable Streams
2
+ // ABOUTME: Defines client-to-server and server-to-client message formats
3
+ /**
4
+ * Type guard for client messages.
5
+ */
6
+ export function isWSClientMessage(msg) {
7
+ if (typeof msg !== 'object' || msg === null)
8
+ return false;
9
+ const { type } = msg;
10
+ return type === 'subscribe' || type === 'unsubscribe' || type === 'ping';
11
+ }
12
+ /**
13
+ * Type guard for server messages.
14
+ */
15
+ export function isWSServerMessage(msg) {
16
+ if (typeof msg !== 'object' || msg === null)
17
+ return false;
18
+ const { type } = msg;
19
+ return (type === 'connected' ||
20
+ type === 'subscribed' ||
21
+ type === 'unsubscribed' ||
22
+ type === 'event' ||
23
+ type === 'status' ||
24
+ type === 'error' ||
25
+ type === 'pong');
26
+ }
27
+ /**
28
+ * Parse a JSON string as a client message.
29
+ */
30
+ export function parseWSClientMessage(json) {
31
+ try {
32
+ const parsed = JSON.parse(json);
33
+ if (isWSClientMessage(parsed)) {
34
+ return parsed;
35
+ }
36
+ return null;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /**
43
+ * Parse a JSON string as a server message.
44
+ */
45
+ export function parseWSServerMessage(json) {
46
+ try {
47
+ const parsed = JSON.parse(json);
48
+ if (isWSServerMessage(parsed)) {
49
+ return parsed;
50
+ }
51
+ return null;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ /**
58
+ * Serialize a message to JSON string.
59
+ */
60
+ export function serializeWSMessage(msg) {
61
+ return JSON.stringify(msg);
62
+ }
63
+ //# sourceMappingURL=websocket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.js","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,yEAAyE;AA4HzE;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAY;IAC5C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,EAAE,IAAI,EAAE,GAAG,GAAyB,CAAC;IAC3C,OAAO,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,aAAa,IAAI,IAAI,KAAK,MAAM,CAAC;AAC3E,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAY;IAC5C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,EAAE,IAAI,EAAE,GAAG,GAAyB,CAAC;IAC3C,OAAO,CACL,IAAI,KAAK,WAAW;QACpB,IAAI,KAAK,YAAY;QACrB,IAAI,KAAK,cAAc;QACvB,IAAI,KAAK,OAAO;QAChB,IAAI,KAAK,QAAQ;QACjB,IAAI,KAAK,OAAO;QAChB,IAAI,KAAK,MAAM,CAChB,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,iBAAiB,CAAC,MAAM,CAAC,EAAE,CAAC;YAC9B,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,iBAAiB,CAAC,MAAM,CAAC,EAAE,CAAC;YAC9B,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAsC;IACvE,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;AAC7B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@inf-minds/streams-protocol",
3
+ "version": "0.0.1",
4
+ "description": "Durable Streams protocol types and utilities",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "typecheck": "tsc --noEmit",
25
+ "lint": "echo 'No linter configured yet'"
26
+ },
27
+ "devDependencies": {
28
+ "typescript": "^5.3.0",
29
+ "vitest": "^2.0.0"
30
+ }
31
+ }
package/src/headers.ts ADDED
@@ -0,0 +1,112 @@
1
+ // ABOUTME: HTTP header parsing and formatting utilities for Durable Streams
2
+ // ABOUTME: Handles Stream-* headers per protocol specification
3
+
4
+ /**
5
+ * Header name for the next offset to read from.
6
+ */
7
+ export const HEADER_NEXT_OFFSET = 'Stream-Next-Offset';
8
+
9
+ /**
10
+ * Header name indicating stream is caught up to current tail.
11
+ */
12
+ export const HEADER_UP_TO_DATE = 'Stream-Up-To-Date';
13
+
14
+ /**
15
+ * Header name indicating stream is permanently closed.
16
+ */
17
+ export const HEADER_CLOSED = 'Stream-Closed';
18
+
19
+ /**
20
+ * Header name for opaque cursor (CDN collapsing support).
21
+ */
22
+ export const HEADER_CURSOR = 'Stream-Cursor';
23
+
24
+ /**
25
+ * Default polling interval in milliseconds for catch-up mode.
26
+ */
27
+ export const DEFAULT_POLL_INTERVAL = 500;
28
+
29
+ /**
30
+ * Parsed stream response headers.
31
+ */
32
+ export interface StreamResponseHeaders {
33
+ /** Next offset to read from */
34
+ nextOffset: string;
35
+ /** True when caught up to stream tail */
36
+ upToDate?: boolean;
37
+ /** True when stream is permanently closed */
38
+ closed?: boolean;
39
+ /** Opaque cursor for CDN request collapsing */
40
+ cursor?: string;
41
+ /** ETag for caching */
42
+ etag?: string;
43
+ }
44
+
45
+ /**
46
+ * Parses stream headers from an HTTP response.
47
+ *
48
+ * @example
49
+ * const headers = response.headers;
50
+ * const stream = parseStreamHeaders(headers);
51
+ * console.log(stream.nextOffset); // '210'
52
+ */
53
+ export function parseStreamHeaders(headers: Headers): StreamResponseHeaders {
54
+ const nextOffset = headers.get(HEADER_NEXT_OFFSET) ?? '';
55
+ const upToDateStr = headers.get(HEADER_UP_TO_DATE);
56
+ const closedStr = headers.get(HEADER_CLOSED);
57
+ const cursor = headers.get(HEADER_CURSOR) ?? undefined;
58
+ const etag = headers.get('ETag') ?? undefined;
59
+
60
+ const result: StreamResponseHeaders = {
61
+ nextOffset,
62
+ };
63
+
64
+ if (upToDateStr !== null) {
65
+ result.upToDate = upToDateStr === 'true';
66
+ }
67
+
68
+ if (closedStr !== null) {
69
+ result.closed = closedStr === 'true';
70
+ }
71
+
72
+ if (cursor) {
73
+ result.cursor = cursor;
74
+ }
75
+
76
+ if (etag) {
77
+ result.etag = etag;
78
+ }
79
+
80
+ return result;
81
+ }
82
+
83
+ /**
84
+ * Formats stream headers for an HTTP response.
85
+ *
86
+ * @example
87
+ * const headers = formatStreamHeaders({ nextOffset: '210', upToDate: true });
88
+ * response.headers = headers;
89
+ */
90
+ export function formatStreamHeaders(stream: StreamResponseHeaders): Headers {
91
+ const headers = new Headers();
92
+
93
+ headers.set(HEADER_NEXT_OFFSET, stream.nextOffset);
94
+
95
+ if (stream.upToDate !== undefined) {
96
+ headers.set(HEADER_UP_TO_DATE, String(stream.upToDate));
97
+ }
98
+
99
+ if (stream.closed !== undefined) {
100
+ headers.set(HEADER_CLOSED, String(stream.closed));
101
+ }
102
+
103
+ if (stream.cursor !== undefined) {
104
+ headers.set(HEADER_CURSOR, stream.cursor);
105
+ }
106
+
107
+ if (stream.etag !== undefined) {
108
+ headers.set('ETag', stream.etag);
109
+ }
110
+
111
+ return headers;
112
+ }
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ // ABOUTME: Durable Streams protocol types and utilities
2
+ // ABOUTME: Zero-dependency package for protocol compliance across clients and servers
3
+
4
+ // Offset encoding/decoding
5
+ export {
6
+ encodeOffset,
7
+ decodeOffset,
8
+ OFFSET_BEGINNING,
9
+ OFFSET_NOW,
10
+ } from './offset.js';
11
+
12
+ // SSE formatting and parsing
13
+ export {
14
+ formatSSEDataEvent,
15
+ formatSSEControlEvent,
16
+ parseSSEEvent,
17
+ SSE_RETRY_MS,
18
+ type SSEDataEvent,
19
+ type SSEControlEvent,
20
+ type SSEEvent,
21
+ type SSEControlPayload,
22
+ } from './sse.js';
23
+
24
+ // HTTP header utilities
25
+ export {
26
+ parseStreamHeaders,
27
+ formatStreamHeaders,
28
+ HEADER_NEXT_OFFSET,
29
+ HEADER_UP_TO_DATE,
30
+ HEADER_CLOSED,
31
+ HEADER_CURSOR,
32
+ DEFAULT_POLL_INTERVAL,
33
+ type StreamResponseHeaders,
34
+ } from './headers.js';
35
+
36
+ // WebSocket protocol types
37
+ export {
38
+ isWSClientMessage,
39
+ isWSServerMessage,
40
+ parseWSClientMessage,
41
+ parseWSServerMessage,
42
+ serializeWSMessage,
43
+ type WSClientMessage,
44
+ type WSServerMessage,
45
+ type WSConnectedMessage,
46
+ type WSSubscribeMessage,
47
+ type WSUnsubscribeMessage,
48
+ type WSPingMessage,
49
+ type WSSubscribedMessage,
50
+ type WSUnsubscribedMessage,
51
+ type WSEventMessage,
52
+ type WSStatusMessage,
53
+ type WSErrorMessage,
54
+ type WSPongMessage,
55
+ } from './websocket.js';
package/src/offset.ts ADDED
@@ -0,0 +1,80 @@
1
+ // ABOUTME: Offset encoding/decoding utilities for Durable Streams protocol
2
+ // ABOUTME: Uses length-prefixed base-36 encoding for lexicographic ordering
3
+
4
+ /**
5
+ * Special offset value indicating "from beginning" of stream.
6
+ */
7
+ export const OFFSET_BEGINNING = '-1';
8
+
9
+ /**
10
+ * Special offset value indicating "from current tail" (live mode).
11
+ */
12
+ export const OFFSET_NOW = 'now';
13
+
14
+ /**
15
+ * Encodes a bigint event ID to a lexicographically sortable offset string.
16
+ *
17
+ * Uses length-prefixed base-36 encoding:
18
+ * - First character indicates the length of the base-36 representation
19
+ * - Remaining characters are the base-36 encoded value
20
+ *
21
+ * This ensures that smaller numeric values always sort before larger ones
22
+ * when using string comparison.
23
+ *
24
+ * @example
25
+ * encodeOffset(0n) // '10' (length 1, value '0')
26
+ * encodeOffset(35n) // '1z' (length 1, value 'z')
27
+ * encodeOffset(36n) // '210' (length 2, value '10')
28
+ * encodeOffset(1295n) // '2zz' (length 2, value 'zz')
29
+ */
30
+ export function encodeOffset(id: bigint): string {
31
+ if (id < 0n) {
32
+ throw new Error('Offset ID must be non-negative');
33
+ }
34
+
35
+ const base36 = id.toString(36);
36
+ const length = base36.length;
37
+
38
+ // Use base-36 character for length prefix (supports up to 35 digits)
39
+ // 35 base-36 digits can represent numbers up to 36^35 - 1, which is enormous
40
+ if (length > 35) {
41
+ throw new Error('Offset ID too large');
42
+ }
43
+
44
+ const lengthPrefix = length.toString(36);
45
+ return lengthPrefix + base36;
46
+ }
47
+
48
+ /**
49
+ * Decodes a lexicographically sortable offset string back to a bigint.
50
+ *
51
+ * Inverse of encodeOffset.
52
+ *
53
+ * @example
54
+ * decodeOffset('10') // 0n
55
+ * decodeOffset('1z') // 35n
56
+ * decodeOffset('210') // 36n
57
+ */
58
+ export function decodeOffset(offset: string): bigint {
59
+ if (offset.length < 2) {
60
+ throw new Error('Invalid offset format');
61
+ }
62
+
63
+ const lengthPrefix = offset[0]!;
64
+ const expectedLength = parseInt(lengthPrefix, 36);
65
+ const base36Value = offset.slice(1);
66
+
67
+ if (base36Value.length !== expectedLength) {
68
+ throw new Error(
69
+ `Invalid offset format: expected ${expectedLength} digits, got ${base36Value.length}`
70
+ );
71
+ }
72
+
73
+ // Parse base-36 string to bigint
74
+ // BigInt doesn't have native base-36 parsing, so we do it manually
75
+ let result = 0n;
76
+ for (const char of base36Value) {
77
+ result = result * 36n + BigInt(parseInt(char, 36));
78
+ }
79
+ return result;
80
+ }
package/src/sse.ts ADDED
@@ -0,0 +1,164 @@
1
+ // ABOUTME: SSE (Server-Sent Events) formatting and parsing utilities
2
+ // ABOUTME: Implements two-event model (data + control) per Durable Streams spec
3
+
4
+ /**
5
+ * Default retry interval in milliseconds for SSE connections.
6
+ */
7
+ export const SSE_RETRY_MS = 3000;
8
+
9
+ /**
10
+ * SSE data event containing actual stream payload.
11
+ */
12
+ export interface SSEDataEvent<T = unknown> {
13
+ event: 'data';
14
+ data: T;
15
+ }
16
+
17
+ /**
18
+ * SSE control event containing stream metadata.
19
+ */
20
+ export interface SSEControlEvent {
21
+ event: 'control';
22
+ streamNextOffset: string;
23
+ streamCursor?: string;
24
+ upToDate?: boolean;
25
+ streamClosed?: boolean;
26
+ }
27
+
28
+ /**
29
+ * Union type for all SSE events in the Durable Streams protocol.
30
+ */
31
+ export type SSEEvent<T = unknown> = SSEDataEvent<T> | SSEControlEvent;
32
+
33
+ /**
34
+ * Control event payload (without the 'event' discriminator).
35
+ */
36
+ export interface SSEControlPayload {
37
+ streamNextOffset: string;
38
+ streamCursor?: string;
39
+ upToDate?: boolean;
40
+ streamClosed?: boolean;
41
+ }
42
+
43
+ /**
44
+ * Formats a data payload as an SSE data event string.
45
+ *
46
+ * @example
47
+ * formatSSEDataEvent({ type: 'message' })
48
+ * // Returns: 'event: data\ndata: {"type":"message"}\n\n'
49
+ */
50
+ export function formatSSEDataEvent(data: unknown): string {
51
+ const json = JSON.stringify(data);
52
+ return `event: data\ndata: ${json}\n\n`;
53
+ }
54
+
55
+ /**
56
+ * Formats control metadata as an SSE control event string.
57
+ *
58
+ * @example
59
+ * formatSSEControlEvent({ streamNextOffset: '210', upToDate: true })
60
+ * // Returns: 'event: control\ndata: {"streamNextOffset":"210","upToDate":true}\n\n'
61
+ */
62
+ export function formatSSEControlEvent(control: SSEControlPayload): string {
63
+ // Only include defined fields
64
+ const payload: Record<string, unknown> = {
65
+ streamNextOffset: control.streamNextOffset,
66
+ };
67
+
68
+ if (control.streamCursor !== undefined) {
69
+ payload.streamCursor = control.streamCursor;
70
+ }
71
+ if (control.upToDate !== undefined) {
72
+ payload.upToDate = control.upToDate;
73
+ }
74
+ if (control.streamClosed !== undefined) {
75
+ payload.streamClosed = control.streamClosed;
76
+ }
77
+
78
+ const json = JSON.stringify(payload);
79
+ return `event: control\ndata: ${json}\n\n`;
80
+ }
81
+
82
+ /**
83
+ * Parses an SSE event block (lines between double newlines).
84
+ *
85
+ * Returns null for empty input, comments, or non-data/control events.
86
+ *
87
+ * @example
88
+ * parseSSEEvent('event: data\ndata: {"type":"message"}')
89
+ * // Returns: { event: 'data', data: { type: 'message' } }
90
+ */
91
+ export function parseSSEEvent(block: string): SSEEvent | null {
92
+ if (!block || block.trim() === '') {
93
+ return null;
94
+ }
95
+
96
+ const lines = block.split('\n');
97
+ let eventType: string | null = null;
98
+ const dataLines: string[] = [];
99
+
100
+ for (const line of lines) {
101
+ // Skip comments
102
+ if (line.startsWith(':')) {
103
+ continue;
104
+ }
105
+
106
+ // Parse event type
107
+ if (line.startsWith('event:')) {
108
+ eventType = line.slice(6).trim();
109
+ continue;
110
+ }
111
+
112
+ // Parse data lines
113
+ if (line.startsWith('data:')) {
114
+ dataLines.push(line.slice(5).trim());
115
+ continue;
116
+ }
117
+
118
+ // Handle "data:" without space (per SSE spec)
119
+ if (line.startsWith('data')) {
120
+ const colonIndex = line.indexOf(':');
121
+ if (colonIndex !== -1) {
122
+ dataLines.push(line.slice(colonIndex + 1).trim());
123
+ }
124
+ }
125
+ }
126
+
127
+ // Only process data and control events
128
+ if (eventType !== 'data' && eventType !== 'control') {
129
+ return null;
130
+ }
131
+
132
+ if (dataLines.length === 0) {
133
+ return null;
134
+ }
135
+
136
+ // Concatenate data lines (per SSE spec, they should be joined with newlines)
137
+ const dataString = dataLines.join('\n');
138
+
139
+ try {
140
+ const parsed = JSON.parse(dataString);
141
+
142
+ if (eventType === 'data') {
143
+ return {
144
+ event: 'data',
145
+ data: parsed,
146
+ };
147
+ }
148
+
149
+ if (eventType === 'control') {
150
+ return {
151
+ event: 'control',
152
+ streamNextOffset: parsed.streamNextOffset,
153
+ streamCursor: parsed.streamCursor,
154
+ upToDate: parsed.upToDate,
155
+ streamClosed: parsed.streamClosed,
156
+ };
157
+ }
158
+ } catch {
159
+ // Invalid JSON - return null
160
+ return null;
161
+ }
162
+
163
+ return null;
164
+ }
@@ -0,0 +1,187 @@
1
+ // ABOUTME: WebSocket protocol types for Durable Streams
2
+ // ABOUTME: Defines client-to-server and server-to-client message formats
3
+
4
+ /**
5
+ * Messages sent from client to server over WebSocket.
6
+ */
7
+ export type WSClientMessage =
8
+ | WSSubscribeMessage
9
+ | WSUnsubscribeMessage
10
+ | WSPingMessage;
11
+
12
+ /**
13
+ * Subscribe to a stream from a given offset.
14
+ */
15
+ export interface WSSubscribeMessage {
16
+ type: 'subscribe';
17
+ /** Stream ID to subscribe to */
18
+ streamId: string;
19
+ /** Starting offset, defaults to '-1' (beginning) */
20
+ offset?: string;
21
+ }
22
+
23
+ /**
24
+ * Unsubscribe from a stream.
25
+ */
26
+ export interface WSUnsubscribeMessage {
27
+ type: 'unsubscribe';
28
+ /** Stream ID to unsubscribe from */
29
+ streamId: string;
30
+ }
31
+
32
+ /**
33
+ * Ping message for keepalive.
34
+ */
35
+ export interface WSPingMessage {
36
+ type: 'ping';
37
+ }
38
+
39
+ /**
40
+ * Messages sent from server to client over WebSocket.
41
+ */
42
+ export type WSServerMessage =
43
+ | WSConnectedMessage
44
+ | WSSubscribedMessage
45
+ | WSUnsubscribedMessage
46
+ | WSEventMessage
47
+ | WSStatusMessage
48
+ | WSErrorMessage
49
+ | WSPongMessage;
50
+
51
+ /**
52
+ * Connection acknowledgment with assigned client ID.
53
+ */
54
+ export interface WSConnectedMessage {
55
+ type: 'connected';
56
+ /** Client ID for this connection */
57
+ clientId: string;
58
+ }
59
+
60
+ /**
61
+ * Confirmation of successful subscription.
62
+ */
63
+ export interface WSSubscribedMessage {
64
+ type: 'subscribed';
65
+ /** Stream ID subscribed to */
66
+ streamId: string;
67
+ /** Current offset position */
68
+ offset: string;
69
+ }
70
+
71
+ /**
72
+ * Confirmation of unsubscription.
73
+ */
74
+ export interface WSUnsubscribedMessage {
75
+ type: 'unsubscribed';
76
+ /** Stream ID unsubscribed from */
77
+ streamId: string;
78
+ }
79
+
80
+ /**
81
+ * Stream event delivery.
82
+ */
83
+ export interface WSEventMessage {
84
+ type: 'event';
85
+ /** Stream ID this event belongs to */
86
+ streamId: string;
87
+ /** Event payload */
88
+ event: unknown;
89
+ /** Offset after this event */
90
+ nextOffset: string;
91
+ }
92
+
93
+ /**
94
+ * Stream status update.
95
+ */
96
+ export interface WSStatusMessage {
97
+ type: 'status';
98
+ /** Stream ID */
99
+ streamId: string;
100
+ /** True when caught up to tail */
101
+ upToDate: boolean;
102
+ /** True when stream is permanently closed */
103
+ closed?: boolean;
104
+ }
105
+
106
+ /**
107
+ * Error message.
108
+ */
109
+ export interface WSErrorMessage {
110
+ type: 'error';
111
+ /** Human-readable error message */
112
+ message: string;
113
+ /** Machine-readable error code */
114
+ code?: string;
115
+ /** Stream ID if error is stream-specific */
116
+ streamId?: string;
117
+ }
118
+
119
+ /**
120
+ * Pong response to ping.
121
+ */
122
+ export interface WSPongMessage {
123
+ type: 'pong';
124
+ }
125
+
126
+ /**
127
+ * Type guard for client messages.
128
+ */
129
+ export function isWSClientMessage(msg: unknown): msg is WSClientMessage {
130
+ if (typeof msg !== 'object' || msg === null) return false;
131
+ const { type } = msg as { type?: unknown };
132
+ return type === 'subscribe' || type === 'unsubscribe' || type === 'ping';
133
+ }
134
+
135
+ /**
136
+ * Type guard for server messages.
137
+ */
138
+ export function isWSServerMessage(msg: unknown): msg is WSServerMessage {
139
+ if (typeof msg !== 'object' || msg === null) return false;
140
+ const { type } = msg as { type?: unknown };
141
+ return (
142
+ type === 'connected' ||
143
+ type === 'subscribed' ||
144
+ type === 'unsubscribed' ||
145
+ type === 'event' ||
146
+ type === 'status' ||
147
+ type === 'error' ||
148
+ type === 'pong'
149
+ );
150
+ }
151
+
152
+ /**
153
+ * Parse a JSON string as a client message.
154
+ */
155
+ export function parseWSClientMessage(json: string): WSClientMessage | null {
156
+ try {
157
+ const parsed = JSON.parse(json);
158
+ if (isWSClientMessage(parsed)) {
159
+ return parsed;
160
+ }
161
+ return null;
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Parse a JSON string as a server message.
169
+ */
170
+ export function parseWSServerMessage(json: string): WSServerMessage | null {
171
+ try {
172
+ const parsed = JSON.parse(json);
173
+ if (isWSServerMessage(parsed)) {
174
+ return parsed;
175
+ }
176
+ return null;
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Serialize a message to JSON string.
184
+ */
185
+ export function serializeWSMessage(msg: WSClientMessage | WSServerMessage): string {
186
+ return JSON.stringify(msg);
187
+ }