@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 +21 -0
- package/README.md +110 -0
- package/dist/LoggingPlugin.d.ts +32 -0
- package/dist/formatters/GroupedFormatter.d.ts +27 -0
- package/dist/formatters/SimpleFormatter.d.ts +26 -0
- package/dist/formatters/index.d.ts +3 -0
- package/dist/index.cjs +392 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +391 -0
- package/dist/monitors/InstanceCountMonitor.d.ts +13 -0
- package/dist/monitors/LifecycleMonitor.d.ts +24 -0
- package/dist/monitors/index.d.ts +3 -0
- package/dist/types.d.ts +69 -0
- package/package.json +68 -0
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
|
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
|
package/dist/index.d.cts
ADDED
package/dist/index.d.ts
ADDED
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
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|