@devms/livetail 0.0.2

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.
@@ -0,0 +1,334 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var LiveTailGateway_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.LiveTailGateway = void 0;
17
+ const common_1 = require("@nestjs/common");
18
+ const websockets_1 = require("@nestjs/websockets");
19
+ const socket_io_1 = require("socket.io");
20
+ const livetail_service_1 = require("./livetail.service");
21
+ const console_capture_service_1 = require("./console-capture.service");
22
+ const livetail_interface_1 = require("./interfaces/livetail.interface");
23
+ const CONSOLE_ROOM = 'console';
24
+ /** History is replayed in chunks to keep WS frames small. */
25
+ const CONSOLE_HISTORY_CHUNK = 500;
26
+ /** Pending batch hard cap — oldest lines are dropped beyond this. */
27
+ const CONSOLE_MAX_PENDING = 5000;
28
+ let LiveTailGateway = LiveTailGateway_1 = class LiveTailGateway {
29
+ constructor(liveTailService, config, consoleCapture) {
30
+ this.liveTailService = liveTailService;
31
+ this.config = config;
32
+ this.consoleCapture = consoleCapture;
33
+ this.logger = new common_1.Logger(LiveTailGateway_1.name);
34
+ this.clients = new Map();
35
+ // Console streaming state
36
+ this.consoleSubscribers = new Set();
37
+ this.consolePending = [];
38
+ this.consoleDropped = 0;
39
+ this.consoleFlushTimer = null;
40
+ }
41
+ onModuleInit() {
42
+ if (this.config.disabled) {
43
+ this.logger.log('LiveTail gateway is disabled');
44
+ return;
45
+ }
46
+ this.subscription = this.liveTailService.logs$.subscribe((log) => {
47
+ this.broadcastToClients(log);
48
+ });
49
+ if (this.consoleCapture?.enabled) {
50
+ this.consoleSubscription = this.consoleCapture.lines$.subscribe((line) => this.queueConsoleLine(line));
51
+ }
52
+ this.logger.log('LiveTail gateway initialized');
53
+ }
54
+ onModuleDestroy() {
55
+ this.subscription?.unsubscribe();
56
+ this.consoleSubscription?.unsubscribe();
57
+ this.stopConsoleFlusher();
58
+ }
59
+ // ─── Connection Lifecycle ─────────────────────────────
60
+ handleConnection(client) {
61
+ // Enforce max clients limit
62
+ if (this.config.maxClients && this.config.maxClients > 0 && this.clients.size >= this.config.maxClients) {
63
+ this.logger.warn(`Connection rejected: max clients (${this.config.maxClients}) reached`);
64
+ client.emit('error', { message: 'Max clients limit reached. Try again later.' });
65
+ client.disconnect(true);
66
+ return;
67
+ }
68
+ this.clients.set(client.id, { filter: {}, paused: false, connectedAt: Date.now() });
69
+ this.logger.debug(`Client connected: ${client.id} (total: ${this.clients.size})`);
70
+ client.emit('connected', {
71
+ clientId: client.id,
72
+ message: 'Live tail connected',
73
+ connectedClients: this.clients.size,
74
+ });
75
+ }
76
+ handleDisconnect(client) {
77
+ this.clients.delete(client.id);
78
+ if (this.consoleSubscribers.delete(client.id)) {
79
+ this.stopConsoleFlusherIfIdle();
80
+ }
81
+ this.logger.debug(`Client disconnected: ${client.id} (total: ${this.clients.size})`);
82
+ }
83
+ // ─── Client Messages ─────────────────────────────────
84
+ /**
85
+ * Client subscribes with filters.
86
+ * Example payload:
87
+ * { environmentId: "xxx", level: ["error", "fatal"], category: "auth" }
88
+ */
89
+ handleSubscribe(client, filter) {
90
+ const state = this.clients.get(client.id);
91
+ if (state) {
92
+ state.filter = filter || {};
93
+ state.paused = false;
94
+ }
95
+ this.logger.debug(`Client ${client.id} subscribed: ${JSON.stringify(filter)}`);
96
+ client.emit('subscribed', { filter });
97
+ }
98
+ /**
99
+ * Client updates filters without disconnecting.
100
+ */
101
+ handleUpdateFilter(client, filter) {
102
+ const state = this.clients.get(client.id);
103
+ if (state) {
104
+ state.filter = filter || {};
105
+ }
106
+ client.emit('filterUpdated', { filter });
107
+ }
108
+ /**
109
+ * Client pauses the stream (still connected but no logs sent).
110
+ */
111
+ handlePause(client) {
112
+ const state = this.clients.get(client.id);
113
+ if (state) {
114
+ state.paused = true;
115
+ }
116
+ client.emit('paused');
117
+ }
118
+ /**
119
+ * Client resumes the stream.
120
+ */
121
+ handleResume(client) {
122
+ const state = this.clients.get(client.id);
123
+ if (state) {
124
+ state.paused = false;
125
+ }
126
+ client.emit('resumed');
127
+ }
128
+ // ─── Console Streaming ────────────────────────────────
129
+ /**
130
+ * Client subscribes to the raw stdout/stderr stream.
131
+ * Replays the ring buffer (chunked), then streams live lines
132
+ * batched on `console-lines` events.
133
+ *
134
+ * Payload: { token?: string, tail?: number }
135
+ */
136
+ handleSubscribeConsole(client, payload) {
137
+ if (!this.consoleCapture?.enabled) {
138
+ client.emit('console-error', {
139
+ code: 'CONSOLE_DISABLED',
140
+ message: 'Console capture is not enabled on this application. Enable it with LiveTailModule.register({ captureConsole: true }).',
141
+ });
142
+ return;
143
+ }
144
+ const requiredToken = this.consoleCapture.options.token;
145
+ if (requiredToken && payload?.token !== requiredToken) {
146
+ this.logger.warn(`Console subscribe rejected for ${client.id}: invalid token`);
147
+ client.emit('console-error', {
148
+ code: 'CONSOLE_UNAUTHORIZED',
149
+ message: 'Invalid or missing console token.',
150
+ });
151
+ return;
152
+ }
153
+ this.consoleSubscribers.add(client.id);
154
+ client.join(CONSOLE_ROOM);
155
+ const history = this.consoleCapture.getHistory(payload?.tail);
156
+ client.emit('console-subscribed', {
157
+ pid: process.pid,
158
+ bufferSize: this.consoleCapture.options.bufferSize,
159
+ totalCaptured: this.consoleCapture.totalCaptured,
160
+ historyCount: history.length,
161
+ });
162
+ // Replay history in chunks so a full buffer never becomes one giant frame
163
+ for (let i = 0; i < history.length; i += CONSOLE_HISTORY_CHUNK) {
164
+ const chunk = history.slice(i, i + CONSOLE_HISTORY_CHUNK);
165
+ client.emit('console-history', {
166
+ lines: chunk,
167
+ done: i + CONSOLE_HISTORY_CHUNK >= history.length,
168
+ });
169
+ }
170
+ if (history.length === 0) {
171
+ client.emit('console-history', { lines: [], done: true });
172
+ }
173
+ this.startConsoleFlusher();
174
+ this.logger.debug(`Client ${client.id} subscribed to console (subscribers: ${this.consoleSubscribers.size})`);
175
+ }
176
+ handleUnsubscribeConsole(client) {
177
+ client.leave(CONSOLE_ROOM);
178
+ if (this.consoleSubscribers.delete(client.id)) {
179
+ this.stopConsoleFlusherIfIdle();
180
+ }
181
+ client.emit('console-unsubscribed');
182
+ }
183
+ queueConsoleLine(line) {
184
+ // No one watching → buffer-only mode, zero broadcast cost
185
+ if (this.consoleSubscribers.size === 0)
186
+ return;
187
+ this.consolePending.push(line);
188
+ if (this.consolePending.length > CONSOLE_MAX_PENDING) {
189
+ const overflow = this.consolePending.length - CONSOLE_MAX_PENDING;
190
+ this.consolePending.splice(0, overflow);
191
+ this.consoleDropped += overflow;
192
+ }
193
+ }
194
+ flushConsolePending() {
195
+ if (this.consolePending.length === 0)
196
+ return;
197
+ const lines = this.consolePending;
198
+ this.consolePending = [];
199
+ const dropped = this.consoleDropped;
200
+ this.consoleDropped = 0;
201
+ this.server.to(CONSOLE_ROOM).emit('console-lines', { lines, dropped });
202
+ }
203
+ startConsoleFlusher() {
204
+ if (this.consoleFlushTimer)
205
+ return;
206
+ const interval = this.consoleCapture?.options.batchInterval ?? 150;
207
+ this.consoleFlushTimer = setInterval(() => this.flushConsolePending(), interval);
208
+ // Don't let the flusher keep the process alive on shutdown
209
+ if (typeof this.consoleFlushTimer.unref === 'function') {
210
+ this.consoleFlushTimer.unref();
211
+ }
212
+ }
213
+ stopConsoleFlusher() {
214
+ if (this.consoleFlushTimer) {
215
+ clearInterval(this.consoleFlushTimer);
216
+ this.consoleFlushTimer = null;
217
+ }
218
+ this.consolePending = [];
219
+ this.consoleDropped = 0;
220
+ }
221
+ stopConsoleFlusherIfIdle() {
222
+ if (this.consoleSubscribers.size === 0) {
223
+ this.stopConsoleFlusher();
224
+ }
225
+ }
226
+ // ─── Broadcasting ─────────────────────────────────────
227
+ broadcastToClients(log) {
228
+ for (const [clientId, state] of this.clients) {
229
+ if (state.paused)
230
+ continue;
231
+ if (!this.matchesFilter(log, state.filter))
232
+ continue;
233
+ this.server.to(clientId).emit('log', log);
234
+ }
235
+ }
236
+ matchesFilter(log, filter) {
237
+ // Scope filters
238
+ if (filter.environmentId && log.environmentId !== filter.environmentId)
239
+ return false;
240
+ if (filter.appId && log.appId !== filter.appId)
241
+ return false;
242
+ if (filter.orgId && log.orgId !== filter.orgId)
243
+ return false;
244
+ // Level filter (single or array)
245
+ if (filter.level) {
246
+ const levels = Array.isArray(filter.level) ? filter.level : [filter.level];
247
+ if (!levels.includes(log.level))
248
+ return false;
249
+ }
250
+ // Category filter (exact match)
251
+ if (filter.category && log.category !== filter.category)
252
+ return false;
253
+ // Action filter (contains, case-insensitive)
254
+ if (filter.action) {
255
+ if (!log.action.toLowerCase().includes(filter.action.toLowerCase()))
256
+ return false;
257
+ }
258
+ // User filter
259
+ if (filter.userId && log.userId !== filter.userId)
260
+ return false;
261
+ // Text search (in message, action, category)
262
+ if (filter.search) {
263
+ const term = filter.search.toLowerCase();
264
+ const haystack = [log.message, log.action, log.category]
265
+ .filter(Boolean)
266
+ .join(' ')
267
+ .toLowerCase();
268
+ if (!haystack.includes(term))
269
+ return false;
270
+ }
271
+ // Tags filter (log must have at least one matching tag)
272
+ if (filter.tags && filter.tags.length > 0) {
273
+ if (!log.tags || !filter.tags.some((t) => log.tags.includes(t)))
274
+ return false;
275
+ }
276
+ return true;
277
+ }
278
+ };
279
+ exports.LiveTailGateway = LiveTailGateway;
280
+ __decorate([
281
+ (0, websockets_1.WebSocketServer)(),
282
+ __metadata("design:type", socket_io_1.Server)
283
+ ], LiveTailGateway.prototype, "server", void 0);
284
+ __decorate([
285
+ (0, websockets_1.SubscribeMessage)('subscribe'),
286
+ __param(0, (0, websockets_1.ConnectedSocket)()),
287
+ __param(1, (0, websockets_1.MessageBody)()),
288
+ __metadata("design:type", Function),
289
+ __metadata("design:paramtypes", [socket_io_1.Socket, Object]),
290
+ __metadata("design:returntype", void 0)
291
+ ], LiveTailGateway.prototype, "handleSubscribe", null);
292
+ __decorate([
293
+ (0, websockets_1.SubscribeMessage)('updateFilter'),
294
+ __param(0, (0, websockets_1.ConnectedSocket)()),
295
+ __param(1, (0, websockets_1.MessageBody)()),
296
+ __metadata("design:type", Function),
297
+ __metadata("design:paramtypes", [socket_io_1.Socket, Object]),
298
+ __metadata("design:returntype", void 0)
299
+ ], LiveTailGateway.prototype, "handleUpdateFilter", null);
300
+ __decorate([
301
+ (0, websockets_1.SubscribeMessage)('pause'),
302
+ __param(0, (0, websockets_1.ConnectedSocket)()),
303
+ __metadata("design:type", Function),
304
+ __metadata("design:paramtypes", [socket_io_1.Socket]),
305
+ __metadata("design:returntype", void 0)
306
+ ], LiveTailGateway.prototype, "handlePause", null);
307
+ __decorate([
308
+ (0, websockets_1.SubscribeMessage)('resume'),
309
+ __param(0, (0, websockets_1.ConnectedSocket)()),
310
+ __metadata("design:type", Function),
311
+ __metadata("design:paramtypes", [socket_io_1.Socket]),
312
+ __metadata("design:returntype", void 0)
313
+ ], LiveTailGateway.prototype, "handleResume", null);
314
+ __decorate([
315
+ (0, websockets_1.SubscribeMessage)('subscribe-console'),
316
+ __param(0, (0, websockets_1.ConnectedSocket)()),
317
+ __param(1, (0, websockets_1.MessageBody)()),
318
+ __metadata("design:type", Function),
319
+ __metadata("design:paramtypes", [socket_io_1.Socket, Object]),
320
+ __metadata("design:returntype", void 0)
321
+ ], LiveTailGateway.prototype, "handleSubscribeConsole", null);
322
+ __decorate([
323
+ (0, websockets_1.SubscribeMessage)('unsubscribe-console'),
324
+ __param(0, (0, websockets_1.ConnectedSocket)()),
325
+ __metadata("design:type", Function),
326
+ __metadata("design:paramtypes", [socket_io_1.Socket]),
327
+ __metadata("design:returntype", void 0)
328
+ ], LiveTailGateway.prototype, "handleUnsubscribeConsole", null);
329
+ exports.LiveTailGateway = LiveTailGateway = LiveTailGateway_1 = __decorate([
330
+ (0, websockets_1.WebSocketGateway)({ namespace: '/live-tail', cors: { origin: '*' } }),
331
+ __param(1, (0, common_1.Inject)(livetail_interface_1.LIVETAIL_CONFIG)),
332
+ __param(2, (0, common_1.Optional)()),
333
+ __metadata("design:paramtypes", [livetail_service_1.LiveTailService, Object, console_capture_service_1.ConsoleCaptureService])
334
+ ], LiveTailGateway);
@@ -0,0 +1,52 @@
1
+ import { DynamicModule } from '@nestjs/common';
2
+ import { LiveTailConfig } from './interfaces/livetail.interface';
3
+ export declare class LiveTailModule {
4
+ /**
5
+ * Register the live tail module with static config.
6
+ *
7
+ * @example
8
+ * // Default (all options)
9
+ * LiveTailModule.register()
10
+ *
11
+ * @example
12
+ * // Custom configuration
13
+ * LiveTailModule.register({
14
+ * cors: ['https://dashboard.example.com'],
15
+ * maxClients: 100,
16
+ * })
17
+ *
18
+ * @example
19
+ * // Disable in specific environments
20
+ * LiveTailModule.register({
21
+ * disabled: process.env.NODE_ENV === 'test',
22
+ * })
23
+ *
24
+ * @example
25
+ * // Stream raw stdout/stderr (terminal console) to the dashboard
26
+ * LiveTailModule.register({
27
+ * captureConsole: {
28
+ * bufferSize: 2000,
29
+ * token: process.env.LIVETAIL_CONSOLE_TOKEN,
30
+ * },
31
+ * })
32
+ */
33
+ static register(config?: LiveTailConfig): DynamicModule;
34
+ /**
35
+ * Register the live tail module with async config (e.g. from ConfigService).
36
+ *
37
+ * @example
38
+ * LiveTailModule.registerAsync({
39
+ * inject: [ConfigService],
40
+ * useFactory: (config: ConfigService) => ({
41
+ * cors: config.get('LIVETAIL_CORS_ORIGIN', '*'),
42
+ * maxClients: parseInt(config.get('LIVETAIL_MAX_CLIENTS', '0')),
43
+ * disabled: config.get('LIVETAIL_DISABLED') === 'true',
44
+ * }),
45
+ * })
46
+ */
47
+ static registerAsync(options: {
48
+ inject?: any[];
49
+ useFactory: (...args: any[]) => LiveTailConfig | Promise<LiveTailConfig>;
50
+ imports?: any[];
51
+ }): DynamicModule;
52
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var LiveTailModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.LiveTailModule = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ const livetail_interface_1 = require("./interfaces/livetail.interface");
13
+ const livetail_service_1 = require("./livetail.service");
14
+ const livetail_gateway_1 = require("./livetail.gateway");
15
+ const console_capture_service_1 = require("./console-capture.service");
16
+ const DEFAULT_CONFIG = {
17
+ namespace: '/live-tail',
18
+ cors: '*',
19
+ maxClients: 0,
20
+ pingInterval: 25000,
21
+ pingTimeout: 20000,
22
+ disabled: false,
23
+ };
24
+ let LiveTailModule = LiveTailModule_1 = class LiveTailModule {
25
+ /**
26
+ * Register the live tail module with static config.
27
+ *
28
+ * @example
29
+ * // Default (all options)
30
+ * LiveTailModule.register()
31
+ *
32
+ * @example
33
+ * // Custom configuration
34
+ * LiveTailModule.register({
35
+ * cors: ['https://dashboard.example.com'],
36
+ * maxClients: 100,
37
+ * })
38
+ *
39
+ * @example
40
+ * // Disable in specific environments
41
+ * LiveTailModule.register({
42
+ * disabled: process.env.NODE_ENV === 'test',
43
+ * })
44
+ *
45
+ * @example
46
+ * // Stream raw stdout/stderr (terminal console) to the dashboard
47
+ * LiveTailModule.register({
48
+ * captureConsole: {
49
+ * bufferSize: 2000,
50
+ * token: process.env.LIVETAIL_CONSOLE_TOKEN,
51
+ * },
52
+ * })
53
+ */
54
+ static register(config) {
55
+ const merged = { ...DEFAULT_CONFIG, ...config };
56
+ return {
57
+ module: LiveTailModule_1,
58
+ providers: [
59
+ { provide: livetail_interface_1.LIVETAIL_CONFIG, useValue: merged },
60
+ livetail_service_1.LiveTailService,
61
+ console_capture_service_1.ConsoleCaptureService,
62
+ ...(merged.disabled ? [] : [livetail_gateway_1.LiveTailGateway]),
63
+ ],
64
+ exports: [livetail_service_1.LiveTailService, console_capture_service_1.ConsoleCaptureService],
65
+ };
66
+ }
67
+ /**
68
+ * Register the live tail module with async config (e.g. from ConfigService).
69
+ *
70
+ * @example
71
+ * LiveTailModule.registerAsync({
72
+ * inject: [ConfigService],
73
+ * useFactory: (config: ConfigService) => ({
74
+ * cors: config.get('LIVETAIL_CORS_ORIGIN', '*'),
75
+ * maxClients: parseInt(config.get('LIVETAIL_MAX_CLIENTS', '0')),
76
+ * disabled: config.get('LIVETAIL_DISABLED') === 'true',
77
+ * }),
78
+ * })
79
+ */
80
+ static registerAsync(options) {
81
+ return {
82
+ module: LiveTailModule_1,
83
+ imports: [...(options.imports || [])],
84
+ providers: [
85
+ {
86
+ provide: livetail_interface_1.LIVETAIL_CONFIG,
87
+ inject: options.inject || [],
88
+ useFactory: async (...args) => {
89
+ const config = await options.useFactory(...args);
90
+ return { ...DEFAULT_CONFIG, ...config };
91
+ },
92
+ },
93
+ livetail_service_1.LiveTailService,
94
+ console_capture_service_1.ConsoleCaptureService,
95
+ livetail_gateway_1.LiveTailGateway,
96
+ ],
97
+ exports: [livetail_service_1.LiveTailService, console_capture_service_1.ConsoleCaptureService],
98
+ };
99
+ }
100
+ };
101
+ exports.LiveTailModule = LiveTailModule;
102
+ exports.LiveTailModule = LiveTailModule = LiveTailModule_1 = __decorate([
103
+ (0, common_1.Global)(),
104
+ (0, common_1.Module)({})
105
+ ], LiveTailModule);
@@ -0,0 +1,38 @@
1
+ import { Observable } from 'rxjs';
2
+ import { LiveTailConfig, LiveTailEnvContext, LiveTailLogEvent } from './interfaces/livetail.interface';
3
+ export declare class LiveTailService {
4
+ private readonly logSubject;
5
+ private readonly isDisabled;
6
+ constructor(config?: LiveTailConfig);
7
+ /**
8
+ * Observable stream of all log events.
9
+ * The gateway subscribes to this to broadcast to connected clients.
10
+ */
11
+ get logs$(): Observable<LiveTailLogEvent>;
12
+ /**
13
+ * Broadcast a single log event to all connected live tail clients.
14
+ * Call this after persisting the log to the database.
15
+ *
16
+ * @param log The log event to broadcast.
17
+ * @param ctx Optional environment context to enrich the log with org/app/env info.
18
+ *
19
+ * @example
20
+ * this.liveTailService.broadcast(logData, {
21
+ * environmentId: env.id,
22
+ * envName: env.name,
23
+ * appId: app.id,
24
+ * appName: app.name,
25
+ * orgId: org.id,
26
+ * orgName: org.name,
27
+ * });
28
+ */
29
+ broadcast(log: LiveTailLogEvent, ctx?: LiveTailEnvContext): void;
30
+ /**
31
+ * Broadcast multiple log events at once.
32
+ *
33
+ * @param logs Array of log events.
34
+ * @param ctx Optional environment context to enrich all logs.
35
+ */
36
+ broadcastMany(logs: LiveTailLogEvent[], ctx?: LiveTailEnvContext): void;
37
+ private enrichLog;
38
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.LiveTailService = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const rxjs_1 = require("rxjs");
18
+ const livetail_interface_1 = require("./interfaces/livetail.interface");
19
+ let LiveTailService = class LiveTailService {
20
+ constructor(config) {
21
+ this.logSubject = new rxjs_1.Subject();
22
+ this.isDisabled = config?.disabled ?? false;
23
+ }
24
+ /**
25
+ * Observable stream of all log events.
26
+ * The gateway subscribes to this to broadcast to connected clients.
27
+ */
28
+ get logs$() {
29
+ return this.logSubject.asObservable();
30
+ }
31
+ /**
32
+ * Broadcast a single log event to all connected live tail clients.
33
+ * Call this after persisting the log to the database.
34
+ *
35
+ * @param log The log event to broadcast.
36
+ * @param ctx Optional environment context to enrich the log with org/app/env info.
37
+ *
38
+ * @example
39
+ * this.liveTailService.broadcast(logData, {
40
+ * environmentId: env.id,
41
+ * envName: env.name,
42
+ * appId: app.id,
43
+ * appName: app.name,
44
+ * orgId: org.id,
45
+ * orgName: org.name,
46
+ * });
47
+ */
48
+ broadcast(log, ctx) {
49
+ if (this.isDisabled)
50
+ return;
51
+ this.logSubject.next(ctx ? this.enrichLog(log, ctx) : log);
52
+ }
53
+ /**
54
+ * Broadcast multiple log events at once.
55
+ *
56
+ * @param logs Array of log events.
57
+ * @param ctx Optional environment context to enrich all logs.
58
+ */
59
+ broadcastMany(logs, ctx) {
60
+ if (this.isDisabled)
61
+ return;
62
+ for (const log of logs) {
63
+ this.logSubject.next(ctx ? this.enrichLog(log, ctx) : log);
64
+ }
65
+ }
66
+ enrichLog(log, ctx) {
67
+ return {
68
+ ...log,
69
+ environmentId: log.environmentId || ctx.environmentId,
70
+ envName: log.envName || ctx.envName,
71
+ appId: log.appId || ctx.appId,
72
+ appName: log.appName || ctx.appName,
73
+ orgId: log.orgId || ctx.orgId,
74
+ orgName: log.orgName || ctx.orgName,
75
+ };
76
+ }
77
+ };
78
+ exports.LiveTailService = LiveTailService;
79
+ exports.LiveTailService = LiveTailService = __decorate([
80
+ (0, common_1.Injectable)(),
81
+ __param(0, (0, common_1.Optional)()),
82
+ __param(0, (0, common_1.Inject)(livetail_interface_1.LIVETAIL_CONFIG)),
83
+ __metadata("design:paramtypes", [Object])
84
+ ], LiveTailService);
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@devms/livetail",
3
+ "version": "0.0.2",
4
+ "description": "NestJS WebSocket live tail module for real-time log streaming. Integrates with any NestJS application to broadcast logs via Socket.IO.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "prepublishOnly": "tsc"
14
+ },
15
+ "keywords": [
16
+ "nestjs",
17
+ "websocket",
18
+ "live-tail",
19
+ "logging",
20
+ "real-time",
21
+ "socket.io",
22
+ "monitoring",
23
+ "observability"
24
+ ],
25
+ "license": "MIT",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "peerDependencies": {
30
+ "@nestjs/common": "^10.0.0 || ^11.0.0",
31
+ "@nestjs/core": "^10.0.0 || ^11.0.0",
32
+ "@nestjs/websockets": "^10.0.0 || ^11.0.0",
33
+ "@nestjs/platform-socket.io": "^10.0.0 || ^11.0.0",
34
+ "rxjs": "^7.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@nestjs/common": "^11.1.3",
38
+ "@nestjs/core": "^11.1.3",
39
+ "@nestjs/websockets": "^11.0.0",
40
+ "@nestjs/platform-socket.io": "^11.0.0",
41
+ "@types/node": "^24.0.0",
42
+ "rxjs": "^7.8.2",
43
+ "socket.io": "^4.8.0",
44
+ "typescript": "^5.8.3"
45
+ }
46
+ }