Skip to content

Commit 0a508ce

Browse files
committed
feat: implement buffering
1 parent 66639a4 commit 0a508ce

File tree

7 files changed

+233
-78
lines changed

7 files changed

+233
-78
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"lint": "biome lint .",
2626
"check": "biome check .",
2727
"check:fix": "biome check --write .",
28-
"tidy": "biome check --write ."
28+
"tidy": "biome check --write . && biome lint --write ."
2929
},
3030
"dependencies": {
3131
"node-datachannel": "^0.29.0",

src/connection-manager.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import { connect, getActiveWebRTCManager, sendLog } from './connection.js';
11+
import { bufferLog } from './log-buffer.js';
1112
import type { Connection, LogData } from './types.js';
1213

1314
/**
@@ -60,26 +61,30 @@ function parseLogLine(line: string, defaultLevel: LogData['type'] = 'info'): Log
6061
let isForwarding = false;
6162

6263
/**
63-
* Send log data through the active WebRTC connection, if available.
64+
* Send log data through the active WebRTC connection AND always buffer it.
6465
*
65-
* Silently no-ops when no connection is active or when called during
66-
* forwarding (to prevent recursion).
66+
* Silently no-ops when called during forwarding (to prevent recursion).
67+
* Always buffers logs for 1 minute, regardless of connection state.
6768
*/
6869
function sendLogData(logData: LogData): void {
6970
if (isForwarding) {
7071
return; // Prevent recursion
7172
}
7273

74+
// ALWAYS buffer every log for 1 minute
75+
bufferLog(logData);
76+
7377
const manager = getActiveWebRTCManager();
7478
if (!manager) {
75-
return; // Silently ignore if no connection
79+
return; // No manager available, log is buffered
7680
}
7781

82+
// Try to send immediately if we have a manager
7883
try {
7984
isForwarding = true;
8085
sendLog(logData);
8186
} catch (_error) {
82-
// Silently ignore send errors to avoid infinite loops
87+
// If send fails, that's okay - log is already buffered
8388
} finally {
8489
isForwarding = false;
8590
}

src/connection.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { WebRTCManager, type WebRTCManager as WebRTCManagerType } from './webrtc-connection.js';
7+
import { getBufferedLogs } from './log-buffer.js';
78
import type { LogData } from './types.js';
89

910
/** Global active WebRTC manager instance */
@@ -13,13 +14,27 @@ let activeManager: WebRTCManagerType | null = null;
1314
export async function connect(connectionString: string): Promise<void> {
1415
if (!activeManager) {
1516
activeManager = WebRTCManager();
17+
18+
// Set up connection handler to send buffered logs when any WebRTC connects
19+
activeManager.onConnection((peerId, connected) => {
20+
if (connected) {
21+
// Get buffered logs WITHOUT clearing them (multiple clients can connect)
22+
const bufferedLogs = getBufferedLogs();
23+
if (bufferedLogs.length > 0) {
24+
console.log(`[OpenPull] Sending ${bufferedLogs.length} buffered logs to new connection ${peerId}`);
25+
bufferedLogs.forEach(logData => {
26+
activeManager?.sendLog(logData);
27+
});
28+
}
29+
}
30+
});
1631
}
17-
32+
1833
// If already connected to the same connection string, reuse
1934
if (activeManager.isConnected()) {
2035
return;
2136
}
22-
37+
2338
await activeManager.connect(connectionString);
2439
}
2540

src/log-buffer.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/**
2+
* Log Buffer Module
3+
*
4+
* Provides in-memory buffering for logs with automatic 1-minute retention.
5+
* Handles buffering logs when WebRTC connection is not yet established,
6+
* and flushes buffered logs once connection is ready.
7+
*/
8+
9+
import type { LogData } from './types.js';
10+
11+
interface BufferedLog {
12+
logData: LogData;
13+
timestamp: number;
14+
}
15+
16+
const BUFFER_RETENTION_MS = 60 * 1000; // 1 minute
17+
18+
/**
19+
* In-memory log buffer that automatically purges old entries
20+
*/
21+
const createLogBuffer = () => {
22+
const buffer: BufferedLog[] = [];
23+
24+
/**
25+
* Add a log entry to the buffer
26+
*/
27+
const addLog = (logData: LogData): void => {
28+
buffer.push({
29+
logData,
30+
timestamp: Date.now()
31+
});
32+
33+
// Clean up old entries (older than 1 minute)
34+
purgeOldLogs();
35+
};
36+
37+
/**
38+
* Remove logs older than the retention period
39+
*/
40+
const purgeOldLogs = (): void => {
41+
const cutoff = Date.now() - BUFFER_RETENTION_MS;
42+
let removeCount = 0;
43+
44+
// Find first non-expired log
45+
for (let i = 0; i < buffer.length; i++) {
46+
if (buffer[i].timestamp >= cutoff) {
47+
break;
48+
}
49+
removeCount++;
50+
}
51+
52+
// Remove expired logs
53+
if (removeCount > 0) {
54+
buffer.splice(0, removeCount);
55+
}
56+
};
57+
58+
/**
59+
* Get all buffered logs and optionally clear the buffer
60+
*/
61+
const getAndClearLogs = (): LogData[] => {
62+
purgeOldLogs(); // Clean up first
63+
const logs = buffer.map(entry => entry.logData);
64+
buffer.length = 0; // Clear the buffer
65+
return logs;
66+
};
67+
68+
/**
69+
* Get buffered logs without clearing them
70+
*/
71+
const getLogs = (): LogData[] => {
72+
purgeOldLogs(); // Clean up first
73+
return buffer.map(entry => entry.logData);
74+
};
75+
76+
/**
77+
* Clear all buffered logs
78+
*/
79+
const clear = (): void => {
80+
buffer.length = 0;
81+
};
82+
83+
/**
84+
* Get the current buffer size
85+
*/
86+
const size = (): number => {
87+
purgeOldLogs(); // Clean up first
88+
return buffer.length;
89+
};
90+
91+
return {
92+
addLog,
93+
getAndClearLogs,
94+
getLogs,
95+
clear,
96+
size
97+
};
98+
};
99+
100+
/** Global log buffer instance */
101+
const globalLogBuffer = createLogBuffer();
102+
103+
/**
104+
* Add a log to the global buffer
105+
*/
106+
export function bufferLog(logData: LogData): void {
107+
globalLogBuffer.addLog(logData);
108+
}
109+
110+
/**
111+
* Get all buffered logs WITHOUT clearing the buffer
112+
* Used when WebRTC connection is established - multiple clients can connect
113+
*/
114+
export function getBufferedLogs(): LogData[] {
115+
return globalLogBuffer.getLogs();
116+
}
117+
118+
/**
119+
* Get all buffered logs and clear the buffer
120+
* Used when WebRTC connection is established
121+
* @deprecated Use getBufferedLogs() instead to support multiple clients
122+
*/
123+
export function flushBufferedLogs(): LogData[] {
124+
return globalLogBuffer.getAndClearLogs();
125+
}
126+
127+
/**
128+
* Get current buffer size (for debugging)
129+
*/
130+
export function getBufferSize(): number {
131+
return globalLogBuffer.size();
132+
}
133+
134+
/**
135+
* Clear all buffered logs
136+
*/
137+
export function clearBuffer(): void {
138+
globalLogBuffer.clear();
139+
}

src/logger.ts

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,29 @@ function outputLog(logData: LogData): void {
3333
process.stdout.write(JSON.stringify(logData) + '\n');
3434
}
3535

36+
/**
37+
* Normalize logger arguments to support both (message, extra) and (extra) patterns.
38+
* @internal
39+
*/
40+
function normalizeArgs(
41+
message: string | Record<string, unknown>,
42+
extra: Record<string, unknown>
43+
): { actualMessage: string; actualExtra: Record<string, unknown> } {
44+
if (typeof message === 'string') {
45+
return { actualMessage: message, actualExtra: extra };
46+
} else {
47+
// If first argument is an object, use it as extra and generate a default message
48+
const messageFromObject = message.message as string || '';
49+
const restOfObject = { ...message };
50+
delete restOfObject.message;
51+
52+
return {
53+
actualMessage: messageFromObject,
54+
actualExtra: { ...restOfObject, ...extra }
55+
};
56+
}
57+
}
58+
3659
/**
3760
* Create a tracer instance for tracking related operations.
3861
* @internal
@@ -43,16 +66,17 @@ function createTracer(
4366
traceFields: Record<string, unknown>
4467
): Tracer {
4568
return {
46-
span(message: string, extra: Record<string, unknown> = {}): Tracer {
69+
span(message: string | Record<string, unknown>, extra: Record<string, unknown> = {}): Tracer {
70+
const { actualMessage, actualExtra } = normalizeArgs(message, extra);
4771
const logData: LogData = {
4872
type: 'trace',
49-
message,
73+
message: actualMessage,
5074
traceId,
5175
spanId: generateSpanId(),
5276
timestamp: new Date().toISOString(),
5377
...defaultFields,
5478
...traceFields,
55-
...extra,
79+
...actualExtra,
5680
};
5781

5882
outputLog(logData);
@@ -92,88 +116,95 @@ export function createLogger(options: LoggerOptions = {}): Logger {
92116
const defaultFields = options.defaultFields ?? {};
93117

94118
return {
95-
info: (message: string, extra: Record<string, unknown> = {}): void => {
119+
info: (message: string | Record<string, unknown>, extra: Record<string, unknown> = {}): void => {
120+
const { actualMessage, actualExtra } = normalizeArgs(message, extra);
96121
const logData: LogData = {
97122
type: 'info',
98-
message,
123+
message: actualMessage,
99124
timestamp: new Date().toISOString(),
100125
...defaultFields,
101-
...extra,
126+
...actualExtra,
102127
};
103128

104129
outputLog(logData);
105130
},
106131

107-
error: (message: string, extra: Record<string, unknown> = {}): void => {
132+
error: (message: string | Record<string, unknown>, extra: Record<string, unknown> = {}): void => {
133+
const { actualMessage, actualExtra } = normalizeArgs(message, extra);
108134
const logData: LogData = {
109135
type: 'error',
110-
message,
136+
message: actualMessage,
111137
timestamp: new Date().toISOString(),
112138
...defaultFields,
113-
...extra,
139+
...actualExtra,
114140
};
115141

116142
outputLog(logData);
117143
},
118144

119-
debug: (message: string, extra: Record<string, unknown> = {}): void => {
145+
debug: (message: string | Record<string, unknown>, extra: Record<string, unknown> = {}): void => {
146+
const { actualMessage, actualExtra } = normalizeArgs(message, extra);
120147
const logData: LogData = {
121148
type: 'debug',
122-
message,
149+
message: actualMessage,
123150
timestamp: new Date().toISOString(),
124151
...defaultFields,
125-
...extra,
152+
...actualExtra,
126153
};
127154

128155
outputLog(logData);
129156
},
130157

131-
warning: (message: string, extra: Record<string, unknown> = {}): void => {
158+
warning: (message: string | Record<string, unknown>, extra: Record<string, unknown> = {}): void => {
159+
const { actualMessage, actualExtra } = normalizeArgs(message, extra);
132160
const logData: LogData = {
133161
type: 'warning',
134-
message,
162+
message: actualMessage,
135163
timestamp: new Date().toISOString(),
136164
...defaultFields,
137-
...extra,
165+
...actualExtra,
138166
};
139167

140168
outputLog(logData);
141169
},
142170

143-
warn: (message: string, extra: Record<string, unknown> = {}): void => {
171+
warn: (message: string | Record<string, unknown>, extra: Record<string, unknown> = {}): void => {
172+
const { actualMessage, actualExtra } = normalizeArgs(message, extra);
144173
const logData: LogData = {
145174
type: 'warning',
146-
message,
175+
message: actualMessage,
147176
timestamp: new Date().toISOString(),
148177
...defaultFields,
149-
...extra,
178+
...actualExtra,
150179
};
151180

152181
outputLog(logData);
153182
},
154183

155-
trace: (message: string, extra: Record<string, unknown> = {}): Tracer => {
184+
trace: (message: string | Record<string, unknown>, extra: Record<string, unknown> = {}): Tracer => {
185+
const { actualMessage, actualExtra } = normalizeArgs(message, extra);
156186
const traceId = generateTraceId();
157-
const tracer = createTracer(traceId, defaultFields, extra);
187+
const tracer = createTracer(traceId, defaultFields, actualExtra);
158188

159189
// Log the initial trace
160190
const logData: LogData = {
161191
type: 'trace',
162-
message,
192+
message: actualMessage,
163193
traceId,
164194
spanId: generateSpanId(),
165195
timestamp: new Date().toISOString(),
166196
...defaultFields,
167-
...extra,
197+
...actualExtra,
168198
};
169199

170200
outputLog(logData);
171201
return tracer;
172202
},
173203

174-
startTrace: (extra: Record<string, unknown> = {}): Tracer => {
204+
startTrace: (message: string | Record<string, unknown>, extra: Record<string, unknown> = {}): Tracer => {
205+
const { actualMessage, actualExtra } = normalizeArgs(message, extra);
175206
const traceId = generateTraceId();
176-
return createTracer(traceId, defaultFields, extra);
207+
return createTracer(traceId, defaultFields, actualExtra);
177208
},
178209
};
179210
}

0 commit comments

Comments
 (0)