@logtape/logtape 1.0.0-dev.236 → 1.0.0-dev.237
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/deno.json +1 -1
- package/dist/sink.cjs +107 -10
- package/dist/sink.d.cts +71 -2
- package/dist/sink.d.cts.map +1 -1
- package/dist/sink.d.ts +71 -2
- package/dist/sink.d.ts.map +1 -1
- package/dist/sink.js +107 -10
- package/dist/sink.js.map +1 -1
- package/package.json +1 -1
- package/sink.test.ts +424 -5
- package/sink.ts +245 -13
package/deno.json
CHANGED
package/dist/sink.cjs
CHANGED
|
@@ -112,22 +112,73 @@ function getStreamSink(stream, options = {}) {
|
|
|
112
112
|
const formatter = options.formatter ?? require_formatter.defaultTextFormatter;
|
|
113
113
|
const encoder = options.encoder ?? new TextEncoder();
|
|
114
114
|
const writer = stream.getWriter();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
115
|
+
if (!options.nonBlocking) {
|
|
116
|
+
let lastPromise = Promise.resolve();
|
|
117
|
+
const sink = (record) => {
|
|
118
|
+
const bytes = encoder.encode(formatter(record));
|
|
119
|
+
lastPromise = lastPromise.then(() => writer.ready).then(() => writer.write(bytes));
|
|
120
|
+
};
|
|
121
|
+
sink[Symbol.asyncDispose] = async () => {
|
|
122
|
+
await lastPromise;
|
|
123
|
+
await writer.close();
|
|
124
|
+
};
|
|
125
|
+
return sink;
|
|
126
|
+
}
|
|
127
|
+
const nonBlockingConfig = options.nonBlocking === true ? {} : options.nonBlocking;
|
|
128
|
+
const bufferSize = nonBlockingConfig.bufferSize ?? 100;
|
|
129
|
+
const flushInterval = nonBlockingConfig.flushInterval ?? 100;
|
|
130
|
+
const buffer = [];
|
|
131
|
+
let flushTimer = null;
|
|
132
|
+
let disposed = false;
|
|
133
|
+
let activeFlush = null;
|
|
134
|
+
const maxBufferSize = bufferSize * 2;
|
|
135
|
+
async function flush() {
|
|
136
|
+
if (buffer.length === 0) return;
|
|
137
|
+
const records = buffer.splice(0);
|
|
138
|
+
for (const record of records) try {
|
|
139
|
+
const bytes = encoder.encode(formatter(record));
|
|
140
|
+
await writer.ready;
|
|
141
|
+
await writer.write(bytes);
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
function scheduleFlush() {
|
|
145
|
+
if (activeFlush) return;
|
|
146
|
+
activeFlush = flush().finally(() => {
|
|
147
|
+
activeFlush = null;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function startFlushTimer() {
|
|
151
|
+
if (flushTimer !== null || disposed) return;
|
|
152
|
+
flushTimer = setInterval(() => {
|
|
153
|
+
scheduleFlush();
|
|
154
|
+
}, flushInterval);
|
|
155
|
+
}
|
|
156
|
+
const nonBlockingSink = (record) => {
|
|
157
|
+
if (disposed) return;
|
|
158
|
+
if (buffer.length >= maxBufferSize) buffer.shift();
|
|
159
|
+
buffer.push(record);
|
|
160
|
+
if (buffer.length >= bufferSize) scheduleFlush();
|
|
161
|
+
else if (flushTimer === null) startFlushTimer();
|
|
119
162
|
};
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
163
|
+
nonBlockingSink[Symbol.asyncDispose] = async () => {
|
|
164
|
+
disposed = true;
|
|
165
|
+
if (flushTimer !== null) {
|
|
166
|
+
clearInterval(flushTimer);
|
|
167
|
+
flushTimer = null;
|
|
168
|
+
}
|
|
169
|
+
await flush();
|
|
170
|
+
try {
|
|
171
|
+
await writer.close();
|
|
172
|
+
} catch {}
|
|
123
173
|
};
|
|
124
|
-
return
|
|
174
|
+
return nonBlockingSink;
|
|
125
175
|
}
|
|
126
176
|
/**
|
|
127
177
|
* A console sink factory that returns a sink that logs to the console.
|
|
128
178
|
*
|
|
129
179
|
* @param options The options for the sink.
|
|
130
|
-
* @returns A sink that logs to the console.
|
|
180
|
+
* @returns A sink that logs to the console. If `nonBlocking` is enabled,
|
|
181
|
+
* returns a sink that also implements {@link Disposable}.
|
|
131
182
|
*/
|
|
132
183
|
function getConsoleSink(options = {}) {
|
|
133
184
|
const formatter = options.formatter ?? require_formatter.defaultConsoleFormatter;
|
|
@@ -141,7 +192,7 @@ function getConsoleSink(options = {}) {
|
|
|
141
192
|
...options.levelMap ?? {}
|
|
142
193
|
};
|
|
143
194
|
const console = options.console ?? globalThis.console;
|
|
144
|
-
|
|
195
|
+
const baseSink = (record) => {
|
|
145
196
|
const args = formatter(record);
|
|
146
197
|
const method = levelMap[record.level];
|
|
147
198
|
if (method === void 0) throw new TypeError(`Invalid log level: ${record.level}.`);
|
|
@@ -150,6 +201,52 @@ function getConsoleSink(options = {}) {
|
|
|
150
201
|
console[method](msg);
|
|
151
202
|
} else console[method](...args);
|
|
152
203
|
};
|
|
204
|
+
if (!options.nonBlocking) return baseSink;
|
|
205
|
+
const nonBlockingConfig = options.nonBlocking === true ? {} : options.nonBlocking;
|
|
206
|
+
const bufferSize = nonBlockingConfig.bufferSize ?? 100;
|
|
207
|
+
const flushInterval = nonBlockingConfig.flushInterval ?? 100;
|
|
208
|
+
const buffer = [];
|
|
209
|
+
let flushTimer = null;
|
|
210
|
+
let disposed = false;
|
|
211
|
+
let flushScheduled = false;
|
|
212
|
+
const maxBufferSize = bufferSize * 2;
|
|
213
|
+
function flush() {
|
|
214
|
+
if (buffer.length === 0) return;
|
|
215
|
+
const records = buffer.splice(0);
|
|
216
|
+
for (const record of records) try {
|
|
217
|
+
baseSink(record);
|
|
218
|
+
} catch {}
|
|
219
|
+
}
|
|
220
|
+
function scheduleFlush() {
|
|
221
|
+
if (flushScheduled) return;
|
|
222
|
+
flushScheduled = true;
|
|
223
|
+
setTimeout(() => {
|
|
224
|
+
flushScheduled = false;
|
|
225
|
+
flush();
|
|
226
|
+
}, 0);
|
|
227
|
+
}
|
|
228
|
+
function startFlushTimer() {
|
|
229
|
+
if (flushTimer !== null || disposed) return;
|
|
230
|
+
flushTimer = setInterval(() => {
|
|
231
|
+
flush();
|
|
232
|
+
}, flushInterval);
|
|
233
|
+
}
|
|
234
|
+
const nonBlockingSink = (record) => {
|
|
235
|
+
if (disposed) return;
|
|
236
|
+
if (buffer.length >= maxBufferSize) buffer.shift();
|
|
237
|
+
buffer.push(record);
|
|
238
|
+
if (buffer.length >= bufferSize) scheduleFlush();
|
|
239
|
+
else if (flushTimer === null) startFlushTimer();
|
|
240
|
+
};
|
|
241
|
+
nonBlockingSink[Symbol.dispose] = () => {
|
|
242
|
+
disposed = true;
|
|
243
|
+
if (flushTimer !== null) {
|
|
244
|
+
clearInterval(flushTimer);
|
|
245
|
+
flushTimer = null;
|
|
246
|
+
}
|
|
247
|
+
flush();
|
|
248
|
+
};
|
|
249
|
+
return nonBlockingSink;
|
|
153
250
|
}
|
|
154
251
|
/**
|
|
155
252
|
* Converts an async sink into a regular sink with proper async handling.
|
package/dist/sink.d.cts
CHANGED
|
@@ -92,6 +92,40 @@ interface StreamSinkOptions {
|
|
|
92
92
|
encoder?: {
|
|
93
93
|
encode(text: string): Uint8Array;
|
|
94
94
|
};
|
|
95
|
+
/**
|
|
96
|
+
* Enable non-blocking mode with optional buffer configuration.
|
|
97
|
+
* When enabled, log records are buffered and flushed in the background.
|
|
98
|
+
*
|
|
99
|
+
* @example Simple non-blocking mode
|
|
100
|
+
* ```typescript
|
|
101
|
+
* getStreamSink(stream, { nonBlocking: true });
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @example Custom buffer configuration
|
|
105
|
+
* ```typescript
|
|
106
|
+
* getStreamSink(stream, {
|
|
107
|
+
* nonBlocking: {
|
|
108
|
+
* bufferSize: 1000,
|
|
109
|
+
* flushInterval: 50
|
|
110
|
+
* }
|
|
111
|
+
* });
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* @default `false`
|
|
115
|
+
* @since 1.0.0
|
|
116
|
+
*/
|
|
117
|
+
nonBlocking?: boolean | {
|
|
118
|
+
/**
|
|
119
|
+
* Maximum number of records to buffer before flushing.
|
|
120
|
+
* @default `100`
|
|
121
|
+
*/
|
|
122
|
+
bufferSize?: number;
|
|
123
|
+
/**
|
|
124
|
+
* Interval in milliseconds between automatic flushes.
|
|
125
|
+
* @default `100`
|
|
126
|
+
*/
|
|
127
|
+
flushInterval?: number;
|
|
128
|
+
};
|
|
95
129
|
}
|
|
96
130
|
/**
|
|
97
131
|
* A factory that returns a sink that writes to a {@link WritableStream}.
|
|
@@ -148,14 +182,49 @@ interface ConsoleSinkOptions {
|
|
|
148
182
|
* The console to log to. Defaults to {@link console}.
|
|
149
183
|
*/
|
|
150
184
|
console?: Console;
|
|
185
|
+
/**
|
|
186
|
+
* Enable non-blocking mode with optional buffer configuration.
|
|
187
|
+
* When enabled, log records are buffered and flushed in the background.
|
|
188
|
+
*
|
|
189
|
+
* @example Simple non-blocking mode
|
|
190
|
+
* ```typescript
|
|
191
|
+
* getConsoleSink({ nonBlocking: true });
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* @example Custom buffer configuration
|
|
195
|
+
* ```typescript
|
|
196
|
+
* getConsoleSink({
|
|
197
|
+
* nonBlocking: {
|
|
198
|
+
* bufferSize: 1000,
|
|
199
|
+
* flushInterval: 50
|
|
200
|
+
* }
|
|
201
|
+
* });
|
|
202
|
+
* ```
|
|
203
|
+
*
|
|
204
|
+
* @default `false`
|
|
205
|
+
* @since 1.0.0
|
|
206
|
+
*/
|
|
207
|
+
nonBlocking?: boolean | {
|
|
208
|
+
/**
|
|
209
|
+
* Maximum number of records to buffer before flushing.
|
|
210
|
+
* @default `100`
|
|
211
|
+
*/
|
|
212
|
+
bufferSize?: number;
|
|
213
|
+
/**
|
|
214
|
+
* Interval in milliseconds between automatic flushes.
|
|
215
|
+
* @default `100`
|
|
216
|
+
*/
|
|
217
|
+
flushInterval?: number;
|
|
218
|
+
};
|
|
151
219
|
}
|
|
152
220
|
/**
|
|
153
221
|
* A console sink factory that returns a sink that logs to the console.
|
|
154
222
|
*
|
|
155
223
|
* @param options The options for the sink.
|
|
156
|
-
* @returns A sink that logs to the console.
|
|
224
|
+
* @returns A sink that logs to the console. If `nonBlocking` is enabled,
|
|
225
|
+
* returns a sink that also implements {@link Disposable}.
|
|
157
226
|
*/
|
|
158
|
-
declare function getConsoleSink(options?: ConsoleSinkOptions): Sink;
|
|
227
|
+
declare function getConsoleSink(options?: ConsoleSinkOptions): Sink | (Sink & Disposable);
|
|
159
228
|
/**
|
|
160
229
|
* Converts an async sink into a regular sink with proper async handling.
|
|
161
230
|
* The returned sink chains async operations to ensure proper ordering and
|
package/dist/sink.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sink.d.cts","names":[],"sources":["../sink.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAmBA;AAWA;;;;AAAsD;AAgBtD;AAA0B,KA3Bd,IAAA,GA2Bc,CAAA,MAAA,EA3BE,SA2BF,EAAA,GAAA,IAAA;;;;AAAsC;AAWhE;AAmCA;;;;AAGG,KAjES,SAAA,GAiET,CAAA,MAAA,EAjE8B,SAiE9B,EAAA,GAjE4C,OAiE5C,CAAA,IAAA,CAAA;;AAAsB;AAsEzB;;;;AAS8C;
|
|
1
|
+
{"version":3,"file":"sink.d.cts","names":[],"sources":["../sink.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAmBA;AAWA;;;;AAAsD;AAgBtD;AAA0B,KA3Bd,IAAA,GA2Bc,CAAA,MAAA,EA3BE,SA2BF,EAAA,GAAA,IAAA;;;;AAAsC;AAWhE;AAmCA;;;;AAGG,KAjES,SAAA,GAiET,CAAA,MAAA,EAjE8B,SAiE9B,EAAA,GAjE4C,OAiE5C,CAAA,IAAA,CAAA;;AAAsB;AAsEzB;;;;AAS8C;AA+D9C;;;;;;AAGyB;AAkGpB,iBApSW,UAAA,CAoSE,IAAA,EApSe,IAoSf,EAAA,MAAA,EApS6B,UAoS7B,CAAA,EApS0C,IAoS1C;AAKlB;;;;AAsBoB,UApTH,iBAAA,CAoTG;EAAQ;;;AAKT;AA8CnB;EAA8B,UAAA,CAAA,EAAA,MAAA;EAAA;;;;AAEF;AA4H5B;EAA6B,aAAA,CAAA,EAAA,MAAA;;;;AAA8C;;;;;;;;;;;;;;;;iBAlc3D,UAAA,OACR,gBACG,oBACR,OAAO;;;;UAsEO,iBAAA;;;;cAIH;;;;;0BAKsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+DpB,aAAA,SACN,0BACC,oBACR,OAAO;KAkGL,aAAA;;;;UAKY,kBAAA;;;;;cAKH,mBAAmB;;;;;;;;;;;;;;;;aAiBpB,OAAO,UAAU;;;;YAKlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8CI,cAAA,WACL,qBACR,QAAQ,OAAO;;;;;;;;;;;;;;;;;;;;;iBA4HF,aAAA,YAAyB,YAAY,OAAO"}
|
package/dist/sink.d.ts
CHANGED
|
@@ -92,6 +92,40 @@ interface StreamSinkOptions {
|
|
|
92
92
|
encoder?: {
|
|
93
93
|
encode(text: string): Uint8Array;
|
|
94
94
|
};
|
|
95
|
+
/**
|
|
96
|
+
* Enable non-blocking mode with optional buffer configuration.
|
|
97
|
+
* When enabled, log records are buffered and flushed in the background.
|
|
98
|
+
*
|
|
99
|
+
* @example Simple non-blocking mode
|
|
100
|
+
* ```typescript
|
|
101
|
+
* getStreamSink(stream, { nonBlocking: true });
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @example Custom buffer configuration
|
|
105
|
+
* ```typescript
|
|
106
|
+
* getStreamSink(stream, {
|
|
107
|
+
* nonBlocking: {
|
|
108
|
+
* bufferSize: 1000,
|
|
109
|
+
* flushInterval: 50
|
|
110
|
+
* }
|
|
111
|
+
* });
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* @default `false`
|
|
115
|
+
* @since 1.0.0
|
|
116
|
+
*/
|
|
117
|
+
nonBlocking?: boolean | {
|
|
118
|
+
/**
|
|
119
|
+
* Maximum number of records to buffer before flushing.
|
|
120
|
+
* @default `100`
|
|
121
|
+
*/
|
|
122
|
+
bufferSize?: number;
|
|
123
|
+
/**
|
|
124
|
+
* Interval in milliseconds between automatic flushes.
|
|
125
|
+
* @default `100`
|
|
126
|
+
*/
|
|
127
|
+
flushInterval?: number;
|
|
128
|
+
};
|
|
95
129
|
}
|
|
96
130
|
/**
|
|
97
131
|
* A factory that returns a sink that writes to a {@link WritableStream}.
|
|
@@ -148,14 +182,49 @@ interface ConsoleSinkOptions {
|
|
|
148
182
|
* The console to log to. Defaults to {@link console}.
|
|
149
183
|
*/
|
|
150
184
|
console?: Console;
|
|
185
|
+
/**
|
|
186
|
+
* Enable non-blocking mode with optional buffer configuration.
|
|
187
|
+
* When enabled, log records are buffered and flushed in the background.
|
|
188
|
+
*
|
|
189
|
+
* @example Simple non-blocking mode
|
|
190
|
+
* ```typescript
|
|
191
|
+
* getConsoleSink({ nonBlocking: true });
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* @example Custom buffer configuration
|
|
195
|
+
* ```typescript
|
|
196
|
+
* getConsoleSink({
|
|
197
|
+
* nonBlocking: {
|
|
198
|
+
* bufferSize: 1000,
|
|
199
|
+
* flushInterval: 50
|
|
200
|
+
* }
|
|
201
|
+
* });
|
|
202
|
+
* ```
|
|
203
|
+
*
|
|
204
|
+
* @default `false`
|
|
205
|
+
* @since 1.0.0
|
|
206
|
+
*/
|
|
207
|
+
nonBlocking?: boolean | {
|
|
208
|
+
/**
|
|
209
|
+
* Maximum number of records to buffer before flushing.
|
|
210
|
+
* @default `100`
|
|
211
|
+
*/
|
|
212
|
+
bufferSize?: number;
|
|
213
|
+
/**
|
|
214
|
+
* Interval in milliseconds between automatic flushes.
|
|
215
|
+
* @default `100`
|
|
216
|
+
*/
|
|
217
|
+
flushInterval?: number;
|
|
218
|
+
};
|
|
151
219
|
}
|
|
152
220
|
/**
|
|
153
221
|
* A console sink factory that returns a sink that logs to the console.
|
|
154
222
|
*
|
|
155
223
|
* @param options The options for the sink.
|
|
156
|
-
* @returns A sink that logs to the console.
|
|
224
|
+
* @returns A sink that logs to the console. If `nonBlocking` is enabled,
|
|
225
|
+
* returns a sink that also implements {@link Disposable}.
|
|
157
226
|
*/
|
|
158
|
-
declare function getConsoleSink(options?: ConsoleSinkOptions): Sink;
|
|
227
|
+
declare function getConsoleSink(options?: ConsoleSinkOptions): Sink | (Sink & Disposable);
|
|
159
228
|
/**
|
|
160
229
|
* Converts an async sink into a regular sink with proper async handling.
|
|
161
230
|
* The returned sink chains async operations to ensure proper ordering and
|
package/dist/sink.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sink.d.ts","names":[],"sources":["../sink.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAmBA;AAWA;;;;AAAsD;AAgBtD;AAA0B,KA3Bd,IAAA,GA2Bc,CAAA,MAAA,EA3BE,SA2BF,EAAA,GAAA,IAAA;;;;AAAsC;AAWhE;AAmCA;;;;AAGG,KAjES,SAAA,GAiET,CAAA,MAAA,EAjE8B,SAiE9B,EAAA,GAjE4C,OAiE5C,CAAA,IAAA,CAAA;;AAAsB;AAsEzB;;;;AAS8C;
|
|
1
|
+
{"version":3,"file":"sink.d.ts","names":[],"sources":["../sink.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAmBA;AAWA;;;;AAAsD;AAgBtD;AAA0B,KA3Bd,IAAA,GA2Bc,CAAA,MAAA,EA3BE,SA2BF,EAAA,GAAA,IAAA;;;;AAAsC;AAWhE;AAmCA;;;;AAGG,KAjES,SAAA,GAiET,CAAA,MAAA,EAjE8B,SAiE9B,EAAA,GAjE4C,OAiE5C,CAAA,IAAA,CAAA;;AAAsB;AAsEzB;;;;AAS8C;AA+D9C;;;;;;AAGyB;AAkGpB,iBApSW,UAAA,CAoSE,IAAA,EApSe,IAoSf,EAAA,MAAA,EApS6B,UAoS7B,CAAA,EApS0C,IAoS1C;AAKlB;;;;AAsBoB,UApTH,iBAAA,CAoTG;EAAQ;;;AAKT;AA8CnB;EAA8B,UAAA,CAAA,EAAA,MAAA;EAAA;;;;AAEF;AA4H5B;EAA6B,aAAA,CAAA,EAAA,MAAA;;;;AAA8C;;;;;;;;;;;;;;;;iBAlc3D,UAAA,OACR,gBACG,oBACR,OAAO;;;;UAsEO,iBAAA;;;;cAIH;;;;;0BAKsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+DpB,aAAA,SACN,0BACC,oBACR,OAAO;KAkGL,aAAA;;;;UAKY,kBAAA;;;;;cAKH,mBAAmB;;;;;;;;;;;;;;;;aAiBpB,OAAO,UAAU;;;;YAKlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8CI,cAAA,WACL,qBACR,QAAQ,OAAO;;;;;;;;;;;;;;;;;;;;;iBA4HF,aAAA,YAAyB,YAAY,OAAO"}
|
package/dist/sink.js
CHANGED
|
@@ -112,22 +112,73 @@ function getStreamSink(stream, options = {}) {
|
|
|
112
112
|
const formatter = options.formatter ?? defaultTextFormatter;
|
|
113
113
|
const encoder = options.encoder ?? new TextEncoder();
|
|
114
114
|
const writer = stream.getWriter();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
115
|
+
if (!options.nonBlocking) {
|
|
116
|
+
let lastPromise = Promise.resolve();
|
|
117
|
+
const sink = (record) => {
|
|
118
|
+
const bytes = encoder.encode(formatter(record));
|
|
119
|
+
lastPromise = lastPromise.then(() => writer.ready).then(() => writer.write(bytes));
|
|
120
|
+
};
|
|
121
|
+
sink[Symbol.asyncDispose] = async () => {
|
|
122
|
+
await lastPromise;
|
|
123
|
+
await writer.close();
|
|
124
|
+
};
|
|
125
|
+
return sink;
|
|
126
|
+
}
|
|
127
|
+
const nonBlockingConfig = options.nonBlocking === true ? {} : options.nonBlocking;
|
|
128
|
+
const bufferSize = nonBlockingConfig.bufferSize ?? 100;
|
|
129
|
+
const flushInterval = nonBlockingConfig.flushInterval ?? 100;
|
|
130
|
+
const buffer = [];
|
|
131
|
+
let flushTimer = null;
|
|
132
|
+
let disposed = false;
|
|
133
|
+
let activeFlush = null;
|
|
134
|
+
const maxBufferSize = bufferSize * 2;
|
|
135
|
+
async function flush() {
|
|
136
|
+
if (buffer.length === 0) return;
|
|
137
|
+
const records = buffer.splice(0);
|
|
138
|
+
for (const record of records) try {
|
|
139
|
+
const bytes = encoder.encode(formatter(record));
|
|
140
|
+
await writer.ready;
|
|
141
|
+
await writer.write(bytes);
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
function scheduleFlush() {
|
|
145
|
+
if (activeFlush) return;
|
|
146
|
+
activeFlush = flush().finally(() => {
|
|
147
|
+
activeFlush = null;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function startFlushTimer() {
|
|
151
|
+
if (flushTimer !== null || disposed) return;
|
|
152
|
+
flushTimer = setInterval(() => {
|
|
153
|
+
scheduleFlush();
|
|
154
|
+
}, flushInterval);
|
|
155
|
+
}
|
|
156
|
+
const nonBlockingSink = (record) => {
|
|
157
|
+
if (disposed) return;
|
|
158
|
+
if (buffer.length >= maxBufferSize) buffer.shift();
|
|
159
|
+
buffer.push(record);
|
|
160
|
+
if (buffer.length >= bufferSize) scheduleFlush();
|
|
161
|
+
else if (flushTimer === null) startFlushTimer();
|
|
119
162
|
};
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
163
|
+
nonBlockingSink[Symbol.asyncDispose] = async () => {
|
|
164
|
+
disposed = true;
|
|
165
|
+
if (flushTimer !== null) {
|
|
166
|
+
clearInterval(flushTimer);
|
|
167
|
+
flushTimer = null;
|
|
168
|
+
}
|
|
169
|
+
await flush();
|
|
170
|
+
try {
|
|
171
|
+
await writer.close();
|
|
172
|
+
} catch {}
|
|
123
173
|
};
|
|
124
|
-
return
|
|
174
|
+
return nonBlockingSink;
|
|
125
175
|
}
|
|
126
176
|
/**
|
|
127
177
|
* A console sink factory that returns a sink that logs to the console.
|
|
128
178
|
*
|
|
129
179
|
* @param options The options for the sink.
|
|
130
|
-
* @returns A sink that logs to the console.
|
|
180
|
+
* @returns A sink that logs to the console. If `nonBlocking` is enabled,
|
|
181
|
+
* returns a sink that also implements {@link Disposable}.
|
|
131
182
|
*/
|
|
132
183
|
function getConsoleSink(options = {}) {
|
|
133
184
|
const formatter = options.formatter ?? defaultConsoleFormatter;
|
|
@@ -141,7 +192,7 @@ function getConsoleSink(options = {}) {
|
|
|
141
192
|
...options.levelMap ?? {}
|
|
142
193
|
};
|
|
143
194
|
const console = options.console ?? globalThis.console;
|
|
144
|
-
|
|
195
|
+
const baseSink = (record) => {
|
|
145
196
|
const args = formatter(record);
|
|
146
197
|
const method = levelMap[record.level];
|
|
147
198
|
if (method === void 0) throw new TypeError(`Invalid log level: ${record.level}.`);
|
|
@@ -150,6 +201,52 @@ function getConsoleSink(options = {}) {
|
|
|
150
201
|
console[method](msg);
|
|
151
202
|
} else console[method](...args);
|
|
152
203
|
};
|
|
204
|
+
if (!options.nonBlocking) return baseSink;
|
|
205
|
+
const nonBlockingConfig = options.nonBlocking === true ? {} : options.nonBlocking;
|
|
206
|
+
const bufferSize = nonBlockingConfig.bufferSize ?? 100;
|
|
207
|
+
const flushInterval = nonBlockingConfig.flushInterval ?? 100;
|
|
208
|
+
const buffer = [];
|
|
209
|
+
let flushTimer = null;
|
|
210
|
+
let disposed = false;
|
|
211
|
+
let flushScheduled = false;
|
|
212
|
+
const maxBufferSize = bufferSize * 2;
|
|
213
|
+
function flush() {
|
|
214
|
+
if (buffer.length === 0) return;
|
|
215
|
+
const records = buffer.splice(0);
|
|
216
|
+
for (const record of records) try {
|
|
217
|
+
baseSink(record);
|
|
218
|
+
} catch {}
|
|
219
|
+
}
|
|
220
|
+
function scheduleFlush() {
|
|
221
|
+
if (flushScheduled) return;
|
|
222
|
+
flushScheduled = true;
|
|
223
|
+
setTimeout(() => {
|
|
224
|
+
flushScheduled = false;
|
|
225
|
+
flush();
|
|
226
|
+
}, 0);
|
|
227
|
+
}
|
|
228
|
+
function startFlushTimer() {
|
|
229
|
+
if (flushTimer !== null || disposed) return;
|
|
230
|
+
flushTimer = setInterval(() => {
|
|
231
|
+
flush();
|
|
232
|
+
}, flushInterval);
|
|
233
|
+
}
|
|
234
|
+
const nonBlockingSink = (record) => {
|
|
235
|
+
if (disposed) return;
|
|
236
|
+
if (buffer.length >= maxBufferSize) buffer.shift();
|
|
237
|
+
buffer.push(record);
|
|
238
|
+
if (buffer.length >= bufferSize) scheduleFlush();
|
|
239
|
+
else if (flushTimer === null) startFlushTimer();
|
|
240
|
+
};
|
|
241
|
+
nonBlockingSink[Symbol.dispose] = () => {
|
|
242
|
+
disposed = true;
|
|
243
|
+
if (flushTimer !== null) {
|
|
244
|
+
clearInterval(flushTimer);
|
|
245
|
+
flushTimer = null;
|
|
246
|
+
}
|
|
247
|
+
flush();
|
|
248
|
+
};
|
|
249
|
+
return nonBlockingSink;
|
|
153
250
|
}
|
|
154
251
|
/**
|
|
155
252
|
* Converts an async sink into a regular sink with proper async handling.
|
package/dist/sink.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sink.js","names":["sink: Sink","filter: FilterLike","record: LogRecord","options: BufferSinkOptions","buffer: LogRecord[]","flushTimer: ReturnType<typeof setTimeout> | null","bufferedSink: Sink & AsyncDisposable","stream: WritableStream","options: StreamSinkOptions","sink: Sink & AsyncDisposable","options: ConsoleSinkOptions","levelMap: Record<LogLevel, ConsoleMethod>","asyncSink: AsyncSink"],"sources":["../sink.ts"],"sourcesContent":["import { type FilterLike, toFilter } from \"./filter.ts\";\nimport {\n type ConsoleFormatter,\n defaultConsoleFormatter,\n defaultTextFormatter,\n type TextFormatter,\n} from \"./formatter.ts\";\nimport type { LogLevel } from \"./level.ts\";\nimport type { LogRecord } from \"./record.ts\";\n\n/**\n * A sink is a function that accepts a log record and prints it somewhere.\n * Thrown exceptions will be suppressed and then logged to the meta logger,\n * a {@link Logger} with the category `[\"logtape\", \"meta\"]`. (In that case,\n * the meta log record will not be passed to the sink to avoid infinite\n * recursion.)\n *\n * @param record The log record to sink.\n */\nexport type Sink = (record: LogRecord) => void;\n\n/**\n * An async sink is a function that accepts a log record and asynchronously\n * processes it. This type is used with {@link fromAsyncSink} to create\n * a regular sink that properly handles asynchronous operations.\n *\n * @param record The log record to process asynchronously.\n * @returns A promise that resolves when the record has been processed.\n * @since 1.0.0\n */\nexport type AsyncSink = (record: LogRecord) => Promise<void>;\n\n/**\n * Turns a sink into a filtered sink. The returned sink only logs records that\n * pass the filter.\n *\n * @example Filter a console sink to only log records with the info level\n * ```typescript\n * const sink = withFilter(getConsoleSink(), \"info\");\n * ```\n *\n * @param sink A sink to be filtered.\n * @param filter A filter to apply to the sink. It can be either a filter\n * function or a {@link LogLevel} string.\n * @returns A sink that only logs records that pass the filter.\n */\nexport function withFilter(sink: Sink, filter: FilterLike): Sink {\n const filterFunc = toFilter(filter);\n return (record: LogRecord) => {\n if (filterFunc(record)) sink(record);\n };\n}\n\n/**\n * Options for the {@link withBuffer} function.\n * @since 1.0.0\n */\nexport interface BufferSinkOptions {\n /**\n * The maximum number of log records to buffer before flushing to the\n * underlying sink.\n * @default `10`\n */\n bufferSize?: number;\n\n /**\n * The maximum time in milliseconds to wait before flushing buffered records\n * to the underlying sink. Defaults to 5000 (5 seconds). Set to 0 or\n * negative to disable time-based flushing.\n * @default `5000`\n */\n flushInterval?: number;\n}\n\n/**\n * Turns a sink into a buffered sink. The returned sink buffers log records\n * in memory and flushes them to the underlying sink when the buffer is full\n * or after a specified time interval.\n *\n * @example Buffer a console sink with custom options\n * ```typescript\n * const sink = withBuffer(getConsoleSink(), {\n * bufferSize: 5,\n * flushInterval: 1000\n * });\n * ```\n *\n * @param sink A sink to be buffered.\n * @param options Options for the buffered sink.\n * @returns A buffered sink that flushes records periodically.\n * @since 1.0.0\n */\nexport function withBuffer(\n sink: Sink,\n options: BufferSinkOptions = {},\n): Sink & AsyncDisposable {\n const bufferSize = options.bufferSize ?? 10;\n const flushInterval = options.flushInterval ?? 5000;\n\n const buffer: LogRecord[] = [];\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n let disposed = false;\n\n function flush(): void {\n if (buffer.length === 0) return;\n\n const records = buffer.splice(0);\n for (const record of records) {\n try {\n sink(record);\n } catch (error) {\n // Errors are handled by the sink infrastructure\n throw error;\n }\n }\n\n if (flushTimer !== null) {\n clearTimeout(flushTimer);\n flushTimer = null;\n }\n }\n\n function scheduleFlush(): void {\n if (flushInterval <= 0 || flushTimer !== null || disposed) return;\n\n flushTimer = setTimeout(() => {\n flushTimer = null;\n flush();\n }, flushInterval);\n }\n\n const bufferedSink: Sink & AsyncDisposable = (record: LogRecord) => {\n if (disposed) return;\n\n buffer.push(record);\n\n if (buffer.length >= bufferSize) {\n flush();\n } else {\n scheduleFlush();\n }\n };\n\n bufferedSink[Symbol.asyncDispose] = async () => {\n disposed = true;\n if (flushTimer !== null) {\n clearTimeout(flushTimer);\n flushTimer = null;\n }\n flush();\n\n // Dispose the underlying sink if it's disposable\n if (Symbol.asyncDispose in sink) {\n await (sink as AsyncDisposable)[Symbol.asyncDispose]();\n } else if (Symbol.dispose in sink) {\n (sink as Disposable)[Symbol.dispose]();\n }\n };\n\n return bufferedSink;\n}\n\n/**\n * Options for the {@link getStreamSink} function.\n */\nexport interface StreamSinkOptions {\n /**\n * The text formatter to use. Defaults to {@link defaultTextFormatter}.\n */\n formatter?: TextFormatter;\n\n /**\n * The text encoder to use. Defaults to an instance of {@link TextEncoder}.\n */\n encoder?: { encode(text: string): Uint8Array };\n}\n\n/**\n * A factory that returns a sink that writes to a {@link WritableStream}.\n *\n * Note that the `stream` is of Web Streams API, which is different from\n * Node.js streams. You can convert a Node.js stream to a Web Streams API\n * stream using [`stream.Writable.toWeb()`] method.\n *\n * [`stream.Writable.toWeb()`]: https://nodejs.org/api/stream.html#streamwritabletowebstreamwritable\n *\n * @example Sink to the standard error in Deno\n * ```typescript\n * const stderrSink = getStreamSink(Deno.stderr.writable);\n * ```\n *\n * @example Sink to the standard error in Node.js\n * ```typescript\n * import stream from \"node:stream\";\n * const stderrSink = getStreamSink(stream.Writable.toWeb(process.stderr));\n * ```\n *\n * @param stream The stream to write to.\n * @param options The options for the sink.\n * @returns A sink that writes to the stream.\n */\nexport function getStreamSink(\n stream: WritableStream,\n options: StreamSinkOptions = {},\n): Sink & AsyncDisposable {\n const formatter = options.formatter ?? defaultTextFormatter;\n const encoder = options.encoder ?? new TextEncoder();\n const writer = stream.getWriter();\n let lastPromise = Promise.resolve();\n const sink: Sink & AsyncDisposable = (record: LogRecord) => {\n const bytes = encoder.encode(formatter(record));\n lastPromise = lastPromise\n .then(() => writer.ready)\n .then(() => writer.write(bytes));\n };\n sink[Symbol.asyncDispose] = async () => {\n await lastPromise;\n await writer.close();\n };\n return sink;\n}\n\ntype ConsoleMethod = \"debug\" | \"info\" | \"log\" | \"warn\" | \"error\";\n\n/**\n * Options for the {@link getConsoleSink} function.\n */\nexport interface ConsoleSinkOptions {\n /**\n * The console formatter or text formatter to use.\n * Defaults to {@link defaultConsoleFormatter}.\n */\n formatter?: ConsoleFormatter | TextFormatter;\n\n /**\n * The mapping from log levels to console methods. Defaults to:\n *\n * ```typescript\n * {\n * trace: \"trace\",\n * debug: \"debug\",\n * info: \"info\",\n * warning: \"warn\",\n * error: \"error\",\n * fatal: \"error\",\n * }\n * ```\n * @since 0.9.0\n */\n levelMap?: Record<LogLevel, ConsoleMethod>;\n\n /**\n * The console to log to. Defaults to {@link console}.\n */\n console?: Console;\n}\n\n/**\n * A console sink factory that returns a sink that logs to the console.\n *\n * @param options The options for the sink.\n * @returns A sink that logs to the console.\n */\nexport function getConsoleSink(options: ConsoleSinkOptions = {}): Sink {\n const formatter = options.formatter ?? defaultConsoleFormatter;\n const levelMap: Record<LogLevel, ConsoleMethod> = {\n trace: \"debug\",\n debug: \"debug\",\n info: \"info\",\n warning: \"warn\",\n error: \"error\",\n fatal: \"error\",\n ...(options.levelMap ?? {}),\n };\n const console = options.console ?? globalThis.console;\n return (record: LogRecord) => {\n const args = formatter(record);\n const method = levelMap[record.level];\n if (method === undefined) {\n throw new TypeError(`Invalid log level: ${record.level}.`);\n }\n if (typeof args === \"string\") {\n const msg = args.replace(/\\r?\\n$/, \"\");\n console[method](msg);\n } else {\n console[method](...args);\n }\n };\n}\n\n/**\n * Converts an async sink into a regular sink with proper async handling.\n * The returned sink chains async operations to ensure proper ordering and\n * implements AsyncDisposable to wait for all pending operations on disposal.\n *\n * @example Create a sink that asynchronously posts to a webhook\n * ```typescript\n * const asyncSink: AsyncSink = async (record) => {\n * await fetch(\"https://example.com/logs\", {\n * method: \"POST\",\n * body: JSON.stringify(record),\n * });\n * };\n * const sink = fromAsyncSink(asyncSink);\n * ```\n *\n * @param asyncSink The async sink function to convert.\n * @returns A sink that properly handles async operations and disposal.\n * @since 1.0.0\n */\nexport function fromAsyncSink(asyncSink: AsyncSink): Sink & AsyncDisposable {\n let lastPromise = Promise.resolve();\n const sink: Sink & AsyncDisposable = (record: LogRecord) => {\n lastPromise = lastPromise\n .then(() => asyncSink(record))\n .catch(() => {\n // Errors are handled by the sink infrastructure\n });\n };\n sink[Symbol.asyncDispose] = async () => {\n await lastPromise;\n };\n return sink;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sink.js","names":["sink: Sink","filter: FilterLike","record: LogRecord","options: BufferSinkOptions","buffer: LogRecord[]","flushTimer: ReturnType<typeof setTimeout> | null","bufferedSink: Sink & AsyncDisposable","stream: WritableStream","options: StreamSinkOptions","sink: Sink & AsyncDisposable","flushTimer: ReturnType<typeof setInterval> | null","activeFlush: Promise<void> | null","nonBlockingSink: Sink & AsyncDisposable","options: ConsoleSinkOptions","levelMap: Record<LogLevel, ConsoleMethod>","nonBlockingSink: Sink & Disposable","asyncSink: AsyncSink"],"sources":["../sink.ts"],"sourcesContent":["import { type FilterLike, toFilter } from \"./filter.ts\";\nimport {\n type ConsoleFormatter,\n defaultConsoleFormatter,\n defaultTextFormatter,\n type TextFormatter,\n} from \"./formatter.ts\";\nimport type { LogLevel } from \"./level.ts\";\nimport type { LogRecord } from \"./record.ts\";\n\n/**\n * A sink is a function that accepts a log record and prints it somewhere.\n * Thrown exceptions will be suppressed and then logged to the meta logger,\n * a {@link Logger} with the category `[\"logtape\", \"meta\"]`. (In that case,\n * the meta log record will not be passed to the sink to avoid infinite\n * recursion.)\n *\n * @param record The log record to sink.\n */\nexport type Sink = (record: LogRecord) => void;\n\n/**\n * An async sink is a function that accepts a log record and asynchronously\n * processes it. This type is used with {@link fromAsyncSink} to create\n * a regular sink that properly handles asynchronous operations.\n *\n * @param record The log record to process asynchronously.\n * @returns A promise that resolves when the record has been processed.\n * @since 1.0.0\n */\nexport type AsyncSink = (record: LogRecord) => Promise<void>;\n\n/**\n * Turns a sink into a filtered sink. The returned sink only logs records that\n * pass the filter.\n *\n * @example Filter a console sink to only log records with the info level\n * ```typescript\n * const sink = withFilter(getConsoleSink(), \"info\");\n * ```\n *\n * @param sink A sink to be filtered.\n * @param filter A filter to apply to the sink. It can be either a filter\n * function or a {@link LogLevel} string.\n * @returns A sink that only logs records that pass the filter.\n */\nexport function withFilter(sink: Sink, filter: FilterLike): Sink {\n const filterFunc = toFilter(filter);\n return (record: LogRecord) => {\n if (filterFunc(record)) sink(record);\n };\n}\n\n/**\n * Options for the {@link withBuffer} function.\n * @since 1.0.0\n */\nexport interface BufferSinkOptions {\n /**\n * The maximum number of log records to buffer before flushing to the\n * underlying sink.\n * @default `10`\n */\n bufferSize?: number;\n\n /**\n * The maximum time in milliseconds to wait before flushing buffered records\n * to the underlying sink. Defaults to 5000 (5 seconds). Set to 0 or\n * negative to disable time-based flushing.\n * @default `5000`\n */\n flushInterval?: number;\n}\n\n/**\n * Turns a sink into a buffered sink. The returned sink buffers log records\n * in memory and flushes them to the underlying sink when the buffer is full\n * or after a specified time interval.\n *\n * @example Buffer a console sink with custom options\n * ```typescript\n * const sink = withBuffer(getConsoleSink(), {\n * bufferSize: 5,\n * flushInterval: 1000\n * });\n * ```\n *\n * @param sink A sink to be buffered.\n * @param options Options for the buffered sink.\n * @returns A buffered sink that flushes records periodically.\n * @since 1.0.0\n */\nexport function withBuffer(\n sink: Sink,\n options: BufferSinkOptions = {},\n): Sink & AsyncDisposable {\n const bufferSize = options.bufferSize ?? 10;\n const flushInterval = options.flushInterval ?? 5000;\n\n const buffer: LogRecord[] = [];\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n let disposed = false;\n\n function flush(): void {\n if (buffer.length === 0) return;\n\n const records = buffer.splice(0);\n for (const record of records) {\n try {\n sink(record);\n } catch (error) {\n // Errors are handled by the sink infrastructure\n throw error;\n }\n }\n\n if (flushTimer !== null) {\n clearTimeout(flushTimer);\n flushTimer = null;\n }\n }\n\n function scheduleFlush(): void {\n if (flushInterval <= 0 || flushTimer !== null || disposed) return;\n\n flushTimer = setTimeout(() => {\n flushTimer = null;\n flush();\n }, flushInterval);\n }\n\n const bufferedSink: Sink & AsyncDisposable = (record: LogRecord) => {\n if (disposed) return;\n\n buffer.push(record);\n\n if (buffer.length >= bufferSize) {\n flush();\n } else {\n scheduleFlush();\n }\n };\n\n bufferedSink[Symbol.asyncDispose] = async () => {\n disposed = true;\n if (flushTimer !== null) {\n clearTimeout(flushTimer);\n flushTimer = null;\n }\n flush();\n\n // Dispose the underlying sink if it's disposable\n if (Symbol.asyncDispose in sink) {\n await (sink as AsyncDisposable)[Symbol.asyncDispose]();\n } else if (Symbol.dispose in sink) {\n (sink as Disposable)[Symbol.dispose]();\n }\n };\n\n return bufferedSink;\n}\n\n/**\n * Options for the {@link getStreamSink} function.\n */\nexport interface StreamSinkOptions {\n /**\n * The text formatter to use. Defaults to {@link defaultTextFormatter}.\n */\n formatter?: TextFormatter;\n\n /**\n * The text encoder to use. Defaults to an instance of {@link TextEncoder}.\n */\n encoder?: { encode(text: string): Uint8Array };\n\n /**\n * Enable non-blocking mode with optional buffer configuration.\n * When enabled, log records are buffered and flushed in the background.\n *\n * @example Simple non-blocking mode\n * ```typescript\n * getStreamSink(stream, { nonBlocking: true });\n * ```\n *\n * @example Custom buffer configuration\n * ```typescript\n * getStreamSink(stream, {\n * nonBlocking: {\n * bufferSize: 1000,\n * flushInterval: 50\n * }\n * });\n * ```\n *\n * @default `false`\n * @since 1.0.0\n */\n nonBlocking?: boolean | {\n /**\n * Maximum number of records to buffer before flushing.\n * @default `100`\n */\n bufferSize?: number;\n\n /**\n * Interval in milliseconds between automatic flushes.\n * @default `100`\n */\n flushInterval?: number;\n };\n}\n\n/**\n * A factory that returns a sink that writes to a {@link WritableStream}.\n *\n * Note that the `stream` is of Web Streams API, which is different from\n * Node.js streams. You can convert a Node.js stream to a Web Streams API\n * stream using [`stream.Writable.toWeb()`] method.\n *\n * [`stream.Writable.toWeb()`]: https://nodejs.org/api/stream.html#streamwritabletowebstreamwritable\n *\n * @example Sink to the standard error in Deno\n * ```typescript\n * const stderrSink = getStreamSink(Deno.stderr.writable);\n * ```\n *\n * @example Sink to the standard error in Node.js\n * ```typescript\n * import stream from \"node:stream\";\n * const stderrSink = getStreamSink(stream.Writable.toWeb(process.stderr));\n * ```\n *\n * @param stream The stream to write to.\n * @param options The options for the sink.\n * @returns A sink that writes to the stream.\n */\nexport function getStreamSink(\n stream: WritableStream,\n options: StreamSinkOptions = {},\n): Sink & AsyncDisposable {\n const formatter = options.formatter ?? defaultTextFormatter;\n const encoder = options.encoder ?? new TextEncoder();\n const writer = stream.getWriter();\n\n if (!options.nonBlocking) {\n let lastPromise = Promise.resolve();\n const sink: Sink & AsyncDisposable = (record: LogRecord) => {\n const bytes = encoder.encode(formatter(record));\n lastPromise = lastPromise\n .then(() => writer.ready)\n .then(() => writer.write(bytes));\n };\n sink[Symbol.asyncDispose] = async () => {\n await lastPromise;\n await writer.close();\n };\n return sink;\n }\n\n // Non-blocking mode implementation\n const nonBlockingConfig = options.nonBlocking === true\n ? {}\n : options.nonBlocking;\n const bufferSize = nonBlockingConfig.bufferSize ?? 100;\n const flushInterval = nonBlockingConfig.flushInterval ?? 100;\n\n const buffer: LogRecord[] = [];\n let flushTimer: ReturnType<typeof setInterval> | null = null;\n let disposed = false;\n let activeFlush: Promise<void> | null = null;\n const maxBufferSize = bufferSize * 2; // Overflow protection\n\n async function flush(): Promise<void> {\n if (buffer.length === 0) return;\n\n const records = buffer.splice(0);\n for (const record of records) {\n try {\n const bytes = encoder.encode(formatter(record));\n await writer.ready;\n await writer.write(bytes);\n } catch {\n // Silently ignore errors in non-blocking mode to avoid disrupting the application\n }\n }\n }\n\n function scheduleFlush(): void {\n if (activeFlush) return;\n\n activeFlush = flush().finally(() => {\n activeFlush = null;\n });\n }\n\n function startFlushTimer(): void {\n if (flushTimer !== null || disposed) return;\n\n flushTimer = setInterval(() => {\n scheduleFlush();\n }, flushInterval);\n }\n\n const nonBlockingSink: Sink & AsyncDisposable = (record: LogRecord) => {\n if (disposed) return;\n\n // Buffer overflow protection: drop oldest records if buffer is too large\n if (buffer.length >= maxBufferSize) {\n buffer.shift(); // Remove oldest record\n }\n\n buffer.push(record);\n\n if (buffer.length >= bufferSize) {\n scheduleFlush();\n } else if (flushTimer === null) {\n startFlushTimer();\n }\n };\n\n nonBlockingSink[Symbol.asyncDispose] = async () => {\n disposed = true;\n if (flushTimer !== null) {\n clearInterval(flushTimer);\n flushTimer = null;\n }\n await flush();\n try {\n await writer.close();\n } catch {\n // Writer might already be closed or errored\n }\n };\n\n return nonBlockingSink;\n}\n\ntype ConsoleMethod = \"debug\" | \"info\" | \"log\" | \"warn\" | \"error\";\n\n/**\n * Options for the {@link getConsoleSink} function.\n */\nexport interface ConsoleSinkOptions {\n /**\n * The console formatter or text formatter to use.\n * Defaults to {@link defaultConsoleFormatter}.\n */\n formatter?: ConsoleFormatter | TextFormatter;\n\n /**\n * The mapping from log levels to console methods. Defaults to:\n *\n * ```typescript\n * {\n * trace: \"trace\",\n * debug: \"debug\",\n * info: \"info\",\n * warning: \"warn\",\n * error: \"error\",\n * fatal: \"error\",\n * }\n * ```\n * @since 0.9.0\n */\n levelMap?: Record<LogLevel, ConsoleMethod>;\n\n /**\n * The console to log to. Defaults to {@link console}.\n */\n console?: Console;\n\n /**\n * Enable non-blocking mode with optional buffer configuration.\n * When enabled, log records are buffered and flushed in the background.\n *\n * @example Simple non-blocking mode\n * ```typescript\n * getConsoleSink({ nonBlocking: true });\n * ```\n *\n * @example Custom buffer configuration\n * ```typescript\n * getConsoleSink({\n * nonBlocking: {\n * bufferSize: 1000,\n * flushInterval: 50\n * }\n * });\n * ```\n *\n * @default `false`\n * @since 1.0.0\n */\n nonBlocking?: boolean | {\n /**\n * Maximum number of records to buffer before flushing.\n * @default `100`\n */\n bufferSize?: number;\n\n /**\n * Interval in milliseconds between automatic flushes.\n * @default `100`\n */\n flushInterval?: number;\n };\n}\n\n/**\n * A console sink factory that returns a sink that logs to the console.\n *\n * @param options The options for the sink.\n * @returns A sink that logs to the console. If `nonBlocking` is enabled,\n * returns a sink that also implements {@link Disposable}.\n */\nexport function getConsoleSink(\n options: ConsoleSinkOptions = {},\n): Sink | (Sink & Disposable) {\n const formatter = options.formatter ?? defaultConsoleFormatter;\n const levelMap: Record<LogLevel, ConsoleMethod> = {\n trace: \"debug\",\n debug: \"debug\",\n info: \"info\",\n warning: \"warn\",\n error: \"error\",\n fatal: \"error\",\n ...(options.levelMap ?? {}),\n };\n const console = options.console ?? globalThis.console;\n\n const baseSink = (record: LogRecord) => {\n const args = formatter(record);\n const method = levelMap[record.level];\n if (method === undefined) {\n throw new TypeError(`Invalid log level: ${record.level}.`);\n }\n if (typeof args === \"string\") {\n const msg = args.replace(/\\r?\\n$/, \"\");\n console[method](msg);\n } else {\n console[method](...args);\n }\n };\n\n if (!options.nonBlocking) {\n return baseSink;\n }\n\n // Non-blocking mode implementation\n const nonBlockingConfig = options.nonBlocking === true\n ? {}\n : options.nonBlocking;\n const bufferSize = nonBlockingConfig.bufferSize ?? 100;\n const flushInterval = nonBlockingConfig.flushInterval ?? 100;\n\n const buffer: LogRecord[] = [];\n let flushTimer: ReturnType<typeof setInterval> | null = null;\n let disposed = false;\n let flushScheduled = false;\n const maxBufferSize = bufferSize * 2; // Overflow protection\n\n function flush(): void {\n if (buffer.length === 0) return;\n\n const records = buffer.splice(0);\n for (const record of records) {\n try {\n baseSink(record);\n } catch {\n // Silently ignore errors in non-blocking mode to avoid disrupting the application\n }\n }\n }\n\n function scheduleFlush(): void {\n if (flushScheduled) return;\n\n flushScheduled = true;\n setTimeout(() => {\n flushScheduled = false;\n flush();\n }, 0);\n }\n\n function startFlushTimer(): void {\n if (flushTimer !== null || disposed) return;\n\n flushTimer = setInterval(() => {\n flush();\n }, flushInterval);\n }\n\n const nonBlockingSink: Sink & Disposable = (record: LogRecord) => {\n if (disposed) return;\n\n // Buffer overflow protection: drop oldest records if buffer is too large\n if (buffer.length >= maxBufferSize) {\n buffer.shift(); // Remove oldest record\n }\n\n buffer.push(record);\n\n if (buffer.length >= bufferSize) {\n scheduleFlush();\n } else if (flushTimer === null) {\n startFlushTimer();\n }\n };\n\n nonBlockingSink[Symbol.dispose] = () => {\n disposed = true;\n if (flushTimer !== null) {\n clearInterval(flushTimer);\n flushTimer = null;\n }\n flush();\n };\n\n return nonBlockingSink;\n}\n\n/**\n * Converts an async sink into a regular sink with proper async handling.\n * The returned sink chains async operations to ensure proper ordering and\n * implements AsyncDisposable to wait for all pending operations on disposal.\n *\n * @example Create a sink that asynchronously posts to a webhook\n * ```typescript\n * const asyncSink: AsyncSink = async (record) => {\n * await fetch(\"https://example.com/logs\", {\n * method: \"POST\",\n * body: JSON.stringify(record),\n * });\n * };\n * const sink = fromAsyncSink(asyncSink);\n * ```\n *\n * @param asyncSink The async sink function to convert.\n * @returns A sink that properly handles async operations and disposal.\n * @since 1.0.0\n */\nexport function fromAsyncSink(asyncSink: AsyncSink): Sink & AsyncDisposable {\n let lastPromise = Promise.resolve();\n const sink: Sink & AsyncDisposable = (record: LogRecord) => {\n lastPromise = lastPromise\n .then(() => asyncSink(record))\n .catch(() => {\n // Errors are handled by the sink infrastructure\n });\n };\n sink[Symbol.asyncDispose] = async () => {\n await lastPromise;\n };\n return sink;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/sink.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { suite } from "@alinea/suite";
|
|
2
|
+
import { assert } from "@std/assert/assert";
|
|
2
3
|
import { assertEquals } from "@std/assert/equals";
|
|
3
4
|
import { assertInstanceOf } from "@std/assert/instance-of";
|
|
4
5
|
import { assertThrows } from "@std/assert/throws";
|
|
@@ -67,6 +68,254 @@ test("getStreamSink()", async () => {
|
|
|
67
68
|
);
|
|
68
69
|
});
|
|
69
70
|
|
|
71
|
+
test("getStreamSink() with nonBlocking - simple boolean", async () => {
|
|
72
|
+
let buffer: string = "";
|
|
73
|
+
const decoder = new TextDecoder();
|
|
74
|
+
const sink = getStreamSink(
|
|
75
|
+
new WritableStream({
|
|
76
|
+
write(chunk: Uint8Array) {
|
|
77
|
+
buffer += decoder.decode(chunk);
|
|
78
|
+
return Promise.resolve();
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
{ nonBlocking: true },
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Check that it returns AsyncDisposable
|
|
85
|
+
assertInstanceOf(sink, Function);
|
|
86
|
+
assert(Symbol.asyncDispose in sink);
|
|
87
|
+
|
|
88
|
+
// Add records - they should not be written immediately
|
|
89
|
+
sink(trace);
|
|
90
|
+
sink(debug);
|
|
91
|
+
assertEquals(buffer, ""); // Not written yet
|
|
92
|
+
|
|
93
|
+
// Wait for flush interval (default 100ms)
|
|
94
|
+
await delay(150);
|
|
95
|
+
assertEquals(
|
|
96
|
+
buffer,
|
|
97
|
+
`2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!
|
|
98
|
+
2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
|
|
99
|
+
`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
await sink[Symbol.asyncDispose]();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("getStreamSink() with nonBlocking - custom buffer config", async () => {
|
|
106
|
+
let buffer: string = "";
|
|
107
|
+
const decoder = new TextDecoder();
|
|
108
|
+
const sink = getStreamSink(
|
|
109
|
+
new WritableStream({
|
|
110
|
+
write(chunk: Uint8Array) {
|
|
111
|
+
buffer += decoder.decode(chunk);
|
|
112
|
+
return Promise.resolve();
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
{
|
|
116
|
+
nonBlocking: {
|
|
117
|
+
bufferSize: 2,
|
|
118
|
+
flushInterval: 50,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Add records up to buffer size
|
|
124
|
+
sink(trace);
|
|
125
|
+
assertEquals(buffer, ""); // Not flushed yet
|
|
126
|
+
|
|
127
|
+
sink(debug); // This should trigger immediate flush (buffer size = 2)
|
|
128
|
+
await delay(10); // Small delay for async flush
|
|
129
|
+
assertEquals(
|
|
130
|
+
buffer,
|
|
131
|
+
`2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!
|
|
132
|
+
2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
|
|
133
|
+
`,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Add more records
|
|
137
|
+
const prevLength = buffer.length;
|
|
138
|
+
sink(info);
|
|
139
|
+
assertEquals(buffer.length, prevLength); // Not flushed yet
|
|
140
|
+
|
|
141
|
+
// Wait for flush interval
|
|
142
|
+
await delay(60);
|
|
143
|
+
assertEquals(
|
|
144
|
+
buffer.substring(prevLength),
|
|
145
|
+
`2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
|
|
146
|
+
`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
await sink[Symbol.asyncDispose]();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("getStreamSink() with nonBlocking - no operations after dispose", async () => {
|
|
153
|
+
let buffer: string = "";
|
|
154
|
+
const decoder = new TextDecoder();
|
|
155
|
+
const sink = getStreamSink(
|
|
156
|
+
new WritableStream({
|
|
157
|
+
write(chunk: Uint8Array) {
|
|
158
|
+
buffer += decoder.decode(chunk);
|
|
159
|
+
return Promise.resolve();
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
{ nonBlocking: true },
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Dispose immediately
|
|
166
|
+
await sink[Symbol.asyncDispose]();
|
|
167
|
+
|
|
168
|
+
// Try to add records after dispose
|
|
169
|
+
sink(trace);
|
|
170
|
+
sink(debug);
|
|
171
|
+
|
|
172
|
+
// No records should be written
|
|
173
|
+
assertEquals(buffer, "");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("getStreamSink() with nonBlocking - error handling", async () => {
|
|
177
|
+
const sink = getStreamSink(
|
|
178
|
+
new WritableStream({
|
|
179
|
+
write() {
|
|
180
|
+
return Promise.reject(new Error("Write error"));
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
{ nonBlocking: true },
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Should not throw when adding records
|
|
187
|
+
sink(trace);
|
|
188
|
+
sink(info);
|
|
189
|
+
sink(error);
|
|
190
|
+
|
|
191
|
+
// Wait for flush - errors should be silently ignored
|
|
192
|
+
await delay(150);
|
|
193
|
+
|
|
194
|
+
// Dispose - should not throw
|
|
195
|
+
await sink[Symbol.asyncDispose]();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("getStreamSink() with nonBlocking - flush on dispose", async () => {
|
|
199
|
+
let buffer: string = "";
|
|
200
|
+
const decoder = new TextDecoder();
|
|
201
|
+
const sink = getStreamSink(
|
|
202
|
+
new WritableStream({
|
|
203
|
+
write(chunk: Uint8Array) {
|
|
204
|
+
buffer += decoder.decode(chunk);
|
|
205
|
+
return Promise.resolve();
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
{
|
|
209
|
+
nonBlocking: {
|
|
210
|
+
bufferSize: 100,
|
|
211
|
+
flushInterval: 5000, // Very long interval
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Add records
|
|
217
|
+
sink(trace);
|
|
218
|
+
sink(debug);
|
|
219
|
+
sink(info);
|
|
220
|
+
assertEquals(buffer, ""); // Not flushed yet due to large buffer and long interval
|
|
221
|
+
|
|
222
|
+
// Dispose should flush all remaining records
|
|
223
|
+
await sink[Symbol.asyncDispose]();
|
|
224
|
+
assertEquals(
|
|
225
|
+
buffer,
|
|
226
|
+
`2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!
|
|
227
|
+
2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
|
|
228
|
+
2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
|
|
229
|
+
`,
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("getStreamSink() with nonBlocking - buffer overflow protection", async () => {
|
|
234
|
+
let buffer: string = "";
|
|
235
|
+
const decoder = new TextDecoder();
|
|
236
|
+
let recordsReceived = 0;
|
|
237
|
+
const sink = getStreamSink(
|
|
238
|
+
new WritableStream({
|
|
239
|
+
write(chunk: Uint8Array) {
|
|
240
|
+
const text = decoder.decode(chunk);
|
|
241
|
+
buffer += text;
|
|
242
|
+
// Count how many log records we actually receive
|
|
243
|
+
recordsReceived += text.split("\n").filter((line) =>
|
|
244
|
+
line.trim() !== ""
|
|
245
|
+
).length;
|
|
246
|
+
return Promise.resolve();
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
{
|
|
250
|
+
nonBlocking: {
|
|
251
|
+
bufferSize: 3,
|
|
252
|
+
flushInterval: 50, // Short interval to ensure flushes happen
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Add many more records than maxBufferSize (6) very rapidly
|
|
258
|
+
// This should trigger multiple flushes and potentially overflow protection
|
|
259
|
+
for (let i = 0; i < 20; i++) {
|
|
260
|
+
sink(trace);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Wait for all flushes to complete
|
|
264
|
+
await delay(200);
|
|
265
|
+
|
|
266
|
+
// Force final flush
|
|
267
|
+
await sink[Symbol.asyncDispose]();
|
|
268
|
+
|
|
269
|
+
// Due to overflow protection, we should receive fewer than 20 records
|
|
270
|
+
// The exact number depends on timing, but some should be dropped
|
|
271
|
+
assert(
|
|
272
|
+
recordsReceived < 20,
|
|
273
|
+
`Expected < 20 records due to potential overflow, got ${recordsReceived}`,
|
|
274
|
+
);
|
|
275
|
+
assert(recordsReceived > 0, "Expected some records to be logged");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("getStreamSink() with nonBlocking - high volume non-blocking behavior", async () => {
|
|
279
|
+
let buffer: string = "";
|
|
280
|
+
const decoder = new TextDecoder();
|
|
281
|
+
const sink = getStreamSink(
|
|
282
|
+
new WritableStream({
|
|
283
|
+
write(chunk: Uint8Array) {
|
|
284
|
+
buffer += decoder.decode(chunk);
|
|
285
|
+
return Promise.resolve();
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
{
|
|
289
|
+
nonBlocking: {
|
|
290
|
+
bufferSize: 3,
|
|
291
|
+
flushInterval: 50,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Simulate rapid logging - this should not block
|
|
297
|
+
const startTime = performance.now();
|
|
298
|
+
for (let i = 0; i < 100; i++) {
|
|
299
|
+
sink(trace);
|
|
300
|
+
}
|
|
301
|
+
const endTime = performance.now();
|
|
302
|
+
|
|
303
|
+
// Adding logs should be very fast (non-blocking)
|
|
304
|
+
const duration = endTime - startTime;
|
|
305
|
+
assert(
|
|
306
|
+
duration < 100,
|
|
307
|
+
`Adding 100 logs took ${duration}ms, should be much faster`,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// Wait for flushes to complete
|
|
311
|
+
await delay(200);
|
|
312
|
+
|
|
313
|
+
// Should have logged some records
|
|
314
|
+
assert(buffer.length > 0, "Expected some records to be logged");
|
|
315
|
+
|
|
316
|
+
await sink[Symbol.asyncDispose]();
|
|
317
|
+
});
|
|
318
|
+
|
|
70
319
|
test("getConsoleSink()", () => {
|
|
71
320
|
// @ts-ignore: consolemock is not typed
|
|
72
321
|
const mock: ConsoleMock = makeConsoleMock();
|
|
@@ -257,6 +506,176 @@ test("getConsoleSink()", () => {
|
|
|
257
506
|
]);
|
|
258
507
|
});
|
|
259
508
|
|
|
509
|
+
test("getConsoleSink() with nonBlocking - simple boolean", async () => {
|
|
510
|
+
// @ts-ignore: consolemock is not typed
|
|
511
|
+
const mock: ConsoleMock = makeConsoleMock();
|
|
512
|
+
const sink = getConsoleSink({ console: mock, nonBlocking: true });
|
|
513
|
+
|
|
514
|
+
// Check that it returns a Disposable
|
|
515
|
+
assertInstanceOf(sink, Function);
|
|
516
|
+
assert(Symbol.dispose in sink);
|
|
517
|
+
|
|
518
|
+
// Add records - they should not be logged immediately
|
|
519
|
+
sink(trace);
|
|
520
|
+
sink(debug);
|
|
521
|
+
assertEquals(mock.history().length, 0); // Not logged yet
|
|
522
|
+
|
|
523
|
+
// Wait for flush interval (default 100ms)
|
|
524
|
+
await delay(150);
|
|
525
|
+
assertEquals(mock.history().length, 2); // Now they should be logged
|
|
526
|
+
|
|
527
|
+
// Dispose the sink
|
|
528
|
+
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("getConsoleSink() with nonBlocking - custom buffer config", async () => {
|
|
532
|
+
// @ts-ignore: consolemock is not typed
|
|
533
|
+
const mock: ConsoleMock = makeConsoleMock();
|
|
534
|
+
const sink = getConsoleSink({
|
|
535
|
+
console: mock,
|
|
536
|
+
nonBlocking: {
|
|
537
|
+
bufferSize: 3,
|
|
538
|
+
flushInterval: 50,
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// Add records up to buffer size
|
|
543
|
+
sink(trace);
|
|
544
|
+
sink(debug);
|
|
545
|
+
assertEquals(mock.history().length, 0); // Not flushed yet
|
|
546
|
+
|
|
547
|
+
sink(info); // This should trigger scheduled flush (buffer size = 3)
|
|
548
|
+
await delay(10); // Wait for scheduled flush to execute
|
|
549
|
+
assertEquals(mock.history().length, 3); // Flushed due to buffer size
|
|
550
|
+
|
|
551
|
+
// Add more records
|
|
552
|
+
sink(warning);
|
|
553
|
+
assertEquals(mock.history().length, 3); // Not flushed yet
|
|
554
|
+
|
|
555
|
+
// Wait for flush interval
|
|
556
|
+
await delay(60);
|
|
557
|
+
assertEquals(mock.history().length, 4); // Flushed due to interval
|
|
558
|
+
|
|
559
|
+
// Dispose and check remaining records are flushed
|
|
560
|
+
sink(error);
|
|
561
|
+
sink(fatal);
|
|
562
|
+
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
563
|
+
assertEquals(mock.history().length, 6); // All records flushed on dispose
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test("getConsoleSink() with nonBlocking - no operations after dispose", () => {
|
|
567
|
+
// @ts-ignore: consolemock is not typed
|
|
568
|
+
const mock: ConsoleMock = makeConsoleMock();
|
|
569
|
+
const sink = getConsoleSink({ console: mock, nonBlocking: true });
|
|
570
|
+
|
|
571
|
+
// Dispose immediately
|
|
572
|
+
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
573
|
+
|
|
574
|
+
// Try to add records after dispose
|
|
575
|
+
sink(trace);
|
|
576
|
+
sink(debug);
|
|
577
|
+
|
|
578
|
+
// No records should be logged
|
|
579
|
+
assertEquals(mock.history().length, 0);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("getConsoleSink() with nonBlocking - error handling", async () => {
|
|
583
|
+
const errorConsole = {
|
|
584
|
+
...console,
|
|
585
|
+
debug: () => {
|
|
586
|
+
throw new Error("Console error");
|
|
587
|
+
},
|
|
588
|
+
info: () => {
|
|
589
|
+
throw new Error("Console error");
|
|
590
|
+
},
|
|
591
|
+
warn: () => {
|
|
592
|
+
throw new Error("Console error");
|
|
593
|
+
},
|
|
594
|
+
error: () => {
|
|
595
|
+
throw new Error("Console error");
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const sink = getConsoleSink({
|
|
600
|
+
console: errorConsole,
|
|
601
|
+
nonBlocking: true,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Should not throw when adding records
|
|
605
|
+
sink(trace);
|
|
606
|
+
sink(info);
|
|
607
|
+
sink(error);
|
|
608
|
+
|
|
609
|
+
// Wait for flush - errors should be silently ignored
|
|
610
|
+
await delay(150);
|
|
611
|
+
|
|
612
|
+
// Dispose - should not throw
|
|
613
|
+
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("getConsoleSink() with nonBlocking - buffer overflow protection", async () => {
|
|
617
|
+
// @ts-ignore: consolemock is not typed
|
|
618
|
+
const mock: ConsoleMock = makeConsoleMock();
|
|
619
|
+
const sink = getConsoleSink({
|
|
620
|
+
console: mock,
|
|
621
|
+
nonBlocking: {
|
|
622
|
+
bufferSize: 5,
|
|
623
|
+
flushInterval: 1000, // Long interval to prevent automatic flushing
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Add more records than 2x buffer size (which should trigger overflow protection)
|
|
628
|
+
for (let i = 0; i < 12; i++) {
|
|
629
|
+
sink(trace);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Should have dropped oldest records, keeping buffer size manageable
|
|
633
|
+
// Wait a bit for any scheduled flushes
|
|
634
|
+
await delay(10);
|
|
635
|
+
|
|
636
|
+
// Force flush by disposing
|
|
637
|
+
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
638
|
+
|
|
639
|
+
// Should have logged records, but not more than maxBufferSize (10)
|
|
640
|
+
const historyLength = mock.history().length;
|
|
641
|
+
assert(historyLength <= 10, `Expected <= 10 records, got ${historyLength}`);
|
|
642
|
+
assert(historyLength > 0, "Expected some records to be logged");
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test("getConsoleSink() with nonBlocking - high volume non-blocking behavior", async () => {
|
|
646
|
+
// @ts-ignore: consolemock is not typed
|
|
647
|
+
const mock: ConsoleMock = makeConsoleMock();
|
|
648
|
+
const sink = getConsoleSink({
|
|
649
|
+
console: mock,
|
|
650
|
+
nonBlocking: {
|
|
651
|
+
bufferSize: 3,
|
|
652
|
+
flushInterval: 50,
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// Simulate rapid logging - this should not block
|
|
657
|
+
const startTime = performance.now();
|
|
658
|
+
for (let i = 0; i < 100; i++) {
|
|
659
|
+
sink(trace);
|
|
660
|
+
}
|
|
661
|
+
const endTime = performance.now();
|
|
662
|
+
|
|
663
|
+
// Adding logs should be very fast (non-blocking)
|
|
664
|
+
const duration = endTime - startTime;
|
|
665
|
+
assert(
|
|
666
|
+
duration < 100,
|
|
667
|
+
`Adding 100 logs took ${duration}ms, should be much faster`,
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
// Wait for flushes to complete
|
|
671
|
+
await delay(200);
|
|
672
|
+
|
|
673
|
+
// Should have logged some records
|
|
674
|
+
assert(mock.history().length > 0, "Expected some records to be logged");
|
|
675
|
+
|
|
676
|
+
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
677
|
+
});
|
|
678
|
+
|
|
260
679
|
test("withBuffer() - buffer size limit", async () => {
|
|
261
680
|
const buffer: LogRecord[] = [];
|
|
262
681
|
const sink = withBuffer(buffer.push.bind(buffer), { bufferSize: 3 });
|
|
@@ -381,7 +800,7 @@ test("withBuffer() - disposes underlying AsyncDisposable sink", async () => {
|
|
|
381
800
|
|
|
382
801
|
await bufferedSink[Symbol.asyncDispose]();
|
|
383
802
|
|
|
384
|
-
|
|
803
|
+
assert(disposed); // Underlying sink should be disposed
|
|
385
804
|
});
|
|
386
805
|
|
|
387
806
|
test("withBuffer() - disposes underlying Disposable sink", async () => {
|
|
@@ -399,7 +818,7 @@ test("withBuffer() - disposes underlying Disposable sink", async () => {
|
|
|
399
818
|
|
|
400
819
|
await bufferedSink[Symbol.asyncDispose]();
|
|
401
820
|
|
|
402
|
-
|
|
821
|
+
assert(disposed); // Underlying sink should be disposed
|
|
403
822
|
});
|
|
404
823
|
|
|
405
824
|
test("withBuffer() - handles non-disposable sink gracefully", async () => {
|
|
@@ -414,7 +833,7 @@ test("withBuffer() - handles non-disposable sink gracefully", async () => {
|
|
|
414
833
|
await bufferedSink[Symbol.asyncDispose]();
|
|
415
834
|
|
|
416
835
|
// This test passes if no error is thrown
|
|
417
|
-
|
|
836
|
+
assert(true);
|
|
418
837
|
});
|
|
419
838
|
|
|
420
839
|
test("withBuffer() - edge case: bufferSize 1", async () => {
|
|
@@ -597,7 +1016,7 @@ test("withBuffer() - edge case: underlying AsyncDisposable throws error", async
|
|
|
597
1016
|
} catch (error) {
|
|
598
1017
|
assertInstanceOf(error, Error);
|
|
599
1018
|
assertEquals(error.message, "Dispose error");
|
|
600
|
-
|
|
1019
|
+
assert(disposed); // Should still be disposed
|
|
601
1020
|
assertEquals(buffer.length, 1); // Buffer should have been flushed before dispose error
|
|
602
1021
|
}
|
|
603
1022
|
});
|
|
@@ -834,5 +1253,5 @@ test("fromAsyncSink() - empty async sink", async () => {
|
|
|
834
1253
|
await sink[Symbol.asyncDispose]();
|
|
835
1254
|
|
|
836
1255
|
// Test passes if no errors thrown
|
|
837
|
-
|
|
1256
|
+
assert(true);
|
|
838
1257
|
});
|
package/sink.ts
CHANGED
|
@@ -173,6 +173,42 @@ export interface StreamSinkOptions {
|
|
|
173
173
|
* The text encoder to use. Defaults to an instance of {@link TextEncoder}.
|
|
174
174
|
*/
|
|
175
175
|
encoder?: { encode(text: string): Uint8Array };
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Enable non-blocking mode with optional buffer configuration.
|
|
179
|
+
* When enabled, log records are buffered and flushed in the background.
|
|
180
|
+
*
|
|
181
|
+
* @example Simple non-blocking mode
|
|
182
|
+
* ```typescript
|
|
183
|
+
* getStreamSink(stream, { nonBlocking: true });
|
|
184
|
+
* ```
|
|
185
|
+
*
|
|
186
|
+
* @example Custom buffer configuration
|
|
187
|
+
* ```typescript
|
|
188
|
+
* getStreamSink(stream, {
|
|
189
|
+
* nonBlocking: {
|
|
190
|
+
* bufferSize: 1000,
|
|
191
|
+
* flushInterval: 50
|
|
192
|
+
* }
|
|
193
|
+
* });
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* @default `false`
|
|
197
|
+
* @since 1.0.0
|
|
198
|
+
*/
|
|
199
|
+
nonBlocking?: boolean | {
|
|
200
|
+
/**
|
|
201
|
+
* Maximum number of records to buffer before flushing.
|
|
202
|
+
* @default `100`
|
|
203
|
+
*/
|
|
204
|
+
bufferSize?: number;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Interval in milliseconds between automatic flushes.
|
|
208
|
+
* @default `100`
|
|
209
|
+
*/
|
|
210
|
+
flushInterval?: number;
|
|
211
|
+
};
|
|
176
212
|
}
|
|
177
213
|
|
|
178
214
|
/**
|
|
@@ -206,18 +242,98 @@ export function getStreamSink(
|
|
|
206
242
|
const formatter = options.formatter ?? defaultTextFormatter;
|
|
207
243
|
const encoder = options.encoder ?? new TextEncoder();
|
|
208
244
|
const writer = stream.getWriter();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
.
|
|
214
|
-
|
|
245
|
+
|
|
246
|
+
if (!options.nonBlocking) {
|
|
247
|
+
let lastPromise = Promise.resolve();
|
|
248
|
+
const sink: Sink & AsyncDisposable = (record: LogRecord) => {
|
|
249
|
+
const bytes = encoder.encode(formatter(record));
|
|
250
|
+
lastPromise = lastPromise
|
|
251
|
+
.then(() => writer.ready)
|
|
252
|
+
.then(() => writer.write(bytes));
|
|
253
|
+
};
|
|
254
|
+
sink[Symbol.asyncDispose] = async () => {
|
|
255
|
+
await lastPromise;
|
|
256
|
+
await writer.close();
|
|
257
|
+
};
|
|
258
|
+
return sink;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Non-blocking mode implementation
|
|
262
|
+
const nonBlockingConfig = options.nonBlocking === true
|
|
263
|
+
? {}
|
|
264
|
+
: options.nonBlocking;
|
|
265
|
+
const bufferSize = nonBlockingConfig.bufferSize ?? 100;
|
|
266
|
+
const flushInterval = nonBlockingConfig.flushInterval ?? 100;
|
|
267
|
+
|
|
268
|
+
const buffer: LogRecord[] = [];
|
|
269
|
+
let flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
270
|
+
let disposed = false;
|
|
271
|
+
let activeFlush: Promise<void> | null = null;
|
|
272
|
+
const maxBufferSize = bufferSize * 2; // Overflow protection
|
|
273
|
+
|
|
274
|
+
async function flush(): Promise<void> {
|
|
275
|
+
if (buffer.length === 0) return;
|
|
276
|
+
|
|
277
|
+
const records = buffer.splice(0);
|
|
278
|
+
for (const record of records) {
|
|
279
|
+
try {
|
|
280
|
+
const bytes = encoder.encode(formatter(record));
|
|
281
|
+
await writer.ready;
|
|
282
|
+
await writer.write(bytes);
|
|
283
|
+
} catch {
|
|
284
|
+
// Silently ignore errors in non-blocking mode to avoid disrupting the application
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function scheduleFlush(): void {
|
|
290
|
+
if (activeFlush) return;
|
|
291
|
+
|
|
292
|
+
activeFlush = flush().finally(() => {
|
|
293
|
+
activeFlush = null;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function startFlushTimer(): void {
|
|
298
|
+
if (flushTimer !== null || disposed) return;
|
|
299
|
+
|
|
300
|
+
flushTimer = setInterval(() => {
|
|
301
|
+
scheduleFlush();
|
|
302
|
+
}, flushInterval);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const nonBlockingSink: Sink & AsyncDisposable = (record: LogRecord) => {
|
|
306
|
+
if (disposed) return;
|
|
307
|
+
|
|
308
|
+
// Buffer overflow protection: drop oldest records if buffer is too large
|
|
309
|
+
if (buffer.length >= maxBufferSize) {
|
|
310
|
+
buffer.shift(); // Remove oldest record
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
buffer.push(record);
|
|
314
|
+
|
|
315
|
+
if (buffer.length >= bufferSize) {
|
|
316
|
+
scheduleFlush();
|
|
317
|
+
} else if (flushTimer === null) {
|
|
318
|
+
startFlushTimer();
|
|
319
|
+
}
|
|
215
320
|
};
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
321
|
+
|
|
322
|
+
nonBlockingSink[Symbol.asyncDispose] = async () => {
|
|
323
|
+
disposed = true;
|
|
324
|
+
if (flushTimer !== null) {
|
|
325
|
+
clearInterval(flushTimer);
|
|
326
|
+
flushTimer = null;
|
|
327
|
+
}
|
|
328
|
+
await flush();
|
|
329
|
+
try {
|
|
330
|
+
await writer.close();
|
|
331
|
+
} catch {
|
|
332
|
+
// Writer might already be closed or errored
|
|
333
|
+
}
|
|
219
334
|
};
|
|
220
|
-
|
|
335
|
+
|
|
336
|
+
return nonBlockingSink;
|
|
221
337
|
}
|
|
222
338
|
|
|
223
339
|
type ConsoleMethod = "debug" | "info" | "log" | "warn" | "error";
|
|
@@ -253,15 +369,54 @@ export interface ConsoleSinkOptions {
|
|
|
253
369
|
* The console to log to. Defaults to {@link console}.
|
|
254
370
|
*/
|
|
255
371
|
console?: Console;
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Enable non-blocking mode with optional buffer configuration.
|
|
375
|
+
* When enabled, log records are buffered and flushed in the background.
|
|
376
|
+
*
|
|
377
|
+
* @example Simple non-blocking mode
|
|
378
|
+
* ```typescript
|
|
379
|
+
* getConsoleSink({ nonBlocking: true });
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* @example Custom buffer configuration
|
|
383
|
+
* ```typescript
|
|
384
|
+
* getConsoleSink({
|
|
385
|
+
* nonBlocking: {
|
|
386
|
+
* bufferSize: 1000,
|
|
387
|
+
* flushInterval: 50
|
|
388
|
+
* }
|
|
389
|
+
* });
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* @default `false`
|
|
393
|
+
* @since 1.0.0
|
|
394
|
+
*/
|
|
395
|
+
nonBlocking?: boolean | {
|
|
396
|
+
/**
|
|
397
|
+
* Maximum number of records to buffer before flushing.
|
|
398
|
+
* @default `100`
|
|
399
|
+
*/
|
|
400
|
+
bufferSize?: number;
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Interval in milliseconds between automatic flushes.
|
|
404
|
+
* @default `100`
|
|
405
|
+
*/
|
|
406
|
+
flushInterval?: number;
|
|
407
|
+
};
|
|
256
408
|
}
|
|
257
409
|
|
|
258
410
|
/**
|
|
259
411
|
* A console sink factory that returns a sink that logs to the console.
|
|
260
412
|
*
|
|
261
413
|
* @param options The options for the sink.
|
|
262
|
-
* @returns A sink that logs to the console.
|
|
414
|
+
* @returns A sink that logs to the console. If `nonBlocking` is enabled,
|
|
415
|
+
* returns a sink that also implements {@link Disposable}.
|
|
263
416
|
*/
|
|
264
|
-
export function getConsoleSink(
|
|
417
|
+
export function getConsoleSink(
|
|
418
|
+
options: ConsoleSinkOptions = {},
|
|
419
|
+
): Sink | (Sink & Disposable) {
|
|
265
420
|
const formatter = options.formatter ?? defaultConsoleFormatter;
|
|
266
421
|
const levelMap: Record<LogLevel, ConsoleMethod> = {
|
|
267
422
|
trace: "debug",
|
|
@@ -273,7 +428,8 @@ export function getConsoleSink(options: ConsoleSinkOptions = {}): Sink {
|
|
|
273
428
|
...(options.levelMap ?? {}),
|
|
274
429
|
};
|
|
275
430
|
const console = options.console ?? globalThis.console;
|
|
276
|
-
|
|
431
|
+
|
|
432
|
+
const baseSink = (record: LogRecord) => {
|
|
277
433
|
const args = formatter(record);
|
|
278
434
|
const method = levelMap[record.level];
|
|
279
435
|
if (method === undefined) {
|
|
@@ -286,6 +442,82 @@ export function getConsoleSink(options: ConsoleSinkOptions = {}): Sink {
|
|
|
286
442
|
console[method](...args);
|
|
287
443
|
}
|
|
288
444
|
};
|
|
445
|
+
|
|
446
|
+
if (!options.nonBlocking) {
|
|
447
|
+
return baseSink;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Non-blocking mode implementation
|
|
451
|
+
const nonBlockingConfig = options.nonBlocking === true
|
|
452
|
+
? {}
|
|
453
|
+
: options.nonBlocking;
|
|
454
|
+
const bufferSize = nonBlockingConfig.bufferSize ?? 100;
|
|
455
|
+
const flushInterval = nonBlockingConfig.flushInterval ?? 100;
|
|
456
|
+
|
|
457
|
+
const buffer: LogRecord[] = [];
|
|
458
|
+
let flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
459
|
+
let disposed = false;
|
|
460
|
+
let flushScheduled = false;
|
|
461
|
+
const maxBufferSize = bufferSize * 2; // Overflow protection
|
|
462
|
+
|
|
463
|
+
function flush(): void {
|
|
464
|
+
if (buffer.length === 0) return;
|
|
465
|
+
|
|
466
|
+
const records = buffer.splice(0);
|
|
467
|
+
for (const record of records) {
|
|
468
|
+
try {
|
|
469
|
+
baseSink(record);
|
|
470
|
+
} catch {
|
|
471
|
+
// Silently ignore errors in non-blocking mode to avoid disrupting the application
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function scheduleFlush(): void {
|
|
477
|
+
if (flushScheduled) return;
|
|
478
|
+
|
|
479
|
+
flushScheduled = true;
|
|
480
|
+
setTimeout(() => {
|
|
481
|
+
flushScheduled = false;
|
|
482
|
+
flush();
|
|
483
|
+
}, 0);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function startFlushTimer(): void {
|
|
487
|
+
if (flushTimer !== null || disposed) return;
|
|
488
|
+
|
|
489
|
+
flushTimer = setInterval(() => {
|
|
490
|
+
flush();
|
|
491
|
+
}, flushInterval);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const nonBlockingSink: Sink & Disposable = (record: LogRecord) => {
|
|
495
|
+
if (disposed) return;
|
|
496
|
+
|
|
497
|
+
// Buffer overflow protection: drop oldest records if buffer is too large
|
|
498
|
+
if (buffer.length >= maxBufferSize) {
|
|
499
|
+
buffer.shift(); // Remove oldest record
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
buffer.push(record);
|
|
503
|
+
|
|
504
|
+
if (buffer.length >= bufferSize) {
|
|
505
|
+
scheduleFlush();
|
|
506
|
+
} else if (flushTimer === null) {
|
|
507
|
+
startFlushTimer();
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
nonBlockingSink[Symbol.dispose] = () => {
|
|
512
|
+
disposed = true;
|
|
513
|
+
if (flushTimer !== null) {
|
|
514
|
+
clearInterval(flushTimer);
|
|
515
|
+
flushTimer = null;
|
|
516
|
+
}
|
|
517
|
+
flush();
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
return nonBlockingSink;
|
|
289
521
|
}
|
|
290
522
|
|
|
291
523
|
/**
|