@mugenlabs/logtape-devtools 0.2.1 → 0.4.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/README.md +193 -20
- package/assets/panel.png +0 -0
- package/dist/chunk-FDWUISCJ.mjs +300 -0
- package/dist/chunk-FDWUISCJ.mjs.map +1 -0
- package/dist/index.d.ts +36 -74
- package/dist/index.mjs +408 -547
- package/dist/index.mjs.map +1 -1
- package/dist/sink.d.ts +140 -0
- package/dist/sink.mjs +13 -0
- package/dist/sink.mjs.map +1 -0
- package/package.json +47 -26
package/README.md
CHANGED
|
@@ -4,13 +4,17 @@ A [TanStack DevTools](https://tanstack.com/devtools) plugin for inspecting [LogT
|
|
|
4
4
|
|
|
5
5
|
Filter, search, and inspect structured logs in real time — without leaving the browser DevTools overlay.
|
|
6
6
|
|
|
7
|
+

|
|
8
|
+
|
|
7
9
|
## Features
|
|
8
10
|
|
|
9
11
|
- **Live log streaming** — see logs as they happen
|
|
10
12
|
- **Level filtering** — toggle trace, debug, info, warning, error, fatal
|
|
11
13
|
- **Category search** — filter by logger category
|
|
12
|
-
- **Structured inspection** — expand logs to view properties and
|
|
14
|
+
- **Structured inspection** — expand logs to view properties and source location
|
|
13
15
|
- **Pause & resume** — freeze the log stream to inspect entries
|
|
16
|
+
- **Bounded memory** — a ring buffer keeps a fixed number of records
|
|
17
|
+
- **Cheap when closed** — notifications are batched, the list is virtualized, and stack capture is skipped while no panel is subscribed
|
|
14
18
|
|
|
15
19
|
## Installation
|
|
16
20
|
|
|
@@ -20,63 +24,232 @@ pnpm add @mugenlabs/logtape-devtools @logtape/logtape @tanstack/react-devtools
|
|
|
20
24
|
|
|
21
25
|
## Quick Start
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
`createLogTapeDevtools()` returns a sink and a panel plugin already wired to the same store. This is the recommended path.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { configure, getLogger } from "@logtape/logtape";
|
|
31
|
+
import { createLogTapeDevtools } from "@mugenlabs/logtape-devtools";
|
|
32
|
+
import { TanStackDevtools } from "@tanstack/react-devtools";
|
|
33
|
+
|
|
34
|
+
const { sink, plugin } = createLogTapeDevtools();
|
|
35
|
+
|
|
36
|
+
await configure({
|
|
37
|
+
sinks: { devtools: sink },
|
|
38
|
+
loggers: [{ category: [], lowestLevel: "trace", sinks: ["devtools"] }],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
getLogger(["app", "auth"]).info("User {username} logged in", { username: "john_doe" });
|
|
42
|
+
|
|
43
|
+
function App() {
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<YourApp />
|
|
47
|
+
<TanStackDevtools plugins={[plugin]} />
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Advanced: two factories with an explicit store
|
|
54
|
+
|
|
55
|
+
Use the individual factories when you need to hold on to the store — for example to clear it from a test, to run several isolated stores, or to configure LogTape in a file that must not import React.
|
|
24
56
|
|
|
25
57
|
```ts
|
|
58
|
+
// logging.ts
|
|
26
59
|
import { configure } from "@logtape/logtape";
|
|
27
|
-
import { createDevtoolsSink } from "@mugenlabs/logtape-devtools";
|
|
60
|
+
import { createDevtoolsSink, createLogStore } from "@mugenlabs/logtape-devtools";
|
|
61
|
+
|
|
62
|
+
export const logStore = createLogStore({ maxRecords: 5000 });
|
|
28
63
|
|
|
29
64
|
await configure({
|
|
30
|
-
sinks: { devtools: createDevtoolsSink() },
|
|
31
|
-
loggers: [{ category: ["app"],
|
|
65
|
+
sinks: { devtools: createDevtoolsSink({ store: logStore }) },
|
|
66
|
+
loggers: [{ category: ["app"], lowestLevel: "debug", sinks: ["devtools"] }],
|
|
32
67
|
});
|
|
33
68
|
```
|
|
34
69
|
|
|
35
|
-
### 2. Add the DevTools plugin
|
|
36
|
-
|
|
37
70
|
```tsx
|
|
38
|
-
|
|
71
|
+
// app.tsx
|
|
39
72
|
import { createLogTapeDevtoolsPlugin } from "@mugenlabs/logtape-devtools";
|
|
73
|
+
import { TanStackDevtools } from "@tanstack/react-devtools";
|
|
74
|
+
import { logStore } from "./logging";
|
|
40
75
|
|
|
41
|
-
const
|
|
76
|
+
const plugin = createLogTapeDevtoolsPlugin({ store: logStore });
|
|
42
77
|
|
|
43
|
-
function App() {
|
|
78
|
+
export function App() {
|
|
79
|
+
return <TanStackDevtools plugins={[plugin]} />;
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The sink and the plugin must be given **the same** store instance, otherwise the panel stays empty.
|
|
84
|
+
|
|
85
|
+
### React-free subpath: `@mugenlabs/logtape-devtools/sink`
|
|
86
|
+
|
|
87
|
+
The package root pulls in React (the panel is a React component). If your LogTape configuration is shared with a server entry point, a worker, or any non-React bundle, import from `@mugenlabs/logtape-devtools/sink` instead — it exports the sink, the store, and the types with no React dependency.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { createDevtoolsSink, createLogStore } from "@mugenlabs/logtape-devtools/sink";
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Exported from `/sink`: `createDevtoolsSink`, `createLogStore`, `defaultLogStore`, `LOG_LEVELS`, and the types `DevtoolsSink`, `DevtoolsSinkOptions`, `LogStore`, `LogStoreOptions`, `DevtoolsLogRecord`, `LogLevel`.
|
|
94
|
+
|
|
95
|
+
## Production
|
|
96
|
+
|
|
97
|
+
The panel is a development tool. Guard the mount so bundlers can tree-shake it — and `@tanstack/react-devtools` along with it — out of production builds:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { lazy, Suspense } from "react";
|
|
101
|
+
|
|
102
|
+
const Devtools = import.meta.env.DEV
|
|
103
|
+
? lazy(() => import("./devtools"))
|
|
104
|
+
: () => null;
|
|
105
|
+
|
|
106
|
+
export function App() {
|
|
44
107
|
return (
|
|
45
108
|
<>
|
|
46
|
-
|
|
47
|
-
<
|
|
109
|
+
<YourApp />
|
|
110
|
+
<Suspense fallback={null}>
|
|
111
|
+
<Devtools />
|
|
112
|
+
</Suspense>
|
|
48
113
|
</>
|
|
49
114
|
);
|
|
50
115
|
}
|
|
51
116
|
```
|
|
52
117
|
|
|
118
|
+
```tsx
|
|
119
|
+
// devtools.tsx — only ever imported in development
|
|
120
|
+
import { createLogTapeDevtoolsPlugin } from "@mugenlabs/logtape-devtools";
|
|
121
|
+
import { TanStackDevtools } from "@tanstack/react-devtools";
|
|
122
|
+
import { logStore } from "./logging";
|
|
123
|
+
|
|
124
|
+
// Create the plugin once, outside the component, so it is not re-created on every render.
|
|
125
|
+
const plugin = createLogTapeDevtoolsPlugin({ store: logStore });
|
|
126
|
+
|
|
127
|
+
export default function Devtools() {
|
|
128
|
+
return <TanStackDevtools plugins={[plugin]} />;
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
With webpack or other bundlers, `process.env.NODE_ENV !== "production"` works the same way — the important part is that the `import()` sits behind a statically analyzable flag.
|
|
133
|
+
|
|
134
|
+
Two related behaviours make the sink itself cheap to leave in place:
|
|
135
|
+
|
|
136
|
+
- **Stack capture auto-disables in production.** When `process.env.NODE_ENV === "production"`, `captureStackTrace` is ignored (minified bundles produce meaningless file and line references, and browsers do not apply source maps to `Error.stack`). Set `forceStackTrace: true` to override.
|
|
137
|
+
- **Stack capture is skipped while no panel is open.** The sink checks `store.hasListeners()` on every record and does no stack work while nothing is subscribed. Message text is rendered lazily, so an unmounted panel costs one property clone per record.
|
|
138
|
+
|
|
139
|
+
Records are still buffered in memory when no panel is mounted. If you do not want that in production either, keep the sink out of your production LogTape configuration, or pass `createLogStore({ maxRecords: 0 })` — the sink then returns before doing any work at all.
|
|
140
|
+
|
|
53
141
|
## API
|
|
54
142
|
|
|
143
|
+
### `createLogTapeDevtools(options?)`
|
|
144
|
+
|
|
145
|
+
Creates a LogTape sink and its matching DevTools plugin, both wired to the same store. Returns `{ sink, plugin }`.
|
|
146
|
+
|
|
147
|
+
| Option | Type | Default | Description |
|
|
148
|
+
| --- | --- | --- | --- |
|
|
149
|
+
| `sink` | `Omit<DevtoolsSinkOptions, "store">` | `{}` | Sink options; the shared store is wired automatically |
|
|
150
|
+
| `plugin` | `Omit<LogTapeDevtoolsPluginOptions, "store">` | `{}` | Plugin options; the shared store is wired automatically |
|
|
151
|
+
| `store` | `LogStore` | `defaultLogStore` | Store shared by the sink and the plugin |
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const { sink, plugin } = createLogTapeDevtools({
|
|
155
|
+
sink: { captureStackTrace: true },
|
|
156
|
+
plugin: { defaultOpen: false, name: "Logs" },
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
55
160
|
### `createDevtoolsSink(options?)`
|
|
56
161
|
|
|
57
|
-
Creates a LogTape sink that forwards log records to the devtools store.
|
|
162
|
+
Creates a LogTape sink that forwards log records to the devtools store. Returns a `DevtoolsSink` — a LogTape `Sink` that is also `Disposable`, so LogTape can dispose it when its configuration is reset (a disposed sink stops writing).
|
|
58
163
|
|
|
59
164
|
| Option | Type | Default | Description |
|
|
60
165
|
| --- | --- | --- | --- |
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
166
|
+
| `captureStackTrace` | `boolean` | `true` | Capture the source location (`file:line:col`) of each log call by parsing `new Error().stack`. Experimental and engine-dependent. Ignored in production builds and skipped while no panel is subscribed, unless `forceStackTrace` is set |
|
|
167
|
+
| `forceStackTrace` | `boolean` | `false` | Capture stack traces even in production builds and while the panel is closed. Only has an effect when `captureStackTrace` is `true` |
|
|
168
|
+
| `store` | `LogStore` | `defaultLogStore` | Store to write records into. Must match the store given to the plugin |
|
|
64
169
|
|
|
65
170
|
### `createLogTapeDevtoolsPlugin(options?)`
|
|
66
171
|
|
|
67
|
-
Creates
|
|
172
|
+
Creates a TanStack DevTools plugin (`TanStackDevtoolsReactPlugin` from `@tanstack/react-devtools`) to pass to `<TanStackDevtools plugins={[...]} />`. Create it once at module scope rather than inside a component.
|
|
68
173
|
|
|
69
174
|
| Option | Type | Default | Description |
|
|
70
175
|
| --- | --- | --- | --- |
|
|
71
|
-
| `
|
|
176
|
+
| `defaultOpen` | `boolean` | `true` | Whether the LogTape panel starts expanded |
|
|
177
|
+
| `name` | `string` | `"LogTape"` | Display name shown in the DevTools tab bar |
|
|
178
|
+
| `store` | `LogStore` | `defaultLogStore` | Store to read records from. Must match the store given to the sink |
|
|
72
179
|
|
|
73
180
|
### `createLogStore(options?)`
|
|
74
181
|
|
|
75
|
-
Creates
|
|
182
|
+
Creates an isolated log store. Records live in a fixed-capacity ring buffer, so appending is O(1) and the oldest record is evicted once the buffer is full. Listener notifications are coalesced into a microtask, so a synchronous burst of log calls triggers a single re-render.
|
|
76
183
|
|
|
77
184
|
| Option | Type | Default | Description |
|
|
78
185
|
| --- | --- | --- | --- |
|
|
79
|
-
| `maxRecords` | `number` | `1000` | Maximum records to retain |
|
|
186
|
+
| `maxRecords` | `number` | `1000` | Maximum number of records to retain. `0` disables buffering entirely |
|
|
187
|
+
|
|
188
|
+
The returned `LogStore` exposes:
|
|
189
|
+
|
|
190
|
+
| Member | Signature | Description |
|
|
191
|
+
| --- | --- | --- |
|
|
192
|
+
| `addRecord` | `(record: DevtoolsLogRecord) => void` | Appends a record, evicting the oldest when full |
|
|
193
|
+
| `clear` | `() => void` | Removes every record and notifies subscribers |
|
|
194
|
+
| `getSnapshot` | `() => DevtoolsLogRecord[]` | Current records, oldest first. Reference is stable between mutations (safe for `useSyncExternalStore`) |
|
|
195
|
+
| `getMaxRecords` | `() => number` | Current retention limit. Optional for custom stores; when it returns `0` the sink skips all work |
|
|
196
|
+
| `hasListeners` | `() => boolean` | Whether anything is currently subscribed |
|
|
197
|
+
| `setMaxSize` | `(size: number) => void` | Updates the retention limit, trimming if needed |
|
|
198
|
+
| `subscribe` | `(listener: () => void) => () => void` | Registers a change listener; returns an unsubscribe function |
|
|
199
|
+
|
|
200
|
+
> **Breaking change in 0.2:** custom `LogStore` implementations must now provide `hasListeners()`.
|
|
201
|
+
|
|
202
|
+
### `defaultLogStore`
|
|
203
|
+
|
|
204
|
+
The shared `LogStore` instance (created with the default `maxRecords: 1000`) used whenever no explicit store is passed.
|
|
205
|
+
|
|
206
|
+
### Types
|
|
207
|
+
|
|
208
|
+
`DevtoolsLogRecord` — a LogTape record normalized for display:
|
|
209
|
+
|
|
210
|
+
| Field | Type | Description |
|
|
211
|
+
| --- | --- | --- |
|
|
212
|
+
| `id` | `string` | Unique identifier, stable for the record's lifetime |
|
|
213
|
+
| `timestamp` | `number` | Creation time in ms since the Unix epoch |
|
|
214
|
+
| `level` | `LogLevel` | Severity of the record |
|
|
215
|
+
| `category` | `string[]` | Logger category path, e.g. `["app", "auth"]` |
|
|
216
|
+
| `message` | `unknown[]` | Raw message parts, alternating literals and interpolated values |
|
|
217
|
+
| `messageText` | `string` | Message parts rendered into one searchable string (computed lazily on first access) |
|
|
218
|
+
| `properties` | `Record<string, unknown>` | Structured properties, deep-cloned for safety |
|
|
219
|
+
| `caller` | `string \| undefined` | Source location, present only when stack capture ran |
|
|
220
|
+
|
|
221
|
+
`LogLevel` — `"trace" | "debug" | "info" | "warning" | "error" | "fatal"`, also available as the ordered array `LOG_LEVELS`.
|
|
222
|
+
|
|
223
|
+
## Troubleshooting
|
|
224
|
+
|
|
225
|
+
**No logs appear in the panel.**
|
|
226
|
+
|
|
227
|
+
- Check that the sink is actually referenced by a logger. Registering it under `sinks` is not enough — every logger you care about needs it in its own `sinks` array. `{ category: [], lowestLevel: "trace", sinks: ["devtools"] }` catches everything.
|
|
228
|
+
- Check `lowestLevel`. LogTape filters below it before the sink ever runs, so `lowestLevel: "info"` means `logger.debug(...)` never reaches the panel.
|
|
229
|
+
- Check for a store mismatch. If you pass a custom store, the *same instance* must go to both `createDevtoolsSink` and `createLogTapeDevtoolsPlugin`. Passing it to only one of them leaves the panel reading an empty store. `createLogTapeDevtools()` avoids this by wiring both for you.
|
|
230
|
+
- Watch out for duplicate module instances (two copies of the package in the dependency tree, or a dev server that re-evaluates the module) — `defaultLogStore` is per module instance.
|
|
231
|
+
|
|
232
|
+
**Source locations (`caller`) are missing.**
|
|
233
|
+
|
|
234
|
+
- In production builds stack capture is disabled by design. Pass `forceStackTrace: true` if your build is unminified and you want it anyway.
|
|
235
|
+
- With the panel closed, capture is skipped because nothing is subscribed. Open the panel and log again, or pass `forceStackTrace: true`.
|
|
236
|
+
- Stack parsing depends on the JS engine's `Error.stack` format. Frames matching `logtape` are skipped; if your own wrapper module has "logtape" in its path, its frames are skipped too and you may see the wrong location.
|
|
237
|
+
|
|
238
|
+
**Several sinks writing into one store.**
|
|
239
|
+
|
|
240
|
+
- This is supported. Each sink generates ids with its own random discriminator, so two sinks sharing a store cannot collide even on identical timestamps.
|
|
241
|
+
- Records from all sinks interleave in one list. Give the loggers distinct categories if you need to tell them apart, and remember `maxRecords` is a budget for the store as a whole.
|
|
242
|
+
|
|
243
|
+
## Compatibility
|
|
244
|
+
|
|
245
|
+
| Peer dependency | Range | Notes |
|
|
246
|
+
| --- | --- | --- |
|
|
247
|
+
| `@logtape/logtape` | `>=2.0.0` | **Optional** — only types are imported; you need it to configure LogTape anyway |
|
|
248
|
+
| `react` | `^18.0.0 \|\| ^19.0.0` | Required for the panel |
|
|
249
|
+
| `react-dom` | `^18.0.0 \|\| ^19.0.0` | Required for the panel |
|
|
250
|
+
| `@tanstack/react-devtools` | `>=0.9.0` | **Optional** — only needed to host the panel |
|
|
251
|
+
|
|
252
|
+
Node `>=18`. The package is published as ESM only (`import` condition, no `require()`). `@tanstack/react-devtools` being optional means you can depend on this package purely for the sink (via `@mugenlabs/logtape-devtools/sink`) without installing the DevTools shell.
|
|
80
253
|
|
|
81
254
|
## Demo
|
|
82
255
|
|
package/assets/panel.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// src/store.ts
|
|
2
|
+
var DEFAULT_MAX_RECORDS = 1e3;
|
|
3
|
+
function normalizeMaxRecords(value) {
|
|
4
|
+
const numeric = Math.floor(Number(value ?? DEFAULT_MAX_RECORDS));
|
|
5
|
+
return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
|
|
6
|
+
}
|
|
7
|
+
function schedule(callback) {
|
|
8
|
+
if (typeof queueMicrotask === "function") {
|
|
9
|
+
queueMicrotask(callback);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
Promise.resolve().then(callback);
|
|
13
|
+
}
|
|
14
|
+
function createLogStore(options) {
|
|
15
|
+
let max = normalizeMaxRecords(options?.maxRecords);
|
|
16
|
+
let buffer = [];
|
|
17
|
+
let start = 0;
|
|
18
|
+
let size = 0;
|
|
19
|
+
let snapshot = [];
|
|
20
|
+
let snapshotDirty = false;
|
|
21
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
22
|
+
let notifyScheduled = false;
|
|
23
|
+
function emit() {
|
|
24
|
+
for (const listener of listeners) {
|
|
25
|
+
listener();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function notify() {
|
|
29
|
+
if (notifyScheduled) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
notifyScheduled = true;
|
|
33
|
+
schedule(() => {
|
|
34
|
+
notifyScheduled = false;
|
|
35
|
+
emit();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function toArray() {
|
|
39
|
+
const result = new Array(size);
|
|
40
|
+
for (let i = 0; i < size; i += 1) {
|
|
41
|
+
result[i] = buffer[(start + i) % buffer.length];
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
function normalize() {
|
|
46
|
+
const trimmed = size > max;
|
|
47
|
+
const kept = trimmed ? toArray().slice(size - max) : toArray();
|
|
48
|
+
buffer = kept;
|
|
49
|
+
start = 0;
|
|
50
|
+
size = kept.length;
|
|
51
|
+
return trimmed;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
addRecord: (record) => {
|
|
55
|
+
if (max === 0) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (size < max) {
|
|
59
|
+
buffer.push(record);
|
|
60
|
+
size += 1;
|
|
61
|
+
} else {
|
|
62
|
+
buffer[start] = record;
|
|
63
|
+
start = (start + 1) % buffer.length;
|
|
64
|
+
}
|
|
65
|
+
snapshotDirty = true;
|
|
66
|
+
notify();
|
|
67
|
+
},
|
|
68
|
+
clear: () => {
|
|
69
|
+
buffer = [];
|
|
70
|
+
start = 0;
|
|
71
|
+
size = 0;
|
|
72
|
+
snapshotDirty = true;
|
|
73
|
+
notify();
|
|
74
|
+
},
|
|
75
|
+
getMaxRecords: () => max,
|
|
76
|
+
getSnapshot: () => {
|
|
77
|
+
if (snapshotDirty) {
|
|
78
|
+
snapshot = toArray();
|
|
79
|
+
snapshotDirty = false;
|
|
80
|
+
}
|
|
81
|
+
return snapshot;
|
|
82
|
+
},
|
|
83
|
+
hasListeners: () => listeners.size > 0,
|
|
84
|
+
setMaxSize: (nextMax) => {
|
|
85
|
+
max = normalizeMaxRecords(nextMax);
|
|
86
|
+
if (normalize()) {
|
|
87
|
+
snapshotDirty = true;
|
|
88
|
+
notify();
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
subscribe: (listener) => {
|
|
92
|
+
listeners.add(listener);
|
|
93
|
+
return () => {
|
|
94
|
+
listeners.delete(listener);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
var defaultLogStore = createLogStore();
|
|
100
|
+
|
|
101
|
+
// src/types.ts
|
|
102
|
+
var LOG_LEVELS = ["trace", "debug", "info", "warning", "error", "fatal"];
|
|
103
|
+
|
|
104
|
+
// src/safe-json.ts
|
|
105
|
+
function toSerializable(value) {
|
|
106
|
+
switch (typeof value) {
|
|
107
|
+
case "bigint":
|
|
108
|
+
return `${value}n`;
|
|
109
|
+
case "symbol":
|
|
110
|
+
return value.toString();
|
|
111
|
+
case "function":
|
|
112
|
+
return `[Function ${value.name || "anonymous"}]`;
|
|
113
|
+
default:
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (value instanceof Error) {
|
|
117
|
+
const error = { message: value.message, name: value.name };
|
|
118
|
+
if (value.stack) {
|
|
119
|
+
error.stack = value.stack;
|
|
120
|
+
}
|
|
121
|
+
if ("cause" in value && value.cause !== void 0) {
|
|
122
|
+
error.cause = value.cause;
|
|
123
|
+
}
|
|
124
|
+
return error;
|
|
125
|
+
}
|
|
126
|
+
if (value instanceof Map) {
|
|
127
|
+
return [...value.entries()];
|
|
128
|
+
}
|
|
129
|
+
if (value instanceof Set) {
|
|
130
|
+
return [...value];
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
function createReplacer() {
|
|
135
|
+
const ancestors = [];
|
|
136
|
+
return function replace(_key, value) {
|
|
137
|
+
while (ancestors.length > 0 && ancestors.at(-1)?.serialized !== this) {
|
|
138
|
+
ancestors.pop();
|
|
139
|
+
}
|
|
140
|
+
if (typeof value === "object" && value !== null) {
|
|
141
|
+
if (ancestors.some((ancestor) => ancestor.original === value)) {
|
|
142
|
+
return "[Circular]";
|
|
143
|
+
}
|
|
144
|
+
const serialized = toSerializable(value);
|
|
145
|
+
if (typeof serialized === "object" && serialized !== null) {
|
|
146
|
+
ancestors.push({ original: value, serialized });
|
|
147
|
+
}
|
|
148
|
+
return serialized;
|
|
149
|
+
}
|
|
150
|
+
return toSerializable(value);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function safeStringify(value, indent) {
|
|
154
|
+
try {
|
|
155
|
+
const json = JSON.stringify(value, createReplacer(), indent);
|
|
156
|
+
if (json !== void 0) {
|
|
157
|
+
return json;
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
return String(value);
|
|
162
|
+
}
|
|
163
|
+
function jsonClone(value) {
|
|
164
|
+
return JSON.parse(safeStringify(value));
|
|
165
|
+
}
|
|
166
|
+
function safeCloneProperties(properties) {
|
|
167
|
+
if (typeof structuredClone === "function") {
|
|
168
|
+
try {
|
|
169
|
+
return structuredClone(properties);
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
return jsonClone(properties);
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
const result = {};
|
|
178
|
+
for (const [key, entry] of Object.entries(properties)) {
|
|
179
|
+
try {
|
|
180
|
+
result[key] = jsonClone(entry);
|
|
181
|
+
} catch {
|
|
182
|
+
result[key] = String(entry);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/sink.ts
|
|
189
|
+
function renderMessage(message) {
|
|
190
|
+
return message.map((part) => {
|
|
191
|
+
if (typeof part === "string") {
|
|
192
|
+
return part;
|
|
193
|
+
}
|
|
194
|
+
if (part === null || part === void 0) {
|
|
195
|
+
return String(part);
|
|
196
|
+
}
|
|
197
|
+
return safeStringify(part);
|
|
198
|
+
}).join("");
|
|
199
|
+
}
|
|
200
|
+
var v8FrameRe = /at\s+(?:.*?\s+\()?(.+?):(\d+):(\d+)\)?$/;
|
|
201
|
+
var smFrameRe = /(?:^|@)(.+?):(\d+):(\d+)$/;
|
|
202
|
+
function captureCallerInfo() {
|
|
203
|
+
const { stack } = new Error();
|
|
204
|
+
if (!stack) {
|
|
205
|
+
return void 0;
|
|
206
|
+
}
|
|
207
|
+
const lines = stack.split("\n");
|
|
208
|
+
for (const line of lines) {
|
|
209
|
+
const trimmed = line.trim();
|
|
210
|
+
if (!trimmed || trimmed === "Error") {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (/logtape/i.test(trimmed)) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const match = v8FrameRe.exec(trimmed) ?? smFrameRe.exec(trimmed);
|
|
217
|
+
if (!match) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const [, filePath, lineNo, colNo] = match;
|
|
221
|
+
const fileName = filePath.replace(/[?#].*$/, "").split("/").pop() ?? filePath;
|
|
222
|
+
return `${fileName}:${lineNo}:${colNo}`;
|
|
223
|
+
}
|
|
224
|
+
return void 0;
|
|
225
|
+
}
|
|
226
|
+
function normalizeRecord(record, captureStack, id) {
|
|
227
|
+
const message = [...record.message];
|
|
228
|
+
const normalized = {
|
|
229
|
+
category: [...record.category],
|
|
230
|
+
id,
|
|
231
|
+
level: record.level,
|
|
232
|
+
message,
|
|
233
|
+
properties: safeCloneProperties(record.properties),
|
|
234
|
+
timestamp: record.timestamp
|
|
235
|
+
};
|
|
236
|
+
let messageText;
|
|
237
|
+
Object.defineProperty(normalized, "messageText", {
|
|
238
|
+
configurable: true,
|
|
239
|
+
enumerable: true,
|
|
240
|
+
get: () => {
|
|
241
|
+
messageText ??= renderMessage(message);
|
|
242
|
+
return messageText;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
if (captureStack) {
|
|
246
|
+
const caller = captureCallerInfo();
|
|
247
|
+
if (caller) {
|
|
248
|
+
normalized.caller = caller;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return normalized;
|
|
252
|
+
}
|
|
253
|
+
function isProductionEnv() {
|
|
254
|
+
try {
|
|
255
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
function createDevtoolsSink(options) {
|
|
263
|
+
const store = options?.store ?? defaultLogStore;
|
|
264
|
+
const wantsStack = options?.captureStackTrace ?? true;
|
|
265
|
+
const forceStack = options?.forceStackTrace ?? false;
|
|
266
|
+
const captureStack = wantsStack && (forceStack || !isProductionEnv());
|
|
267
|
+
const sinkId = Math.random().toString(36).slice(2, 8);
|
|
268
|
+
let counter = 0;
|
|
269
|
+
let disposed = false;
|
|
270
|
+
const sink = (record) => {
|
|
271
|
+
if (disposed || store.getMaxRecords?.() === 0) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const shouldCaptureStack = captureStack && (forceStack || store.hasListeners());
|
|
276
|
+
counter += 1;
|
|
277
|
+
const normalized = normalizeRecord(
|
|
278
|
+
record,
|
|
279
|
+
shouldCaptureStack,
|
|
280
|
+
`log-${sinkId}-${counter}-${record.timestamp}`
|
|
281
|
+
);
|
|
282
|
+
store.addRecord(normalized);
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
return Object.assign(sink, {
|
|
287
|
+
[Symbol.dispose]: () => {
|
|
288
|
+
disposed = true;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export {
|
|
294
|
+
createLogStore,
|
|
295
|
+
defaultLogStore,
|
|
296
|
+
safeStringify,
|
|
297
|
+
LOG_LEVELS,
|
|
298
|
+
createDevtoolsSink
|
|
299
|
+
};
|
|
300
|
+
//# sourceMappingURL=chunk-FDWUISCJ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store.ts","../src/types.ts","../src/safe-json.ts","../src/sink.ts"],"sourcesContent":["import type { DevtoolsLogRecord } from \"./types\";\n\ntype Listener = () => void;\n\n/**\n * Reactive log record container backing the devtools panel.\n *\n * Records are kept in a fixed-capacity ring buffer so appending is O(1); the\n * array returned by {@link LogStore.getSnapshot} is rebuilt lazily at most once\n * per mutation batch. Listener notifications are coalesced into a microtask, so\n * a synchronous burst of {@link LogStore.addRecord} calls results in a single\n * notification.\n */\nexport interface LogStore {\n /** Appends a record, evicting the oldest one when the max size is reached. */\n addRecord: (record: DevtoolsLogRecord) => void;\n /** Removes every record and notifies subscribers. */\n clear: () => void;\n /**\n * The current retention limit. `0` means the store discards everything, which\n * lets the sink skip normalisation entirely. Optional for custom stores.\n */\n getMaxRecords?: () => number;\n /**\n * Returns the current records, oldest first.\n *\n * The reference is stable until the store changes, which makes it safe to use\n * with `useSyncExternalStore`.\n */\n getSnapshot: () => DevtoolsLogRecord[];\n /**\n * Whether anything is currently subscribed to the store.\n *\n * Used by the sink to skip expensive work (such as stack trace capture) while\n * no devtools panel is mounted.\n */\n hasListeners: () => boolean;\n /** Updates the maximum number of retained records, trimming if needed. */\n setMaxSize: (size: number) => void;\n /** Registers a change listener. Returns an unsubscribe function. */\n subscribe: (listener: Listener) => () => void;\n}\n\n/** Options for {@link createLogStore}. */\nexport interface LogStoreOptions {\n /**\n * Maximum number of log records to retain. Older records are evicted first.\n *\n * @default 1000\n */\n maxRecords?: number;\n}\n\nconst DEFAULT_MAX_RECORDS = 1000;\n\n/**\n * Coerce a user-supplied capacity into a non-negative integer. Fractions would\n * defeat the `size < max` capacity check and NaN would wedge the ring buffer.\n */\nfunction normalizeMaxRecords(value: number | undefined): number {\n const numeric = Math.floor(Number(value ?? DEFAULT_MAX_RECORDS));\n return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;\n}\n\nfunction schedule(callback: () => void): void {\n if (typeof queueMicrotask === \"function\") {\n queueMicrotask(callback);\n return;\n }\n Promise.resolve().then(callback);\n}\n\n/**\n * Creates an isolated log store.\n *\n * Pass the same instance to `createDevtoolsSink` and\n * `createLogTapeDevtoolsPlugin` to keep the sink and the panel in sync.\n *\n * ```ts\n * const store = createLogStore({ maxRecords: 5000 });\n * ```\n */\nexport function createLogStore(options?: LogStoreOptions): LogStore {\n let max = normalizeMaxRecords(options?.maxRecords);\n\n // Ring buffer: `start` is the index of the oldest record, `size` the number\n // of live entries. Appending is O(1) — no per-record array copy.\n let buffer: DevtoolsLogRecord[] = [];\n let start = 0;\n let size = 0;\n\n let snapshot: DevtoolsLogRecord[] = [];\n let snapshotDirty = false;\n\n const listeners = new Set<Listener>();\n let notifyScheduled = false;\n\n function emit() {\n for (const listener of listeners) {\n listener();\n }\n }\n\n function notify() {\n if (notifyScheduled) {\n return;\n }\n notifyScheduled = true;\n schedule(() => {\n notifyScheduled = false;\n emit();\n });\n }\n\n /** Materializes the ring buffer into a plain, oldest-first array. */\n function toArray(): DevtoolsLogRecord[] {\n const result: DevtoolsLogRecord[] = new Array(size);\n for (let i = 0; i < size; i += 1) {\n result[i] = buffer[(start + i) % buffer.length];\n }\n return result;\n }\n\n /**\n * Rewrites the buffer in logical order, dropping records beyond `max`.\n * Keeps the invariant that a below-capacity buffer is linear (`start === 0`).\n */\n function normalize(): boolean {\n const trimmed = size > max;\n const kept = trimmed ? toArray().slice(size - max) : toArray();\n buffer = kept;\n start = 0;\n size = kept.length;\n return trimmed;\n }\n\n return {\n addRecord: (record) => {\n if (max === 0) {\n return;\n }\n if (size < max) {\n // Below capacity — the buffer is linear, so a plain append is enough.\n buffer.push(record);\n size += 1;\n } else {\n // At capacity — overwrite the oldest slot.\n buffer[start] = record;\n start = (start + 1) % buffer.length;\n }\n snapshotDirty = true;\n notify();\n },\n clear: () => {\n buffer = [];\n start = 0;\n size = 0;\n snapshotDirty = true;\n notify();\n },\n getMaxRecords: () => max,\n getSnapshot: () => {\n if (snapshotDirty) {\n snapshot = toArray();\n snapshotDirty = false;\n }\n return snapshot;\n },\n hasListeners: () => listeners.size > 0,\n setMaxSize: (nextMax) => {\n max = normalizeMaxRecords(nextMax);\n if (normalize()) {\n snapshotDirty = true;\n notify();\n }\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/** Default shared store instance used when no custom store is provided */\nexport const defaultLogStore = createLogStore();\n","/** Severity levels supported by LogTape, ordered from least to most severe. */\nexport type LogLevel = \"trace\" | \"debug\" | \"info\" | \"warning\" | \"error\" | \"fatal\";\n\n/** Every {@link LogLevel}, ordered from least to most severe. */\nexport const LOG_LEVELS: LogLevel[] = [\"trace\", \"debug\", \"info\", \"warning\", \"error\", \"fatal\"];\n\n/** A LogTape record normalized for display in the devtools panel. */\nexport interface DevtoolsLogRecord {\n /** Source location of the log call (only present when `captureStackTrace` is enabled). */\n caller?: string;\n /** Logger category path, e.g. `[\"app\", \"auth\"]`. */\n category: string[];\n /** Unique identifier, stable for the lifetime of the record. */\n id: string;\n /** Severity of the record. */\n level: LogLevel;\n /** Raw message parts, alternating literals and interpolated values. */\n message: unknown[];\n /** Message parts rendered into a single searchable string. */\n messageText: string;\n /** Structured properties attached to the log call, deep-cloned for safety. */\n properties: Record<string, unknown>;\n /** Creation time in milliseconds since the Unix epoch. */\n timestamp: number;\n}\n","/**\n * JSON helpers shared by the sink and the panel.\n *\n * This module is intentionally React-free so that it can be pulled into the\n * `@mugenlabs/logtape-devtools/sink` entry point.\n */\n\n/**\n * Map values JSON cannot represent (or would silently flatten to `{}`) onto a\n * readable equivalent. Anything else is returned untouched.\n */\nfunction toSerializable(value: unknown): unknown {\n switch (typeof value) {\n case \"bigint\":\n return `${value}n`;\n case \"symbol\":\n return value.toString();\n case \"function\":\n return `[Function ${value.name || \"anonymous\"}]`;\n default:\n break;\n }\n if (value instanceof Error) {\n const error: Record<string, unknown> = { message: value.message, name: value.name };\n if (value.stack) {\n error.stack = value.stack;\n }\n if (\"cause\" in value && value.cause !== undefined) {\n error.cause = value.cause;\n }\n return error;\n }\n if (value instanceof Map) {\n return [...value.entries()];\n }\n if (value instanceof Set) {\n return [...value];\n }\n return value;\n}\n\ninterface Ancestor {\n /** The value as it appeared in the source graph — used for cycle detection. */\n original: object;\n /** What JSON.stringify actually recursed into — used to unwind the stack. */\n serialized: object;\n}\n\n/**\n * Replacer that marks true cycles as `\"[Circular]\"` while leaving shared\n * (non-circular) references intact, and converts unrepresentable values via\n * {@link toSerializable}.\n */\nfunction createReplacer(): (this: unknown, key: string, value: unknown) => unknown {\n const ancestors: Ancestor[] = [];\n return function replace(this: unknown, _key, value) {\n // `this` is the object holding `key`; unwind to the current branch.\n while (ancestors.length > 0 && ancestors.at(-1)?.serialized !== this) {\n ancestors.pop();\n }\n if (typeof value === \"object\" && value !== null) {\n if (ancestors.some((ancestor) => ancestor.original === value)) {\n return \"[Circular]\";\n }\n const serialized = toSerializable(value);\n if (typeof serialized === \"object\" && serialized !== null) {\n ancestors.push({ original: value, serialized });\n }\n return serialized;\n }\n return toSerializable(value);\n };\n}\n\n/**\n * Serializes any value to JSON, never throwing.\n *\n * Errors, Maps, Sets, BigInts, symbols and functions are rendered in a readable\n * form instead of being dropped, circular references become `\"[Circular]\"`,\n * and anything still unserializable falls back to `String(value)`.\n */\nexport function safeStringify(value: unknown, indent?: number): string {\n try {\n const json = JSON.stringify(value, createReplacer(), indent);\n if (json !== undefined) {\n return json;\n }\n } catch {\n // A throwing `toJSON` or getter — fall through.\n }\n return String(value);\n}\n\nfunction jsonClone(value: unknown): unknown {\n return JSON.parse(safeStringify(value));\n}\n\n/**\n * Deep-clones a properties bag, never throwing.\n *\n * Uses `structuredClone` when available and degrades per entry (via a JSON\n * round trip, then `String(value)`) so that a single unclonable value cannot\n * drop the whole object.\n */\nexport function safeCloneProperties(properties: Record<string, unknown>): Record<string, unknown> {\n if (typeof structuredClone === \"function\") {\n try {\n return structuredClone(properties);\n } catch {\n // Functions, DOM nodes, proxies… fall through to the JSON path.\n }\n }\n\n try {\n return jsonClone(properties) as Record<string, unknown>;\n } catch {\n // Fall through to the per-entry path.\n }\n\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(properties)) {\n try {\n result[key] = jsonClone(entry);\n } catch {\n result[key] = String(entry);\n }\n }\n return result;\n}\n","import type { LogRecord, Sink } from \"@logtape/logtape\";\nimport { safeCloneProperties, safeStringify } from \"./safe-json\";\nimport { defaultLogStore, type LogStore } from \"./store\";\nimport type { DevtoolsLogRecord, LogLevel } from \"./types\";\n\nfunction renderMessage(message: readonly unknown[]): string {\n return message\n .map((part) => {\n if (typeof part === \"string\") {\n return part;\n }\n if (part === null || part === undefined) {\n return String(part);\n }\n return safeStringify(part);\n })\n .join(\"\");\n}\n\n// ---------------------------------------------------------------------------\n// Stack trace parser — extracts the caller frame from `new Error().stack`\n// ---------------------------------------------------------------------------\n\n// V8 (Chrome/Node/Edge): \" at functionName (file:line:col)\"\nconst v8FrameRe = /at\\s+(?:.*?\\s+\\()?(.+?):(\\d+):(\\d+)\\)?$/;\n// SpiderMonkey (Firefox) / JSC (Safari): \"functionName@file:line:col\"\nconst smFrameRe = /(?:^|@)(.+?):(\\d+):(\\d+)$/;\n\nfunction captureCallerInfo(): string | undefined {\n // biome-ignore lint/suspicious/useErrorMessage: we only need the stack trace, not the message\n const { stack } = new Error();\n if (!stack) {\n return undefined;\n }\n\n const lines = stack.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip empty lines and the \"Error\" header\n if (!trimmed || trimmed === \"Error\") {\n continue;\n }\n\n // Skip frames from logtape internals (both @logtape/logtape and @mugenlabs/logtape-devtools)\n if (/logtape/i.test(trimmed)) {\n continue;\n }\n\n // Try V8 format first, then SpiderMonkey/JSC\n const match = v8FrameRe.exec(trimmed) ?? smFrameRe.exec(trimmed);\n if (!match) {\n continue;\n }\n\n const [, filePath, lineNo, colNo] = match;\n\n // Extract just the filename from the full path/URL\n const fileName =\n filePath\n .replace(/[?#].*$/, \"\")\n .split(\"/\")\n .pop() ?? filePath;\n\n return `${fileName}:${lineNo}:${colNo}`;\n }\n\n return undefined;\n}\n\nfunction normalizeRecord(record: LogRecord, captureStack: boolean, id: string): DevtoolsLogRecord {\n const message = [...record.message];\n const normalized = {\n category: [...record.category],\n id,\n level: record.level as LogLevel,\n message,\n properties: safeCloneProperties(record.properties),\n timestamp: record.timestamp,\n } as DevtoolsLogRecord;\n\n // Rendering the message serialises every interpolated value, so defer it to\n // the first read (search or display) instead of paying for it on every log.\n let messageText: string | undefined;\n Object.defineProperty(normalized, \"messageText\", {\n configurable: true,\n enumerable: true,\n get: () => {\n messageText ??= renderMessage(message);\n return messageText;\n },\n });\n\n if (captureStack) {\n const caller = captureCallerInfo();\n if (caller) {\n normalized.caller = caller;\n }\n }\n\n return normalized;\n}\n\nfunction isProductionEnv(): boolean {\n try {\n // Bundlers (Vite, webpack, etc.) statically replace this at build time\n if (typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\") {\n return true;\n }\n } catch {\n // process may not exist in browser environments\n }\n return false;\n}\n\n/** Options for {@link createDevtoolsSink}. */\nexport interface DevtoolsSinkOptions {\n /**\n * Capture the source location of each log call via stack trace parsing.\n *\n * Relies on `new Error().stack`, which is engine-dependent. Automatically\n * disabled in production builds (when `process.env.NODE_ENV === \"production\"`)\n * because minified bundles produce meaningless file/line references. Note that\n * source maps do not help here — browsers do not apply source maps to\n * `Error.stack`.\n *\n * Also skipped while no devtools panel is mounted (the store has no\n * subscribers), unless `forceStackTrace` is `true`.\n *\n * @experimental\n * @default true\n */\n captureStackTrace?: boolean;\n /**\n * Force stack trace capture even in production builds and while the panel is\n * closed. Only has an effect when `captureStackTrace` is `true`.\n *\n * @default false\n */\n forceStackTrace?: boolean;\n /** Custom log store instance. Defaults to the shared store. */\n store?: LogStore;\n}\n\n/**\n * A LogTape sink that is also disposable. LogTape disposes sinks when the\n * configuration is reset; a disposed sink stops writing to its store.\n */\nexport type DevtoolsSink = Sink & Disposable;\n\n/**\n * Creates a LogTape sink that forwards log records to the devtools panel.\n *\n * Usage:\n * ```ts\n * import { configure } from \"@logtape/logtape\";\n * import { createDevtoolsSink } from \"@mugenlabs/logtape-devtools\";\n *\n * await configure({\n * sinks: { devtools: createDevtoolsSink() },\n * loggers: [\n * { category: [\"app\"], lowestLevel: \"debug\", sinks: [\"devtools\"] },\n * ],\n * });\n * ```\n */\nexport function createDevtoolsSink(options?: DevtoolsSinkOptions): DevtoolsSink {\n const store = options?.store ?? defaultLogStore;\n const wantsStack = options?.captureStackTrace ?? true;\n const forceStack = options?.forceStackTrace ?? false;\n const captureStack = wantsStack && (forceStack || !isProductionEnv());\n\n // Per-sink counter, plus a random discriminator so that two sinks sharing a\n // single store cannot emit the same id for records with equal timestamps.\n const sinkId = Math.random().toString(36).slice(2, 8);\n let counter = 0;\n let disposed = false;\n\n const sink = (record: LogRecord): void => {\n // A store with no capacity drops every record, so skip normalisation too.\n if (disposed || store.getMaxRecords?.() === 0) {\n return;\n }\n try {\n // Stack capture is the most expensive part of normalization — skip it\n // while nothing is watching the store (i.e. no panel is mounted).\n const shouldCaptureStack = captureStack && (forceStack || store.hasListeners());\n counter += 1;\n const normalized = normalizeRecord(\n record,\n shouldCaptureStack,\n `log-${sinkId}-${counter}-${record.timestamp}`\n );\n store.addRecord(normalized);\n } catch {\n // Fail silently — devtools should never break the application\n }\n };\n\n return Object.assign(sink, {\n [Symbol.dispose]: () => {\n disposed = true;\n },\n });\n}\n"],"mappings":"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","names":[]}
|