@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/logtape",
3
- "version": "1.0.0-dev.236+0c0f47bf",
3
+ "version": "1.0.0-dev.237+0615301b",
4
4
  "license": "MIT",
5
5
  "exports": "./mod.ts",
6
6
  "imports": {
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
- let lastPromise = Promise.resolve();
116
- const sink = (record) => {
117
- const bytes = encoder.encode(formatter(record));
118
- lastPromise = lastPromise.then(() => writer.ready).then(() => writer.write(bytes));
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
- sink[Symbol.asyncDispose] = async () => {
121
- await lastPromise;
122
- await writer.close();
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 sink;
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
- return (record) => {
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
@@ -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;AA2B9C;;;;;;AAGyB;AAkBpB,iBAhLW,UAAA,CAgLE,IAAA,EAhLe,IAgLf,EAAA,MAAA,EAhL6B,UAgL7B,CAAA,EAhL0C,IAgL1C;AAKlB;;;;AAsBoB,UAhMH,iBAAA,CAgMG;EAAQ;;;AAKT;AASnB;EAA8B,UAAA,CAAA,EAAA,MAAA;EAAA;;AAAwC;AA+CtE;;;EAAkD,aAAG,CAAA,EAAA,MAAA;;AAAsB;;;;;;;;;;;;;;;;;;iBA1N3D,UAAA,OACR,gBACG,oBACR,OAAO;;;;UAsEO,iBAAA;;;;cAIH;;;;;0BAKsB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2BpB,aAAA,SACN,0BACC,oBACR,OAAO;KAkBL,aAAA;;;;UAKY,kBAAA;;;;;cAKH,mBAAmB;;;;;;;;;;;;;;;;aAiBpB,OAAO,UAAU;;;;YAKlB;;;;;;;;iBASI,cAAA,WAAwB,qBAA0B;;;;;;;;;;;;;;;;;;;;;iBA+ClD,aAAA,YAAyB,YAAY,OAAO"}
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
@@ -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;AA2B9C;;;;;;AAGyB;AAkBpB,iBAhLW,UAAA,CAgLE,IAAA,EAhLe,IAgLf,EAAA,MAAA,EAhL6B,UAgL7B,CAAA,EAhL0C,IAgL1C;AAKlB;;;;AAsBoB,UAhMH,iBAAA,CAgMG;EAAQ;;;AAKT;AASnB;EAA8B,UAAA,CAAA,EAAA,MAAA;EAAA;;AAAwC;AA+CtE;;;EAAkD,aAAG,CAAA,EAAA,MAAA;;AAAsB;;;;;;;;;;;;;;;;;;iBA1N3D,UAAA,OACR,gBACG,oBACR,OAAO;;;;UAsEO,iBAAA;;;;cAIH;;;;;0BAKsB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2BpB,aAAA,SACN,0BACC,oBACR,OAAO;KAkBL,aAAA;;;;UAKY,kBAAA;;;;;cAKH,mBAAmB;;;;;;;;;;;;;;;;aAiBpB,OAAO,UAAU;;;;YAKlB;;;;;;;;iBASI,cAAA,WAAwB,qBAA0B;;;;;;;;;;;;;;;;;;;;;iBA+ClD,aAAA,YAAyB,YAAY,OAAO"}
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
- let lastPromise = Promise.resolve();
116
- const sink = (record) => {
117
- const bytes = encoder.encode(formatter(record));
118
- lastPromise = lastPromise.then(() => writer.ready).then(() => writer.write(bytes));
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
- sink[Symbol.asyncDispose] = async () => {
121
- await lastPromise;
122
- await writer.close();
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 sink;
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
- return (record) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/logtape",
3
- "version": "1.0.0-dev.236+0c0f47bf",
3
+ "version": "1.0.0-dev.237+0615301b",
4
4
  "description": "Simple logging library with zero dependencies for Deno/Node.js/Bun/browsers",
5
5
  "keywords": [
6
6
  "logging",
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
- assertEquals(disposed, true); // Underlying sink should be disposed
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
- assertEquals(disposed, true); // Underlying sink should be disposed
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
- assertEquals(true, true);
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
- assertEquals(disposed, true); // Should still be disposed
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
- assertEquals(true, true);
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
- let lastPromise = Promise.resolve();
210
- const sink: Sink & AsyncDisposable = (record: LogRecord) => {
211
- const bytes = encoder.encode(formatter(record));
212
- lastPromise = lastPromise
213
- .then(() => writer.ready)
214
- .then(() => writer.write(bytes));
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
- sink[Symbol.asyncDispose] = async () => {
217
- await lastPromise;
218
- await writer.close();
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
- return sink;
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(options: ConsoleSinkOptions = {}): Sink {
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
- return (record: LogRecord) => {
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
  /**