@delta-comic/logger 3.0.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +33 -0
- package/dist/index.d.mts +141 -0
- package/dist/index.mjs +414 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @delta-comic/logger
|
|
2
|
+
|
|
3
|
+
Delta Comic 的统一日志 SDK 与 Tauri 2 插件。浏览器端自动回退到控制台;Tauri 端会把前端、Rust 和插件日志汇聚到同一套异步日志文件中。
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { logger } from '@delta-comic/logger'
|
|
7
|
+
|
|
8
|
+
const log = logger.scoped('my-plugin:sync')
|
|
9
|
+
log.info('sync started', { itemCount: 12 })
|
|
10
|
+
log.error('sync failed', error)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
宿主应用应尽早安装全局捕获,用于复制 `console` 输出并捕获未处理错误:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { installGlobalLogger } from '@delta-comic/logger'
|
|
17
|
+
|
|
18
|
+
installGlobalLogger()
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Rust 宿主需要在其他插件之前注册插件,之后可直接使用 `tracing`:
|
|
22
|
+
|
|
23
|
+
```rust
|
|
24
|
+
tauri::Builder::default()
|
|
25
|
+
.plugin(tauri_plugin_logger::init())
|
|
26
|
+
.plugin(other_plugin);
|
|
27
|
+
|
|
28
|
+
tracing::info!(target: "my_plugin::sync", items = 12, "sync started");
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
日志格式固定为 `[yyyy/mm/dd hh:MM:ss] (scope) level > content`。文件按日和 5 MiB 分块,30 天后 gzip 归档,90 天后删除。前端传输使用批量队列,Rust tracing 事件入口使用有界非阻塞队列,文件写入由 Tokio worker 完成。
|
|
32
|
+
|
|
33
|
+
原生读取与导出 API 只接受插件列出的安全文件名,不接受绝对路径或目录穿越。`listLogFiles`、`readLogFile` 和 `exportLogs` 需要宿主授予 `logger:default` capability。
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
//#region lib/types.d.ts
|
|
2
|
+
declare const LOG_LEVELS: readonly ['trace', 'debug', 'info', 'warn', 'error'];
|
|
3
|
+
type LogLevel = (typeof LOG_LEVELS)[number];
|
|
4
|
+
interface LogEntry {
|
|
5
|
+
/** ISO-8601 timestamp generated at the call site. */
|
|
6
|
+
timestamp: string;
|
|
7
|
+
scope: string;
|
|
8
|
+
level: LogLevel;
|
|
9
|
+
/** A cycle-safe, JSON-like rendering of all arguments. */
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
interface LogFileInfo {
|
|
13
|
+
name: string;
|
|
14
|
+
path: string;
|
|
15
|
+
size: number;
|
|
16
|
+
modifiedAt: string;
|
|
17
|
+
archived: boolean;
|
|
18
|
+
}
|
|
19
|
+
interface LogFileContent {
|
|
20
|
+
path: string;
|
|
21
|
+
content: string;
|
|
22
|
+
size: number;
|
|
23
|
+
/** True when only the most recent portion of a large file was returned. */
|
|
24
|
+
truncated: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface ExportLogsOptions {
|
|
27
|
+
/** Omit to export every active and archived log file. */
|
|
28
|
+
paths?: string[];
|
|
29
|
+
}
|
|
30
|
+
type Invoke = (command: string, args?: Record<string, unknown>) => Promise<unknown>;
|
|
31
|
+
interface LoggerClient {
|
|
32
|
+
readonly native: boolean;
|
|
33
|
+
write(entries: readonly LogEntry[]): void;
|
|
34
|
+
flush(): Promise<void>;
|
|
35
|
+
listLogFiles(): Promise<LogFileInfo[]>;
|
|
36
|
+
readLogFile(path: string): Promise<LogFileContent>;
|
|
37
|
+
exportLogs(options?: ExportLogsOptions): Promise<string>;
|
|
38
|
+
dispose(): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
interface LoggerOptions {
|
|
41
|
+
/** Defaults to `trace` in development and `info` in production. */
|
|
42
|
+
minLevel?: LogLevel;
|
|
43
|
+
/** Maximum entries sent in one invoke call. */
|
|
44
|
+
batchSize?: number;
|
|
45
|
+
/** Maximum delay before a partial batch is sent. */
|
|
46
|
+
flushIntervalMs?: number;
|
|
47
|
+
/** Bounds memory use if the native side becomes unavailable. */
|
|
48
|
+
maxQueueSize?: number;
|
|
49
|
+
/** Supplies an invoke implementation for alternate hosts and tests. */
|
|
50
|
+
invoke?: Invoke;
|
|
51
|
+
/** Override automatic Tauri detection. */
|
|
52
|
+
native?: boolean;
|
|
53
|
+
/** Captures `error` and `unhandledrejection` events. Defaults to true. */
|
|
54
|
+
captureErrors?: boolean;
|
|
55
|
+
/** Flushes on page hide/visibility changes. Defaults to true. */
|
|
56
|
+
flushOnLifecycle?: boolean;
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region lib/client.d.ts
|
|
60
|
+
/** Batched, non-blocking client for the native logger plugin. */
|
|
61
|
+
declare class TauriLoggerClient implements LoggerClient {
|
|
62
|
+
readonly native: boolean;
|
|
63
|
+
private readonly batchSize;
|
|
64
|
+
private readonly flushIntervalMs;
|
|
65
|
+
private readonly maxQueueSize;
|
|
66
|
+
private readonly configuredInvoke?;
|
|
67
|
+
private invokePromise?;
|
|
68
|
+
private queue;
|
|
69
|
+
private timer?;
|
|
70
|
+
private inFlight?;
|
|
71
|
+
private disposed;
|
|
72
|
+
constructor(options?: LoggerOptions);
|
|
73
|
+
write(entries: readonly LogEntry[]): void;
|
|
74
|
+
flush(): Promise<void>;
|
|
75
|
+
listLogFiles(): Promise<LogFileInfo[]>;
|
|
76
|
+
readLogFile(path: string): Promise<LogFileContent>;
|
|
77
|
+
exportLogs(options?: ExportLogsOptions): Promise<string>;
|
|
78
|
+
dispose(): Promise<void>;
|
|
79
|
+
private scheduleFlush;
|
|
80
|
+
private clearTimer;
|
|
81
|
+
private send;
|
|
82
|
+
private call;
|
|
83
|
+
private resolveInvoke;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region lib/logger.d.ts
|
|
87
|
+
declare const formatLogEntry: (entry: LogEntry) => string;
|
|
88
|
+
/** Scoped logger shared by the app, plugins, and regular web builds. */
|
|
89
|
+
declare class Logger {
|
|
90
|
+
readonly scope: string;
|
|
91
|
+
private readonly runtime;
|
|
92
|
+
constructor(scope?: string, options?: LoggerOptions);
|
|
93
|
+
private static fromRuntime;
|
|
94
|
+
get client(): LoggerClient;
|
|
95
|
+
get minLevel(): LogLevel;
|
|
96
|
+
set minLevel(level: LogLevel);
|
|
97
|
+
scoped(scope: string): Logger;
|
|
98
|
+
group(scope: string): Logger;
|
|
99
|
+
trace(...values: unknown[]): void;
|
|
100
|
+
debug(...values: unknown[]): void;
|
|
101
|
+
info(...values: unknown[]): void;
|
|
102
|
+
warn(...values: unknown[]): void;
|
|
103
|
+
error(...values: unknown[]): void;
|
|
104
|
+
/** Copies console output into this logger while retaining the original output. */
|
|
105
|
+
proxyConsole(): () => void;
|
|
106
|
+
captureGlobalErrors(): () => void;
|
|
107
|
+
flushOnLifecycle(): () => void;
|
|
108
|
+
flush(): Promise<void>;
|
|
109
|
+
dispose(): Promise<void>;
|
|
110
|
+
private emit;
|
|
111
|
+
private captureConsole;
|
|
112
|
+
}
|
|
113
|
+
declare const createLogger: (scope?: string, options?: LoggerOptions) => Logger;
|
|
114
|
+
/** Shared queue used by application modules and third-party plugins. */
|
|
115
|
+
declare const logger: Logger;
|
|
116
|
+
/** Typed native reader/export client sharing the global logger transport. */
|
|
117
|
+
declare const loggerClient: LoggerClient;
|
|
118
|
+
declare const listLogFiles: () => Promise<LogFileInfo[]>;
|
|
119
|
+
declare const readLogFile: (path: string) => Promise<LogFileContent>;
|
|
120
|
+
declare const exportLogs: (options?: ExportLogsOptions) => Promise<string>;
|
|
121
|
+
/** Installs console/error/lifecycle capture once for the shared app logger. */
|
|
122
|
+
declare const installGlobalLogger: (target?: Logger) => (() => void);
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region lib/serializer.d.ts
|
|
125
|
+
type JsonScalar = string | number | boolean | null;
|
|
126
|
+
type JsonValue = JsonScalar | JsonValue[] | {
|
|
127
|
+
[key: string]: JsonValue;
|
|
128
|
+
};
|
|
129
|
+
/** Converts arbitrary JS values into a deterministic, JSON-safe structure. */
|
|
130
|
+
declare class LogSerializer {
|
|
131
|
+
serializeArguments(values: readonly unknown[]): string;
|
|
132
|
+
serialize(value: unknown): JsonValue;
|
|
133
|
+
private visit;
|
|
134
|
+
private serializeError;
|
|
135
|
+
private serializeObject;
|
|
136
|
+
}
|
|
137
|
+
declare const logSerializer: LogSerializer;
|
|
138
|
+
declare const serializeLogArguments: (values: readonly unknown[]) => string;
|
|
139
|
+
//#endregion
|
|
140
|
+
export { ExportLogsOptions, Invoke, LOG_LEVELS, LogEntry, LogFileContent, LogFileInfo, LogLevel, LogSerializer, Logger, LoggerClient, LoggerOptions, TauriLoggerClient, createLogger, exportLogs, formatLogEntry, installGlobalLogger, listLogFiles, logSerializer, logger, loggerClient, readLogFile, serializeLogArguments };
|
|
141
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
//#region lib/client.ts
|
|
2
|
+
const COMMAND_PREFIX = "plugin:logger|";
|
|
3
|
+
const isTauriRuntime = () => {
|
|
4
|
+
if (typeof window === "undefined") return false;
|
|
5
|
+
return "__TAURI_INTERNALS__" in window;
|
|
6
|
+
};
|
|
7
|
+
const loadTauriInvoke = async () => {
|
|
8
|
+
const { invoke } = await import("@tauri-apps/api/core");
|
|
9
|
+
return (command, args) => invoke(command, args);
|
|
10
|
+
};
|
|
11
|
+
/** Batched, non-blocking client for the native logger plugin. */
|
|
12
|
+
var TauriLoggerClient = class {
|
|
13
|
+
native;
|
|
14
|
+
batchSize;
|
|
15
|
+
flushIntervalMs;
|
|
16
|
+
maxQueueSize;
|
|
17
|
+
configuredInvoke;
|
|
18
|
+
invokePromise;
|
|
19
|
+
queue = [];
|
|
20
|
+
timer;
|
|
21
|
+
inFlight;
|
|
22
|
+
disposed = false;
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.native = options.native ?? (Boolean(options.invoke) || isTauriRuntime());
|
|
25
|
+
this.configuredInvoke = options.invoke;
|
|
26
|
+
this.batchSize = Math.max(1, options.batchSize ?? 64);
|
|
27
|
+
this.flushIntervalMs = Math.max(0, options.flushIntervalMs ?? 40);
|
|
28
|
+
this.maxQueueSize = Math.max(this.batchSize, options.maxQueueSize ?? 4096);
|
|
29
|
+
}
|
|
30
|
+
write(entries) {
|
|
31
|
+
if (!this.native || this.disposed || entries.length === 0) return;
|
|
32
|
+
const remaining = this.maxQueueSize - this.queue.length;
|
|
33
|
+
if (remaining > 0) this.queue.push(...entries.slice(-remaining));
|
|
34
|
+
if (this.queue.length >= this.batchSize) this.flush();
|
|
35
|
+
else this.scheduleFlush();
|
|
36
|
+
}
|
|
37
|
+
async flush() {
|
|
38
|
+
this.clearTimer();
|
|
39
|
+
if (!this.native) return;
|
|
40
|
+
if (this.inFlight) {
|
|
41
|
+
await this.inFlight;
|
|
42
|
+
if (this.queue.length > 0) await this.flush();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const entries = this.queue.splice(0, this.batchSize);
|
|
46
|
+
if (entries.length === 0) return;
|
|
47
|
+
const task = this.send(entries);
|
|
48
|
+
this.inFlight = task;
|
|
49
|
+
try {
|
|
50
|
+
await task;
|
|
51
|
+
} catch {} finally {
|
|
52
|
+
this.inFlight = void 0;
|
|
53
|
+
}
|
|
54
|
+
if (this.queue.length > 0) await this.flush();
|
|
55
|
+
}
|
|
56
|
+
async listLogFiles() {
|
|
57
|
+
return this.call("list_log_files");
|
|
58
|
+
}
|
|
59
|
+
async readLogFile(path) {
|
|
60
|
+
return this.call("read_log_file", { path });
|
|
61
|
+
}
|
|
62
|
+
async exportLogs(options = {}) {
|
|
63
|
+
return this.call("export_logs", { paths: options.paths });
|
|
64
|
+
}
|
|
65
|
+
async dispose() {
|
|
66
|
+
if (this.disposed) return;
|
|
67
|
+
await this.flush();
|
|
68
|
+
this.disposed = true;
|
|
69
|
+
this.clearTimer();
|
|
70
|
+
}
|
|
71
|
+
scheduleFlush() {
|
|
72
|
+
if (this.timer) return;
|
|
73
|
+
this.timer = setTimeout(() => {
|
|
74
|
+
this.timer = void 0;
|
|
75
|
+
this.flush();
|
|
76
|
+
}, this.flushIntervalMs);
|
|
77
|
+
}
|
|
78
|
+
clearTimer() {
|
|
79
|
+
if (!this.timer) return;
|
|
80
|
+
clearTimeout(this.timer);
|
|
81
|
+
this.timer = void 0;
|
|
82
|
+
}
|
|
83
|
+
async send(entries) {
|
|
84
|
+
await this.call("write_logs", { entries });
|
|
85
|
+
}
|
|
86
|
+
async call(command, args) {
|
|
87
|
+
if (!this.native) throw new Error("Native logger operations are unavailable in a web browser");
|
|
88
|
+
return (await this.resolveInvoke())(`${COMMAND_PREFIX}${command}`, args);
|
|
89
|
+
}
|
|
90
|
+
resolveInvoke() {
|
|
91
|
+
this.invokePromise ??= this.configuredInvoke ? Promise.resolve(this.configuredInvoke) : loadTauriInvoke();
|
|
92
|
+
return this.invokePromise;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region lib/serializer.ts
|
|
97
|
+
const inaccessible = (error) => `[Thrown while serializing: ${error instanceof Error ? error.message : String(error)}]`;
|
|
98
|
+
/** Converts arbitrary JS values into a deterministic, JSON-safe structure. */
|
|
99
|
+
var LogSerializer = class {
|
|
100
|
+
serializeArguments(values) {
|
|
101
|
+
return values.map((value) => {
|
|
102
|
+
if (typeof value === "string") return value;
|
|
103
|
+
const serialized = this.serialize(value);
|
|
104
|
+
return typeof serialized === "string" ? serialized : JSON.stringify(serialized);
|
|
105
|
+
}).join(" ");
|
|
106
|
+
}
|
|
107
|
+
serialize(value) {
|
|
108
|
+
return this.visit(value, "$", /* @__PURE__ */ new WeakMap());
|
|
109
|
+
}
|
|
110
|
+
visit(value, path, seen) {
|
|
111
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
112
|
+
if (typeof value === "number") {
|
|
113
|
+
if (Number.isNaN(value)) return "[NaN]";
|
|
114
|
+
if (value === Number.POSITIVE_INFINITY) return "[Infinity]";
|
|
115
|
+
if (value === Number.NEGATIVE_INFINITY) return "[-Infinity]";
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
if (typeof value === "undefined") return "[undefined]";
|
|
119
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
120
|
+
if (typeof value === "symbol") return value.toString();
|
|
121
|
+
if (typeof value === "function") return `[Function ${value.name || "anonymous"}]`;
|
|
122
|
+
const object = value;
|
|
123
|
+
const previousPath = seen.get(object);
|
|
124
|
+
if (previousPath) return `[Circular ${previousPath}]`;
|
|
125
|
+
seen.set(object, path);
|
|
126
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? "[Invalid Date]" : value.toISOString();
|
|
127
|
+
if (value instanceof RegExp) return value.toString();
|
|
128
|
+
if (value instanceof Error) return this.serializeError(value, path, seen);
|
|
129
|
+
if (Array.isArray(value)) return value.map((item, index) => this.visit(item, `${path}[${index}]`, seen));
|
|
130
|
+
if (value instanceof Map) return {
|
|
131
|
+
$type: "Map",
|
|
132
|
+
entries: [...value.entries()].map(([key, item], index) => [this.visit(key, `${path}.entries[${index}][0]`, seen), this.visit(item, `${path}.entries[${index}][1]`, seen)])
|
|
133
|
+
};
|
|
134
|
+
if (value instanceof Set) return {
|
|
135
|
+
$type: "Set",
|
|
136
|
+
values: [...value].map((item, index) => this.visit(item, `${path}.values[${index}]`, seen))
|
|
137
|
+
};
|
|
138
|
+
return this.serializeObject(object, path, seen);
|
|
139
|
+
}
|
|
140
|
+
serializeError(error, path, seen) {
|
|
141
|
+
const result = {
|
|
142
|
+
name: error.name,
|
|
143
|
+
message: error.message
|
|
144
|
+
};
|
|
145
|
+
if (error.stack) result.stack = error.stack;
|
|
146
|
+
if ("cause" in error && error.cause !== void 0) result.cause = this.visit(error.cause, `${path}.cause`, seen);
|
|
147
|
+
Object.assign(result, this.serializeObject(error, path, seen));
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
serializeObject(object, path, seen) {
|
|
151
|
+
const result = {};
|
|
152
|
+
let keys;
|
|
153
|
+
try {
|
|
154
|
+
keys = Object.keys(object).sort();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return { $error: inaccessible(error) };
|
|
157
|
+
}
|
|
158
|
+
for (const key of keys) try {
|
|
159
|
+
result[key] = this.visit(object[key], `${path}.${key}`, seen);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
result[key] = inaccessible(error);
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const logSerializer = new LogSerializer();
|
|
167
|
+
const serializeLogArguments = (values) => logSerializer.serializeArguments(values);
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region lib/types.ts
|
|
170
|
+
const LOG_LEVELS = [
|
|
171
|
+
"trace",
|
|
172
|
+
"debug",
|
|
173
|
+
"info",
|
|
174
|
+
"warn",
|
|
175
|
+
"error"
|
|
176
|
+
];
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region lib/logger.ts
|
|
179
|
+
const CONSOLE_LEVEL = {
|
|
180
|
+
debug: "debug",
|
|
181
|
+
error: "error",
|
|
182
|
+
info: "info",
|
|
183
|
+
log: "info",
|
|
184
|
+
trace: "trace",
|
|
185
|
+
warn: "warn"
|
|
186
|
+
};
|
|
187
|
+
const LEVEL_CONSOLE = {
|
|
188
|
+
trace: "trace",
|
|
189
|
+
debug: "debug",
|
|
190
|
+
info: "info",
|
|
191
|
+
warn: "warn",
|
|
192
|
+
error: "error"
|
|
193
|
+
};
|
|
194
|
+
const isDevelopment = () => {
|
|
195
|
+
const env = import.meta.env;
|
|
196
|
+
return env?.DEV === true || env?.PROD === false;
|
|
197
|
+
};
|
|
198
|
+
const levelValue = (level) => LOG_LEVELS.indexOf(level);
|
|
199
|
+
const resolveMinLevel = (requested) => {
|
|
200
|
+
const level = requested ?? (isDevelopment() ? "trace" : "info");
|
|
201
|
+
return !isDevelopment() && levelValue(level) < levelValue("info") ? "info" : level;
|
|
202
|
+
};
|
|
203
|
+
const normalizeScope = (scope) => scope.trim() || "app";
|
|
204
|
+
const joinScope = (parent, child) => {
|
|
205
|
+
const normalizedChild = child.trim();
|
|
206
|
+
return normalizedChild ? `${parent}:${normalizedChild}` : parent;
|
|
207
|
+
};
|
|
208
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
209
|
+
const formatLogEntry = (entry) => {
|
|
210
|
+
const date = new Date(entry.timestamp);
|
|
211
|
+
return `[${Number.isNaN(date.getTime()) ? entry.timestamp : `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`}] (${entry.scope}) ${entry.level} > ${entry.content}`;
|
|
212
|
+
};
|
|
213
|
+
/** Scoped logger shared by the app, plugins, and regular web builds. */
|
|
214
|
+
var Logger = class Logger {
|
|
215
|
+
scope;
|
|
216
|
+
runtime;
|
|
217
|
+
constructor(scope = "app", options = {}) {
|
|
218
|
+
this.scope = scope;
|
|
219
|
+
this.scope = normalizeScope(scope);
|
|
220
|
+
this.runtime = {
|
|
221
|
+
client: new TauriLoggerClient(options),
|
|
222
|
+
minLevel: resolveMinLevel(options.minLevel),
|
|
223
|
+
consoleWriters: this.captureConsole(),
|
|
224
|
+
cleanup: /* @__PURE__ */ new Set(),
|
|
225
|
+
errorCaptureInstalled: false,
|
|
226
|
+
lifecycleInstalled: false
|
|
227
|
+
};
|
|
228
|
+
if (options.captureErrors !== false) this.captureGlobalErrors();
|
|
229
|
+
if (options.flushOnLifecycle !== false) this.flushOnLifecycle();
|
|
230
|
+
}
|
|
231
|
+
static fromRuntime(scope, runtime) {
|
|
232
|
+
const logger = Object.create(Logger.prototype);
|
|
233
|
+
Object.defineProperty(logger, "scope", {
|
|
234
|
+
enumerable: true,
|
|
235
|
+
value: scope
|
|
236
|
+
});
|
|
237
|
+
Object.defineProperty(logger, "runtime", { value: runtime });
|
|
238
|
+
return logger;
|
|
239
|
+
}
|
|
240
|
+
get client() {
|
|
241
|
+
return this.runtime.client;
|
|
242
|
+
}
|
|
243
|
+
get minLevel() {
|
|
244
|
+
return this.runtime.minLevel;
|
|
245
|
+
}
|
|
246
|
+
set minLevel(level) {
|
|
247
|
+
this.runtime.minLevel = resolveMinLevel(level);
|
|
248
|
+
}
|
|
249
|
+
scoped(scope) {
|
|
250
|
+
return Logger.fromRuntime(joinScope(this.scope, scope), this.runtime);
|
|
251
|
+
}
|
|
252
|
+
group(scope) {
|
|
253
|
+
return this.scoped(scope);
|
|
254
|
+
}
|
|
255
|
+
trace(...values) {
|
|
256
|
+
this.emit("trace", values);
|
|
257
|
+
}
|
|
258
|
+
debug(...values) {
|
|
259
|
+
this.emit("debug", values);
|
|
260
|
+
}
|
|
261
|
+
info(...values) {
|
|
262
|
+
this.emit("info", values);
|
|
263
|
+
}
|
|
264
|
+
warn(...values) {
|
|
265
|
+
this.emit("warn", values);
|
|
266
|
+
}
|
|
267
|
+
error(...values) {
|
|
268
|
+
this.emit("error", values);
|
|
269
|
+
}
|
|
270
|
+
/** Copies console output into this logger while retaining the original output. */
|
|
271
|
+
proxyConsole() {
|
|
272
|
+
if (this.runtime.consoleRestore) return this.runtime.consoleRestore;
|
|
273
|
+
if (typeof console === "undefined") return () => void 0;
|
|
274
|
+
let writing = false;
|
|
275
|
+
const restores = [];
|
|
276
|
+
for (const method of Object.keys(CONSOLE_LEVEL)) {
|
|
277
|
+
const previous = console[method];
|
|
278
|
+
const original = this.runtime.consoleWriters[method];
|
|
279
|
+
const proxy = (...values) => {
|
|
280
|
+
original(...values);
|
|
281
|
+
if (writing) return;
|
|
282
|
+
writing = true;
|
|
283
|
+
try {
|
|
284
|
+
this.emit(CONSOLE_LEVEL[method], values, false);
|
|
285
|
+
} finally {
|
|
286
|
+
writing = false;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
console[method] = proxy;
|
|
290
|
+
restores.push(() => {
|
|
291
|
+
if (console[method] === proxy) console[method] = previous;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
const restore = () => {
|
|
295
|
+
for (const callback of restores) callback();
|
|
296
|
+
if (this.runtime.consoleRestore === restore) this.runtime.consoleRestore = void 0;
|
|
297
|
+
};
|
|
298
|
+
this.runtime.consoleRestore = restore;
|
|
299
|
+
return restore;
|
|
300
|
+
}
|
|
301
|
+
captureGlobalErrors() {
|
|
302
|
+
if (this.runtime.errorCaptureInstalled || typeof window === "undefined") return () => void 0;
|
|
303
|
+
const onError = (event) => {
|
|
304
|
+
this.emit("error", ["Uncaught error", event.error ?? event.message], true);
|
|
305
|
+
};
|
|
306
|
+
const onUnhandledRejection = (event) => {
|
|
307
|
+
this.emit("error", ["Unhandled promise rejection", event.reason], true);
|
|
308
|
+
};
|
|
309
|
+
window.addEventListener("error", onError);
|
|
310
|
+
window.addEventListener("unhandledrejection", onUnhandledRejection);
|
|
311
|
+
this.runtime.errorCaptureInstalled = true;
|
|
312
|
+
const cleanup = () => {
|
|
313
|
+
window.removeEventListener("error", onError);
|
|
314
|
+
window.removeEventListener("unhandledrejection", onUnhandledRejection);
|
|
315
|
+
this.runtime.errorCaptureInstalled = false;
|
|
316
|
+
this.runtime.cleanup.delete(cleanup);
|
|
317
|
+
};
|
|
318
|
+
this.runtime.cleanup.add(cleanup);
|
|
319
|
+
return cleanup;
|
|
320
|
+
}
|
|
321
|
+
flushOnLifecycle() {
|
|
322
|
+
if (this.runtime.lifecycleInstalled || typeof window === "undefined") return () => void 0;
|
|
323
|
+
const flush = () => void this.flush();
|
|
324
|
+
const onVisibilityChange = () => {
|
|
325
|
+
if (document.visibilityState === "hidden") flush();
|
|
326
|
+
};
|
|
327
|
+
window.addEventListener("pagehide", flush);
|
|
328
|
+
window.addEventListener("beforeunload", flush);
|
|
329
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
330
|
+
this.runtime.lifecycleInstalled = true;
|
|
331
|
+
const cleanup = () => {
|
|
332
|
+
window.removeEventListener("pagehide", flush);
|
|
333
|
+
window.removeEventListener("beforeunload", flush);
|
|
334
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
335
|
+
this.runtime.lifecycleInstalled = false;
|
|
336
|
+
this.runtime.cleanup.delete(cleanup);
|
|
337
|
+
};
|
|
338
|
+
this.runtime.cleanup.add(cleanup);
|
|
339
|
+
return cleanup;
|
|
340
|
+
}
|
|
341
|
+
flush() {
|
|
342
|
+
return this.runtime.client.flush();
|
|
343
|
+
}
|
|
344
|
+
async dispose() {
|
|
345
|
+
this.runtime.consoleRestore?.();
|
|
346
|
+
for (const cleanup of this.runtime.cleanup) cleanup();
|
|
347
|
+
await this.runtime.client.dispose();
|
|
348
|
+
}
|
|
349
|
+
emit(level, values, mirrorConsole = true) {
|
|
350
|
+
if (levelValue(level) < levelValue(this.runtime.minLevel)) return;
|
|
351
|
+
const entry = {
|
|
352
|
+
content: serializeLogArguments(values),
|
|
353
|
+
level,
|
|
354
|
+
scope: this.scope,
|
|
355
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
356
|
+
};
|
|
357
|
+
if (mirrorConsole) {
|
|
358
|
+
const writer = this.runtime.consoleWriters[LEVEL_CONSOLE[level]];
|
|
359
|
+
writer(formatLogEntry(entry));
|
|
360
|
+
}
|
|
361
|
+
this.runtime.client.write([entry]);
|
|
362
|
+
}
|
|
363
|
+
captureConsole() {
|
|
364
|
+
const noop = () => void 0;
|
|
365
|
+
if (typeof console === "undefined") return {
|
|
366
|
+
debug: noop,
|
|
367
|
+
error: noop,
|
|
368
|
+
info: noop,
|
|
369
|
+
log: noop,
|
|
370
|
+
trace: noop,
|
|
371
|
+
warn: noop
|
|
372
|
+
};
|
|
373
|
+
return {
|
|
374
|
+
debug: console.debug.bind(console),
|
|
375
|
+
error: console.error.bind(console),
|
|
376
|
+
info: console.info.bind(console),
|
|
377
|
+
log: console.log.bind(console),
|
|
378
|
+
trace: console.trace.bind(console),
|
|
379
|
+
warn: console.warn.bind(console)
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
const createLogger = (scope = "app", options = {}) => new Logger(scope, options);
|
|
384
|
+
/** Shared queue used by application modules and third-party plugins. */
|
|
385
|
+
const logger = createLogger("delta-comic", {
|
|
386
|
+
captureErrors: false,
|
|
387
|
+
flushOnLifecycle: false
|
|
388
|
+
});
|
|
389
|
+
/** Typed native reader/export client sharing the global logger transport. */
|
|
390
|
+
const loggerClient = logger.client;
|
|
391
|
+
const listLogFiles = () => loggerClient.listLogFiles();
|
|
392
|
+
const readLogFile = (path) => loggerClient.readLogFile(path);
|
|
393
|
+
const exportLogs = (options) => loggerClient.exportLogs(options);
|
|
394
|
+
const globalInstallations = /* @__PURE__ */ new WeakMap();
|
|
395
|
+
/** Installs console/error/lifecycle capture once for the shared app logger. */
|
|
396
|
+
const installGlobalLogger = (target = logger) => {
|
|
397
|
+
const existing = globalInstallations.get(target);
|
|
398
|
+
if (existing) return existing;
|
|
399
|
+
const cleanups = [
|
|
400
|
+
target.proxyConsole(),
|
|
401
|
+
target.captureGlobalErrors(),
|
|
402
|
+
target.flushOnLifecycle()
|
|
403
|
+
];
|
|
404
|
+
const uninstall = () => {
|
|
405
|
+
for (const cleanup of [...cleanups].reverse()) cleanup();
|
|
406
|
+
globalInstallations.delete(target);
|
|
407
|
+
};
|
|
408
|
+
globalInstallations.set(target, uninstall);
|
|
409
|
+
return uninstall;
|
|
410
|
+
};
|
|
411
|
+
//#endregion
|
|
412
|
+
export { LOG_LEVELS, LogSerializer, Logger, TauriLoggerClient, createLogger, exportLogs, formatLogEntry, installGlobalLogger, listLogFiles, logSerializer, logger, loggerClient, readLogFile, serializeLogArguments };
|
|
413
|
+
|
|
414
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../lib/client.ts","../lib/serializer.ts","../lib/types.ts","../lib/logger.ts"],"sourcesContent":["import type {\n ExportLogsOptions,\n Invoke,\n LogEntry,\n LogFileContent,\n LogFileInfo,\n LoggerClient,\n LoggerOptions,\n} from './types'\n\nconst COMMAND_PREFIX = 'plugin:logger|'\n\nconst isTauriRuntime = () => {\n if (typeof window === 'undefined') return false\n return '__TAURI_INTERNALS__' in window\n}\n\nconst loadTauriInvoke = async (): Promise<Invoke> => {\n const { invoke } = await import('@tauri-apps/api/core')\n return (command, args) => invoke(command, args)\n}\n\n/** Batched, non-blocking client for the native logger plugin. */\nexport class TauriLoggerClient implements LoggerClient {\n public readonly native: boolean\n private readonly batchSize: number\n private readonly flushIntervalMs: number\n private readonly maxQueueSize: number\n private readonly configuredInvoke?: Invoke\n private invokePromise?: Promise<Invoke>\n private queue: LogEntry[] = []\n private timer?: ReturnType<typeof setTimeout>\n private inFlight?: Promise<void>\n private disposed = false\n\n constructor(options: LoggerOptions = {}) {\n this.native = options.native ?? (Boolean(options.invoke) || isTauriRuntime())\n this.configuredInvoke = options.invoke\n this.batchSize = Math.max(1, options.batchSize ?? 64)\n this.flushIntervalMs = Math.max(0, options.flushIntervalMs ?? 40)\n this.maxQueueSize = Math.max(this.batchSize, options.maxQueueSize ?? 4096)\n }\n\n public write(entries: readonly LogEntry[]): void {\n if (!this.native || this.disposed || entries.length === 0) return\n const remaining = this.maxQueueSize - this.queue.length\n if (remaining > 0) this.queue.push(...entries.slice(-remaining))\n if (this.queue.length >= this.batchSize) void this.flush()\n else this.scheduleFlush()\n }\n\n public async flush(): Promise<void> {\n this.clearTimer()\n if (!this.native) return\n if (this.inFlight) {\n await this.inFlight\n if (this.queue.length > 0) await this.flush()\n return\n }\n const entries = this.queue.splice(0, this.batchSize)\n if (entries.length === 0) return\n const task = this.send(entries)\n this.inFlight = task\n try {\n await task\n } catch {\n // Logging must never surface a transport failure as an unhandled rejection or break the app.\n } finally {\n this.inFlight = undefined\n }\n if (this.queue.length > 0) await this.flush()\n }\n\n public async listLogFiles(): Promise<LogFileInfo[]> {\n return this.call<LogFileInfo[]>('list_log_files')\n }\n\n public async readLogFile(path: string): Promise<LogFileContent> {\n return this.call<LogFileContent>('read_log_file', { path })\n }\n\n public async exportLogs(options: ExportLogsOptions = {}): Promise<string> {\n return this.call<string>('export_logs', { paths: options.paths })\n }\n\n public async dispose(): Promise<void> {\n if (this.disposed) return\n await this.flush()\n this.disposed = true\n this.clearTimer()\n }\n\n private scheduleFlush(): void {\n if (this.timer) return\n this.timer = setTimeout(() => {\n this.timer = undefined\n void this.flush()\n }, this.flushIntervalMs)\n }\n\n private clearTimer(): void {\n if (!this.timer) return\n clearTimeout(this.timer)\n this.timer = undefined\n }\n\n private async send(entries: LogEntry[]): Promise<void> {\n await this.call<void>('write_logs', { entries })\n }\n\n private async call<T>(command: string, args?: Record<string, unknown>): Promise<T> {\n if (!this.native) throw new Error('Native logger operations are unavailable in a web browser')\n const invoke = await this.resolveInvoke()\n return invoke(`${COMMAND_PREFIX}${command}`, args) as Promise<T>\n }\n\n private resolveInvoke(): Promise<Invoke> {\n this.invokePromise ??= this.configuredInvoke\n ? Promise.resolve(this.configuredInvoke)\n : loadTauriInvoke()\n return this.invokePromise\n }\n}","type JsonScalar = string | number | boolean | null\ntype JsonValue = JsonScalar | JsonValue[] | { [key: string]: JsonValue }\n\nconst inaccessible = (error: unknown) =>\n `[Thrown while serializing: ${error instanceof Error ? error.message : String(error)}]`\n\n/** Converts arbitrary JS values into a deterministic, JSON-safe structure. */\nexport class LogSerializer {\n public serializeArguments(values: readonly unknown[]): string {\n return values\n .map(value => {\n if (typeof value === 'string') return value\n const serialized = this.serialize(value)\n return typeof serialized === 'string' ? serialized : JSON.stringify(serialized)\n })\n .join(' ')\n }\n\n public serialize(value: unknown): JsonValue {\n return this.visit(value, '$', new WeakMap<object, string>())\n }\n\n private visit(value: unknown, path: string, seen: WeakMap<object, string>): JsonValue {\n if (value === null || typeof value === 'string' || typeof value === 'boolean') return value\n if (typeof value === 'number') {\n if (Number.isNaN(value)) return '[NaN]'\n if (value === Number.POSITIVE_INFINITY) return '[Infinity]'\n if (value === Number.NEGATIVE_INFINITY) return '[-Infinity]'\n return value\n }\n if (typeof value === 'undefined') return '[undefined]'\n if (typeof value === 'bigint') return `${value}n`\n if (typeof value === 'symbol') return value.toString()\n if (typeof value === 'function') return `[Function ${value.name || 'anonymous'}]`\n\n const object = value as object\n const previousPath = seen.get(object)\n if (previousPath) return `[Circular ${previousPath}]`\n seen.set(object, path)\n\n if (value instanceof Date)\n return Number.isNaN(value.getTime()) ? '[Invalid Date]' : value.toISOString()\n if (value instanceof RegExp) return value.toString()\n if (value instanceof Error) return this.serializeError(value, path, seen)\n if (Array.isArray(value))\n return value.map((item, index) => this.visit(item, `${path}[${index}]`, seen))\n if (value instanceof Map)\n return {\n $type: 'Map',\n entries: [...value.entries()].map(([key, item], index) => [\n this.visit(key, `${path}.entries[${index}][0]`, seen),\n this.visit(item, `${path}.entries[${index}][1]`, seen),\n ]),\n }\n if (value instanceof Set)\n return {\n $type: 'Set',\n values: [...value].map((item, index) => this.visit(item, `${path}.values[${index}]`, seen)),\n }\n\n return this.serializeObject(object, path, seen)\n }\n\n private serializeError(\n error: Error,\n path: string,\n seen: WeakMap<object, string>,\n ): { [key: string]: JsonValue } {\n const result: { [key: string]: JsonValue } = { name: error.name, message: error.message }\n if (error.stack) result.stack = error.stack\n if ('cause' in error && error.cause !== undefined)\n result.cause = this.visit(error.cause, `${path}.cause`, seen)\n Object.assign(result, this.serializeObject(error, path, seen))\n return result\n }\n\n private serializeObject(\n object: object,\n path: string,\n seen: WeakMap<object, string>,\n ): { [key: string]: JsonValue } {\n const result: { [key: string]: JsonValue } = {}\n let keys: string[]\n try {\n keys = Object.keys(object).sort()\n } catch (error) {\n return { $error: inaccessible(error) }\n }\n for (const key of keys) {\n try {\n result[key] = this.visit((object as Record<string, unknown>)[key], `${path}.${key}`, seen)\n } catch (error) {\n result[key] = inaccessible(error)\n }\n }\n return result\n }\n}\n\nexport const logSerializer = new LogSerializer()\n\nexport const serializeLogArguments = (values: readonly unknown[]) =>\n logSerializer.serializeArguments(values)","export const LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error'] as const\n\nexport type LogLevel = (typeof LOG_LEVELS)[number]\n\nexport interface LogEntry {\n /** ISO-8601 timestamp generated at the call site. */\n timestamp: string\n scope: string\n level: LogLevel\n /** A cycle-safe, JSON-like rendering of all arguments. */\n content: string\n}\n\nexport interface LogFileInfo {\n name: string\n path: string\n size: number\n modifiedAt: string\n archived: boolean\n}\n\nexport interface LogFileContent {\n path: string\n content: string\n size: number\n /** True when only the most recent portion of a large file was returned. */\n truncated: boolean\n}\n\nexport interface ExportLogsOptions {\n /** Omit to export every active and archived log file. */\n paths?: string[]\n}\n\nexport type Invoke = (command: string, args?: Record<string, unknown>) => Promise<unknown>\n\nexport interface LoggerClient {\n readonly native: boolean\n write(entries: readonly LogEntry[]): void\n flush(): Promise<void>\n listLogFiles(): Promise<LogFileInfo[]>\n readLogFile(path: string): Promise<LogFileContent>\n exportLogs(options?: ExportLogsOptions): Promise<string>\n dispose(): Promise<void>\n}\n\nexport interface LoggerOptions {\n /** Defaults to `trace` in development and `info` in production. */\n minLevel?: LogLevel\n /** Maximum entries sent in one invoke call. */\n batchSize?: number\n /** Maximum delay before a partial batch is sent. */\n flushIntervalMs?: number\n /** Bounds memory use if the native side becomes unavailable. */\n maxQueueSize?: number\n /** Supplies an invoke implementation for alternate hosts and tests. */\n invoke?: Invoke\n /** Override automatic Tauri detection. */\n native?: boolean\n /** Captures `error` and `unhandledrejection` events. Defaults to true. */\n captureErrors?: boolean\n /** Flushes on page hide/visibility changes. Defaults to true. */\n flushOnLifecycle?: boolean\n}","import { TauriLoggerClient } from './client'\nimport { serializeLogArguments } from './serializer'\nimport {\n LOG_LEVELS,\n type ExportLogsOptions,\n type LogEntry,\n type LogFileContent,\n type LogFileInfo,\n type LogLevel,\n type LoggerClient,\n type LoggerOptions,\n} from './types'\n\ntype ConsoleMethod = 'debug' | 'error' | 'info' | 'log' | 'trace' | 'warn'\ntype ConsoleWriter = (...values: unknown[]) => void\n\nconst CONSOLE_LEVEL: Record<ConsoleMethod, LogLevel> = {\n debug: 'debug',\n error: 'error',\n info: 'info',\n log: 'info',\n trace: 'trace',\n warn: 'warn',\n}\n\nconst LEVEL_CONSOLE: Record<LogLevel, ConsoleMethod> = {\n trace: 'trace',\n debug: 'debug',\n info: 'info',\n warn: 'warn',\n error: 'error',\n}\n\nconst isDevelopment = (): boolean => {\n const env = (import.meta as ImportMeta & { env?: { DEV?: boolean; PROD?: boolean } }).env\n return env?.DEV === true || env?.PROD === false\n}\n\nconst levelValue = (level: LogLevel) => LOG_LEVELS.indexOf(level)\n\nconst resolveMinLevel = (requested?: LogLevel): LogLevel => {\n const level = requested ?? (isDevelopment() ? 'trace' : 'info')\n return !isDevelopment() && levelValue(level) < levelValue('info') ? 'info' : level\n}\n\nconst normalizeScope = (scope: string) => scope.trim() || 'app'\n\nconst joinScope = (parent: string, child: string) => {\n const normalizedChild = child.trim()\n return normalizedChild ? `${parent}:${normalizedChild}` : parent\n}\n\nconst pad = (value: number) => String(value).padStart(2, '0')\n\nexport const formatLogEntry = (entry: LogEntry): string => {\n const date = new Date(entry.timestamp)\n const timestamp = Number.isNaN(date.getTime())\n ? entry.timestamp\n : `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`\n return `[${timestamp}] (${entry.scope}) ${entry.level} > ${entry.content}`\n}\n\ninterface LoggerRuntime {\n client: LoggerClient\n minLevel: LogLevel\n consoleWriters: Record<ConsoleMethod, ConsoleWriter>\n cleanup: Set<() => void>\n consoleRestore?: () => void\n errorCaptureInstalled: boolean\n lifecycleInstalled: boolean\n}\n\n/** Scoped logger shared by the app, plugins, and regular web builds. */\nexport class Logger {\n private readonly runtime: LoggerRuntime\n\n constructor(\n public readonly scope = 'app',\n options: LoggerOptions = {},\n ) {\n this.scope = normalizeScope(scope)\n this.runtime = {\n client: new TauriLoggerClient(options),\n minLevel: resolveMinLevel(options.minLevel),\n consoleWriters: this.captureConsole(),\n cleanup: new Set(),\n errorCaptureInstalled: false,\n lifecycleInstalled: false,\n }\n if (options.captureErrors !== false) this.captureGlobalErrors()\n if (options.flushOnLifecycle !== false) this.flushOnLifecycle()\n }\n\n private static fromRuntime(scope: string, runtime: LoggerRuntime): Logger {\n const logger = Object.create(Logger.prototype) as Logger\n Object.defineProperty(logger, 'scope', { enumerable: true, value: scope })\n Object.defineProperty(logger, 'runtime', { value: runtime })\n return logger\n }\n\n public get client(): LoggerClient {\n return this.runtime.client\n }\n\n public get minLevel(): LogLevel {\n return this.runtime.minLevel\n }\n\n public set minLevel(level: LogLevel) {\n this.runtime.minLevel = resolveMinLevel(level)\n }\n\n public scoped(scope: string): Logger {\n return Logger.fromRuntime(joinScope(this.scope, scope), this.runtime)\n }\n\n public group(scope: string): Logger {\n return this.scoped(scope)\n }\n\n public trace(...values: unknown[]): void {\n this.emit('trace', values)\n }\n\n public debug(...values: unknown[]): void {\n this.emit('debug', values)\n }\n\n public info(...values: unknown[]): void {\n this.emit('info', values)\n }\n\n public warn(...values: unknown[]): void {\n this.emit('warn', values)\n }\n\n public error(...values: unknown[]): void {\n this.emit('error', values)\n }\n\n /** Copies console output into this logger while retaining the original output. */\n public proxyConsole(): () => void {\n if (this.runtime.consoleRestore) return this.runtime.consoleRestore\n if (typeof console === 'undefined') return () => undefined\n let writing = false\n const restores: Array<() => void> = []\n for (const method of Object.keys(CONSOLE_LEVEL) as ConsoleMethod[]) {\n const previous = console[method] as ConsoleWriter\n const original = this.runtime.consoleWriters[method]\n const proxy = (...values: unknown[]) => {\n original(...values)\n if (writing) return\n writing = true\n try {\n this.emit(CONSOLE_LEVEL[method], values, false)\n } finally {\n writing = false\n }\n }\n console[method] = proxy as (typeof console)[typeof method]\n restores.push(() => {\n if (console[method] === proxy) console[method] = previous as never\n })\n }\n const restore = () => {\n for (const callback of restores) callback()\n if (this.runtime.consoleRestore === restore) this.runtime.consoleRestore = undefined\n }\n this.runtime.consoleRestore = restore\n return restore\n }\n\n public captureGlobalErrors(): () => void {\n if (this.runtime.errorCaptureInstalled || typeof window === 'undefined') return () => undefined\n const onError = (event: ErrorEvent) => {\n this.emit('error', ['Uncaught error', event.error ?? event.message], true)\n }\n const onUnhandledRejection = (event: PromiseRejectionEvent) => {\n this.emit('error', ['Unhandled promise rejection', event.reason], true)\n }\n window.addEventListener('error', onError)\n window.addEventListener('unhandledrejection', onUnhandledRejection)\n this.runtime.errorCaptureInstalled = true\n const cleanup = () => {\n window.removeEventListener('error', onError)\n window.removeEventListener('unhandledrejection', onUnhandledRejection)\n this.runtime.errorCaptureInstalled = false\n this.runtime.cleanup.delete(cleanup)\n }\n this.runtime.cleanup.add(cleanup)\n return cleanup\n }\n\n public flushOnLifecycle(): () => void {\n if (this.runtime.lifecycleInstalled || typeof window === 'undefined') return () => undefined\n const flush = () => void this.flush()\n const onVisibilityChange = () => {\n if (document.visibilityState === 'hidden') flush()\n }\n window.addEventListener('pagehide', flush)\n window.addEventListener('beforeunload', flush)\n document.addEventListener('visibilitychange', onVisibilityChange)\n this.runtime.lifecycleInstalled = true\n const cleanup = () => {\n window.removeEventListener('pagehide', flush)\n window.removeEventListener('beforeunload', flush)\n document.removeEventListener('visibilitychange', onVisibilityChange)\n this.runtime.lifecycleInstalled = false\n this.runtime.cleanup.delete(cleanup)\n }\n this.runtime.cleanup.add(cleanup)\n return cleanup\n }\n\n public flush(): Promise<void> {\n return this.runtime.client.flush()\n }\n\n public async dispose(): Promise<void> {\n this.runtime.consoleRestore?.()\n for (const cleanup of this.runtime.cleanup) cleanup()\n await this.runtime.client.dispose()\n }\n\n private emit(level: LogLevel, values: readonly unknown[], mirrorConsole = true): void {\n if (levelValue(level) < levelValue(this.runtime.minLevel)) return\n const entry: LogEntry = {\n content: serializeLogArguments(values),\n level,\n scope: this.scope,\n timestamp: new Date().toISOString(),\n }\n if (mirrorConsole) {\n const writer = this.runtime.consoleWriters[LEVEL_CONSOLE[level]]\n writer(formatLogEntry(entry))\n }\n this.runtime.client.write([entry])\n }\n\n private captureConsole(): Record<ConsoleMethod, ConsoleWriter> {\n const noop = () => undefined\n if (typeof console === 'undefined')\n return { debug: noop, error: noop, info: noop, log: noop, trace: noop, warn: noop }\n return {\n debug: console.debug.bind(console),\n error: console.error.bind(console),\n info: console.info.bind(console),\n log: console.log.bind(console),\n trace: console.trace.bind(console),\n warn: console.warn.bind(console),\n }\n }\n}\n\nexport const createLogger = (scope = 'app', options: LoggerOptions = {}) =>\n new Logger(scope, options)\n\n/** Shared queue used by application modules and third-party plugins. */\nexport const logger = createLogger('delta-comic', { captureErrors: false, flushOnLifecycle: false })\n\n/** Typed native reader/export client sharing the global logger transport. */\nexport const loggerClient = logger.client\n\nexport const listLogFiles = (): Promise<LogFileInfo[]> => loggerClient.listLogFiles()\n\nexport const readLogFile = (path: string): Promise<LogFileContent> => loggerClient.readLogFile(path)\n\nexport const exportLogs = (options?: ExportLogsOptions): Promise<string> =>\n loggerClient.exportLogs(options)\n\nconst globalInstallations = new WeakMap<Logger, () => void>()\n\n/** Installs console/error/lifecycle capture once for the shared app logger. */\nexport const installGlobalLogger = (target: Logger = logger): (() => void) => {\n const existing = globalInstallations.get(target)\n if (existing) return existing\n const cleanups = [target.proxyConsole(), target.captureGlobalErrors(), target.flushOnLifecycle()]\n const uninstall = () => {\n for (const cleanup of [...cleanups].reverse()) cleanup()\n globalInstallations.delete(target)\n }\n globalInstallations.set(target, uninstall)\n return uninstall\n}"],"mappings":"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"}
|