@jeffusion/bungee-types 5.14.0 → 5.15.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.d.ts CHANGED
@@ -3,3 +3,4 @@ export * from './config-v2.js';
3
3
  export * from './rate-limit.js';
4
4
  export * from './daemon-control.js';
5
5
  export * from './publication-policy.js';
6
+ export * from './sse-log.js';
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@ export * from './config-v2.js';
3
3
  export * from './rate-limit.js';
4
4
  export * from './daemon-control.js';
5
5
  export * from './publication-policy.js';
6
+ export * from './sse-log.js';
@@ -0,0 +1,6 @@
1
+ export interface SSELogMessage {
2
+ event: string;
3
+ data: unknown;
4
+ }
5
+ /** Format a log copy only. reserve accounts for additional parsing allocations. */
6
+ export declare function formatSSELog(body: unknown, contentType?: string, reserve?: (bytes: number) => void, requestAccept?: string, parsedMessages?: SSELogMessage[]): unknown;
@@ -0,0 +1,99 @@
1
+ function parseData(text) {
2
+ try {
3
+ return JSON.parse(text);
4
+ }
5
+ catch {
6
+ return text;
7
+ }
8
+ }
9
+ // Without response metadata, require both explicit negotiation and recognizable
10
+ // frames. Scan without building an array so ordinary errors stay plain text.
11
+ function isNegotiatedSSE(body, accept) {
12
+ const requested = accept.split(',').some(range => {
13
+ const [type, ...parameters] = range.trim().toLowerCase().split(';');
14
+ return type.trim() === 'text/event-stream'
15
+ && !parameters.some(parameter => /^\s*q\s*=\s*0(?:\.0*)?\s*$/.test(parameter));
16
+ });
17
+ if (!requested)
18
+ return false;
19
+ let hasData = false;
20
+ const lines = /([^\r\n]*)(?:\r\n|\r|\n|$)/g;
21
+ const text = body.startsWith('\uFEFF') ? body.slice(1) : body;
22
+ let match;
23
+ while ((match = lines.exec(text)) && match[0].length) {
24
+ const line = match[1];
25
+ if (!line || line.startsWith(':'))
26
+ continue;
27
+ const colon = line.indexOf(':');
28
+ const field = colon < 0 ? line : line.slice(0, colon);
29
+ if (field === 'data')
30
+ hasData = true;
31
+ else if (!['event', 'id', 'retry'].includes(field))
32
+ return false;
33
+ }
34
+ return hasData;
35
+ }
36
+ /** Format a log copy only. reserve accounts for additional parsing allocations. */
37
+ export function formatSSELog(body, contentType = '', reserve, requestAccept = '', parsedMessages) {
38
+ // Historical structured logs are projected on read, without rewriting their files.
39
+ if (body && typeof body === 'object' && !Array.isArray(body)
40
+ && 'kind' in body && body.kind === 'sse_messages' && 'messages' in body && Array.isArray(body.messages)) {
41
+ if (!body.messages.every(message => message && typeof message === 'object'
42
+ && (typeof message.dataText === 'string' || 'data' in message || message.done === true)))
43
+ return body;
44
+ return body.messages.map(message => ({
45
+ event: typeof message.event === 'string' && message.event ? message.event : 'message',
46
+ data: message.done === true ? '[DONE]' : message.data !== undefined ? message.data : parseData(message.dataText ?? ''),
47
+ }));
48
+ }
49
+ if (typeof body !== 'string')
50
+ return body;
51
+ const media = contentType.trim();
52
+ if (!/^text\/event-stream(?:\s*;|$)/i.test(media)
53
+ && (media || !isNegotiatedSSE(body, requestAccept)))
54
+ return body;
55
+ // A trusted gateway cache avoids parsing the same data again; classification above still wins.
56
+ if (parsedMessages !== undefined)
57
+ return parsedMessages;
58
+ const messages = [];
59
+ let event = '';
60
+ let data = [];
61
+ let dataLength = 0;
62
+ const dispatch = () => {
63
+ if (data.length) {
64
+ reserve?.(128 + dataLength * 8);
65
+ messages.push({ event: event || 'message', data: parseData(data.join('\n')) });
66
+ }
67
+ event = '';
68
+ data = [];
69
+ dataLength = 0;
70
+ };
71
+ // Iterate lines rather than splitting the whole stream into an unbounded line array.
72
+ const lines = /([^\r\n]*)(?:\r\n|\r|\n|$)/g;
73
+ const text = body.startsWith('\uFEFF') ? body.slice(1) : body;
74
+ let match;
75
+ while ((match = lines.exec(text)) && match[0].length) {
76
+ const line = match[1];
77
+ if (!line) {
78
+ dispatch();
79
+ continue;
80
+ }
81
+ if (line.startsWith(':'))
82
+ continue;
83
+ const colon = line.indexOf(':');
84
+ const field = colon < 0 ? line : line.slice(0, colon);
85
+ let value = colon < 0 ? '' : line.slice(colon + 1);
86
+ if (value.startsWith(' '))
87
+ value = value.slice(1);
88
+ if (field === 'event')
89
+ event = value;
90
+ else if (field === 'data') {
91
+ reserve?.(64 + value.length * 2);
92
+ dataLength += value.length + (data.length ? 1 : 0);
93
+ data.push(value);
94
+ }
95
+ }
96
+ // Retain the last captured data block even when its trailing blank line is absent.
97
+ dispatch();
98
+ return messages;
99
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffusion/bungee-types",
3
- "version": "5.14.0",
3
+ "version": "5.15.0",
4
4
  "description": "TypeScript type definitions for Bungee",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",