@djangocfg/debuger 2.1.219
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 +345 -0
- package/dist/chunk-ESIQODSI.mjs +115 -0
- package/dist/chunk-ESIQODSI.mjs.map +1 -0
- package/dist/chunk-PAWJFY3S.mjs +6 -0
- package/dist/chunk-PAWJFY3S.mjs.map +1 -0
- package/dist/chunk-YQE3KTBG.mjs +64 -0
- package/dist/chunk-YQE3KTBG.mjs.map +1 -0
- package/dist/customEmitter-BO-1IWxm.d.ts +57 -0
- package/dist/emitters/index.cjs +74 -0
- package/dist/emitters/index.cjs.map +1 -0
- package/dist/emitters/index.d.ts +33 -0
- package/dist/emitters/index.mjs +4 -0
- package/dist/emitters/index.mjs.map +1 -0
- package/dist/index.cjs +887 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +162 -0
- package/dist/index.mjs +694 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger/index.cjs +123 -0
- package/dist/logger/index.cjs.map +1 -0
- package/dist/logger/index.d.ts +50 -0
- package/dist/logger/index.mjs +4 -0
- package/dist/logger/index.mjs.map +1 -0
- package/package.json +67 -0
- package/src/DebugButton.tsx +143 -0
- package/src/DebugPanel.tsx +171 -0
- package/src/bridges/index.ts +1 -0
- package/src/bridges/monitorBridge.ts +63 -0
- package/src/emitters/Emitter.ts +51 -0
- package/src/emitters/audioEmitter.ts +74 -0
- package/src/emitters/customEmitter.ts +42 -0
- package/src/emitters/index.ts +10 -0
- package/src/hooks/useAudioEventLog.ts +147 -0
- package/src/hooks/useCustomEventLog.ts +39 -0
- package/src/hooks/useDebugShortcut.ts +27 -0
- package/src/hooks/useStoreSnapshot.ts +70 -0
- package/src/index.ts +62 -0
- package/src/logger/index.ts +3 -0
- package/src/logger/logStore.ts +89 -0
- package/src/logger/logger.ts +95 -0
- package/src/logger/types.ts +39 -0
- package/src/panels/AudioDebugPanel.tsx +157 -0
- package/src/panels/LogsPanel.tsx +267 -0
- package/src/panels/StorePanel.tsx +71 -0
- package/src/store/debugStore.ts +42 -0
- package/src/styles/index.css +5 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useState, useEffect } from 'react';
|
|
4
|
+
import { installMonitorBridge } from './bridges';
|
|
5
|
+
import {
|
|
6
|
+
Button,
|
|
7
|
+
Badge,
|
|
8
|
+
Tooltip,
|
|
9
|
+
TooltipContent,
|
|
10
|
+
TooltipTrigger,
|
|
11
|
+
} from '@djangocfg/ui-core/components';
|
|
12
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
13
|
+
import { useDebugErrorCount } from './logger';
|
|
14
|
+
import {
|
|
15
|
+
X, Bug, Minimize2, Maximize2,
|
|
16
|
+
ScrollText, Radio,
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { useDebugStore } from './store/debugStore';
|
|
19
|
+
import type { DebugTab } from './store/debugStore';
|
|
20
|
+
import { LogsPanel } from './panels/LogsPanel';
|
|
21
|
+
import { AudioDebugPanel } from './panels/AudioDebugPanel';
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Types
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
export interface CustomDebugTab {
|
|
28
|
+
/** Unique id — will be added to DebugTab union at runtime */
|
|
29
|
+
id: string;
|
|
30
|
+
label: string;
|
|
31
|
+
icon: React.ElementType;
|
|
32
|
+
panel: React.ComponentType<{ isActive: boolean }>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DebugPanelProps {
|
|
36
|
+
/** Additional app-specific tabs */
|
|
37
|
+
tabs?: CustomDebugTab[];
|
|
38
|
+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
39
|
+
defaultHeight?: number;
|
|
40
|
+
defaultWidth?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Built-in tabs
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
const BUILTIN_TABS = [
|
|
48
|
+
{ id: 'logs', label: 'Logs', icon: ScrollText },
|
|
49
|
+
{ id: 'audio', label: 'Audio', icon: Radio },
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
const POSITION_CLASSES: Record<NonNullable<DebugPanelProps['position']>, string> = {
|
|
53
|
+
'bottom-right': 'bottom-4 right-4',
|
|
54
|
+
'bottom-left': 'bottom-4 left-4',
|
|
55
|
+
'top-right': 'top-4 right-4',
|
|
56
|
+
'top-left': 'top-4 left-4',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Component
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
export function DebugPanel({
|
|
64
|
+
tabs: customTabs = [],
|
|
65
|
+
position = 'bottom-left',
|
|
66
|
+
defaultHeight = 480,
|
|
67
|
+
defaultWidth = 560,
|
|
68
|
+
}: DebugPanelProps) {
|
|
69
|
+
const isOpen = useDebugStore((s) => s.isOpen);
|
|
70
|
+
const activeTab = useDebugStore((s) => s.tab);
|
|
71
|
+
const setTab = useDebugStore((s) => s.setTab);
|
|
72
|
+
const close = useDebugStore((s) => s.close);
|
|
73
|
+
const errorCount = useDebugErrorCount();
|
|
74
|
+
|
|
75
|
+
const [isMinimized, setIsMinimized] = useState(false);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const unsub = installMonitorBridge();
|
|
79
|
+
return () => unsub?.();
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const allTabs = useMemo(() => [
|
|
83
|
+
...BUILTIN_TABS,
|
|
84
|
+
...customTabs.map((t) => ({ id: t.id, label: t.label, icon: t.icon })),
|
|
85
|
+
], [customTabs]);
|
|
86
|
+
|
|
87
|
+
const panelHeight = isMinimized ? 48 : defaultHeight;
|
|
88
|
+
|
|
89
|
+
if (!isOpen) return null;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
className={cn(
|
|
94
|
+
'fixed z-[100] flex flex-col overflow-hidden rounded-lg border border-border bg-background shadow-xl',
|
|
95
|
+
POSITION_CLASSES[position]
|
|
96
|
+
)}
|
|
97
|
+
style={{ width: defaultWidth, height: panelHeight }}
|
|
98
|
+
>
|
|
99
|
+
{/* Header */}
|
|
100
|
+
<div className="flex items-center gap-2 border-b border-border bg-muted/50 px-3 py-2 shrink-0">
|
|
101
|
+
<Bug className="h-4 w-4 text-primary" />
|
|
102
|
+
<span className="text-sm font-medium">Debug</span>
|
|
103
|
+
|
|
104
|
+
{errorCount > 0 && (
|
|
105
|
+
<Badge variant="destructive" className="ml-1 text-[10px]">{errorCount} errors</Badge>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{/* Tab bar */}
|
|
109
|
+
{!isMinimized && (
|
|
110
|
+
<div className="flex items-center gap-0.5 ml-3">
|
|
111
|
+
{allTabs.map((tab) => {
|
|
112
|
+
const Icon = tab.icon;
|
|
113
|
+
const isActive = activeTab === tab.id;
|
|
114
|
+
return (
|
|
115
|
+
<button
|
|
116
|
+
key={tab.id}
|
|
117
|
+
type="button"
|
|
118
|
+
onClick={() => setTab(tab.id as DebugTab)}
|
|
119
|
+
className={cn(
|
|
120
|
+
'flex items-center gap-1.5 px-2 py-0.5 rounded text-xs transition-colors',
|
|
121
|
+
isActive
|
|
122
|
+
? 'bg-background text-foreground shadow-sm'
|
|
123
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
<Icon className="h-3 w-3" />
|
|
127
|
+
{tab.label}
|
|
128
|
+
</button>
|
|
129
|
+
);
|
|
130
|
+
})}
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
<div className="flex items-center gap-1 ml-auto">
|
|
135
|
+
<Tooltip>
|
|
136
|
+
<TooltipTrigger asChild>
|
|
137
|
+
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setIsMinimized((v) => !v)}>
|
|
138
|
+
{isMinimized ? <Maximize2 className="h-3.5 w-3.5" /> : <Minimize2 className="h-3.5 w-3.5" />}
|
|
139
|
+
</Button>
|
|
140
|
+
</TooltipTrigger>
|
|
141
|
+
<TooltipContent>{isMinimized ? 'Expand' : 'Minimize'}</TooltipContent>
|
|
142
|
+
</Tooltip>
|
|
143
|
+
<Tooltip>
|
|
144
|
+
<TooltipTrigger asChild>
|
|
145
|
+
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={close}>
|
|
146
|
+
<X className="h-3.5 w-3.5" />
|
|
147
|
+
</Button>
|
|
148
|
+
</TooltipTrigger>
|
|
149
|
+
<TooltipContent>Close</TooltipContent>
|
|
150
|
+
</Tooltip>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{/* Panel content */}
|
|
155
|
+
{!isMinimized && (
|
|
156
|
+
<div className="flex-1 overflow-hidden">
|
|
157
|
+
{activeTab === 'logs' && <LogsPanel isActive={activeTab === 'logs'} />}
|
|
158
|
+
{activeTab === 'audio' && <AudioDebugPanel isActive={activeTab === 'audio'} />}
|
|
159
|
+
{customTabs.map((tab) => {
|
|
160
|
+
const Panel = tab.panel;
|
|
161
|
+
return (
|
|
162
|
+
<div key={tab.id} className={cn('h-full', activeTab !== tab.id && 'hidden')}>
|
|
163
|
+
<Panel isActive={activeTab === tab.id} />
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { installMonitorBridge } from './monitorBridge';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* monitorBridge — subscribes to @djangocfg/monitor store and forwards
|
|
5
|
+
* captured events into debugger's logStore.
|
|
6
|
+
*
|
|
7
|
+
* Call once at app startup (e.g. inside DebugPanel or a provider).
|
|
8
|
+
* Safe to call multiple times — only installs one subscription.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { installMonitorBridge } from '@djangocfg/debuger/bridges'
|
|
12
|
+
* installMonitorBridge()
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { monitorStore } from '@djangocfg/monitor/client';
|
|
16
|
+
import { useDebugLogStore } from '../logger/logStore';
|
|
17
|
+
import type { LogLevel } from '../logger/types';
|
|
18
|
+
|
|
19
|
+
let _installed = false;
|
|
20
|
+
|
|
21
|
+
export function installMonitorBridge(): (() => void) | undefined {
|
|
22
|
+
if (typeof window === 'undefined') return;
|
|
23
|
+
if (_installed) return;
|
|
24
|
+
|
|
25
|
+
let prevLen = monitorStore.getState().buffer.length;
|
|
26
|
+
|
|
27
|
+
const unsub = monitorStore.subscribe((state) => {
|
|
28
|
+
const buffer = state.buffer;
|
|
29
|
+
if (buffer.length <= prevLen) { prevLen = buffer.length; return; }
|
|
30
|
+
const newEvents = buffer.slice(prevLen);
|
|
31
|
+
prevLen = buffer.length;
|
|
32
|
+
|
|
33
|
+
for (const event of newEvents) {
|
|
34
|
+
const level: LogLevel =
|
|
35
|
+
event.level === 'error' ? 'error'
|
|
36
|
+
: event.level === 'warn' ? 'warn'
|
|
37
|
+
: 'info';
|
|
38
|
+
|
|
39
|
+
useDebugLogStore.getState().addLog({
|
|
40
|
+
level,
|
|
41
|
+
component: `monitor:${event.event_type ?? 'event'}`,
|
|
42
|
+
message: event.message ?? '',
|
|
43
|
+
data: {
|
|
44
|
+
...(event.url && { url: event.url }),
|
|
45
|
+
...(event.session_id && { session_id: event.session_id }),
|
|
46
|
+
...(event.http_status !== undefined && {
|
|
47
|
+
http_status: event.http_status,
|
|
48
|
+
http_method: event.http_method,
|
|
49
|
+
http_url: event.http_url,
|
|
50
|
+
}),
|
|
51
|
+
...(event.extra ? { extra: event.extra } : {}),
|
|
52
|
+
},
|
|
53
|
+
stack: event.stack_trace,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
_installed = true;
|
|
59
|
+
return () => {
|
|
60
|
+
unsub();
|
|
61
|
+
_installed = false;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic typed event emitter
|
|
3
|
+
*
|
|
4
|
+
* Zero-cost: emit() is a no-op when no listeners are registered for an event.
|
|
5
|
+
* SSR-safe: no browser APIs used.
|
|
6
|
+
* No dependencies.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* type MyEvents = { 'user:login': { id: string }; 'page:view': { path: string } };
|
|
10
|
+
* export const myEmitter = new Emitter<MyEvents>();
|
|
11
|
+
* myEmitter.on('user:login', ({ id }) => console.log(id));
|
|
12
|
+
* myEmitter.emit('user:login', { id: '123' });
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
type EventMap = Record<string, any>;
|
|
17
|
+
|
|
18
|
+
export class Emitter<TEvents extends EventMap> {
|
|
19
|
+
private listeners = new Map<keyof TEvents, Set<(data: unknown) => void>>();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Emit an event. No-op if no listeners are registered.
|
|
23
|
+
*/
|
|
24
|
+
emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void {
|
|
25
|
+
const handlers = this.listeners.get(event);
|
|
26
|
+
if (!handlers || handlers.size === 0) return;
|
|
27
|
+
handlers.forEach((h) => h(data));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Subscribe to an event. Returns an unsubscribe function.
|
|
32
|
+
*/
|
|
33
|
+
on<K extends keyof TEvents>(event: K, handler: (data: TEvents[K]) => void): () => void {
|
|
34
|
+
if (!this.listeners.has(event)) {
|
|
35
|
+
this.listeners.set(event, new Set());
|
|
36
|
+
}
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
this.listeners.get(event)!.add(handler as any);
|
|
39
|
+
return () => this.listeners.get(event)?.delete(handler as any);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** True when at least one listener is registered for this event (hot-path guard) */
|
|
43
|
+
hasListeners<K extends keyof TEvents>(event: K): boolean {
|
|
44
|
+
return (this.listeners.get(event)?.size ?? 0) > 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Remove all listeners (useful for testing / cleanup) */
|
|
48
|
+
clear(): void {
|
|
49
|
+
this.listeners.clear();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio/Media Engine Event Emitter
|
|
3
|
+
*
|
|
4
|
+
* Universal zero-cost pub/sub for audio/media player debug events.
|
|
5
|
+
* No React dependency — pure TypeScript module.
|
|
6
|
+
*
|
|
7
|
+
* Wire into your audio engine:
|
|
8
|
+
* import { emitAudioEvent } from '@org/debuger/emitters';
|
|
9
|
+
* emitAudioEvent({ kind: 'play', trackId: 'abc', ts: Date.now(), msg: 'started' });
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Emitter } from './Emitter';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export type AudioEventKind =
|
|
19
|
+
| 'play'
|
|
20
|
+
| 'pause'
|
|
21
|
+
| 'seek'
|
|
22
|
+
| 'ended'
|
|
23
|
+
| 'sync' // periodic sync tick (RAF loop)
|
|
24
|
+
| 'load'
|
|
25
|
+
| 'error'
|
|
26
|
+
| 'engine' // engine lifecycle: start/stop/create/destroy
|
|
27
|
+
| 'custom';
|
|
28
|
+
|
|
29
|
+
export interface AudioDebugEvent {
|
|
30
|
+
kind: AudioEventKind;
|
|
31
|
+
ts: number;
|
|
32
|
+
/** Track/player ID (short prefix) */
|
|
33
|
+
trackId?: string;
|
|
34
|
+
msg: string;
|
|
35
|
+
/** Arbitrary metadata: positions, drift, etc. */
|
|
36
|
+
data?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type AudioEvents = { event: AudioDebugEvent };
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Singleton emitter + ring-buffer
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
const _emitter = new Emitter<AudioEvents>();
|
|
46
|
+
|
|
47
|
+
/** Ring-buffer of recent events — replayed to new subscribers */
|
|
48
|
+
const BUFFER_SIZE = 200;
|
|
49
|
+
const _buffer: AudioDebugEvent[] = [];
|
|
50
|
+
|
|
51
|
+
/** Emit an audio debug event. Zero-cost when no panel is open. */
|
|
52
|
+
export const emitAudioEvent = (event: AudioDebugEvent): void => {
|
|
53
|
+
// Always buffer (capped) so late subscribers get recent history
|
|
54
|
+
if (_buffer.length >= BUFFER_SIZE) _buffer.shift();
|
|
55
|
+
_buffer.push(event);
|
|
56
|
+
_emitter.emit('event', event);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Subscribe to audio debug events.
|
|
61
|
+
* Immediately replays buffered events to the new subscriber,
|
|
62
|
+
* then delivers live events. Returns unsubscribe function.
|
|
63
|
+
*/
|
|
64
|
+
export const subscribeAudioEvents = (fn: (e: AudioDebugEvent) => void): (() => void) => {
|
|
65
|
+
// Replay history so the panel sees events that happened before it opened
|
|
66
|
+
for (const e of _buffer) fn(e);
|
|
67
|
+
return _emitter.on('event', fn);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Clear the ring-buffer (called from the panel's Clear button) */
|
|
71
|
+
export const clearAudioEventBuffer = (): void => { _buffer.length = 0; };
|
|
72
|
+
|
|
73
|
+
/** True when at least one listener is active (use as hot-path guard) */
|
|
74
|
+
export const hasAudioListeners = (): boolean => _emitter.hasListeners('event');
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Debug Event Emitter
|
|
3
|
+
*
|
|
4
|
+
* Generic channel for consuming apps to send arbitrary debug events
|
|
5
|
+
* without coupling to any specific domain.
|
|
6
|
+
*
|
|
7
|
+
* import { emitDebugEvent } from '@org/debuger/emitters';
|
|
8
|
+
* emitDebugEvent({ channel: 'pipeline', kind: 'slot:updated', ts: Date.now(), msg: '...' });
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Emitter } from './Emitter';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
export interface CustomDebugEvent {
|
|
18
|
+
/** Logical grouping — matches registered custom tab id */
|
|
19
|
+
channel: string;
|
|
20
|
+
kind: string;
|
|
21
|
+
ts: number;
|
|
22
|
+
msg: string;
|
|
23
|
+
data?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type CustomEvents = { event: CustomDebugEvent };
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Singleton emitter
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
const _emitter = new Emitter<CustomEvents>();
|
|
33
|
+
|
|
34
|
+
/** Emit a custom debug event. Zero-cost when no panel is open. */
|
|
35
|
+
export const emitDebugEvent = (event: CustomDebugEvent): void =>
|
|
36
|
+
_emitter.emit('event', event);
|
|
37
|
+
|
|
38
|
+
/** Subscribe to custom debug events. Returns unsubscribe function. */
|
|
39
|
+
export const subscribeCustomEvents = (fn: (e: CustomDebugEvent) => void): (() => void) =>
|
|
40
|
+
_emitter.on('event', fn);
|
|
41
|
+
|
|
42
|
+
export const hasCustomListeners = (): boolean => _emitter.hasListeners('event');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Generic typed emitter class
|
|
2
|
+
export { Emitter } from './Emitter';
|
|
3
|
+
|
|
4
|
+
// Audio / media engine events
|
|
5
|
+
export { subscribeAudioEvents, emitAudioEvent, hasAudioListeners, clearAudioEventBuffer } from './audioEmitter';
|
|
6
|
+
export type { AudioDebugEvent, AudioEventKind } from './audioEmitter';
|
|
7
|
+
|
|
8
|
+
// Generic custom channel events
|
|
9
|
+
export { subscribeCustomEvents, emitDebugEvent, hasCustomListeners } from './customEmitter';
|
|
10
|
+
export type { CustomDebugEvent } from './customEmitter';
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import { subscribeAudioEvents, clearAudioEventBuffer } from '../emitters/audioEmitter';
|
|
5
|
+
import type { AudioDebugEvent, AudioEventKind } from '../emitters/audioEmitter';
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
export interface AudioLogEntry extends AudioDebugEvent {
|
|
12
|
+
/** Local unique key for React rendering */
|
|
13
|
+
id: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseAudioEventLogResult {
|
|
17
|
+
events: AudioLogEntry[];
|
|
18
|
+
clear: () => void;
|
|
19
|
+
/** Seek events per second (rolling 5s window) */
|
|
20
|
+
seekRate: number;
|
|
21
|
+
/** Average interval between 'sync' ticks in ms (e.g. RAF loop cadence) */
|
|
22
|
+
syncIntervalMs: number | null;
|
|
23
|
+
/** Running count per event kind */
|
|
24
|
+
kindCounts: Partial<Record<AudioEventKind, number>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Constants
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
const MAX_EVENTS = 200;
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Hook
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Subscribes to audio debug events and maintains a ring-buffer log.
|
|
39
|
+
*
|
|
40
|
+
* RAF batching: incoming events are queued in a ref and flushed into React
|
|
41
|
+
* state once per animation frame — prevents render storms at 60fps.
|
|
42
|
+
*
|
|
43
|
+
* Only subscribes while `active` is true (panel tab is visible).
|
|
44
|
+
*/
|
|
45
|
+
export function useAudioEventLog(active: boolean): UseAudioEventLogResult {
|
|
46
|
+
const [events, setEvents] = useState<AudioLogEntry[]>([]);
|
|
47
|
+
const [syncIntervalMs, setSyncIntervalMs] = useState<number | null>(null);
|
|
48
|
+
const [kindCounts, setKindCounts] = useState<Partial<Record<AudioEventKind, number>>>({});
|
|
49
|
+
const [seekRate, setSeekRate] = useState(0);
|
|
50
|
+
|
|
51
|
+
// Refs — mutated in the emitter callback without triggering renders
|
|
52
|
+
const queueRef = useRef<AudioLogEntry[]>([]);
|
|
53
|
+
const counterRef = useRef(0);
|
|
54
|
+
const lastSyncTs = useRef<number | null>(null);
|
|
55
|
+
const seekTimestamps = useRef<number[]>([]);
|
|
56
|
+
const kindCountsRef = useRef<Partial<Record<AudioEventKind, number>>>({});
|
|
57
|
+
|
|
58
|
+
const clear = useCallback(() => {
|
|
59
|
+
clearAudioEventBuffer();
|
|
60
|
+
queueRef.current = [];
|
|
61
|
+
counterRef.current = 0;
|
|
62
|
+
kindCountsRef.current = {};
|
|
63
|
+
seekTimestamps.current = [];
|
|
64
|
+
lastSyncTs.current = null;
|
|
65
|
+
setEvents([]);
|
|
66
|
+
setSyncIntervalMs(null);
|
|
67
|
+
setKindCounts({});
|
|
68
|
+
setSeekRate(0);
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
// Subscribe — enqueue events without touching React state
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!active) return;
|
|
74
|
+
|
|
75
|
+
const unsubscribe = subscribeAudioEvents((event) => {
|
|
76
|
+
const entry: AudioLogEntry = { ...event, id: String(++counterRef.current) };
|
|
77
|
+
queueRef.current.push(entry);
|
|
78
|
+
|
|
79
|
+
// Track sync interval (no setState — RAF will pick it up)
|
|
80
|
+
if (event.kind === 'sync') {
|
|
81
|
+
if (lastSyncTs.current !== null) {
|
|
82
|
+
const interval = event.ts - lastSyncTs.current;
|
|
83
|
+
// Store on ref; RAF flush will call setSyncIntervalMs
|
|
84
|
+
(entry as AudioLogEntry & { _interval?: number })._interval = interval;
|
|
85
|
+
}
|
|
86
|
+
lastSyncTs.current = event.ts;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Track seeks
|
|
90
|
+
if (event.kind === 'seek') {
|
|
91
|
+
seekTimestamps.current.push(event.ts);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Accumulate kind counts on ref
|
|
95
|
+
kindCountsRef.current = {
|
|
96
|
+
...kindCountsRef.current,
|
|
97
|
+
[event.kind]: (kindCountsRef.current[event.kind] ?? 0) + 1,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return unsubscribe;
|
|
102
|
+
}, [active]);
|
|
103
|
+
|
|
104
|
+
// RAF flush loop — drains queue into React state once per frame
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!active) return;
|
|
107
|
+
let rafId: number;
|
|
108
|
+
|
|
109
|
+
const flush = () => {
|
|
110
|
+
if (queueRef.current.length > 0) {
|
|
111
|
+
const incoming = queueRef.current.splice(0);
|
|
112
|
+
|
|
113
|
+
// Extract sync interval from last sync event in batch
|
|
114
|
+
const lastSync = [...incoming].reverse().find((e) => e.kind === 'sync') as
|
|
115
|
+
| (AudioLogEntry & { _interval?: number })
|
|
116
|
+
| undefined;
|
|
117
|
+
if (lastSync?._interval !== undefined) {
|
|
118
|
+
setSyncIntervalMs(lastSync._interval);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setEvents((prev) => {
|
|
122
|
+
const next = [...prev, ...incoming];
|
|
123
|
+
return next.length > MAX_EVENTS ? next.slice(-MAX_EVENTS) : next;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
setKindCounts({ ...kindCountsRef.current });
|
|
127
|
+
}
|
|
128
|
+
rafId = requestAnimationFrame(flush);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
rafId = requestAnimationFrame(flush);
|
|
132
|
+
return () => cancelAnimationFrame(rafId);
|
|
133
|
+
}, [active]);
|
|
134
|
+
|
|
135
|
+
// Rolling seek rate — recompute every second
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!active) return;
|
|
138
|
+
const id = setInterval(() => {
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
seekTimestamps.current = seekTimestamps.current.filter((t) => now - t < 5000);
|
|
141
|
+
setSeekRate(Math.round((seekTimestamps.current.length / 5) * 10) / 10);
|
|
142
|
+
}, 1000);
|
|
143
|
+
return () => clearInterval(id);
|
|
144
|
+
}, [active]);
|
|
145
|
+
|
|
146
|
+
return { events, clear, seekRate, syncIntervalMs, kindCounts };
|
|
147
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import { subscribeCustomEvents } from '../emitters/customEmitter';
|
|
5
|
+
import type { CustomDebugEvent } from '../emitters/customEmitter';
|
|
6
|
+
|
|
7
|
+
export interface CustomLogEntry extends CustomDebugEvent {
|
|
8
|
+
id: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const MAX_EVENTS = 300;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Subscribes to custom channel events and maintains a ring-buffer per channel.
|
|
15
|
+
* Pass `channel` to filter — or undefined to receive all channels.
|
|
16
|
+
*/
|
|
17
|
+
export function useCustomEventLog(active: boolean, channel?: string) {
|
|
18
|
+
const [events, setEvents] = useState<CustomLogEntry[]>([]);
|
|
19
|
+
const counterRef = useRef(0);
|
|
20
|
+
|
|
21
|
+
const clear = useCallback(() => setEvents([]), []);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!active) return;
|
|
25
|
+
|
|
26
|
+
const unsubscribe = subscribeCustomEvents((event) => {
|
|
27
|
+
if (channel && event.channel !== channel) return;
|
|
28
|
+
const entry: CustomLogEntry = { ...event, id: String(++counterRef.current) };
|
|
29
|
+
setEvents((prev) => {
|
|
30
|
+
const next = [...prev, entry];
|
|
31
|
+
return next.length > MAX_EVENTS ? next.slice(-MAX_EVENTS) : next;
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return unsubscribe;
|
|
36
|
+
}, [active, channel]);
|
|
37
|
+
|
|
38
|
+
return { events, clear };
|
|
39
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useHotkey } from '@djangocfg/ui-core/hooks';
|
|
4
|
+
import { useDebugStore } from '../store/debugStore';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registers a global keyboard shortcut to toggle the debug panel.
|
|
8
|
+
* Default: Cmd+D (meta+d) on Mac / Win+D on Windows.
|
|
9
|
+
*
|
|
10
|
+
* Guards:
|
|
11
|
+
* - In production: only fires if `isUnlocked === true`
|
|
12
|
+
*/
|
|
13
|
+
export function useDebugShortcut(): void {
|
|
14
|
+
const toggle = useDebugStore((s) => s.toggle);
|
|
15
|
+
const isUnlocked = useDebugStore((s) => s.isUnlocked);
|
|
16
|
+
const isDev = typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production';
|
|
17
|
+
|
|
18
|
+
useHotkey(
|
|
19
|
+
'meta+d',
|
|
20
|
+
(e) => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
if (!isDev && !isUnlocked) return;
|
|
23
|
+
toggle();
|
|
24
|
+
},
|
|
25
|
+
{ preventDefault: true },
|
|
26
|
+
);
|
|
27
|
+
}
|