@dws-std/logger 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominus Web Services (DWS)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,351 @@
1
+ <p align="center">
2
+ <img src="https://cdn.jsdelivr.net/gh/Dominus-Web-Service/std@main/packages/logger/logo-logger.png" alt="DWS Logger logo" width="200" />
3
+ </p>
4
+
5
+ # đŸŽ¯ DWS Logger
6
+
7
+ Logging in Bun often means choosing between "fast but dumb" or "smart but blocking".
8
+ `@dws-std/logger` gives you both: a type-safe, sink-based system that never blocks your main thread.
9
+
10
+ ## Why this package?
11
+
12
+ The goal is simple: **Stop your logs from slowing down your app.**
13
+
14
+ Most loggers either block on every write or lose type safety when you need structured logging.
15
+ This package runs everything in a worker thread, batches automatically, and still gives you full TypeScript inference on what you log.
16
+
17
+ ## 📌 Table of Contents
18
+
19
+ - [Features](#-features)
20
+ - [Installation](#-installation)
21
+ - [Usage](#-usage)
22
+ - [Custom Sinks](#-custom-sinks)
23
+ - [Type-Safe Logging](#-type-safe-logging)
24
+ - [Error Handling](#-error-handling)
25
+ - [Flushing and Closing](#-flushing-and-closing)
26
+ - [Configuration](#-configuration)
27
+ - [API Reference](#-api-reference)
28
+ - [License](#-license)
29
+ - [Contact](#-contact)
30
+
31
+ ## ✨ Features
32
+
33
+ - ⚡ **Zero Blocking** : Every log goes through a worker thread – your main loop stays fast.
34
+ - 🔒 **Type-Safe** : TypeScript infers the shape of your logs. No more `any` everywhere.
35
+ - đŸŽ¯ **Sink Pattern** : Route logs to console, file, database, or your own custom destination.
36
+ - 🔄 **Smart Batching** : Logs are grouped automatically for better I/O performance.
37
+ - 🔔 **Event-Driven** : Listen to flush, close, and error events when you need them.
38
+
39
+ ## 🔧 Installation
40
+
41
+ ```bash
42
+ bun add @dws-std/logger
43
+ ```
44
+
45
+ ## âš™ī¸ Usage
46
+
47
+ ### Basic Setup
48
+
49
+ Create a logger, attach a sink, and start logging:
50
+
51
+ ```ts
52
+ import { Logger, consoleSink } from '@dws-std/logger';
53
+
54
+ // Create a logger and register a console sink
55
+ const logger = new Logger().registerSink('console', consoleSink);
56
+
57
+ // Log messages (always pass an object)
58
+ logger.info({ message: 'Application started' });
59
+ logger.warn({ message: 'This is a warning' });
60
+ logger.error({ message: 'An error occurred', code: 500 });
61
+ logger.debug({ action: 'debug_info', data: { foo: 'bar' } });
62
+ logger.log({ event: 'generic_log' });
63
+
64
+ // Close the logger when done
65
+ await logger.close();
66
+ ```
67
+
68
+ > â„šī¸ Sinks are **factory functions**, not classes. You pass the factory itself (not the
69
+ > result of calling it) to `registerSink`. The worker re-evaluates the factory string
70
+ > and calls it with the `sinkArgs` you forwarded, so the sink is built **inside** the worker.
71
+
72
+ ### Multiple Sinks
73
+
74
+ Need logs going to different places? Register as many sinks as you want:
75
+
76
+ ```ts
77
+ import { Logger, consoleSink, fileSink } from '@dws-std/logger';
78
+
79
+ // Register multiple sinks
80
+ const logger = new Logger()
81
+ .registerSink('console', consoleSink)
82
+ .registerSink('file', fileSink, './app.log');
83
+
84
+ // Log to all sinks
85
+ logger.info({ message: 'This goes to console and file' });
86
+
87
+ // Log to specific sinks only
88
+ logger.error({ message: 'Only in file' }, ['file']);
89
+ logger.warn({ message: 'Only in console' }, ['console']);
90
+
91
+ await logger.close();
92
+ ```
93
+
94
+ ### Built-in Sinks
95
+
96
+ | Factory | Args | Description |
97
+ | ------------ | ----------- | --------------------------------------------------------- |
98
+ | `consoleSink`| _none_ | Writes JSON log entries to `console` (routed by level). |
99
+ | `fileSink` | `path` | Appends JSON log entries to a file via `Bun.FileSink`. |
100
+ | `devNullSink`| _none_ | Discards everything – useful for benchmarks / dry runs. |
101
+
102
+ ```ts
103
+ import { Logger, devNullSink, fileSink } from '@dws-std/logger';
104
+
105
+ const logger = new Logger()
106
+ .registerSink('applog', fileSink, './app.log')
107
+ .registerSink('silent', devNullSink);
108
+ ```
109
+
110
+ ## đŸ› ī¸ Custom Sinks
111
+
112
+ A sink is a plain object implementing the `LoggerSink` interface — register a **factory**
113
+ function that builds and returns it. The factory is stringify-ed and re-evaluated inside the
114
+ worker, so its body must be **self-contained**: it may use its arguments, runtime globals
115
+ (`Bun`, `console`, `JSON`, â€Ļ) and dynamic `import()`, but **must not** close over
116
+ module-scoped imports or variables from the calling file.
117
+
118
+ ```ts
119
+ import { Logger, type SinkFactory } from '@dws-std/logger';
120
+
121
+ // A self-contained factory: no module-scoped imports captured.
122
+ const databaseSink: SinkFactory<{ query: string }, [dbUrl: string]> = (dbUrl: string) => {
123
+ // Open the connection inside the factory — the worker owns it.
124
+ const connection = /* â€Ļopen using `dbUrl`â€Ļ */ {} as unknown;
125
+ return {
126
+ async log(level, timestamp, object) {
127
+ // object is typed as { query: string }
128
+ await (connection as { write: (s: string) => Promise<void> }).write(
129
+ JSON.stringify({ level, timestamp, object })
130
+ );
131
+ },
132
+ async close() {
133
+ await (connection as { close: () => Promise<void> }).close();
134
+ }
135
+ };
136
+ };
137
+
138
+ const logger = new Logger().registerSink('database', databaseSink, 'postgres://localhost/app');
139
+
140
+ logger.info({ query: 'SELECT 1' });
141
+ await logger.close();
142
+ ```
143
+
144
+ ### The `LoggerSink` interface
145
+
146
+ ```ts
147
+ interface LoggerSink<TLogObject = unknown> {
148
+ log(level: LogLevels, timestamp: number, object: TLogObject): Promise<void> | void;
149
+ flush?(): Promise<void> | void; // called by Logger.flush()
150
+ close?(): Promise<void> | void; // called on Logger.close()
151
+ }
152
+ ```
153
+
154
+ - `log` is the only required method.
155
+ - `flush` is optional — implement it when your sink buffers writes and you want `logger.flush()` to push them through.
156
+ - `close` is optional — implement it to release file handles, connections, etc.
157
+
158
+ ## 🔒 Type-Safe Logging
159
+
160
+ When you define typed sinks, TypeScript knows exactly what shape your logs need. No more
161
+ guessing, no more runtime surprises.
162
+
163
+ ### Single Typed Sink
164
+
165
+ ```ts
166
+ import { Logger, type LoggerSink, type LogLevels, type SinkFactory } from '@dws-std/logger';
167
+
168
+ interface UserLog {
169
+ userId: number;
170
+ action: string;
171
+ timestamp?: Date;
172
+ }
173
+
174
+ // Typed factory: the returned sink only accepts UserLog objects.
175
+ const userLogSink: SinkFactory<UserLog> = () => ({
176
+ log(level: LogLevels, timestamp: number, object: UserLog): void {
177
+ console.log(`User ${object.userId} performed: ${object.action}`);
178
+ }
179
+ });
180
+
181
+ const logger = new Logger().registerSink('userLog', userLogSink);
182
+
183
+ // ✅ TypeScript requires the correct shape
184
+ logger.info({ userId: 123, action: 'login' });
185
+
186
+ // ❌ TypeScript error: Missing required property 'action'
187
+ logger.info({ userId: 123 });
188
+ ```
189
+
190
+ ### Multiple Typed Sinks
191
+
192
+ When logging to multiple sinks at once, TypeScript creates an intersection of all the
193
+ targeted sinks' types — you must satisfy every one of them.
194
+
195
+ ```ts
196
+ interface UserLog {
197
+ userId: number;
198
+ action: string;
199
+ }
200
+
201
+ interface ApiLog {
202
+ endpoint: string;
203
+ method: string;
204
+ statusCode: number;
205
+ }
206
+
207
+ const userLogSink: SinkFactory<UserLog> = () => ({
208
+ async log(_level, _ts, object) {
209
+ // â€Ļ persist object â€Ļ
210
+ void object;
211
+ }
212
+ });
213
+
214
+ const apiLogSink: SinkFactory<ApiLog> = () => ({
215
+ async log(_level, _ts, object) {
216
+ // â€Ļ persist object â€Ļ
217
+ void object;
218
+ }
219
+ });
220
+
221
+ const logger = new Logger()
222
+ .registerSink('user', userLogSink)
223
+ .registerSink('api', apiLogSink);
224
+
225
+ // ✅ Logging to both sinks requires BOTH types combined
226
+ logger.info(
227
+ {
228
+ userId: 123,
229
+ action: 'api_call',
230
+ endpoint: '/users',
231
+ method: 'POST',
232
+ statusCode: 201
233
+ },
234
+ ['user', 'api']
235
+ );
236
+
237
+ // ✅ Logging to only one sink requires only that sink's type
238
+ logger.warn({ userId: 456, action: 'failed_attempt' }, ['user']);
239
+
240
+ // ❌ TypeScript error: Missing api properties
241
+ logger.error({ userId: 789, action: 'error' }, ['user', 'api']);
242
+ ```
243
+
244
+ ### Mixing Typed and Untyped Sinks
245
+
246
+ When you mix typed sinks with untyped ones (like `consoleSink`, which accepts `unknown`),
247
+ things stay flexible: the intersection with `unknown` lets extra properties through.
248
+
249
+ ```ts
250
+ import { Logger, consoleSink, type SinkFactory } from '@dws-std/logger';
251
+
252
+ interface DatabaseLog {
253
+ query: string;
254
+ duration: number;
255
+ }
256
+
257
+ const databaseLogSink: SinkFactory<DatabaseLog> = () => ({
258
+ async log(_level, _ts, object) {
259
+ // â€Ļ persist object â€Ļ
260
+ void object;
261
+ }
262
+ });
263
+
264
+ const logger = new Logger()
265
+ .registerSink('database', databaseLogSink)
266
+ .registerSink('console', consoleSink); // accepts unknown
267
+
268
+ // ✅ Works — the database type is enforced, console accepts anything
269
+ logger.info(
270
+ {
271
+ query: 'SELECT * FROM users',
272
+ duration: 123,
273
+ customData: 'anything goes'
274
+ },
275
+ ['database', 'console']
276
+ );
277
+ ```
278
+
279
+ ## 🚨 Error Handling
280
+
281
+ Things break. When they do, you'll want to know:
282
+
283
+ ```ts
284
+ import { Logger, consoleSink } from '@dws-std/logger';
285
+
286
+ const logger = new Logger().registerSink('console', consoleSink);
287
+
288
+ // Listen for sink errors (a sink throwing inside the worker)
289
+ logger.addListener('sinkError', (error) => {
290
+ console.error('Logger error:', error.message);
291
+ });
292
+
293
+ // Listen for sink registration errors (factory failed to build inside the worker)
294
+ logger.addListener('registerSinkError', (error) => {
295
+ console.error('Failed to register sink:', error.message);
296
+ });
297
+
298
+ logger.info({ message: 'Safe to log' });
299
+ await logger.close();
300
+ ```
301
+
302
+ ## 🧹 Flushing and Closing
303
+
304
+ When you need to make sure everything is written before shutting down:
305
+
306
+ ```ts
307
+ import { Logger, consoleSink } from '@dws-std/logger';
308
+
309
+ const logger = new Logger().registerSink('console', consoleSink);
310
+
311
+ logger.info({ message: 'First message' });
312
+ logger.info({ message: 'Second message' });
313
+
314
+ // Wait for all pending logs to be processed
315
+ await logger.flush();
316
+
317
+ // Close the logger and release resources (internally calls flush)
318
+ await logger.close();
319
+ ```
320
+
321
+ `flush()` drains both the in-memory queue **and** each sink's own buffer (via the
322
+ optional `flush()` method on `LoggerSink`).
323
+
324
+ ## âš™ī¸ Configuration
325
+
326
+ Fine-tune the batching and queue behavior:
327
+
328
+ ```ts
329
+ import { Logger, consoleSink } from '@dws-std/logger';
330
+
331
+ const logger = new Logger({
332
+ maxPendingLogs: 10_000, // Max queued logs (default: 10,000)
333
+ batchSize: 100, // Logs per batch (default: 100)
334
+ batchTimeout: 0.1, // Ms before flushing a partial batch (default: 0.1)
335
+ maxMessagesInFlight: 100, // Max batches being processed (default: 100)
336
+ autoEnd: true, // Auto-close on process exit (default: true)
337
+ flushOnBeforeExit: true // Flush before exit (default: true)
338
+ }).registerSink('console', consoleSink);
339
+ ```
340
+
341
+ ## 📚 API Reference
342
+
343
+ Full docs: [https://dominus-web-service.github.io/std/](https://dominus-web-service.github.io/std/)
344
+
345
+ ## âš–ī¸ License
346
+
347
+ MIT - Feel free to use it.
348
+
349
+ ## 📧 Contact
350
+
351
+ - GitHub: [Dominus-Web-Service](https://github.com/Dominus-Web-Service/packages)
@@ -0,0 +1,21 @@
1
+ import type { EventMap } from '@dws-std/common';
2
+ import type { Exception } from '@dws-std/error';
3
+ export interface LoggerEvent extends EventMap {
4
+ onBeforeExitError: [Exception<{
5
+ error: Error;
6
+ }>];
7
+ registerSinkError: [
8
+ Exception<{
9
+ sinkName: string;
10
+ error: Error;
11
+ }>
12
+ ];
13
+ sinkError: [
14
+ Exception<{
15
+ sinkName: string;
16
+ object?: unknown;
17
+ error: Error;
18
+ }>
19
+ ];
20
+ drained: [];
21
+ }
@@ -0,0 +1,7 @@
1
+ export { LOGGER_ERROR_KEYS, Logger, type LoggerOptions } from './logger';
2
+ export { consoleSink } from './sinks/console-logger';
3
+ export { devNullSink } from './sinks/devnull-logger';
4
+ export { fileSink } from './sinks/file-logger';
5
+ export type { LogLevels } from './types/log-levels';
6
+ export type { LoggerSink } from './types/logger-sink';
7
+ export type { SinkFactory } from './types/sink-factory';
package/dist/index.js ADDED
@@ -0,0 +1,448 @@
1
+ // @bun
2
+ // src/logger.ts
3
+ import { TypedEventEmitter } from "@dws-std/common";
4
+ import { Exception } from "@dws-std/error";
5
+
6
+ // src/worker-logger.ts
7
+ var workerFunction = () => {
8
+ const sinks = {};
9
+ const self = globalThis;
10
+ const processLogEntry = (log) => {
11
+ const { sinkNames, level, timestamp, object } = log;
12
+ const len = sinkNames.length;
13
+ let promises;
14
+ for (let i = 0;i < len; ++i) {
15
+ const sinkName = sinkNames[i];
16
+ if (sinkName === undefined)
17
+ continue;
18
+ const sink = sinks[sinkName];
19
+ if (!sink)
20
+ continue;
21
+ try {
22
+ const result = sink.log(level, timestamp, object);
23
+ if (result instanceof Promise) {
24
+ const guarded = result.catch((error) => {
25
+ self.postMessage({
26
+ type: "SINK_LOG_ERROR",
27
+ sinkName,
28
+ error,
29
+ object
30
+ });
31
+ });
32
+ (promises ??= []).push(guarded);
33
+ }
34
+ } catch (error) {
35
+ self.postMessage({
36
+ type: "SINK_LOG_ERROR",
37
+ sinkName,
38
+ error,
39
+ object
40
+ });
41
+ }
42
+ }
43
+ return promises === undefined ? undefined : Promise.all(promises).then(() => {
44
+ return;
45
+ });
46
+ };
47
+ self.addEventListener("message", (event) => {
48
+ switch (event.data.type) {
49
+ case "REGISTER_SINK": {
50
+ const { sinkName, sinkFactoryString, sinkArgs } = event.data;
51
+ try {
52
+ const create = new Function("sinkArgs", `return (${sinkFactoryString})(...sinkArgs);`);
53
+ sinks[sinkName] = create(sinkArgs);
54
+ } catch (error) {
55
+ self.postMessage({
56
+ type: "REGISTER_SINK_ERROR",
57
+ sinkName,
58
+ error
59
+ });
60
+ }
61
+ break;
62
+ }
63
+ case "LOG_BATCH": {
64
+ const { logs } = event.data;
65
+ const len = logs.length;
66
+ let pending;
67
+ for (let i = 0;i < len; ++i) {
68
+ const result = processLogEntry(logs[i]);
69
+ if (result !== undefined)
70
+ (pending ??= []).push(result);
71
+ }
72
+ if (pending === undefined)
73
+ self.postMessage({ type: "BATCH_COMPLETE" });
74
+ else
75
+ Promise.all(pending).then(() => self.postMessage({ type: "BATCH_COMPLETE" }));
76
+ break;
77
+ }
78
+ case "FLUSH": {
79
+ let pending;
80
+ for (const [sinkName, sink] of Object.entries(sinks))
81
+ try {
82
+ const result = sink.flush?.();
83
+ if (result instanceof Promise) {
84
+ const guarded = result.catch((error) => {
85
+ self.postMessage({
86
+ type: "SINK_LOG_ERROR",
87
+ sinkName,
88
+ error,
89
+ object: undefined
90
+ });
91
+ });
92
+ (pending ??= []).push(guarded);
93
+ }
94
+ } catch (error) {
95
+ self.postMessage({
96
+ type: "SINK_LOG_ERROR",
97
+ sinkName,
98
+ error,
99
+ object: undefined
100
+ });
101
+ }
102
+ if (pending === undefined)
103
+ self.postMessage({ type: "FLUSH_COMPLETE" });
104
+ else
105
+ Promise.all(pending).then(() => self.postMessage({ type: "FLUSH_COMPLETE" }));
106
+ break;
107
+ }
108
+ case "CLOSE": {
109
+ const entries = Object.entries(sinks);
110
+ for (const [name, sink] of entries)
111
+ try {
112
+ sink.close?.();
113
+ } catch (error) {
114
+ self.postMessage({
115
+ type: "SINK_CLOSE_ERROR",
116
+ sinkName: name,
117
+ error
118
+ });
119
+ }
120
+ self.postMessage({ type: "CLOSE_COMPLETE" });
121
+ break;
122
+ }
123
+ default:
124
+ break;
125
+ }
126
+ });
127
+ };
128
+
129
+ // src/logger.ts
130
+ var LOGGER_ERROR_KEYS = {
131
+ SINK_ALREADY_ADDED: "logger.sink-already-added",
132
+ NO_SINKS_PROVIDED: "logger.no-sinks-provided",
133
+ SINK_LOG_ERROR: "logger.sink-log-error",
134
+ SINK_CLOSE_ERROR: "logger.sink-close-error",
135
+ REGISTER_SINK_ERROR: "logger.register-sink-error",
136
+ BEFORE_EXIT_FLUSH_ERROR: "logger.before-exit-flush-error",
137
+ BEFORE_EXIT_CLOSE_ERROR: "logger.before-exit-close-error"
138
+ };
139
+
140
+ class Logger extends TypedEventEmitter {
141
+ sinks;
142
+ sinkKeys = [];
143
+ worker;
144
+ workerUrl;
145
+ maxPendingLogs;
146
+ maxMessagesInFlight;
147
+ batchSize;
148
+ batchTimeout;
149
+ autoEnd;
150
+ flushOnBeforeExit;
151
+ pendingLogs = [];
152
+ pendingHead = 0;
153
+ messagesInFlight = 0;
154
+ batchTimer = null;
155
+ isWriting = false;
156
+ flushResolvers = [];
157
+ flushingResolvers = [];
158
+ flushPending = false;
159
+ closeResolver = null;
160
+ backpressureResolver = null;
161
+ workerTerminated = false;
162
+ handleExit = () => {
163
+ this.terminateWorker();
164
+ };
165
+ handleWorkerClose = () => {
166
+ process.off("beforeExit", this.handleBeforeExit);
167
+ process.off("exit", this.handleExit);
168
+ };
169
+ constructor({
170
+ autoEnd = true,
171
+ batchSize = 100,
172
+ batchTimeout = 0.1,
173
+ flushOnBeforeExit = true,
174
+ maxMessagesInFlight = 100,
175
+ maxPendingLogs = 1e4
176
+ } = {}) {
177
+ super();
178
+ this.sinks = {};
179
+ this.maxPendingLogs = maxPendingLogs;
180
+ this.maxMessagesInFlight = maxMessagesInFlight;
181
+ this.batchSize = batchSize;
182
+ this.batchTimeout = batchTimeout;
183
+ this.autoEnd = autoEnd;
184
+ this.flushOnBeforeExit = flushOnBeforeExit;
185
+ this.workerUrl = URL.createObjectURL(new Blob([`(${workerFunction.toString()})()`], { type: "application/javascript" }));
186
+ this.worker = new Worker(this.workerUrl, { type: "module" });
187
+ this.setupWorkerMessages();
188
+ if (this.autoEnd)
189
+ this.setupAutoEnd();
190
+ }
191
+ registerSink(sinkName, sinkFactory, ...sinkArgs) {
192
+ if (this.sinks[sinkName] !== undefined)
193
+ throw new Exception("Sink is already registered", {
194
+ key: LOGGER_ERROR_KEYS.SINK_ALREADY_ADDED,
195
+ cause: { sinkName }
196
+ });
197
+ this.worker.postMessage({
198
+ type: "REGISTER_SINK",
199
+ sinkName,
200
+ sinkFactoryString: sinkFactory.toString(),
201
+ sinkArgs
202
+ });
203
+ this.sinks[sinkName] = sinkFactory;
204
+ this.sinkKeys.push(sinkName);
205
+ return this;
206
+ }
207
+ error(object, sinkNames = this.sinkKeys) {
208
+ this.enqueue("ERROR", object, sinkNames);
209
+ }
210
+ warn(object, sinkNames = this.sinkKeys) {
211
+ this.enqueue("WARN", object, sinkNames);
212
+ }
213
+ info(object, sinkNames = this.sinkKeys) {
214
+ this.enqueue("INFO", object, sinkNames);
215
+ }
216
+ debug(object, sinkNames = this.sinkKeys) {
217
+ this.enqueue("DEBUG", object, sinkNames);
218
+ }
219
+ log(object, sinkNames = this.sinkKeys) {
220
+ this.enqueue("LOG", object, sinkNames);
221
+ }
222
+ flush() {
223
+ return new Promise((resolve) => {
224
+ this.flushResolvers.push(resolve);
225
+ this.tryFlush();
226
+ });
227
+ }
228
+ async close() {
229
+ await this.flush();
230
+ return new Promise((resolve) => {
231
+ this.closeResolver = resolve;
232
+ this.worker.postMessage({ type: "CLOSE" });
233
+ });
234
+ }
235
+ enqueue(level, object, sinkNames) {
236
+ if (this.sinkKeys.length === 0)
237
+ throw new Exception("No sinks provided", {
238
+ key: LOGGER_ERROR_KEYS.NO_SINKS_PROVIDED,
239
+ cause: { level, object }
240
+ });
241
+ const pendingLength = this.pendingLogs.length - this.pendingHead;
242
+ if (pendingLength >= this.maxPendingLogs)
243
+ return;
244
+ this.pendingLogs.push({
245
+ sinkNames: sinkNames ?? this.sinkKeys,
246
+ level,
247
+ timestamp: Date.now(),
248
+ object
249
+ });
250
+ if (pendingLength + 1 >= this.batchSize) {
251
+ if (this.batchTimer !== null) {
252
+ clearTimeout(this.batchTimer);
253
+ this.batchTimer = null;
254
+ }
255
+ this.triggerProcessing();
256
+ } else if (this.batchTimer === null && this.batchTimeout > 0)
257
+ this.batchTimer = setTimeout(() => {
258
+ this.batchTimer = null;
259
+ this.triggerProcessing();
260
+ }, this.batchTimeout);
261
+ }
262
+ triggerProcessing() {
263
+ if (this.isWriting)
264
+ return;
265
+ this.isWriting = true;
266
+ this.processPendingLogs();
267
+ }
268
+ async processPendingLogs() {
269
+ while (this.pendingLogs.length - this.pendingHead > 0) {
270
+ if (this.messagesInFlight >= this.maxMessagesInFlight)
271
+ await new Promise((resolve) => {
272
+ this.backpressureResolver = resolve;
273
+ });
274
+ const end = Math.min(this.pendingHead + this.batchSize, this.pendingLogs.length);
275
+ const batch = this.pendingLogs.slice(this.pendingHead, end);
276
+ this.pendingHead = end;
277
+ this.reclaimPending();
278
+ this.messagesInFlight++;
279
+ this.worker.postMessage({
280
+ type: "LOG_BATCH",
281
+ logs: batch
282
+ });
283
+ }
284
+ this.isWriting = false;
285
+ this.emit("drained");
286
+ }
287
+ reclaimPending() {
288
+ if (this.pendingHead === this.pendingLogs.length) {
289
+ this.pendingLogs.length = 0;
290
+ this.pendingHead = 0;
291
+ } else if (this.pendingHead > this.batchSize && this.pendingHead * 2 >= this.pendingLogs.length) {
292
+ this.pendingLogs.splice(0, this.pendingHead);
293
+ this.pendingHead = 0;
294
+ }
295
+ }
296
+ releaseBatch() {
297
+ this.messagesInFlight--;
298
+ if (this.backpressureResolver !== null) {
299
+ this.backpressureResolver();
300
+ this.backpressureResolver = null;
301
+ }
302
+ }
303
+ tryFlush() {
304
+ if (this.flushResolvers.length === 0)
305
+ return;
306
+ if (this.pendingLogs.length - this.pendingHead > 0) {
307
+ if (!this.isWriting) {
308
+ this.isWriting = true;
309
+ this.processPendingLogs();
310
+ }
311
+ return;
312
+ }
313
+ if (this.messagesInFlight === 0 && !this.flushPending) {
314
+ this.flushPending = true;
315
+ this.flushingResolvers.push(...this.flushResolvers);
316
+ this.flushResolvers.length = 0;
317
+ this.worker.postMessage({ type: "FLUSH" });
318
+ }
319
+ }
320
+ setupWorkerMessages() {
321
+ this.worker.addEventListener("message", (event) => {
322
+ switch (event.data.type) {
323
+ case "BATCH_COMPLETE":
324
+ this.releaseBatch();
325
+ this.tryFlush();
326
+ break;
327
+ case "FLUSH_COMPLETE": {
328
+ this.flushPending = false;
329
+ const resolvers = this.flushingResolvers.splice(0, this.flushingResolvers.length);
330
+ for (const resolve of resolvers)
331
+ resolve();
332
+ this.tryFlush();
333
+ break;
334
+ }
335
+ case "SINK_LOG_ERROR":
336
+ this.emit("sinkError", new Exception("Sink failed to log message", {
337
+ key: LOGGER_ERROR_KEYS.SINK_LOG_ERROR,
338
+ cause: {
339
+ sinkName: event.data.sinkName,
340
+ object: event.data.object,
341
+ error: event.data.error
342
+ }
343
+ }));
344
+ break;
345
+ case "SINK_CLOSE_ERROR":
346
+ this.emit("sinkError", new Exception("Sink failed to close", {
347
+ key: LOGGER_ERROR_KEYS.SINK_CLOSE_ERROR,
348
+ cause: { sinkName: event.data.sinkName, error: event.data.error }
349
+ }));
350
+ break;
351
+ case "REGISTER_SINK_ERROR":
352
+ this.emit("registerSinkError", new Exception("Failed to register sink", {
353
+ key: LOGGER_ERROR_KEYS.REGISTER_SINK_ERROR,
354
+ cause: { sinkName: event.data.sinkName, error: event.data.error }
355
+ }));
356
+ break;
357
+ case "CLOSE_COMPLETE":
358
+ this.terminateWorker();
359
+ if (this.closeResolver) {
360
+ this.closeResolver();
361
+ this.closeResolver = null;
362
+ }
363
+ break;
364
+ default:
365
+ break;
366
+ }
367
+ });
368
+ }
369
+ terminateWorker() {
370
+ if (this.workerTerminated)
371
+ return;
372
+ this.workerTerminated = true;
373
+ this.worker.terminate();
374
+ URL.revokeObjectURL(this.workerUrl);
375
+ }
376
+ setupAutoEnd() {
377
+ process.on("beforeExit", this.handleBeforeExit);
378
+ process.on("exit", this.handleExit);
379
+ this.worker.addEventListener("close", this.handleWorkerClose);
380
+ }
381
+ handleBeforeExit = () => {
382
+ if (this.flushOnBeforeExit)
383
+ this.flush().then(() => this.close()).catch((error) => {
384
+ this.emit("onBeforeExitError", new Exception("Failed to flush before exit", {
385
+ key: LOGGER_ERROR_KEYS.BEFORE_EXIT_FLUSH_ERROR,
386
+ cause: { error }
387
+ }));
388
+ });
389
+ else
390
+ this.close().catch((error) => {
391
+ this.emit("onBeforeExitError", new Exception("Failed to close before exit", {
392
+ key: LOGGER_ERROR_KEYS.BEFORE_EXIT_CLOSE_ERROR,
393
+ cause: { error }
394
+ }));
395
+ });
396
+ };
397
+ }
398
+ // src/sinks/console-logger.ts
399
+ var consoleSink = () => ({
400
+ log(level, timestamp, object) {
401
+ const logEntry = { timestamp, level, content: object };
402
+ const logLevel = level.toLowerCase();
403
+ const methods = {
404
+ error: console.error,
405
+ warn: console.warn,
406
+ info: console.info,
407
+ debug: console.debug,
408
+ log: console.log,
409
+ trace: console.trace
410
+ };
411
+ methods[logLevel](JSON.stringify(logEntry));
412
+ }
413
+ });
414
+ // src/sinks/devnull-logger.ts
415
+ var devNullSink = () => ({
416
+ log() {}
417
+ });
418
+ // src/sinks/file-logger.ts
419
+ var fileSink = (path) => {
420
+ const sink = Bun.file(path).writer();
421
+ let isClosed = false;
422
+ return {
423
+ log(level, timestamp, object) {
424
+ if (isClosed)
425
+ return;
426
+ sink.write(JSON.stringify({ timestamp, level, content: object }) + `
427
+ `);
428
+ },
429
+ async flush() {
430
+ if (isClosed)
431
+ return;
432
+ await sink.flush();
433
+ },
434
+ async close() {
435
+ if (isClosed)
436
+ return;
437
+ isClosed = true;
438
+ await sink.end();
439
+ }
440
+ };
441
+ };
442
+ export {
443
+ fileSink,
444
+ devNullSink,
445
+ consoleSink,
446
+ Logger,
447
+ LOGGER_ERROR_KEYS
448
+ };
@@ -0,0 +1,213 @@
1
+ import { TypedEventEmitter } from '@dws-std/common';
2
+ import type { LoggerEvent } from './events/logger-events';
3
+ import type { LoggerSink } from './types/logger-sink';
4
+ import type { SinkBodiesIntersection } from './types/sink-bodies-intersection';
5
+ import type { SinkMap } from './types/sink-map';
6
+ /**
7
+ * Configuration options for the {@link Logger} constructor.
8
+ */
9
+ export interface LoggerOptions {
10
+ /**
11
+ * Maximum number of log messages that can be queued in memory before dropping new logs.
12
+ * @default 10_000
13
+ */
14
+ maxPendingLogs?: number;
15
+ /**
16
+ * Maximum number of messages that can be sent to the worker without acknowledgment.
17
+ * Controls backpressure to prevent overwhelming the worker thread.
18
+ * @default 100
19
+ */
20
+ maxMessagesInFlight?: number;
21
+ /**
22
+ * Maximum number of logs to batch together before sending to the worker.
23
+ * Higher values = better throughput but higher latency.
24
+ * @default 100
25
+ */
26
+ batchSize?: number;
27
+ /**
28
+ * Maximum time in milliseconds to wait before flushing a partial batch.
29
+ * Prevents logs from being delayed indefinitely when batch size is not reached.
30
+ * Set to 0 to disable time-based flushing.
31
+ * @default 0.1 (milliseconds)
32
+ */
33
+ batchTimeout?: number;
34
+ /**
35
+ * Whether to automatically flush and close the logger when the process exits.
36
+ * When enabled, hooks are installed on `process.on('beforeExit')` and `process.on('exit')`.
37
+ * @default true
38
+ */
39
+ autoEnd?: boolean;
40
+ /**
41
+ * Whether to flush pending logs before the process exits.
42
+ * Only applies when `autoEnd` is true.
43
+ * @default true
44
+ */
45
+ flushOnBeforeExit?: boolean;
46
+ }
47
+ export declare const LOGGER_ERROR_KEYS: {
48
+ readonly SINK_ALREADY_ADDED: "logger.sink-already-added";
49
+ readonly NO_SINKS_PROVIDED: "logger.no-sinks-provided";
50
+ readonly SINK_LOG_ERROR: "logger.sink-log-error";
51
+ readonly SINK_CLOSE_ERROR: "logger.sink-close-error";
52
+ readonly REGISTER_SINK_ERROR: "logger.register-sink-error";
53
+ readonly BEFORE_EXIT_FLUSH_ERROR: "logger.before-exit-flush-error";
54
+ readonly BEFORE_EXIT_CLOSE_ERROR: "logger.before-exit-close-error";
55
+ };
56
+ export declare class Logger<TSinks extends SinkMap = Record<never, never>> extends TypedEventEmitter<LoggerEvent> {
57
+ /** Map of registered sinks (logging destinations) */
58
+ private readonly sinks;
59
+ /** List of registered sink keys for quick access */
60
+ private readonly sinkKeys;
61
+ /** Worker instance handling log processing */
62
+ private readonly worker;
63
+ /** Object URL backing the worker module, revoked once the worker has loaded */
64
+ private readonly workerUrl;
65
+ /** Maximum number of pending log messages allowed in the queue */
66
+ private readonly maxPendingLogs;
67
+ /** Maximum number of messages in flight to the worker */
68
+ private readonly maxMessagesInFlight;
69
+ /** Number of logs to batch before sending to worker */
70
+ private readonly batchSize;
71
+ /** Timeout in milliseconds before flushing incomplete batch */
72
+ private readonly batchTimeout;
73
+ /** Whether to auto flush and close on process exit */
74
+ private readonly autoEnd;
75
+ /** Whether to flush before process exit */
76
+ private readonly flushOnBeforeExit;
77
+ /** Backing buffer of pending log messages (consumed from {@link pendingHead}) */
78
+ private readonly pendingLogs;
79
+ /** Index of the first not-yet-dispatched entry in {@link pendingLogs} */
80
+ private pendingHead;
81
+ /** Number of log messages currently being processed by the worker */
82
+ private messagesInFlight;
83
+ /** Timer for batching log messages */
84
+ private batchTimer;
85
+ /** Whether the logger is currently writing log messages to the worker */
86
+ private isWriting;
87
+ /** Resolvers for flush promises awaiting the next flush round */
88
+ private readonly flushResolvers;
89
+ /** Resolvers captured by the flush round currently in flight */
90
+ private readonly flushingResolvers;
91
+ /** Whether a FLUSH round-trip is currently in flight to the worker */
92
+ private flushPending;
93
+ /** Resolver for the close promise */
94
+ private closeResolver;
95
+ /** Resolver for backpressure when maxMessagesInFlight is reached */
96
+ private backpressureResolver;
97
+ /** Whether the worker has already been terminated (guards double teardown) */
98
+ private workerTerminated;
99
+ /** Handle the exit event */
100
+ private readonly handleExit;
101
+ /** Handle the worker close event */
102
+ private readonly handleWorkerClose;
103
+ /**
104
+ * Creates a new Logger instance with the specified options.
105
+ *
106
+ * @param options - Configuration options for the logger
107
+ */
108
+ constructor({ autoEnd, batchSize, batchTimeout, flushOnBeforeExit, maxMessagesInFlight, maxPendingLogs }?: LoggerOptions);
109
+ /**
110
+ * Registers a new sink (logging destination) with the logger.
111
+ *
112
+ * The factory is serialized and re-evaluated inside the worker, so its body must
113
+ * be self-contained (no module-scoped imports captured by closure).
114
+ *
115
+ * @param sinkName - The name of the sink
116
+ * @param sinkFactory - The factory building the sink
117
+ * @param sinkArgs - The factory arguments
118
+ *
119
+ * @returns The logger instance with new type (for chaining)
120
+ */
121
+ registerSink<TSinkName extends string, TSink extends LoggerSink, TSinkArgs extends unknown[]>(sinkName: TSinkName, sinkFactory: (...args: TSinkArgs) => TSink, ...sinkArgs: TSinkArgs): Logger<TSinks & Record<TSinkName, (...args: TSinkArgs) => TSink>>;
122
+ /**
123
+ * Logs a message at the ERROR level to the specified sinks.
124
+ *
125
+ * @param object - The log message object
126
+ * @param sinkNames - Optional array of sink names to log to; logs to all sinks if omitted
127
+ */
128
+ error<SNames extends (keyof TSinks)[] = (keyof TSinks)[]>(object: SinkBodiesIntersection<TSinks, SNames[number]>, sinkNames?: SNames): void;
129
+ /**
130
+ * Logs a message at the WARN level to the specified sinks.
131
+ *
132
+ * @param object - The log message object
133
+ * @param sinkNames - Optional array of sink names to log to; logs to all sinks if omitted
134
+ */
135
+ warn<SNames extends (keyof TSinks)[] = (keyof TSinks)[]>(object: SinkBodiesIntersection<TSinks, SNames[number]>, sinkNames?: SNames): void;
136
+ /**
137
+ * Logs a message at the INFO level to the specified sinks.
138
+ *
139
+ * @param object - The log message object
140
+ * @param sinkNames - Optional array of sink names to log to; logs to all sinks if omitted
141
+ */
142
+ info<SNames extends (keyof TSinks)[] = (keyof TSinks)[]>(object: SinkBodiesIntersection<TSinks, SNames[number]>, sinkNames?: SNames): void;
143
+ /**
144
+ * Logs a message at the DEBUG level to the specified sinks.
145
+ *
146
+ * @param object - The log message object
147
+ * @param sinkNames - Optional array of sink names to log to; logs to all sinks if omitted
148
+ */
149
+ debug<SNames extends (keyof TSinks)[] = (keyof TSinks)[]>(object: SinkBodiesIntersection<TSinks, SNames[number]>, sinkNames?: SNames): void;
150
+ /**
151
+ * Logs a message at the TRACE level to the specified sinks.
152
+ *
153
+ * @param object - The log message object
154
+ * @param sinkNames - Optional array of sink names to log to; logs to all sinks if omitted
155
+ */
156
+ log<SNames extends (keyof TSinks)[] = (keyof TSinks)[]>(object: SinkBodiesIntersection<TSinks, SNames[number]>, sinkNames?: SNames): void;
157
+ /**
158
+ * Flushes all pending logs and the sinks' buffers, and waits for completion.
159
+ */
160
+ flush(): Promise<void>;
161
+ /**
162
+ * Closes the logger, flushes pending logs, and releases resources.
163
+ */
164
+ close(): Promise<void>;
165
+ /**
166
+ * Enqueues a log message to be processed by the worker.
167
+ *
168
+ * @param level - The log level
169
+ * @param object - The log message object
170
+ * @param sinkNames - Optional array of sink names to log to; logs to all sinks if omitted
171
+ */
172
+ private enqueue;
173
+ /**
174
+ * Triggers processing of pending logs.
175
+ */
176
+ private triggerProcessing;
177
+ /**
178
+ * Processes pending log messages by sending them to the worker in batches.
179
+ */
180
+ private processPendingLogs;
181
+ /**
182
+ * Reclaims consumed slots in the pending buffer.
183
+ * Fully resets the buffer when drained, otherwise compacts only once the consumed
184
+ * prefix dominates the backing array, keeping dequeue amortized O(1).
185
+ */
186
+ private reclaimPending;
187
+ /**
188
+ * Releases a batch by decrementing the in-flight counter and resolving backpressure if needed.
189
+ */
190
+ private releaseBatch;
191
+ /**
192
+ * Advances the flush state machine: drains pending logs, then asks the worker to
193
+ * flush the sinks' buffers once everything has been dispatched and acknowledged.
194
+ */
195
+ private tryFlush;
196
+ /**
197
+ * Sets up message handling for the worker.
198
+ */
199
+ private setupWorkerMessages;
200
+ /**
201
+ * Terminates the worker and releases the object URL backing its module.
202
+ * Idempotent: safe to call from both the close handshake and process exit.
203
+ */
204
+ private terminateWorker;
205
+ /**
206
+ * Sets up automatic flushing and closing of the logger on process exit.
207
+ */
208
+ private setupAutoEnd;
209
+ /**
210
+ * Handles the beforeExit event.
211
+ */
212
+ private readonly handleBeforeExit;
213
+ }
@@ -0,0 +1,8 @@
1
+ import type { LoggerSink } from '#/types/logger-sink';
2
+ /**
3
+ * Creates a sink that writes JSON log entries to the console, routed to the
4
+ * matching `console` method for each level (`error`, `warn`, `info`, â€Ļ).
5
+ *
6
+ * @typeParam TLogObject - The shape of the objects this sink accepts.
7
+ */
8
+ export declare const consoleSink: <TLogObject = unknown>() => LoggerSink<TLogObject>;
@@ -0,0 +1,8 @@
1
+ import type { LoggerSink } from '#/types/logger-sink';
2
+ /**
3
+ * Creates a sink that discards all logs (like `/dev/null`).
4
+ * Useful for benchmarking the logger overhead without I/O.
5
+ *
6
+ * @typeParam TLogObject - The shape of the objects this sink accepts.
7
+ */
8
+ export declare const devNullSink: <TLogObject = unknown>() => LoggerSink<TLogObject>;
@@ -0,0 +1,9 @@
1
+ import type { LoggerSink } from '#/types/logger-sink';
2
+ /**
3
+ * Creates a sink that appends JSON log entries to a file.
4
+ * Uses `Bun.FileSink` for efficient buffered appending (runs in the worker context).
5
+ *
6
+ * @typeParam TLogObject - The shape of the objects this sink accepts.
7
+ * @param path - The destination file path.
8
+ */
9
+ export declare const fileSink: <TLogObject = unknown>(path: string) => LoggerSink<TLogObject>;
@@ -0,0 +1 @@
1
+ export type LogLevels = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'LOG' | 'TRACE';
@@ -0,0 +1,36 @@
1
+ import type { LogLevels } from './log-levels';
2
+ /**
3
+ * A logging destination.
4
+ *
5
+ * This is the object a {@link SinkFactory} returns. The factory runs inside the
6
+ * logger worker, so the sink closes over its own state (file handles, buffers, â€Ļ)
7
+ * instead of exposing it. All values the sink needs must be created **inside** the
8
+ * factory body — it cannot close over module-scoped imports (use dynamic `import()`
9
+ * or runtime globals such as `Bun` instead).
10
+ */
11
+ export interface LoggerSink<TLogObject = unknown, TConfig = unknown> {
12
+ /** Optional configuration for the sink */
13
+ readonly config?: TConfig;
14
+ /**
15
+ * Logs a message with the sink's implementation.
16
+ *
17
+ * @param level - The log level at which the message should be logged.
18
+ * @param timestamp - The epoch timestamp (ms) at which the message was logged.
19
+ * @param object - The object to log.
20
+ *
21
+ * @returns A promise when the sink is asynchronous; it is awaited by {@link Logger.flush}.
22
+ */
23
+ log(level: LogLevels, timestamp: number, object: TLogObject): Promise<void> | void;
24
+ /**
25
+ * Flushes any buffered data to the underlying destination.
26
+ * Called by {@link Logger.flush}; implement it when your sink buffers writes
27
+ * (e.g. a file writer) and durability on flush matters.
28
+ */
29
+ flush?(): Promise<void> | void;
30
+ /**
31
+ * Closes the sink and releases its resources.
32
+ * Called when the logger is closing. Optional - implement this if your sink needs
33
+ * cleanup (e.g. closing file handles, database connections).
34
+ */
35
+ close?(): Promise<void> | void;
36
+ }
@@ -0,0 +1,2 @@
1
+ import type { SinkBody } from './sink-body';
2
+ export type SinkBodiesIntersection<TSinks, K extends keyof TSinks> = (K extends unknown ? (object: SinkBody<TSinks, K>) => void : never) extends (object: infer I) => void ? I : never;
@@ -0,0 +1,2 @@
1
+ import type { LoggerSink } from './logger-sink';
2
+ export type SinkBody<TSink, Key extends keyof TSink> = TSink[Key] extends (...args: any[]) => LoggerSink<infer TBody> ? TBody : never;
@@ -0,0 +1,12 @@
1
+ import type { LoggerSink } from './logger-sink';
2
+ /**
3
+ * A factory that builds a {@link LoggerSink}.
4
+ *
5
+ * The factory is serialized and re-evaluated inside the logger worker, so its body
6
+ * must be self-contained: it can only rely on its arguments, runtime globals, and
7
+ * dynamic `import()` — never on module-scoped imports captured by closure.
8
+ *
9
+ * @typeParam TLogObject - The shape of the objects this sink accepts.
10
+ * @typeParam TArgs - The factory arguments forwarded from `registerSink`.
11
+ */
12
+ export type SinkFactory<TLogObject = unknown, TArgs extends readonly unknown[] = readonly unknown[]> = (...args: TArgs) => LoggerSink<TLogObject>;
@@ -0,0 +1,2 @@
1
+ import type { LoggerSink } from './logger-sink';
2
+ export type SinkMap = Record<string, (...args: any[]) => LoggerSink>;
@@ -0,0 +1 @@
1
+ export declare const workerFunction: () => void;
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@dws-std/logger",
3
+ "version": "1.0.0",
4
+ "description": "Type-safe logging library for Bun, modular sink pattern, transform streams, and immutable API design.",
5
+ "keywords": [
6
+ "bun",
7
+ "dws",
8
+ "open-source",
9
+ "types",
10
+ "typescript",
11
+ "utilities"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Dominus Web Services (DWS)",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/Dominus-Web-Service/std",
18
+ "directory": "packages/logger"
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "type": "module",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "scripts": {
31
+ "build": "bun builder.ts",
32
+ "docs": "bunx typedoc --tsconfig tsconfig.build.json",
33
+ "fmt:check": "oxfmt --check",
34
+ "fmt": "oxfmt",
35
+ "lint:fix": "oxlint --type-aware --type-check --fix ./src",
36
+ "lint:github": "oxlint --type-aware --type-check --format=github ./src",
37
+ "lint": "oxlint --type-aware --type-check ./src",
38
+ "test": "bun test --pass-with-no-tests --coverage"
39
+ },
40
+ "dependencies": {
41
+ "@dws-std/common": "workspace:^",
42
+ "@dws-std/error": "workspace:^"
43
+ },
44
+ "devDependencies": {
45
+ "@types/bun": "catalog:base",
46
+ "mitata": "^1.0.34",
47
+ "oxfmt": "catalog:base",
48
+ "oxlint": "catalog:base",
49
+ "oxlint-tsgolint": "catalog:base",
50
+ "pino": "^10.3.1",
51
+ "typescript": "catalog:base"
52
+ },
53
+ "peerDependencies": {
54
+ "@dws-std/common": "workspace:^",
55
+ "@dws-std/error": "workspace:^"
56
+ }
57
+ }