logora-websocket 1.0.3

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 boseba
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,290 @@
1
+ # logora-websocket
2
+
3
+ [![NPM version](https://img.shields.io/npm/v/logora-websocket?style=flat-square)](https://www.npmjs.com/package/logora-websocket)
4
+ [![Coverage Status](https://coveralls.io/repos/github/boseba/logora-websocket/badge.svg?branch=main)](https://coveralls.io/github/boseba/logora-websocket?branch=main)
5
+
6
+ logora-websocket is the official WebSocket output module for the [Logora](https://www.npmjs.com/package/logora) logging framework.
7
+
8
+ It forwards Logora writer instructions as structured JSON messages through a broadcaster abstraction, making it easy to stream server logs to remote clients, dashboards, or custom live viewers.
9
+
10
+ * * *
11
+
12
+ ## Features
13
+
14
+ - Structured WebSocket transport for Logora writer instructions
15
+ - Supports `log`, `print`, `title`, `empty`, and `clear`
16
+ - Safe serialization for complex values:
17
+ - `Error`
18
+ - `Date`
19
+ - `BigInt`
20
+ - `Symbol`
21
+ - functions
22
+ - circular references
23
+ - Broadcast-oriented design for live log viewers
24
+ - No built-in server lifecycle logic
25
+ - Compatible with custom broadcasters and `ws`
26
+ - Non-blocking design aligned with the Logora plugin model
27
+
28
+ * * *
29
+
30
+ ## Installation
31
+
32
+ npm install logora logora-websocket
33
+
34
+ If you want to use the provided `ws` broadcaster helper:
35
+
36
+ npm install ws
37
+
38
+ * * *
39
+
40
+ ## Basic Usage
41
+
42
+ import { createServer } from "node:http";
43
+ import { createLogger, LogLevel } from "logora";
44
+ import {
45
+ createWebSocketOutput,
46
+ createWsBroadcaster,
47
+ } from "logora-websocket";
48
+ import { WebSocketServer } from "ws";
49
+
50
+ const httpServer = createServer();
51
+ const webSocketServer = new WebSocketServer({ server: httpServer });
52
+
53
+ const logger = createLogger({ level: LogLevel.Info });
54
+
55
+ logger.addLogOutput(
56
+ createWebSocketOutput({
57
+ broadcaster: createWsBroadcaster(webSocketServer),
58
+ }),
59
+ );
60
+
61
+ logger.info("Server started on port {0}", 3000);
62
+
63
+ httpServer.listen(3000);
64
+
65
+ * * *
66
+
67
+ ## Scoped Logging
68
+
69
+ Scoped loggers are supported exactly like in the rest of the Logora ecosystem:
70
+
71
+ const apiLogger = logger.getScoped("API");
72
+
73
+ apiLogger.info("Request received: {0}", "/users");
74
+ apiLogger.warning("Rate limit reached for client {0}", clientId);
75
+
76
+ When a scope is present, it is included in the serialized payload.
77
+
78
+ * * *
79
+
80
+ ## How It Works
81
+
82
+ logora-websocket does not create or manage a WebSocket server.
83
+
84
+ Your application remains responsible for:
85
+
86
+ - creating the server
87
+ - authenticating clients
88
+ - deciding which clients receive logs
89
+ - closing connections
90
+ - integrating with your HTTP server or framework
91
+
92
+ The module only transforms Logora writer calls into structured JSON messages and forwards them through a `WebSocketBroadcaster`.
93
+
94
+ * * *
95
+
96
+ ## Broadcaster Abstraction
97
+
98
+ The transport relies on a very small broadcaster contract:
99
+
100
+ export interface WebSocketBroadcaster {
101
+ broadcast(message: string): void;
102
+ }
103
+
104
+ This keeps the module focused and lets you adapt it to your own WebSocket infrastructure.
105
+
106
+ ### Example with a custom broadcaster
107
+
108
+ import { createWebSocketOutput } from "logora-websocket";
109
+
110
+ const broadcaster = {
111
+ broadcast(message: string): void {
112
+ myCustomSocketHub.sendToAll(message);
113
+ },
114
+ };
115
+
116
+ logger.addLogOutput(
117
+ createWebSocketOutput({
118
+ broadcaster,
119
+ }),
120
+ );
121
+
122
+ * * *
123
+
124
+ ## Using the `ws` Broadcaster Helper
125
+
126
+ If your server uses [`ws`](https://www.npmjs.com/package/ws), the module provides a helper that broadcasts to all open clients:
127
+
128
+ import { createServer } from "node:http";
129
+ import { WebSocketServer } from "ws";
130
+ import {
131
+ createWebSocketOutput,
132
+ createWsBroadcaster,
133
+ } from "logora-websocket";
134
+
135
+ const httpServer = createServer();
136
+ const webSocketServer = new WebSocketServer({ server: httpServer });
137
+
138
+ const output = createWebSocketOutput({
139
+ broadcaster: createWsBroadcaster(webSocketServer),
140
+ });
141
+
142
+ This helper:
143
+
144
+ - iterates over connected clients
145
+ - sends only to open sockets
146
+ - ignores failures from individual clients
147
+
148
+ * * *
149
+
150
+ ## Message Format
151
+
152
+ Each writer instruction is sent as a JSON message.
153
+
154
+ ### Log message
155
+
156
+ {
157
+ "kind": "log",
158
+ "entry": {
159
+ "timestamp": "2026-04-04T12:00:00.000Z",
160
+ "type": "warning",
161
+ "message": "Something happened",
162
+ "args": [123],
163
+ "scope": "API"
164
+ }
165
+ }
166
+
167
+ ### Print message
168
+
169
+ {
170
+ "kind": "print",
171
+ "message": "Server listening",
172
+ "args": [3000]
173
+ }
174
+
175
+ ### Title message
176
+
177
+ {
178
+ "kind": "title",
179
+ "title": "HTTP"
180
+ }
181
+
182
+ ### Empty message
183
+
184
+ {
185
+ "kind": "empty",
186
+ "count": 2
187
+ }
188
+
189
+ ### Clear message
190
+
191
+ {
192
+ "kind": "clear"
193
+ }
194
+
195
+ * * *
196
+
197
+ ## Serialized Log Entry
198
+
199
+ A `log` instruction contains a serialized Logora entry with the following shape:
200
+
201
+ | Field | Type | Description |
202
+ |---|---|---|
203
+ | `timestamp` | `string` | ISO timestamp of the log entry |
204
+ | `type` | `string` | Log type name (`debug`, `info`, `success`, `warning`, `error`, `highlight`, `raw`) |
205
+ | `message` | `string` | Main log message |
206
+ | `args` | `SerializedValue[]` | Serialized argument list |
207
+ | `scope` | `string \| undefined` | Optional logger scope |
208
+
209
+ * * *
210
+
211
+ ## Value Serialization
212
+
213
+ Arguments are serialized before being sent through the broadcaster.
214
+
215
+ ### Supported conversions
216
+
217
+ - `string`, `number`, `boolean`, `null`
218
+ - `Date` → structured date payload
219
+ - `BigInt` → structured bigint payload
220
+ - `Error` → structured error payload
221
+ - `Symbol` → structured symbol payload
222
+ - `function` → structured function payload
223
+ - arrays and plain objects
224
+
225
+ ### Safety behavior
226
+
227
+ The default serializer also handles:
228
+
229
+ - circular references
230
+ - deep objects
231
+ - very large arrays
232
+ - very large objects
233
+
234
+ Unsupported or truncated values are replaced with safe fallback representations.
235
+
236
+ * * *
237
+
238
+ ## Configuration Options
239
+
240
+ | Option | Type | Default | Description |
241
+ |---|---|---|---|
242
+ | `broadcaster` | `WebSocketBroadcaster` | — | Broadcaster used to forward serialized messages |
243
+ | `serializer` | `WebSocketInstructionSerializer` | `DefaultWebSocketInstructionSerializer` | Serializer used to convert instructions and values |
244
+ | `level` | `LogLevel \| undefined` | `undefined` | Optional minimum level for this output |
245
+
246
+ * * *
247
+
248
+ ## Advanced Usage
249
+
250
+ ### Custom serializer
251
+
252
+ import {
253
+ createWebSocketOutput,
254
+ DefaultWebSocketInstructionSerializer,
255
+ } from "logora-websocket";
256
+
257
+ const output = createWebSocketOutput({
258
+ broadcaster,
259
+ serializer: new DefaultWebSocketInstructionSerializer({
260
+ maxDepth: 8,
261
+ maxArrayLength: 200,
262
+ maxObjectKeys: 200,
263
+ }),
264
+ });
265
+
266
+ ### Per-output level filtering
267
+
268
+ import { LogLevel } from "logora";
269
+
270
+ const output = createWebSocketOutput({
271
+ broadcaster,
272
+ level: LogLevel.Warning,
273
+ });
274
+
275
+ In this case, the WebSocket output will only receive warning and error logs according to the Logora output filtering rules.
276
+
277
+ * * *
278
+
279
+ ## Notes
280
+
281
+ - This module is designed for standard WebSocket-based integrations.
282
+ - It does not implement Socket.IO-specific protocol behavior.
283
+ - It does not buffer, replay, or persist messages.
284
+ - It is intended as a live transport module, not a storage backend.
285
+
286
+ * * *
287
+
288
+ ## License
289
+
290
+ MIT © Sébastien Bosmans
@@ -0,0 +1,110 @@
1
+ import { ILogoraOutputOptions, LogLevel, ILogoraOutput } from 'logora';
2
+ import { ILogoraWriter } from 'logora/module';
3
+
4
+ interface SerializedError {
5
+ __type: "Error";
6
+ name: string;
7
+ message: string;
8
+ stack?: string;
9
+ cause?: SerializedValue;
10
+ }
11
+
12
+ interface SerializedDateValue {
13
+ __type: "Date";
14
+ value: string;
15
+ }
16
+ interface SerializedBigIntValue {
17
+ __type: "BigInt";
18
+ value: string;
19
+ }
20
+ interface SerializedFunctionValue {
21
+ __type: "Function";
22
+ name: string;
23
+ }
24
+ interface SerializedSymbolValue {
25
+ __type: "Symbol";
26
+ value: string;
27
+ }
28
+
29
+ type SerializedPrimitive = string | number | boolean | null;
30
+ interface SerializedObject {
31
+ [key: string]: SerializedValue;
32
+ }
33
+ type SerializedValue = SerializedPrimitive | SerializedValue[] | SerializedObject | SerializedError | SerializedDateValue | SerializedBigIntValue | SerializedFunctionValue | SerializedSymbolValue;
34
+
35
+ interface SerializedLogEntry {
36
+ timestamp: string;
37
+ type: string;
38
+ message: string;
39
+ args: SerializedValue[];
40
+ scope?: string;
41
+ }
42
+
43
+ interface WebSocketLogInstruction {
44
+ kind: "log";
45
+ entry: SerializedLogEntry;
46
+ }
47
+ interface WebSocketPrintInstruction {
48
+ kind: "print";
49
+ message: string;
50
+ args: SerializedValue[];
51
+ }
52
+ interface WebSocketTitleInstruction {
53
+ kind: "title";
54
+ title: string;
55
+ }
56
+ interface WebSocketEmptyInstruction {
57
+ kind: "empty";
58
+ count: number;
59
+ }
60
+ interface WebSocketClearInstruction {
61
+ kind: "clear";
62
+ }
63
+ type WebSocketInstruction = WebSocketLogInstruction | WebSocketPrintInstruction | WebSocketTitleInstruction | WebSocketEmptyInstruction | WebSocketClearInstruction;
64
+
65
+ interface WebSocketInstructionSerializer {
66
+ serialize(instruction: WebSocketInstruction): string;
67
+ serializeValue(value: unknown): SerializedValue;
68
+ }
69
+
70
+ interface WebSocketBroadcaster {
71
+ broadcast(message: string): void;
72
+ }
73
+
74
+ declare class WebSocketOutputOptions implements ILogoraOutputOptions {
75
+ level?: LogLevel;
76
+ broadcaster: WebSocketBroadcaster;
77
+ serializer: WebSocketInstructionSerializer;
78
+ constructor(overrides?: Partial<WebSocketOutputOptions>);
79
+ }
80
+
81
+ declare class WebSocketOutput implements ILogoraOutput {
82
+ name: string;
83
+ options: WebSocketOutputOptions;
84
+ writer: ILogoraWriter;
85
+ constructor(config?: Partial<WebSocketOutputOptions>);
86
+ }
87
+
88
+ type WsLikeClient = {
89
+ readyState: number;
90
+ send(data: string): void;
91
+ };
92
+ type WsLikeServer = {
93
+ clients: Iterable<WsLikeClient>;
94
+ };
95
+ declare function createWsBroadcaster(server: WsLikeServer): WebSocketBroadcaster;
96
+
97
+ /**
98
+ * Creates a new WebSocket output transport for Logora.
99
+ *
100
+ * This function initializes a WebSocketOutput instance, which can be
101
+ * attached to a Logora logger to forward writer instructions as
102
+ * structured messages through a broadcaster abstraction.
103
+ *
104
+ * @param config Optional partial configuration to customize broadcaster
105
+ * and serialization behavior.
106
+ * @returns A fully initialized WebSocketOutput instance ready to use with Logora.
107
+ */
108
+ declare function createWebSocketOutput(config?: Partial<WebSocketOutputOptions>): WebSocketOutput;
109
+
110
+ export { type SerializedBigIntValue, type SerializedDateValue, type SerializedError, type SerializedFunctionValue, type SerializedLogEntry, type SerializedObject, type SerializedPrimitive, type SerializedSymbolValue, type SerializedValue, type WebSocketBroadcaster, type WebSocketClearInstruction, type WebSocketEmptyInstruction, type WebSocketInstruction, type WebSocketLogInstruction, WebSocketOutputOptions, type WebSocketPrintInstruction, type WebSocketTitleInstruction, createWebSocketOutput, createWsBroadcaster };
@@ -0,0 +1,110 @@
1
+ import { ILogoraOutputOptions, LogLevel, ILogoraOutput } from 'logora';
2
+ import { ILogoraWriter } from 'logora/module';
3
+
4
+ interface SerializedError {
5
+ __type: "Error";
6
+ name: string;
7
+ message: string;
8
+ stack?: string;
9
+ cause?: SerializedValue;
10
+ }
11
+
12
+ interface SerializedDateValue {
13
+ __type: "Date";
14
+ value: string;
15
+ }
16
+ interface SerializedBigIntValue {
17
+ __type: "BigInt";
18
+ value: string;
19
+ }
20
+ interface SerializedFunctionValue {
21
+ __type: "Function";
22
+ name: string;
23
+ }
24
+ interface SerializedSymbolValue {
25
+ __type: "Symbol";
26
+ value: string;
27
+ }
28
+
29
+ type SerializedPrimitive = string | number | boolean | null;
30
+ interface SerializedObject {
31
+ [key: string]: SerializedValue;
32
+ }
33
+ type SerializedValue = SerializedPrimitive | SerializedValue[] | SerializedObject | SerializedError | SerializedDateValue | SerializedBigIntValue | SerializedFunctionValue | SerializedSymbolValue;
34
+
35
+ interface SerializedLogEntry {
36
+ timestamp: string;
37
+ type: string;
38
+ message: string;
39
+ args: SerializedValue[];
40
+ scope?: string;
41
+ }
42
+
43
+ interface WebSocketLogInstruction {
44
+ kind: "log";
45
+ entry: SerializedLogEntry;
46
+ }
47
+ interface WebSocketPrintInstruction {
48
+ kind: "print";
49
+ message: string;
50
+ args: SerializedValue[];
51
+ }
52
+ interface WebSocketTitleInstruction {
53
+ kind: "title";
54
+ title: string;
55
+ }
56
+ interface WebSocketEmptyInstruction {
57
+ kind: "empty";
58
+ count: number;
59
+ }
60
+ interface WebSocketClearInstruction {
61
+ kind: "clear";
62
+ }
63
+ type WebSocketInstruction = WebSocketLogInstruction | WebSocketPrintInstruction | WebSocketTitleInstruction | WebSocketEmptyInstruction | WebSocketClearInstruction;
64
+
65
+ interface WebSocketInstructionSerializer {
66
+ serialize(instruction: WebSocketInstruction): string;
67
+ serializeValue(value: unknown): SerializedValue;
68
+ }
69
+
70
+ interface WebSocketBroadcaster {
71
+ broadcast(message: string): void;
72
+ }
73
+
74
+ declare class WebSocketOutputOptions implements ILogoraOutputOptions {
75
+ level?: LogLevel;
76
+ broadcaster: WebSocketBroadcaster;
77
+ serializer: WebSocketInstructionSerializer;
78
+ constructor(overrides?: Partial<WebSocketOutputOptions>);
79
+ }
80
+
81
+ declare class WebSocketOutput implements ILogoraOutput {
82
+ name: string;
83
+ options: WebSocketOutputOptions;
84
+ writer: ILogoraWriter;
85
+ constructor(config?: Partial<WebSocketOutputOptions>);
86
+ }
87
+
88
+ type WsLikeClient = {
89
+ readyState: number;
90
+ send(data: string): void;
91
+ };
92
+ type WsLikeServer = {
93
+ clients: Iterable<WsLikeClient>;
94
+ };
95
+ declare function createWsBroadcaster(server: WsLikeServer): WebSocketBroadcaster;
96
+
97
+ /**
98
+ * Creates a new WebSocket output transport for Logora.
99
+ *
100
+ * This function initializes a WebSocketOutput instance, which can be
101
+ * attached to a Logora logger to forward writer instructions as
102
+ * structured messages through a broadcaster abstraction.
103
+ *
104
+ * @param config Optional partial configuration to customize broadcaster
105
+ * and serialization behavior.
106
+ * @returns A fully initialized WebSocketOutput instance ready to use with Logora.
107
+ */
108
+ declare function createWebSocketOutput(config?: Partial<WebSocketOutputOptions>): WebSocketOutput;
109
+
110
+ export { type SerializedBigIntValue, type SerializedDateValue, type SerializedError, type SerializedFunctionValue, type SerializedLogEntry, type SerializedObject, type SerializedPrimitive, type SerializedSymbolValue, type SerializedValue, type WebSocketBroadcaster, type WebSocketClearInstruction, type WebSocketEmptyInstruction, type WebSocketInstruction, type WebSocketLogInstruction, WebSocketOutputOptions, type WebSocketPrintInstruction, type WebSocketTitleInstruction, createWebSocketOutput, createWsBroadcaster };
package/dist/index.js ADDED
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ WebSocketOutputOptions: () => WebSocketOutputOptions,
24
+ createWebSocketOutput: () => createWebSocketOutput,
25
+ createWsBroadcaster: () => createWsBroadcaster
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/core/instruction-serializer.ts
30
+ var DefaultWebSocketInstructionSerializer = class {
31
+ constructor(config) {
32
+ var _a, _b, _c;
33
+ this._maxDepth = (_a = config == null ? void 0 : config.maxDepth) != null ? _a : 6;
34
+ this._maxArrayLength = (_b = config == null ? void 0 : config.maxArrayLength) != null ? _b : 100;
35
+ this._maxObjectKeys = (_c = config == null ? void 0 : config.maxObjectKeys) != null ? _c : 100;
36
+ }
37
+ serialize(instruction) {
38
+ return JSON.stringify(instruction);
39
+ }
40
+ serializeValue(value) {
41
+ return this._serializeInternal(value, {
42
+ visitedObjects: /* @__PURE__ */ new WeakSet(),
43
+ depth: 0
44
+ });
45
+ }
46
+ _serializeInternal(value, context) {
47
+ if (value === null || value === void 0) {
48
+ return null;
49
+ }
50
+ if (typeof value === "string") {
51
+ return value;
52
+ }
53
+ if (typeof value === "number") {
54
+ return Number.isFinite(value) ? value : String(value);
55
+ }
56
+ if (typeof value === "boolean") {
57
+ return value;
58
+ }
59
+ if (typeof value === "symbol") {
60
+ return {
61
+ __type: "Symbol",
62
+ value: value.toString()
63
+ };
64
+ }
65
+ if (typeof value === "function") {
66
+ return {
67
+ __type: "Function",
68
+ name: value.name || "anonymous"
69
+ };
70
+ }
71
+ if (value instanceof Date) {
72
+ return {
73
+ __type: "Date",
74
+ value: value.toISOString()
75
+ };
76
+ }
77
+ if (value instanceof Error) {
78
+ return this._serializeError(value, context);
79
+ }
80
+ if (context.depth >= this._maxDepth) {
81
+ return "[MaxDepthExceeded]";
82
+ }
83
+ if (Array.isArray(value)) {
84
+ return this._serializeArray(value, context);
85
+ }
86
+ if (typeof value === "object") {
87
+ return this._serializeObject(value, context);
88
+ }
89
+ return "[UnsupportedValue]";
90
+ }
91
+ _serializeError(value, context) {
92
+ const serializedError = {
93
+ __type: "Error",
94
+ name: value.name,
95
+ message: value.message
96
+ };
97
+ if (value.stack) {
98
+ serializedError.stack = value.stack;
99
+ }
100
+ const errorWithCause = value;
101
+ if (errorWithCause.cause !== void 0) {
102
+ serializedError.cause = this._serializeInternal(errorWithCause.cause, {
103
+ ...context,
104
+ depth: context.depth + 1
105
+ });
106
+ }
107
+ return serializedError;
108
+ }
109
+ _serializeArray(value, context) {
110
+ return value.slice(0, this._maxArrayLength).map(
111
+ (item) => this._serializeInternal(item, {
112
+ ...context,
113
+ depth: context.depth + 1
114
+ })
115
+ );
116
+ }
117
+ _serializeObject(value, context) {
118
+ if (context.visitedObjects.has(value)) {
119
+ return "[Circular]";
120
+ }
121
+ context.visitedObjects.add(value);
122
+ const serializedObject = {};
123
+ const entries = Object.entries(value).slice(
124
+ 0,
125
+ this._maxObjectKeys
126
+ );
127
+ for (const [key, entryValue] of entries) {
128
+ serializedObject[key] = this._serializeInternal(entryValue, {
129
+ ...context,
130
+ depth: context.depth + 1
131
+ });
132
+ }
133
+ return serializedObject;
134
+ }
135
+ };
136
+
137
+ // src/config/websocket-output-options.ts
138
+ var WebSocketOutputOptions = class {
139
+ constructor(overrides) {
140
+ this.serializer = new DefaultWebSocketInstructionSerializer();
141
+ Object.assign(this, overrides);
142
+ if (!this.broadcaster) {
143
+ throw new Error(
144
+ "WebSocketOutputOptions requires a broadcaster instance."
145
+ );
146
+ }
147
+ }
148
+ };
149
+
150
+ // src/core/instruction-factory.ts
151
+ var import_logora = require("logora");
152
+ var WebSocketInstructionFactory = class {
153
+ constructor(_serializer) {
154
+ this._serializer = _serializer;
155
+ }
156
+ createLog(entry) {
157
+ return {
158
+ kind: "log",
159
+ entry: this._serializeLogEntry(entry)
160
+ };
161
+ }
162
+ createPrint(message, args) {
163
+ return {
164
+ kind: "print",
165
+ message,
166
+ args: args.map(
167
+ (argument) => this._serializer.serializeValue(argument)
168
+ )
169
+ };
170
+ }
171
+ createTitle(title) {
172
+ return {
173
+ kind: "title",
174
+ title
175
+ };
176
+ }
177
+ createEmpty(count) {
178
+ return {
179
+ kind: "empty",
180
+ count
181
+ };
182
+ }
183
+ createClear() {
184
+ return {
185
+ kind: "clear"
186
+ };
187
+ }
188
+ _serializeLogEntry(entry) {
189
+ return {
190
+ timestamp: entry.timestamp.toISOString(),
191
+ type: this._serializeLogType(entry.type),
192
+ message: entry.message,
193
+ args: entry.args.map(
194
+ (argument) => this._serializer.serializeValue(argument)
195
+ ),
196
+ scope: entry.scope
197
+ };
198
+ }
199
+ _serializeLogType(type) {
200
+ switch (type) {
201
+ case import_logora.LogType.Debug:
202
+ return "debug";
203
+ case import_logora.LogType.Info:
204
+ return "info";
205
+ case import_logora.LogType.Success:
206
+ return "success";
207
+ case import_logora.LogType.Warning:
208
+ return "warning";
209
+ case import_logora.LogType.Error:
210
+ return "error";
211
+ case import_logora.LogType.Highlight:
212
+ return "highlight";
213
+ case import_logora.LogType.Raw:
214
+ return "raw";
215
+ default:
216
+ return String(type);
217
+ }
218
+ }
219
+ };
220
+
221
+ // src/core/writer.ts
222
+ var WebSocketWriter = class {
223
+ constructor(_options) {
224
+ this._options = _options;
225
+ this._instructionFactory = new WebSocketInstructionFactory(
226
+ this._options.serializer
227
+ );
228
+ }
229
+ log(entry) {
230
+ this._dispatch(this._instructionFactory.createLog(entry));
231
+ }
232
+ title(title) {
233
+ this._dispatch(this._instructionFactory.createTitle(title));
234
+ }
235
+ empty(count = 1) {
236
+ this._dispatch(this._instructionFactory.createEmpty(Math.max(0, count)));
237
+ }
238
+ clear() {
239
+ this._dispatch(this._instructionFactory.createClear());
240
+ }
241
+ print(message, ...args) {
242
+ this._dispatch(this._instructionFactory.createPrint(message, args));
243
+ }
244
+ _dispatch(instruction) {
245
+ const payload = this._options.serializer.serialize(instruction);
246
+ this._options.broadcaster.broadcast(payload);
247
+ }
248
+ };
249
+
250
+ // src/core/output.ts
251
+ var WebSocketOutput = class {
252
+ constructor(config) {
253
+ this.name = "websocket";
254
+ this.options = new WebSocketOutputOptions(config);
255
+ this.writer = new WebSocketWriter(this.options);
256
+ }
257
+ };
258
+
259
+ // src/core/ws-broadcaster.ts
260
+ var OPEN_READY_STATE = 1;
261
+ function createWsBroadcaster(server) {
262
+ return {
263
+ broadcast(message) {
264
+ for (const client of server.clients) {
265
+ if (client.readyState !== OPEN_READY_STATE) {
266
+ continue;
267
+ }
268
+ try {
269
+ client.send(message);
270
+ } catch {
271
+ }
272
+ }
273
+ }
274
+ };
275
+ }
276
+
277
+ // src/index.ts
278
+ function createWebSocketOutput(config) {
279
+ return new WebSocketOutput(config);
280
+ }
281
+ // Annotate the CommonJS export names for ESM import in node:
282
+ 0 && (module.exports = {
283
+ WebSocketOutputOptions,
284
+ createWebSocketOutput,
285
+ createWsBroadcaster
286
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,257 @@
1
+ // src/core/instruction-serializer.ts
2
+ var DefaultWebSocketInstructionSerializer = class {
3
+ constructor(config) {
4
+ var _a, _b, _c;
5
+ this._maxDepth = (_a = config == null ? void 0 : config.maxDepth) != null ? _a : 6;
6
+ this._maxArrayLength = (_b = config == null ? void 0 : config.maxArrayLength) != null ? _b : 100;
7
+ this._maxObjectKeys = (_c = config == null ? void 0 : config.maxObjectKeys) != null ? _c : 100;
8
+ }
9
+ serialize(instruction) {
10
+ return JSON.stringify(instruction);
11
+ }
12
+ serializeValue(value) {
13
+ return this._serializeInternal(value, {
14
+ visitedObjects: /* @__PURE__ */ new WeakSet(),
15
+ depth: 0
16
+ });
17
+ }
18
+ _serializeInternal(value, context) {
19
+ if (value === null || value === void 0) {
20
+ return null;
21
+ }
22
+ if (typeof value === "string") {
23
+ return value;
24
+ }
25
+ if (typeof value === "number") {
26
+ return Number.isFinite(value) ? value : String(value);
27
+ }
28
+ if (typeof value === "boolean") {
29
+ return value;
30
+ }
31
+ if (typeof value === "symbol") {
32
+ return {
33
+ __type: "Symbol",
34
+ value: value.toString()
35
+ };
36
+ }
37
+ if (typeof value === "function") {
38
+ return {
39
+ __type: "Function",
40
+ name: value.name || "anonymous"
41
+ };
42
+ }
43
+ if (value instanceof Date) {
44
+ return {
45
+ __type: "Date",
46
+ value: value.toISOString()
47
+ };
48
+ }
49
+ if (value instanceof Error) {
50
+ return this._serializeError(value, context);
51
+ }
52
+ if (context.depth >= this._maxDepth) {
53
+ return "[MaxDepthExceeded]";
54
+ }
55
+ if (Array.isArray(value)) {
56
+ return this._serializeArray(value, context);
57
+ }
58
+ if (typeof value === "object") {
59
+ return this._serializeObject(value, context);
60
+ }
61
+ return "[UnsupportedValue]";
62
+ }
63
+ _serializeError(value, context) {
64
+ const serializedError = {
65
+ __type: "Error",
66
+ name: value.name,
67
+ message: value.message
68
+ };
69
+ if (value.stack) {
70
+ serializedError.stack = value.stack;
71
+ }
72
+ const errorWithCause = value;
73
+ if (errorWithCause.cause !== void 0) {
74
+ serializedError.cause = this._serializeInternal(errorWithCause.cause, {
75
+ ...context,
76
+ depth: context.depth + 1
77
+ });
78
+ }
79
+ return serializedError;
80
+ }
81
+ _serializeArray(value, context) {
82
+ return value.slice(0, this._maxArrayLength).map(
83
+ (item) => this._serializeInternal(item, {
84
+ ...context,
85
+ depth: context.depth + 1
86
+ })
87
+ );
88
+ }
89
+ _serializeObject(value, context) {
90
+ if (context.visitedObjects.has(value)) {
91
+ return "[Circular]";
92
+ }
93
+ context.visitedObjects.add(value);
94
+ const serializedObject = {};
95
+ const entries = Object.entries(value).slice(
96
+ 0,
97
+ this._maxObjectKeys
98
+ );
99
+ for (const [key, entryValue] of entries) {
100
+ serializedObject[key] = this._serializeInternal(entryValue, {
101
+ ...context,
102
+ depth: context.depth + 1
103
+ });
104
+ }
105
+ return serializedObject;
106
+ }
107
+ };
108
+
109
+ // src/config/websocket-output-options.ts
110
+ var WebSocketOutputOptions = class {
111
+ constructor(overrides) {
112
+ this.serializer = new DefaultWebSocketInstructionSerializer();
113
+ Object.assign(this, overrides);
114
+ if (!this.broadcaster) {
115
+ throw new Error(
116
+ "WebSocketOutputOptions requires a broadcaster instance."
117
+ );
118
+ }
119
+ }
120
+ };
121
+
122
+ // src/core/instruction-factory.ts
123
+ import { LogType } from "logora";
124
+ var WebSocketInstructionFactory = class {
125
+ constructor(_serializer) {
126
+ this._serializer = _serializer;
127
+ }
128
+ createLog(entry) {
129
+ return {
130
+ kind: "log",
131
+ entry: this._serializeLogEntry(entry)
132
+ };
133
+ }
134
+ createPrint(message, args) {
135
+ return {
136
+ kind: "print",
137
+ message,
138
+ args: args.map(
139
+ (argument) => this._serializer.serializeValue(argument)
140
+ )
141
+ };
142
+ }
143
+ createTitle(title) {
144
+ return {
145
+ kind: "title",
146
+ title
147
+ };
148
+ }
149
+ createEmpty(count) {
150
+ return {
151
+ kind: "empty",
152
+ count
153
+ };
154
+ }
155
+ createClear() {
156
+ return {
157
+ kind: "clear"
158
+ };
159
+ }
160
+ _serializeLogEntry(entry) {
161
+ return {
162
+ timestamp: entry.timestamp.toISOString(),
163
+ type: this._serializeLogType(entry.type),
164
+ message: entry.message,
165
+ args: entry.args.map(
166
+ (argument) => this._serializer.serializeValue(argument)
167
+ ),
168
+ scope: entry.scope
169
+ };
170
+ }
171
+ _serializeLogType(type) {
172
+ switch (type) {
173
+ case LogType.Debug:
174
+ return "debug";
175
+ case LogType.Info:
176
+ return "info";
177
+ case LogType.Success:
178
+ return "success";
179
+ case LogType.Warning:
180
+ return "warning";
181
+ case LogType.Error:
182
+ return "error";
183
+ case LogType.Highlight:
184
+ return "highlight";
185
+ case LogType.Raw:
186
+ return "raw";
187
+ default:
188
+ return String(type);
189
+ }
190
+ }
191
+ };
192
+
193
+ // src/core/writer.ts
194
+ var WebSocketWriter = class {
195
+ constructor(_options) {
196
+ this._options = _options;
197
+ this._instructionFactory = new WebSocketInstructionFactory(
198
+ this._options.serializer
199
+ );
200
+ }
201
+ log(entry) {
202
+ this._dispatch(this._instructionFactory.createLog(entry));
203
+ }
204
+ title(title) {
205
+ this._dispatch(this._instructionFactory.createTitle(title));
206
+ }
207
+ empty(count = 1) {
208
+ this._dispatch(this._instructionFactory.createEmpty(Math.max(0, count)));
209
+ }
210
+ clear() {
211
+ this._dispatch(this._instructionFactory.createClear());
212
+ }
213
+ print(message, ...args) {
214
+ this._dispatch(this._instructionFactory.createPrint(message, args));
215
+ }
216
+ _dispatch(instruction) {
217
+ const payload = this._options.serializer.serialize(instruction);
218
+ this._options.broadcaster.broadcast(payload);
219
+ }
220
+ };
221
+
222
+ // src/core/output.ts
223
+ var WebSocketOutput = class {
224
+ constructor(config) {
225
+ this.name = "websocket";
226
+ this.options = new WebSocketOutputOptions(config);
227
+ this.writer = new WebSocketWriter(this.options);
228
+ }
229
+ };
230
+
231
+ // src/core/ws-broadcaster.ts
232
+ var OPEN_READY_STATE = 1;
233
+ function createWsBroadcaster(server) {
234
+ return {
235
+ broadcast(message) {
236
+ for (const client of server.clients) {
237
+ if (client.readyState !== OPEN_READY_STATE) {
238
+ continue;
239
+ }
240
+ try {
241
+ client.send(message);
242
+ } catch {
243
+ }
244
+ }
245
+ }
246
+ };
247
+ }
248
+
249
+ // src/index.ts
250
+ function createWebSocketOutput(config) {
251
+ return new WebSocketOutput(config);
252
+ }
253
+ export {
254
+ WebSocketOutputOptions,
255
+ createWebSocketOutput,
256
+ createWsBroadcaster
257
+ };
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "logora-websocket",
3
+ "version": "1.0.3",
4
+ "description": "websocket plugin for Logora – enables real-time communication.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "exports": {
11
+ ".": {
12
+ "require": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "lint": "eslint src tests --ext .ts",
18
+ "lint:fix": "eslint src tests --ext .ts --fix",
19
+ "test": "vitest run",
20
+ "test:dev": "vitest",
21
+ "test:coverage": "vitest run --coverage",
22
+ "build": "tsup",
23
+ "build:dev": "tsup --watch",
24
+ "build:prod": "npm run clean && npm run lint && npm run build",
25
+ "clean": "rimraf dist",
26
+ "check": "npm run lint && npm run test && npm run build",
27
+ "prepare": "npm run build",
28
+ "prepublishOnly": "npm run build:prod"
29
+ },
30
+ "keywords": [
31
+ "logger",
32
+ "logora",
33
+ "typescript",
34
+ "logging",
35
+ "websocket"
36
+ ],
37
+ "author": "Sébastien Bosmans <https://github.com/boseba>",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/boseba/logora-websocket.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/boseba/logora-websocket/issues"
45
+ },
46
+ "homepage": "https://github.com/boseba/logora-websocket#readme",
47
+ "engines": {
48
+ "node": ">=16"
49
+ },
50
+ "devDependencies": {
51
+ "@eslint/js": "^9.25.1",
52
+ "@vitest/coverage-v8": "^3.1.2",
53
+ "eslint": "^9.25.1",
54
+ "rimraf": "^6.0.1",
55
+ "tsup": "^8.4.0",
56
+ "typescript": "^5.8.3",
57
+ "typescript-eslint": "^8.31.0",
58
+ "vitest": "^3.1.2"
59
+ },
60
+ "dependencies": {
61
+ "dayjs": "^1.11.13",
62
+ "logora": "^2.0.5"
63
+ }
64
+ }