@blac/logging-plugin 2.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Facebook, Inc. and its affiliates.
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,110 @@
1
+ # @blac/logging-plugin
2
+
3
+ Logging and debugging plugin for BlaC state management. Logs instance lifecycle, state changes, events, and monitors for potential memory issues.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @blac/logging-plugin
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { getPluginManager } from '@blac/core';
15
+ import { LoggingPlugin } from '@blac/logging-plugin';
16
+
17
+ getPluginManager().install(new LoggingPlugin(), {
18
+ environment: 'development',
19
+ });
20
+ ```
21
+
22
+ ## Configuration
23
+
24
+ ```typescript
25
+ new LoggingPlugin({
26
+ // Log verbosity: 'minimal' | 'info' | 'debug' | 'verbose'
27
+ level: 'info',
28
+
29
+ // Output format: 'simple' (one-line) | 'grouped' (console.group)
30
+ format: 'grouped',
31
+
32
+ // Filtering
33
+ include: ['CounterCubit', 'CartVertex'], // Only log these classes
34
+ exclude: ['AnalyticsCubit'], // Exclude these classes
35
+ filter: ({ className, isIsolated }) => !isIsolated, // Custom filter
36
+
37
+ // Feature toggles
38
+ logLifecycle: true, // Log create/dispose
39
+ logStateChanges: true, // Log state transitions
40
+ logEvents: true, // Log Vertex events
41
+ includeCallstack: false, // Include callstack in logs
42
+
43
+ // Memory monitoring
44
+ instanceCountWarningThreshold: 50, // Warn when exceeded
45
+ detectRapidLifecycles: true, // Detect create/dispose loops
46
+ rapidLifecycleWindowMs: 1000, // Time window for detection
47
+ rapidLifecycleThreshold: 5, // Cycles to trigger warning
48
+
49
+ // State change batching
50
+ debounceStateChanges: true, // Group rapid successive changes
51
+ debounceWindowMs: 100, // Time window for batching
52
+
53
+ // Customization
54
+ prefix: '[BlaC]', // Log prefix
55
+ logger: console, // Custom logger
56
+ });
57
+ ```
58
+
59
+ ## Log Levels
60
+
61
+ | Level | Logs |
62
+ | --------- | -------------------------------------------------- |
63
+ | `minimal` | Warnings only (memory issues, suspicious activity) |
64
+ | `info` | Lifecycle + warnings |
65
+ | `debug` | + state changes + events |
66
+ | `verbose` | + callstacks + full state objects |
67
+
68
+ ## Output Examples
69
+
70
+ **Simple format:**
71
+
72
+ ```
73
+ [BlaC] Created CounterCubit#abc12345 (isolated)
74
+ [BlaC] CounterCubit#abc12345 state: 0 → 1
75
+ [BlaC] Disposed CounterCubit#abc12345 (lived 5.2s)
76
+ ```
77
+
78
+ **Grouped format:**
79
+
80
+ ```
81
+ ▼ [BlaC] CounterCubit#abc12345 state changed
82
+ Previous: 0
83
+ Current: 1
84
+ ```
85
+
86
+ **Batched state changes (when multiple changes happen rapidly):**
87
+
88
+ ```
89
+ [BlaC] CounterCubit#abc12345 state: 0 → 5 (5 changes batched)
90
+ ```
91
+
92
+ ```
93
+ ▼ [BlaC] CounterCubit#abc12345 state changed (5 batched)
94
+ Initial: 0
95
+ Final: 5
96
+ ▼ All changes
97
+ 1. 0 → 1
98
+ 2. 1 → 2
99
+ 3. 2 → 3
100
+ 4. 3 → 4
101
+ 5. 4 → 5
102
+ ```
103
+
104
+ **Warnings:**
105
+
106
+ ```
107
+ [BlaC] ⚠️ High instance count: FormCubit has 52 instances
108
+ [BlaC] ⚠️ Rapid lifecycle: CounterCubit created/disposed 5 times in 1000ms
109
+ [BlaC] ⚠️ Unused instance: FormCubit#abc12345 disposed without state changes
110
+ ```
@@ -0,0 +1,32 @@
1
+ import type { BlacPlugin, PluginContext, StateContainer, Vertex, DiscriminatedEvent } from '@blac/core';
2
+ import type { LoggingPluginConfig } from './types';
3
+ export declare class LoggingPlugin implements BlacPlugin {
4
+ readonly name = "LoggingPlugin";
5
+ readonly version = "1.0.0";
6
+ private config;
7
+ private context?;
8
+ private simpleFormatter;
9
+ private groupedFormatter;
10
+ private instanceCountMonitor;
11
+ private lifecycleMonitor;
12
+ private stateChangeCount;
13
+ private stateChangeWindowStart;
14
+ private stateChangeLoggingDisabled;
15
+ constructor(config?: LoggingPluginConfig);
16
+ onInstall(context: PluginContext): void;
17
+ onUninstall(): void;
18
+ onInstanceCreated(instance: StateContainer<any>, context: PluginContext): void;
19
+ onStateChanged<S extends object>(instance: StateContainer<S>, previousState: S, currentState: S, callstack: string | undefined, context: PluginContext): void;
20
+ onEventAdded<E extends DiscriminatedEvent>(vertex: Vertex<any, E>, event: E & {
21
+ timestamp?: number;
22
+ source?: string;
23
+ }, context: PluginContext): void;
24
+ onInstanceDisposed(instance: StateContainer<any>, context: PluginContext): void;
25
+ logStats(): void;
26
+ private shouldLog;
27
+ private shouldLogLifecycle;
28
+ private shouldLogStateChanges;
29
+ private shouldLogEvents;
30
+ private logWarning;
31
+ }
32
+ //# sourceMappingURL=LoggingPlugin.d.ts.map
@@ -0,0 +1,27 @@
1
+ import type { InstanceMetadata } from '@blac/core';
2
+ import type { ResolvedConfig } from '../types';
3
+ export declare class GroupedFormatter {
4
+ private logger;
5
+ private prefix;
6
+ constructor(config: ResolvedConfig);
7
+ private formatInstanceId;
8
+ private group;
9
+ private groupEnd;
10
+ logInstall(stats: {
11
+ registeredTypes: number;
12
+ totalInstances: number;
13
+ }): void;
14
+ logUninstall(): void;
15
+ logInstanceCreated(metadata: InstanceMetadata, initialState: unknown): void;
16
+ logInstanceDisposed(metadata: InstanceMetadata, lifespanMs: number, stateChangeCount: number): void;
17
+ logStateChanged(metadata: InstanceMetadata, previousState: unknown, currentState: unknown, callstack: string | undefined, includeCallstack: boolean): void;
18
+ logEventAdded(className: string, instanceId: string, eventType: string, event: unknown): void;
19
+ logWarning(message: string, details?: Record<string, unknown>): void;
20
+ logStats(stats: {
21
+ registeredTypes: number;
22
+ totalInstances: number;
23
+ typeBreakdown: Record<string, number>;
24
+ }): void;
25
+ private formatDuration;
26
+ }
27
+ //# sourceMappingURL=GroupedFormatter.d.ts.map
@@ -0,0 +1,26 @@
1
+ import type { InstanceMetadata } from '@blac/core';
2
+ import type { ResolvedConfig } from '../types';
3
+ export declare class SimpleFormatter {
4
+ private logger;
5
+ private prefix;
6
+ constructor(config: ResolvedConfig);
7
+ private formatInstanceId;
8
+ logInstall(stats: {
9
+ registeredTypes: number;
10
+ totalInstances: number;
11
+ }): void;
12
+ logUninstall(): void;
13
+ logInstanceCreated(metadata: InstanceMetadata): void;
14
+ logInstanceDisposed(metadata: InstanceMetadata, lifespanMs: number): void;
15
+ logStateChanged(metadata: InstanceMetadata, previousState: unknown, currentState: unknown, _callstack: string | undefined, includeCallstack: boolean): void;
16
+ logEventAdded(className: string, instanceId: string, eventType: string, payload: unknown): void;
17
+ logWarning(message: string): void;
18
+ logStats(stats: {
19
+ registeredTypes: number;
20
+ totalInstances: number;
21
+ typeBreakdown: Record<string, number>;
22
+ }): void;
23
+ private formatState;
24
+ private formatDuration;
25
+ }
26
+ //# sourceMappingURL=SimpleFormatter.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { SimpleFormatter } from './SimpleFormatter';
2
+ export { GroupedFormatter } from './GroupedFormatter';
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.cjs ADDED
@@ -0,0 +1,392 @@
1
+
2
+ //#region src/formatters/SimpleFormatter.ts
3
+ var SimpleFormatter = class {
4
+ constructor(config) {
5
+ this.logger = config.logger;
6
+ this.prefix = config.prefix;
7
+ }
8
+ formatInstanceId(className, id) {
9
+ return `${className}#${id.length > 8 ? id.slice(0, 8) : id}`;
10
+ }
11
+ logInstall(stats) {
12
+ this.logger.log(`${this.prefix} Plugin installed (${stats.registeredTypes} types, ${stats.totalInstances} instances)`);
13
+ }
14
+ logUninstall() {
15
+ this.logger.log(`${this.prefix} Plugin uninstalled`);
16
+ }
17
+ logInstanceCreated(metadata) {
18
+ const instanceId = this.formatInstanceId(metadata.className, metadata.id);
19
+ const isolated = metadata.isIsolated ? " (isolated)" : "";
20
+ this.logger.log(`${this.prefix} Created ${instanceId}${isolated}`);
21
+ }
22
+ logInstanceDisposed(metadata, lifespanMs) {
23
+ const instanceId = this.formatInstanceId(metadata.className, metadata.id);
24
+ const lifespan = this.formatDuration(lifespanMs);
25
+ this.logger.log(`${this.prefix} Disposed ${instanceId} (lived ${lifespan})`);
26
+ }
27
+ logStateChanged(metadata, previousState, currentState, _callstack, includeCallstack) {
28
+ const instanceId = this.formatInstanceId(metadata.className, metadata.id);
29
+ const prev = this.formatState(previousState);
30
+ const curr = this.formatState(currentState);
31
+ this.logger.log(`${this.prefix} ${instanceId} state: ${prev} → ${curr}`);
32
+ if (includeCallstack && _callstack) this.logger.log(`${this.prefix} Callstack: ${_callstack}`);
33
+ }
34
+ logEventAdded(className, instanceId, eventType, payload) {
35
+ const id = this.formatInstanceId(className, instanceId);
36
+ const payloadStr = this.formatState(payload);
37
+ this.logger.log(`${this.prefix} ${id} event: ${eventType} ${payloadStr}`);
38
+ }
39
+ logWarning(message) {
40
+ this.logger.warn(`${this.prefix} ⚠️ ${message}`);
41
+ }
42
+ logStats(stats) {
43
+ this.logger.log(`${this.prefix} Stats: ${stats.totalInstances} instances across ${stats.registeredTypes} types`);
44
+ }
45
+ formatState(state) {
46
+ if (state === null) return "null";
47
+ if (state === void 0) return "undefined";
48
+ if (typeof state === "object") try {
49
+ const str = JSON.stringify(state);
50
+ return str.length > 50 ? str.slice(0, 50) + "..." : str;
51
+ } catch {
52
+ return "[Object]";
53
+ }
54
+ return String(state);
55
+ }
56
+ formatDuration(ms) {
57
+ if (ms < 1e3) return `${ms}ms`;
58
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
59
+ return `${(ms / 6e4).toFixed(1)}m`;
60
+ }
61
+ };
62
+
63
+ //#endregion
64
+ //#region src/formatters/GroupedFormatter.ts
65
+ var GroupedFormatter = class {
66
+ constructor(config) {
67
+ this.logger = config.logger;
68
+ this.prefix = config.prefix;
69
+ }
70
+ formatInstanceId(className, id) {
71
+ return `${className}#${id.length > 8 ? id.slice(0, 8) : id}`;
72
+ }
73
+ group(label, collapsed = true) {
74
+ if (collapsed && this.logger.groupCollapsed) this.logger.groupCollapsed(label);
75
+ else if (this.logger.group) this.logger.group(label);
76
+ else this.logger.log(label);
77
+ }
78
+ groupEnd() {
79
+ if (this.logger.groupEnd) this.logger.groupEnd();
80
+ }
81
+ logInstall(stats) {
82
+ this.group(`${this.prefix} Plugin installed`);
83
+ this.logger.log("Registered types:", stats.registeredTypes);
84
+ this.logger.log("Total instances:", stats.totalInstances);
85
+ this.groupEnd();
86
+ }
87
+ logUninstall() {
88
+ this.logger.log(`${this.prefix} Plugin uninstalled`);
89
+ }
90
+ logInstanceCreated(metadata, initialState) {
91
+ const instanceId = this.formatInstanceId(metadata.className, metadata.id);
92
+ this.group(`${this.prefix} Created ${instanceId}`);
93
+ this.logger.log("Class:", metadata.className);
94
+ this.logger.log("Instance ID:", metadata.id);
95
+ this.logger.log("Isolated:", metadata.isIsolated);
96
+ this.logger.log("Initial state:", initialState);
97
+ if (metadata.callstack) this.logger.log("Callstack:", metadata.callstack);
98
+ this.groupEnd();
99
+ }
100
+ logInstanceDisposed(metadata, lifespanMs, stateChangeCount) {
101
+ const instanceId = this.formatInstanceId(metadata.className, metadata.id);
102
+ this.group(`${this.prefix} Disposed ${instanceId}`);
103
+ this.logger.log("Lifespan:", this.formatDuration(lifespanMs));
104
+ this.logger.log("State changes:", stateChangeCount);
105
+ this.logger.log("Final state:", metadata.state);
106
+ this.groupEnd();
107
+ }
108
+ logStateChanged(metadata, previousState, currentState, callstack, includeCallstack) {
109
+ const instanceId = this.formatInstanceId(metadata.className, metadata.id);
110
+ this.group(`${this.prefix} ${instanceId} state changed`);
111
+ this.logger.log("Previous:", previousState);
112
+ this.logger.log("Current:", currentState);
113
+ if (includeCallstack && callstack) this.logger.log("Callstack:", callstack);
114
+ this.groupEnd();
115
+ }
116
+ logEventAdded(className, instanceId, eventType, event) {
117
+ const id = this.formatInstanceId(className, instanceId);
118
+ this.group(`${this.prefix} ${id} event: ${eventType}`);
119
+ this.logger.log("Event:", event);
120
+ this.groupEnd();
121
+ }
122
+ logWarning(message, details) {
123
+ if (details) {
124
+ this.group(`${this.prefix} ⚠️ ${message}`, false);
125
+ for (const [key, value] of Object.entries(details)) this.logger.log(`${key}:`, value);
126
+ this.groupEnd();
127
+ } else this.logger.warn(`${this.prefix} ⚠️ ${message}`);
128
+ }
129
+ logStats(stats) {
130
+ this.group(`${this.prefix} Stats`);
131
+ this.logger.log("Registered types:", stats.registeredTypes);
132
+ this.logger.log("Total instances:", stats.totalInstances);
133
+ this.logger.log("Breakdown:", stats.typeBreakdown);
134
+ this.groupEnd();
135
+ }
136
+ formatDuration(ms) {
137
+ if (ms < 1e3) return `${ms}ms`;
138
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
139
+ return `${(ms / 6e4).toFixed(1)}m`;
140
+ }
141
+ };
142
+
143
+ //#endregion
144
+ //#region src/monitors/InstanceCountMonitor.ts
145
+ var InstanceCountMonitor = class {
146
+ constructor(config, onWarning) {
147
+ this.instanceCounts = /* @__PURE__ */ new Map();
148
+ this.warnedClasses = /* @__PURE__ */ new Set();
149
+ this.threshold = config.instanceCountWarningThreshold;
150
+ this.onWarning = onWarning;
151
+ }
152
+ onInstanceCreated(className) {
153
+ const count = (this.instanceCounts.get(className) ?? 0) + 1;
154
+ this.instanceCounts.set(className, count);
155
+ if (count >= this.threshold && !this.warnedClasses.has(className)) {
156
+ this.warnedClasses.add(className);
157
+ this.onWarning(`High instance count: ${className} has ${count} instances`, {
158
+ className,
159
+ count,
160
+ threshold: this.threshold
161
+ });
162
+ }
163
+ }
164
+ onInstanceDisposed(className) {
165
+ const count = this.instanceCounts.get(className) ?? 0;
166
+ if (count > 0) this.instanceCounts.set(className, count - 1);
167
+ if (count - 1 < this.threshold) this.warnedClasses.delete(className);
168
+ }
169
+ getStats() {
170
+ return new Map(this.instanceCounts);
171
+ }
172
+ reset() {
173
+ this.instanceCounts.clear();
174
+ this.warnedClasses.clear();
175
+ }
176
+ };
177
+
178
+ //#endregion
179
+ //#region src/monitors/LifecycleMonitor.ts
180
+ var LifecycleMonitor = class {
181
+ constructor(config, onWarning) {
182
+ this.lifecycleHistory = /* @__PURE__ */ new Map();
183
+ this.instanceInfo = /* @__PURE__ */ new Map();
184
+ this.warnedClasses = /* @__PURE__ */ new Set();
185
+ this.windowMs = config.rapidLifecycleWindowMs;
186
+ this.threshold = config.rapidLifecycleThreshold;
187
+ this.enabled = config.detectRapidLifecycles;
188
+ this.onWarning = onWarning;
189
+ }
190
+ onInstanceCreated(className, instanceId) {
191
+ if (!this.enabled) return;
192
+ this.instanceInfo.set(instanceId, {
193
+ createdAt: Date.now(),
194
+ stateChangeCount: 0
195
+ });
196
+ this.recordEvent(className, "created");
197
+ this.checkRapidLifecycle(className);
198
+ }
199
+ onInstanceDisposed(className, instanceId) {
200
+ if (!this.enabled) return;
201
+ this.instanceInfo.delete(instanceId);
202
+ this.recordEvent(className, "disposed");
203
+ this.checkRapidLifecycle(className);
204
+ }
205
+ onStateChanged(instanceId) {
206
+ const info = this.instanceInfo.get(instanceId);
207
+ if (info) info.stateChangeCount++;
208
+ }
209
+ getInstanceInfo(instanceId) {
210
+ return this.instanceInfo.get(instanceId);
211
+ }
212
+ recordEvent(className, type) {
213
+ const now = Date.now();
214
+ const events = this.lifecycleHistory.get(className) ?? [];
215
+ events.push({
216
+ timestamp: now,
217
+ type
218
+ });
219
+ const cutoff = now - this.windowMs;
220
+ const filtered = events.filter((e) => e.timestamp > cutoff);
221
+ this.lifecycleHistory.set(className, filtered);
222
+ }
223
+ checkRapidLifecycle(className) {
224
+ const events = this.lifecycleHistory.get(className) ?? [];
225
+ const cycleCount = Math.min(events.filter((e) => e.type === "created").length, events.filter((e) => e.type === "disposed").length);
226
+ if (cycleCount >= this.threshold && !this.warnedClasses.has(className)) {
227
+ this.warnedClasses.add(className);
228
+ this.onWarning(`Rapid lifecycle: ${className} created/disposed ${cycleCount} times in ${this.windowMs}ms`, {
229
+ className,
230
+ cycleCount,
231
+ windowMs: this.windowMs,
232
+ threshold: this.threshold
233
+ });
234
+ setTimeout(() => {
235
+ this.warnedClasses.delete(className);
236
+ }, this.windowMs);
237
+ }
238
+ }
239
+ reset() {
240
+ this.lifecycleHistory.clear();
241
+ this.instanceInfo.clear();
242
+ this.warnedClasses.clear();
243
+ }
244
+ };
245
+
246
+ //#endregion
247
+ //#region src/types.ts
248
+ function resolveConfig(config = {}) {
249
+ return {
250
+ level: config.level ?? "info",
251
+ format: config.format ?? "grouped",
252
+ logger: config.logger ?? console,
253
+ filter: config.filter,
254
+ include: config.include,
255
+ exclude: config.exclude,
256
+ logLifecycle: config.logLifecycle ?? true,
257
+ logStateChanges: config.logStateChanges ?? true,
258
+ logEvents: config.logEvents ?? true,
259
+ includeCallstack: config.includeCallstack ?? false,
260
+ instanceCountWarningThreshold: config.instanceCountWarningThreshold ?? 50,
261
+ detectRapidLifecycles: config.detectRapidLifecycles ?? true,
262
+ rapidLifecycleWindowMs: config.rapidLifecycleWindowMs ?? 1e3,
263
+ rapidLifecycleThreshold: config.rapidLifecycleThreshold ?? 5,
264
+ prefix: config.prefix ?? "[BlaC]"
265
+ };
266
+ }
267
+
268
+ //#endregion
269
+ //#region src/LoggingPlugin.ts
270
+ const STATE_CHANGE_RATE_LIMIT = 1e3;
271
+ var LoggingPlugin = class {
272
+ constructor(config = {}) {
273
+ this.name = "LoggingPlugin";
274
+ this.version = "1.0.0";
275
+ this.stateChangeCount = 0;
276
+ this.stateChangeWindowStart = Date.now();
277
+ this.stateChangeLoggingDisabled = false;
278
+ this.config = resolveConfig(config);
279
+ const warningHandler = (message, details) => {
280
+ this.logWarning(message, details);
281
+ };
282
+ this.simpleFormatter = new SimpleFormatter(this.config);
283
+ this.groupedFormatter = new GroupedFormatter(this.config);
284
+ this.instanceCountMonitor = new InstanceCountMonitor(this.config, warningHandler);
285
+ this.lifecycleMonitor = new LifecycleMonitor(this.config, warningHandler);
286
+ }
287
+ onInstall(context) {
288
+ this.context = context;
289
+ const stats = context.getStats();
290
+ if (this.config.format === "simple") this.simpleFormatter.logInstall(stats);
291
+ else this.groupedFormatter.logInstall(stats);
292
+ }
293
+ onUninstall() {
294
+ if (this.config.format === "simple") this.simpleFormatter.logUninstall();
295
+ else this.groupedFormatter.logUninstall();
296
+ this.instanceCountMonitor.reset();
297
+ this.lifecycleMonitor.reset();
298
+ this.stateChangeCount = 0;
299
+ this.stateChangeWindowStart = Date.now();
300
+ this.stateChangeLoggingDisabled = false;
301
+ this.context = void 0;
302
+ }
303
+ onInstanceCreated(instance, context) {
304
+ const metadata = context.getInstanceMetadata(instance);
305
+ if (!this.shouldLog(instance, metadata.className, metadata.id, metadata.isIsolated)) return;
306
+ this.instanceCountMonitor.onInstanceCreated(metadata.className);
307
+ this.lifecycleMonitor.onInstanceCreated(metadata.className, metadata.id);
308
+ if (!this.shouldLogLifecycle()) return;
309
+ if (this.config.format === "simple") this.simpleFormatter.logInstanceCreated(metadata);
310
+ else this.groupedFormatter.logInstanceCreated(metadata, context.getState(instance));
311
+ }
312
+ onStateChanged(instance, previousState, currentState, callstack, context) {
313
+ const metadata = context.getInstanceMetadata(instance);
314
+ if (!this.shouldLog(instance, metadata.className, metadata.id, metadata.isIsolated)) return;
315
+ this.lifecycleMonitor.onStateChanged(metadata.id);
316
+ if (!this.shouldLogStateChanges()) return;
317
+ if (this.stateChangeLoggingDisabled) return;
318
+ const now = Date.now();
319
+ if (now - this.stateChangeWindowStart >= 1e3) {
320
+ this.stateChangeCount = 0;
321
+ this.stateChangeWindowStart = now;
322
+ }
323
+ this.stateChangeCount++;
324
+ if (this.stateChangeCount > STATE_CHANGE_RATE_LIMIT) {
325
+ this.stateChangeLoggingDisabled = true;
326
+ this.logWarning(`State change logging disabled: exceeded ${STATE_CHANGE_RATE_LIMIT} changes/second`);
327
+ return;
328
+ }
329
+ if (this.config.format === "simple") this.simpleFormatter.logStateChanged(metadata, previousState, currentState, callstack, this.config.includeCallstack);
330
+ else this.groupedFormatter.logStateChanged(metadata, previousState, currentState, callstack, this.config.includeCallstack);
331
+ }
332
+ onEventAdded(vertex, event, context) {
333
+ const metadata = context.getInstanceMetadata(vertex);
334
+ if (!this.shouldLog(vertex, metadata.className, metadata.id, metadata.isIsolated)) return;
335
+ if (!this.shouldLogEvents()) return;
336
+ const eventType = event.type ?? "UnknownEvent";
337
+ if (this.config.format === "simple") this.simpleFormatter.logEventAdded(metadata.className, metadata.id, eventType, event);
338
+ else this.groupedFormatter.logEventAdded(metadata.className, metadata.id, eventType, event);
339
+ }
340
+ onInstanceDisposed(instance, context) {
341
+ const metadata = context.getInstanceMetadata(instance);
342
+ if (!this.shouldLog(instance, metadata.className, metadata.id, metadata.isIsolated)) return;
343
+ const info = this.lifecycleMonitor.getInstanceInfo(metadata.id);
344
+ const lifespanMs = info ? Date.now() - info.createdAt : 0;
345
+ const stateChangeCount = info?.stateChangeCount ?? 0;
346
+ this.instanceCountMonitor.onInstanceDisposed(metadata.className);
347
+ this.lifecycleMonitor.onInstanceDisposed(metadata.className, metadata.id);
348
+ if (!this.shouldLogLifecycle()) return;
349
+ if (this.config.format === "simple") this.simpleFormatter.logInstanceDisposed(metadata, lifespanMs);
350
+ else this.groupedFormatter.logInstanceDisposed(metadata, lifespanMs, stateChangeCount);
351
+ }
352
+ logStats() {
353
+ if (!this.context) return;
354
+ const stats = this.context.getStats();
355
+ if (this.config.format === "simple") this.simpleFormatter.logStats(stats);
356
+ else this.groupedFormatter.logStats(stats);
357
+ }
358
+ shouldLog(instance, className, instanceId, isIsolated) {
359
+ if (this.config.include && !this.config.include.includes(className)) return false;
360
+ if (this.config.exclude?.includes(className)) return false;
361
+ if (this.config.filter) {
362
+ const ctx = {
363
+ instance,
364
+ className,
365
+ instanceId,
366
+ isIsolated
367
+ };
368
+ return this.config.filter(ctx);
369
+ }
370
+ return true;
371
+ }
372
+ shouldLogLifecycle() {
373
+ if (!this.config.logLifecycle) return false;
374
+ return this.config.level !== "minimal";
375
+ }
376
+ shouldLogStateChanges() {
377
+ if (!this.config.logStateChanges) return false;
378
+ return this.config.level === "info" || this.config.level === "debug" || this.config.level === "verbose";
379
+ }
380
+ shouldLogEvents() {
381
+ if (!this.config.logEvents) return false;
382
+ return this.config.level === "debug" || this.config.level === "verbose";
383
+ }
384
+ logWarning(message, details) {
385
+ if (this.config.format === "simple") this.simpleFormatter.logWarning(message);
386
+ else this.groupedFormatter.logWarning(message, details);
387
+ }
388
+ };
389
+
390
+ //#endregion
391
+ exports.LoggingPlugin = LoggingPlugin;
392
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1,3 @@
1
+ export { LoggingPlugin } from './LoggingPlugin';
2
+ export type { LoggingPluginConfig, LogLevel, LogFormat, Logger, FilterContext, FilterFn, } from './types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { LoggingPlugin } from './LoggingPlugin';
2
+ export type { LoggingPluginConfig, LogLevel, LogFormat, Logger, FilterContext, FilterFn, } from './types';
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,391 @@
1
+ //#region src/formatters/SimpleFormatter.ts
2
+ var SimpleFormatter = class {
3
+ constructor(config) {
4
+ this.logger = config.logger;
5
+ this.prefix = config.prefix;
6
+ }
7
+ formatInstanceId(className, id) {
8
+ return `${className}#${id.length > 8 ? id.slice(0, 8) : id}`;
9
+ }
10
+ logInstall(stats) {
11
+ this.logger.log(`${this.prefix} Plugin installed (${stats.registeredTypes} types, ${stats.totalInstances} instances)`);
12
+ }
13
+ logUninstall() {
14
+ this.logger.log(`${this.prefix} Plugin uninstalled`);
15
+ }
16
+ logInstanceCreated(metadata) {
17
+ const instanceId = this.formatInstanceId(metadata.className, metadata.id);
18
+ const isolated = metadata.isIsolated ? " (isolated)" : "";
19
+ this.logger.log(`${this.prefix} Created ${instanceId}${isolated}`);
20
+ }
21
+ logInstanceDisposed(metadata, lifespanMs) {
22
+ const instanceId = this.formatInstanceId(metadata.className, metadata.id);
23
+ const lifespan = this.formatDuration(lifespanMs);
24
+ this.logger.log(`${this.prefix} Disposed ${instanceId} (lived ${lifespan})`);
25
+ }
26
+ logStateChanged(metadata, previousState, currentState, _callstack, includeCallstack) {
27
+ const instanceId = this.formatInstanceId(metadata.className, metadata.id);
28
+ const prev = this.formatState(previousState);
29
+ const curr = this.formatState(currentState);
30
+ this.logger.log(`${this.prefix} ${instanceId} state: ${prev} → ${curr}`);
31
+ if (includeCallstack && _callstack) this.logger.log(`${this.prefix} Callstack: ${_callstack}`);
32
+ }
33
+ logEventAdded(className, instanceId, eventType, payload) {
34
+ const id = this.formatInstanceId(className, instanceId);
35
+ const payloadStr = this.formatState(payload);
36
+ this.logger.log(`${this.prefix} ${id} event: ${eventType} ${payloadStr}`);
37
+ }
38
+ logWarning(message) {
39
+ this.logger.warn(`${this.prefix} ⚠️ ${message}`);
40
+ }
41
+ logStats(stats) {
42
+ this.logger.log(`${this.prefix} Stats: ${stats.totalInstances} instances across ${stats.registeredTypes} types`);
43
+ }
44
+ formatState(state) {
45
+ if (state === null) return "null";
46
+ if (state === void 0) return "undefined";
47
+ if (typeof state === "object") try {
48
+ const str = JSON.stringify(state);
49
+ return str.length > 50 ? str.slice(0, 50) + "..." : str;
50
+ } catch {
51
+ return "[Object]";
52
+ }
53
+ return String(state);
54
+ }
55
+ formatDuration(ms) {
56
+ if (ms < 1e3) return `${ms}ms`;
57
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
58
+ return `${(ms / 6e4).toFixed(1)}m`;
59
+ }
60
+ };
61
+
62
+ //#endregion
63
+ //#region src/formatters/GroupedFormatter.ts
64
+ var GroupedFormatter = class {
65
+ constructor(config) {
66
+ this.logger = config.logger;
67
+ this.prefix = config.prefix;
68
+ }
69
+ formatInstanceId(className, id) {
70
+ return `${className}#${id.length > 8 ? id.slice(0, 8) : id}`;
71
+ }
72
+ group(label, collapsed = true) {
73
+ if (collapsed && this.logger.groupCollapsed) this.logger.groupCollapsed(label);
74
+ else if (this.logger.group) this.logger.group(label);
75
+ else this.logger.log(label);
76
+ }
77
+ groupEnd() {
78
+ if (this.logger.groupEnd) this.logger.groupEnd();
79
+ }
80
+ logInstall(stats) {
81
+ this.group(`${this.prefix} Plugin installed`);
82
+ this.logger.log("Registered types:", stats.registeredTypes);
83
+ this.logger.log("Total instances:", stats.totalInstances);
84
+ this.groupEnd();
85
+ }
86
+ logUninstall() {
87
+ this.logger.log(`${this.prefix} Plugin uninstalled`);
88
+ }
89
+ logInstanceCreated(metadata, initialState) {
90
+ const instanceId = this.formatInstanceId(metadata.className, metadata.id);
91
+ this.group(`${this.prefix} Created ${instanceId}`);
92
+ this.logger.log("Class:", metadata.className);
93
+ this.logger.log("Instance ID:", metadata.id);
94
+ this.logger.log("Isolated:", metadata.isIsolated);
95
+ this.logger.log("Initial state:", initialState);
96
+ if (metadata.callstack) this.logger.log("Callstack:", metadata.callstack);
97
+ this.groupEnd();
98
+ }
99
+ logInstanceDisposed(metadata, lifespanMs, stateChangeCount) {
100
+ const instanceId = this.formatInstanceId(metadata.className, metadata.id);
101
+ this.group(`${this.prefix} Disposed ${instanceId}`);
102
+ this.logger.log("Lifespan:", this.formatDuration(lifespanMs));
103
+ this.logger.log("State changes:", stateChangeCount);
104
+ this.logger.log("Final state:", metadata.state);
105
+ this.groupEnd();
106
+ }
107
+ logStateChanged(metadata, previousState, currentState, callstack, includeCallstack) {
108
+ const instanceId = this.formatInstanceId(metadata.className, metadata.id);
109
+ this.group(`${this.prefix} ${instanceId} state changed`);
110
+ this.logger.log("Previous:", previousState);
111
+ this.logger.log("Current:", currentState);
112
+ if (includeCallstack && callstack) this.logger.log("Callstack:", callstack);
113
+ this.groupEnd();
114
+ }
115
+ logEventAdded(className, instanceId, eventType, event) {
116
+ const id = this.formatInstanceId(className, instanceId);
117
+ this.group(`${this.prefix} ${id} event: ${eventType}`);
118
+ this.logger.log("Event:", event);
119
+ this.groupEnd();
120
+ }
121
+ logWarning(message, details) {
122
+ if (details) {
123
+ this.group(`${this.prefix} ⚠️ ${message}`, false);
124
+ for (const [key, value] of Object.entries(details)) this.logger.log(`${key}:`, value);
125
+ this.groupEnd();
126
+ } else this.logger.warn(`${this.prefix} ⚠️ ${message}`);
127
+ }
128
+ logStats(stats) {
129
+ this.group(`${this.prefix} Stats`);
130
+ this.logger.log("Registered types:", stats.registeredTypes);
131
+ this.logger.log("Total instances:", stats.totalInstances);
132
+ this.logger.log("Breakdown:", stats.typeBreakdown);
133
+ this.groupEnd();
134
+ }
135
+ formatDuration(ms) {
136
+ if (ms < 1e3) return `${ms}ms`;
137
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
138
+ return `${(ms / 6e4).toFixed(1)}m`;
139
+ }
140
+ };
141
+
142
+ //#endregion
143
+ //#region src/monitors/InstanceCountMonitor.ts
144
+ var InstanceCountMonitor = class {
145
+ constructor(config, onWarning) {
146
+ this.instanceCounts = /* @__PURE__ */ new Map();
147
+ this.warnedClasses = /* @__PURE__ */ new Set();
148
+ this.threshold = config.instanceCountWarningThreshold;
149
+ this.onWarning = onWarning;
150
+ }
151
+ onInstanceCreated(className) {
152
+ const count = (this.instanceCounts.get(className) ?? 0) + 1;
153
+ this.instanceCounts.set(className, count);
154
+ if (count >= this.threshold && !this.warnedClasses.has(className)) {
155
+ this.warnedClasses.add(className);
156
+ this.onWarning(`High instance count: ${className} has ${count} instances`, {
157
+ className,
158
+ count,
159
+ threshold: this.threshold
160
+ });
161
+ }
162
+ }
163
+ onInstanceDisposed(className) {
164
+ const count = this.instanceCounts.get(className) ?? 0;
165
+ if (count > 0) this.instanceCounts.set(className, count - 1);
166
+ if (count - 1 < this.threshold) this.warnedClasses.delete(className);
167
+ }
168
+ getStats() {
169
+ return new Map(this.instanceCounts);
170
+ }
171
+ reset() {
172
+ this.instanceCounts.clear();
173
+ this.warnedClasses.clear();
174
+ }
175
+ };
176
+
177
+ //#endregion
178
+ //#region src/monitors/LifecycleMonitor.ts
179
+ var LifecycleMonitor = class {
180
+ constructor(config, onWarning) {
181
+ this.lifecycleHistory = /* @__PURE__ */ new Map();
182
+ this.instanceInfo = /* @__PURE__ */ new Map();
183
+ this.warnedClasses = /* @__PURE__ */ new Set();
184
+ this.windowMs = config.rapidLifecycleWindowMs;
185
+ this.threshold = config.rapidLifecycleThreshold;
186
+ this.enabled = config.detectRapidLifecycles;
187
+ this.onWarning = onWarning;
188
+ }
189
+ onInstanceCreated(className, instanceId) {
190
+ if (!this.enabled) return;
191
+ this.instanceInfo.set(instanceId, {
192
+ createdAt: Date.now(),
193
+ stateChangeCount: 0
194
+ });
195
+ this.recordEvent(className, "created");
196
+ this.checkRapidLifecycle(className);
197
+ }
198
+ onInstanceDisposed(className, instanceId) {
199
+ if (!this.enabled) return;
200
+ this.instanceInfo.delete(instanceId);
201
+ this.recordEvent(className, "disposed");
202
+ this.checkRapidLifecycle(className);
203
+ }
204
+ onStateChanged(instanceId) {
205
+ const info = this.instanceInfo.get(instanceId);
206
+ if (info) info.stateChangeCount++;
207
+ }
208
+ getInstanceInfo(instanceId) {
209
+ return this.instanceInfo.get(instanceId);
210
+ }
211
+ recordEvent(className, type) {
212
+ const now = Date.now();
213
+ const events = this.lifecycleHistory.get(className) ?? [];
214
+ events.push({
215
+ timestamp: now,
216
+ type
217
+ });
218
+ const cutoff = now - this.windowMs;
219
+ const filtered = events.filter((e) => e.timestamp > cutoff);
220
+ this.lifecycleHistory.set(className, filtered);
221
+ }
222
+ checkRapidLifecycle(className) {
223
+ const events = this.lifecycleHistory.get(className) ?? [];
224
+ const cycleCount = Math.min(events.filter((e) => e.type === "created").length, events.filter((e) => e.type === "disposed").length);
225
+ if (cycleCount >= this.threshold && !this.warnedClasses.has(className)) {
226
+ this.warnedClasses.add(className);
227
+ this.onWarning(`Rapid lifecycle: ${className} created/disposed ${cycleCount} times in ${this.windowMs}ms`, {
228
+ className,
229
+ cycleCount,
230
+ windowMs: this.windowMs,
231
+ threshold: this.threshold
232
+ });
233
+ setTimeout(() => {
234
+ this.warnedClasses.delete(className);
235
+ }, this.windowMs);
236
+ }
237
+ }
238
+ reset() {
239
+ this.lifecycleHistory.clear();
240
+ this.instanceInfo.clear();
241
+ this.warnedClasses.clear();
242
+ }
243
+ };
244
+
245
+ //#endregion
246
+ //#region src/types.ts
247
+ function resolveConfig(config = {}) {
248
+ return {
249
+ level: config.level ?? "info",
250
+ format: config.format ?? "grouped",
251
+ logger: config.logger ?? console,
252
+ filter: config.filter,
253
+ include: config.include,
254
+ exclude: config.exclude,
255
+ logLifecycle: config.logLifecycle ?? true,
256
+ logStateChanges: config.logStateChanges ?? true,
257
+ logEvents: config.logEvents ?? true,
258
+ includeCallstack: config.includeCallstack ?? false,
259
+ instanceCountWarningThreshold: config.instanceCountWarningThreshold ?? 50,
260
+ detectRapidLifecycles: config.detectRapidLifecycles ?? true,
261
+ rapidLifecycleWindowMs: config.rapidLifecycleWindowMs ?? 1e3,
262
+ rapidLifecycleThreshold: config.rapidLifecycleThreshold ?? 5,
263
+ prefix: config.prefix ?? "[BlaC]"
264
+ };
265
+ }
266
+
267
+ //#endregion
268
+ //#region src/LoggingPlugin.ts
269
+ const STATE_CHANGE_RATE_LIMIT = 1e3;
270
+ var LoggingPlugin = class {
271
+ constructor(config = {}) {
272
+ this.name = "LoggingPlugin";
273
+ this.version = "1.0.0";
274
+ this.stateChangeCount = 0;
275
+ this.stateChangeWindowStart = Date.now();
276
+ this.stateChangeLoggingDisabled = false;
277
+ this.config = resolveConfig(config);
278
+ const warningHandler = (message, details) => {
279
+ this.logWarning(message, details);
280
+ };
281
+ this.simpleFormatter = new SimpleFormatter(this.config);
282
+ this.groupedFormatter = new GroupedFormatter(this.config);
283
+ this.instanceCountMonitor = new InstanceCountMonitor(this.config, warningHandler);
284
+ this.lifecycleMonitor = new LifecycleMonitor(this.config, warningHandler);
285
+ }
286
+ onInstall(context) {
287
+ this.context = context;
288
+ const stats = context.getStats();
289
+ if (this.config.format === "simple") this.simpleFormatter.logInstall(stats);
290
+ else this.groupedFormatter.logInstall(stats);
291
+ }
292
+ onUninstall() {
293
+ if (this.config.format === "simple") this.simpleFormatter.logUninstall();
294
+ else this.groupedFormatter.logUninstall();
295
+ this.instanceCountMonitor.reset();
296
+ this.lifecycleMonitor.reset();
297
+ this.stateChangeCount = 0;
298
+ this.stateChangeWindowStart = Date.now();
299
+ this.stateChangeLoggingDisabled = false;
300
+ this.context = void 0;
301
+ }
302
+ onInstanceCreated(instance, context) {
303
+ const metadata = context.getInstanceMetadata(instance);
304
+ if (!this.shouldLog(instance, metadata.className, metadata.id, metadata.isIsolated)) return;
305
+ this.instanceCountMonitor.onInstanceCreated(metadata.className);
306
+ this.lifecycleMonitor.onInstanceCreated(metadata.className, metadata.id);
307
+ if (!this.shouldLogLifecycle()) return;
308
+ if (this.config.format === "simple") this.simpleFormatter.logInstanceCreated(metadata);
309
+ else this.groupedFormatter.logInstanceCreated(metadata, context.getState(instance));
310
+ }
311
+ onStateChanged(instance, previousState, currentState, callstack, context) {
312
+ const metadata = context.getInstanceMetadata(instance);
313
+ if (!this.shouldLog(instance, metadata.className, metadata.id, metadata.isIsolated)) return;
314
+ this.lifecycleMonitor.onStateChanged(metadata.id);
315
+ if (!this.shouldLogStateChanges()) return;
316
+ if (this.stateChangeLoggingDisabled) return;
317
+ const now = Date.now();
318
+ if (now - this.stateChangeWindowStart >= 1e3) {
319
+ this.stateChangeCount = 0;
320
+ this.stateChangeWindowStart = now;
321
+ }
322
+ this.stateChangeCount++;
323
+ if (this.stateChangeCount > STATE_CHANGE_RATE_LIMIT) {
324
+ this.stateChangeLoggingDisabled = true;
325
+ this.logWarning(`State change logging disabled: exceeded ${STATE_CHANGE_RATE_LIMIT} changes/second`);
326
+ return;
327
+ }
328
+ if (this.config.format === "simple") this.simpleFormatter.logStateChanged(metadata, previousState, currentState, callstack, this.config.includeCallstack);
329
+ else this.groupedFormatter.logStateChanged(metadata, previousState, currentState, callstack, this.config.includeCallstack);
330
+ }
331
+ onEventAdded(vertex, event, context) {
332
+ const metadata = context.getInstanceMetadata(vertex);
333
+ if (!this.shouldLog(vertex, metadata.className, metadata.id, metadata.isIsolated)) return;
334
+ if (!this.shouldLogEvents()) return;
335
+ const eventType = event.type ?? "UnknownEvent";
336
+ if (this.config.format === "simple") this.simpleFormatter.logEventAdded(metadata.className, metadata.id, eventType, event);
337
+ else this.groupedFormatter.logEventAdded(metadata.className, metadata.id, eventType, event);
338
+ }
339
+ onInstanceDisposed(instance, context) {
340
+ const metadata = context.getInstanceMetadata(instance);
341
+ if (!this.shouldLog(instance, metadata.className, metadata.id, metadata.isIsolated)) return;
342
+ const info = this.lifecycleMonitor.getInstanceInfo(metadata.id);
343
+ const lifespanMs = info ? Date.now() - info.createdAt : 0;
344
+ const stateChangeCount = info?.stateChangeCount ?? 0;
345
+ this.instanceCountMonitor.onInstanceDisposed(metadata.className);
346
+ this.lifecycleMonitor.onInstanceDisposed(metadata.className, metadata.id);
347
+ if (!this.shouldLogLifecycle()) return;
348
+ if (this.config.format === "simple") this.simpleFormatter.logInstanceDisposed(metadata, lifespanMs);
349
+ else this.groupedFormatter.logInstanceDisposed(metadata, lifespanMs, stateChangeCount);
350
+ }
351
+ logStats() {
352
+ if (!this.context) return;
353
+ const stats = this.context.getStats();
354
+ if (this.config.format === "simple") this.simpleFormatter.logStats(stats);
355
+ else this.groupedFormatter.logStats(stats);
356
+ }
357
+ shouldLog(instance, className, instanceId, isIsolated) {
358
+ if (this.config.include && !this.config.include.includes(className)) return false;
359
+ if (this.config.exclude?.includes(className)) return false;
360
+ if (this.config.filter) {
361
+ const ctx = {
362
+ instance,
363
+ className,
364
+ instanceId,
365
+ isIsolated
366
+ };
367
+ return this.config.filter(ctx);
368
+ }
369
+ return true;
370
+ }
371
+ shouldLogLifecycle() {
372
+ if (!this.config.logLifecycle) return false;
373
+ return this.config.level !== "minimal";
374
+ }
375
+ shouldLogStateChanges() {
376
+ if (!this.config.logStateChanges) return false;
377
+ return this.config.level === "info" || this.config.level === "debug" || this.config.level === "verbose";
378
+ }
379
+ shouldLogEvents() {
380
+ if (!this.config.logEvents) return false;
381
+ return this.config.level === "debug" || this.config.level === "verbose";
382
+ }
383
+ logWarning(message, details) {
384
+ if (this.config.format === "simple") this.simpleFormatter.logWarning(message);
385
+ else this.groupedFormatter.logWarning(message, details);
386
+ }
387
+ };
388
+
389
+ //#endregion
390
+ export { LoggingPlugin };
391
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ import type { ResolvedConfig } from '../types';
2
+ export declare class InstanceCountMonitor {
3
+ private instanceCounts;
4
+ private warnedClasses;
5
+ private threshold;
6
+ private onWarning;
7
+ constructor(config: ResolvedConfig, onWarning: (message: string, details?: Record<string, unknown>) => void);
8
+ onInstanceCreated(className: string): void;
9
+ onInstanceDisposed(className: string): void;
10
+ getStats(): Map<string, number>;
11
+ reset(): void;
12
+ }
13
+ //# sourceMappingURL=InstanceCountMonitor.d.ts.map
@@ -0,0 +1,24 @@
1
+ import type { ResolvedConfig } from '../types';
2
+ interface InstanceInfo {
3
+ createdAt: number;
4
+ stateChangeCount: number;
5
+ }
6
+ export declare class LifecycleMonitor {
7
+ private lifecycleHistory;
8
+ private instanceInfo;
9
+ private windowMs;
10
+ private threshold;
11
+ private enabled;
12
+ private warnedClasses;
13
+ private onWarning;
14
+ constructor(config: ResolvedConfig, onWarning: (message: string, details?: Record<string, unknown>) => void);
15
+ onInstanceCreated(className: string, instanceId: string): void;
16
+ onInstanceDisposed(className: string, instanceId: string): void;
17
+ onStateChanged(instanceId: string): void;
18
+ getInstanceInfo(instanceId: string): InstanceInfo | undefined;
19
+ private recordEvent;
20
+ private checkRapidLifecycle;
21
+ reset(): void;
22
+ }
23
+ export {};
24
+ //# sourceMappingURL=LifecycleMonitor.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { InstanceCountMonitor } from './InstanceCountMonitor';
2
+ export { LifecycleMonitor } from './LifecycleMonitor';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,69 @@
1
+ import type { StateContainer } from '@blac/core';
2
+ export type LogLevel = 'minimal' | 'info' | 'debug' | 'verbose';
3
+ export type LogFormat = 'simple' | 'grouped';
4
+ export interface Logger {
5
+ log(...args: unknown[]): void;
6
+ warn(...args: unknown[]): void;
7
+ error(...args: unknown[]): void;
8
+ group?(label: string): void;
9
+ groupEnd?(): void;
10
+ groupCollapsed?(label: string): void;
11
+ }
12
+ export interface FilterContext {
13
+ instance: StateContainer<any>;
14
+ className: string;
15
+ instanceId: string;
16
+ isIsolated: boolean;
17
+ }
18
+ export type FilterFn = (ctx: FilterContext) => boolean;
19
+ export interface LoggingPluginConfig {
20
+ /** Log verbosity level (default: 'info') */
21
+ level?: LogLevel;
22
+ /** Output format (default: 'grouped') */
23
+ format?: LogFormat;
24
+ /** Custom logger implementation (default: console) */
25
+ logger?: Logger;
26
+ /** Filter which instances to log */
27
+ filter?: FilterFn;
28
+ /** Include class names to log (whitelist) */
29
+ include?: string[];
30
+ /** Exclude class names from logging (blacklist) */
31
+ exclude?: string[];
32
+ /** Log instance creation/disposal (default: true) */
33
+ logLifecycle?: boolean;
34
+ /** Log state changes (default: true) */
35
+ logStateChanges?: boolean;
36
+ /** Log Vertex events (default: true) */
37
+ logEvents?: boolean;
38
+ /** Include callstack in state change logs (default: false) */
39
+ includeCallstack?: boolean;
40
+ /** Warn when instance count exceeds threshold (default: 50) */
41
+ instanceCountWarningThreshold?: number;
42
+ /** Warn on rapid init/dispose cycles (default: true) */
43
+ detectRapidLifecycles?: boolean;
44
+ /** Time window for rapid lifecycle detection in ms (default: 1000) */
45
+ rapidLifecycleWindowMs?: number;
46
+ /** Number of cycles in window to trigger warning (default: 5) */
47
+ rapidLifecycleThreshold?: number;
48
+ /** Log prefix (default: '[BlaC]') */
49
+ prefix?: string;
50
+ }
51
+ export interface ResolvedConfig {
52
+ level: LogLevel;
53
+ format: LogFormat;
54
+ logger: Logger;
55
+ filter: FilterFn | undefined;
56
+ include: string[] | undefined;
57
+ exclude: string[] | undefined;
58
+ logLifecycle: boolean;
59
+ logStateChanges: boolean;
60
+ logEvents: boolean;
61
+ includeCallstack: boolean;
62
+ instanceCountWarningThreshold: number;
63
+ detectRapidLifecycles: boolean;
64
+ rapidLifecycleWindowMs: number;
65
+ rapidLifecycleThreshold: number;
66
+ prefix: string;
67
+ }
68
+ export declare function resolveConfig(config?: LoggingPluginConfig): ResolvedConfig;
69
+ //# sourceMappingURL=types.d.ts.map
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@blac/logging-plugin",
3
+ "version": "2.0.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "author": "Brendan Mullins <jsnanigans@gmail.com>",
7
+ "description": "Logging and debugging plugin for BlaC state management",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/jsnanigans/blac.git",
11
+ "directory": "packages/logging-plugin"
12
+ },
13
+ "homepage": "https://github.com/jsnanigans/blac#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/jsnanigans/blac/issues"
16
+ },
17
+ "main": "./dist/index.js",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "import": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/index.d.cts",
28
+ "default": "./dist/index.cjs"
29
+ }
30
+ }
31
+ },
32
+ "files": [
33
+ "dist/**/*.js",
34
+ "dist/**/*.cjs",
35
+ "dist/**/*.d.ts",
36
+ "dist/**/*.d.cts",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "sideEffects": false,
41
+ "keywords": [
42
+ "blac",
43
+ "logging",
44
+ "debugging",
45
+ "state-management",
46
+ "devtools"
47
+ ],
48
+ "peerDependencies": {
49
+ "@blac/core": "2.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "prettier": "^3.6.2",
53
+ "typescript": "^5.9.2",
54
+ "vitest": "3.2.4",
55
+ "tsdown": "^0.15.7",
56
+ "@blac/core": "2.0.0"
57
+ },
58
+ "scripts": {
59
+ "dev": "tsdown --watch",
60
+ "build": "tsdown && tsc -p tsconfig.build.json && cp dist/index.d.ts dist/index.d.cts",
61
+ "clean": "rm -rf dist",
62
+ "format": "prettier --write \".\"",
63
+ "lint": "eslint src --ext .ts,.tsx",
64
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
65
+ "typecheck": "tsc --noEmit",
66
+ "deploy": "pnpm publish --access public"
67
+ }
68
+ }