proteum 2.1.0 → 2.1.2
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/AGENTS.md +44 -98
- package/README.md +143 -10
- package/agents/framework/AGENTS.md +146 -886
- package/agents/project/AGENTS.md +73 -127
- package/agents/project/client/AGENTS.md +22 -93
- package/agents/project/client/pages/AGENTS.md +24 -26
- package/agents/project/server/routes/AGENTS.md +10 -8
- package/agents/project/server/services/AGENTS.md +22 -159
- package/agents/project/tests/AGENTS.md +11 -8
- package/cli/app/config.ts +7 -20
- package/cli/bin.js +8 -0
- package/cli/commands/command.ts +243 -0
- package/cli/commands/commandLocalRunner.js +198 -0
- package/cli/commands/create.ts +5 -0
- package/cli/commands/deploy/web.ts +1 -2
- package/cli/commands/dev.ts +98 -2
- package/cli/commands/doctor.ts +8 -74
- package/cli/commands/explain.ts +8 -186
- package/cli/commands/init.ts +2 -94
- package/cli/commands/trace.ts +228 -0
- package/cli/compiler/artifacts/commands.ts +217 -0
- package/cli/compiler/artifacts/manifest.ts +35 -21
- package/cli/compiler/artifacts/services.ts +300 -1
- package/cli/compiler/client/index.ts +43 -8
- package/cli/compiler/common/commands.ts +175 -0
- package/cli/compiler/common/index.ts +1 -1
- package/cli/compiler/common/proteumManifest.ts +15 -114
- package/cli/compiler/index.ts +25 -2
- package/cli/compiler/server/index.ts +31 -6
- package/cli/index.ts +1 -4
- package/cli/paths.ts +16 -1
- package/cli/presentation/commands.ts +104 -14
- package/cli/presentation/devSession.ts +22 -3
- package/cli/presentation/proteum_logo_400x400_square_icon.txt +400 -0
- package/cli/runtime/commands.ts +121 -4
- package/cli/scaffold/index.ts +720 -0
- package/cli/scaffold/templates.ts +344 -0
- package/cli/scaffold/types.ts +26 -0
- package/cli/tsconfig.json +4 -1
- package/cli/utils/check.ts +1 -1
- package/client/app/component.tsx +13 -9
- package/client/dev/profiler/index.tsx +2511 -0
- package/client/dev/profiler/noop.tsx +5 -0
- package/client/dev/profiler/runtime.noop.ts +116 -0
- package/client/dev/profiler/runtime.ts +840 -0
- package/client/services/router/components/router.tsx +30 -2
- package/client/services/router/index.tsx +27 -3
- package/client/services/router/request/api.ts +133 -17
- package/commands/proteum/diagnostics.ts +11 -0
- package/common/dev/commands.ts +50 -0
- package/common/dev/diagnostics.ts +298 -0
- package/common/dev/profiler.ts +92 -0
- package/common/dev/proteumManifest.ts +135 -0
- package/common/dev/requestTrace.ts +115 -0
- package/common/env/proteumEnv.ts +284 -0
- package/common/router/index.ts +4 -22
- package/docs/dev-commands.md +93 -0
- package/docs/diagnostics.md +88 -0
- package/docs/request-tracing.md +132 -0
- package/eslint.js +11 -6
- package/package.json +3 -3
- package/server/app/commands.ts +35 -370
- package/server/app/commandsManager.ts +393 -0
- package/server/app/container/config.ts +11 -49
- package/server/app/container/console/index.ts +2 -3
- package/server/app/container/index.ts +5 -2
- package/server/app/container/trace/index.ts +364 -0
- package/server/app/devCommands.ts +192 -0
- package/server/app/devDiagnostics.ts +53 -0
- package/server/app/index.ts +29 -6
- package/server/index.ts +0 -1
- package/server/services/auth/index.ts +525 -61
- package/server/services/auth/router/index.ts +106 -7
- package/server/services/cron/CronTask.ts +73 -5
- package/server/services/cron/index.ts +34 -11
- package/server/services/fetch/index.ts +3 -10
- package/server/services/prisma/index.ts +66 -4
- package/server/services/router/http/index.ts +173 -6
- package/server/services/router/index.ts +200 -12
- package/server/services/router/request/api.ts +30 -1
- package/server/services/router/response/index.ts +83 -10
- package/server/services/router/response/page/document.tsx +16 -0
- package/server/services/router/response/page/index.tsx +27 -1
- package/skills/clean-project-code/SKILL.md +7 -2
- package/test-results/.last-run.json +4 -0
- package/types/aliases.d.ts +6 -0
- package/types/global/utils.d.ts +7 -14
- package/Rte.zip +0 -0
- package/agents/project/agents.md.zip +0 -0
- package/doc/TODO.md +0 -71
- package/doc/front/router.md +0 -27
- package/doc/workspace/workspace.png +0 -0
- package/doc/workspace/workspace2.png +0 -0
- package/doc/workspace/workspace_26.01.22.png +0 -0
- package/server/services/router/http/session.ts.old +0 -40
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import {
|
|
2
|
+
profilerOriginHeader,
|
|
3
|
+
profilerParentRequestIdHeader,
|
|
4
|
+
profilerSessionIdHeader,
|
|
5
|
+
profilerTraceRequestIdHeader,
|
|
6
|
+
type TProfilerCronTask,
|
|
7
|
+
type TProfilerNavigationSession,
|
|
8
|
+
type TProfilerNavigationStep,
|
|
9
|
+
type TProfilerPanel,
|
|
10
|
+
type TProfilerSessionTrace,
|
|
11
|
+
type TProfilerSessionTraceKind,
|
|
12
|
+
type TProfilerUiState,
|
|
13
|
+
} from '@common/dev/profiler';
|
|
14
|
+
import type { TDevCommandDefinition, TDevCommandExecution } from '@common/dev/commands';
|
|
15
|
+
import type { TDoctorResponse } from '@common/dev/diagnostics';
|
|
16
|
+
import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
17
|
+
import type { TRequestTrace } from '@common/dev/requestTrace';
|
|
18
|
+
|
|
19
|
+
type TProfilerCommandsState = {
|
|
20
|
+
commands: TDevCommandDefinition[];
|
|
21
|
+
errorMessage?: string;
|
|
22
|
+
executions: { [path: string]: TDevCommandExecution };
|
|
23
|
+
lastLoadedAt?: string;
|
|
24
|
+
status: 'idle' | 'loading' | 'ready' | 'error';
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type TProfilerCronState = {
|
|
28
|
+
automaticExecution: boolean;
|
|
29
|
+
errorMessage?: string;
|
|
30
|
+
lastLoadedAt?: string;
|
|
31
|
+
status: 'idle' | 'loading' | 'ready' | 'error';
|
|
32
|
+
tasks: TProfilerCronTask[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type TProfilerDoctorState = {
|
|
36
|
+
errorMessage?: string;
|
|
37
|
+
lastLoadedAt?: string;
|
|
38
|
+
response?: TDoctorResponse;
|
|
39
|
+
status: 'idle' | 'loading' | 'ready' | 'error';
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type TProfilerExplainState = {
|
|
43
|
+
errorMessage?: string;
|
|
44
|
+
lastLoadedAt?: string;
|
|
45
|
+
manifest?: TProteumManifest;
|
|
46
|
+
status: 'idle' | 'loading' | 'ready' | 'error';
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type TProfilerState = {
|
|
50
|
+
activePanel: TProfilerPanel;
|
|
51
|
+
commands: TProfilerCommandsState;
|
|
52
|
+
cron: TProfilerCronState;
|
|
53
|
+
doctor: TProfilerDoctorState;
|
|
54
|
+
explain: TProfilerExplainState;
|
|
55
|
+
currentSessionId?: string;
|
|
56
|
+
selectedSessionId?: string;
|
|
57
|
+
sessions: TProfilerNavigationSession[];
|
|
58
|
+
uiState: TProfilerUiState;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type TStartTraceInput = {
|
|
62
|
+
fetcherIds?: string[];
|
|
63
|
+
label: string;
|
|
64
|
+
method: string;
|
|
65
|
+
path: string;
|
|
66
|
+
requestId?: string;
|
|
67
|
+
sessionId?: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const profilerStorageKey = 'proteum.dev.profiler.ui-state';
|
|
71
|
+
const nowIso = () => new Date().toISOString();
|
|
72
|
+
const durationMs = (startedAt: string, finishedAt: string) => Math.max(0, Date.parse(finishedAt) - Date.parse(startedAt));
|
|
73
|
+
const safeSessionStorage =
|
|
74
|
+
typeof window !== 'undefined'
|
|
75
|
+
? {
|
|
76
|
+
get: (key: string) => {
|
|
77
|
+
try {
|
|
78
|
+
return window.sessionStorage.getItem(key);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
set: (key: string, value: string) => {
|
|
84
|
+
try {
|
|
85
|
+
window.sessionStorage.setItem(key, value);
|
|
86
|
+
} catch (error) {}
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
: { get: (_key: string) => null, set: (_key: string, _value: string) => undefined };
|
|
90
|
+
|
|
91
|
+
const isProfilerUiState = (value: string | null): value is TProfilerUiState =>
|
|
92
|
+
value === 'expanded' || value === 'minimized' || value === 'pinned-handle';
|
|
93
|
+
|
|
94
|
+
const initialUiState = () => {
|
|
95
|
+
const stored = safeSessionStorage.get(profilerStorageKey);
|
|
96
|
+
return isProfilerUiState(stored) ? stored : 'minimized';
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const cloneCommand = (command: TDevCommandDefinition) => ({ ...command, sourceLocation: { ...command.sourceLocation } });
|
|
100
|
+
const cloneCommandExecution = (execution: TDevCommandExecution): TDevCommandExecution => ({
|
|
101
|
+
...execution,
|
|
102
|
+
command: cloneCommand(execution.command),
|
|
103
|
+
result: execution.result
|
|
104
|
+
? {
|
|
105
|
+
...execution.result,
|
|
106
|
+
json: execution.result.json === undefined ? undefined : JSON.parse(JSON.stringify(execution.result.json)),
|
|
107
|
+
summary: execution.result.summary,
|
|
108
|
+
}
|
|
109
|
+
: undefined,
|
|
110
|
+
});
|
|
111
|
+
const cloneCronTask = (task: TProfilerCronTask) => ({ ...task, frequency: { ...task.frequency } });
|
|
112
|
+
const cloneDoctorResponse = (response: TDoctorResponse): TDoctorResponse => JSON.parse(JSON.stringify(response)) as TDoctorResponse;
|
|
113
|
+
const cloneManifest = (manifest: TProteumManifest): TProteumManifest => JSON.parse(JSON.stringify(manifest)) as TProteumManifest;
|
|
114
|
+
const cloneStep = (step: TProfilerNavigationStep) => ({ ...step, details: step.details ? { ...step.details } : undefined });
|
|
115
|
+
const cloneTrace = (trace: TProfilerSessionTrace) => ({ ...trace });
|
|
116
|
+
const cloneSession = (session: TProfilerNavigationSession) => ({
|
|
117
|
+
...session,
|
|
118
|
+
steps: session.steps.map(cloneStep),
|
|
119
|
+
traces: session.traces.map(cloneTrace),
|
|
120
|
+
});
|
|
121
|
+
const cloneCronState = (cron: TProfilerCronState) => ({
|
|
122
|
+
...cron,
|
|
123
|
+
tasks: cron.tasks.map(cloneCronTask),
|
|
124
|
+
});
|
|
125
|
+
const cloneDoctorState = (doctor: TProfilerDoctorState) => ({
|
|
126
|
+
...doctor,
|
|
127
|
+
response: doctor.response ? cloneDoctorResponse(doctor.response) : undefined,
|
|
128
|
+
});
|
|
129
|
+
const cloneExplainState = (explain: TProfilerExplainState) => ({
|
|
130
|
+
...explain,
|
|
131
|
+
manifest: explain.manifest ? cloneManifest(explain.manifest) : undefined,
|
|
132
|
+
});
|
|
133
|
+
const cloneCommandsState = (commands: TProfilerCommandsState) => ({
|
|
134
|
+
...commands,
|
|
135
|
+
commands: commands.commands.map(cloneCommand),
|
|
136
|
+
executions: Object.fromEntries(
|
|
137
|
+
Object.entries(commands.executions).map(([commandPath, execution]) => [commandPath, cloneCommandExecution(execution)]),
|
|
138
|
+
),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
class ProfilerRuntime {
|
|
142
|
+
private listeners = new Set<() => void>();
|
|
143
|
+
private navigationCounter = 0;
|
|
144
|
+
private stepCounter = 0;
|
|
145
|
+
private traceCounter = 0;
|
|
146
|
+
private traceFetches = new Map<string, Promise<TRequestTrace | undefined>>();
|
|
147
|
+
private state: TProfilerState = {
|
|
148
|
+
activePanel: 'summary',
|
|
149
|
+
commands: {
|
|
150
|
+
commands: [],
|
|
151
|
+
executions: {},
|
|
152
|
+
status: 'idle',
|
|
153
|
+
},
|
|
154
|
+
cron: {
|
|
155
|
+
automaticExecution: false,
|
|
156
|
+
status: 'idle',
|
|
157
|
+
tasks: [],
|
|
158
|
+
},
|
|
159
|
+
doctor: {
|
|
160
|
+
status: 'idle',
|
|
161
|
+
},
|
|
162
|
+
explain: {
|
|
163
|
+
status: 'idle',
|
|
164
|
+
},
|
|
165
|
+
sessions: [],
|
|
166
|
+
uiState: initialUiState(),
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
public subscribe = (listener: () => void) => {
|
|
170
|
+
this.listeners.add(listener);
|
|
171
|
+
return () => this.listeners.delete(listener);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
public getState = () => this.state;
|
|
175
|
+
|
|
176
|
+
public setUiState(nextState: TProfilerUiState) {
|
|
177
|
+
this.state = { ...this.state, uiState: nextState };
|
|
178
|
+
safeSessionStorage.set(profilerStorageKey, nextState);
|
|
179
|
+
this.emit();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public openPanel(panel: TProfilerPanel) {
|
|
183
|
+
this.state = { ...this.state, activePanel: panel, uiState: 'expanded' };
|
|
184
|
+
safeSessionStorage.set(profilerStorageKey, 'expanded');
|
|
185
|
+
this.emit();
|
|
186
|
+
if (panel === 'commands') void this.refreshCommands();
|
|
187
|
+
if (panel === 'cron') void this.refreshCronTasks();
|
|
188
|
+
if (panel === 'doctor') void this.refreshDoctor();
|
|
189
|
+
if (panel === 'doctor' || panel === 'explain') void this.refreshExplain();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public selectSession(sessionId: string) {
|
|
193
|
+
this.state = { ...this.state, selectedSessionId: sessionId };
|
|
194
|
+
this.emit();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public async refreshCommands() {
|
|
198
|
+
this.state = {
|
|
199
|
+
...this.state,
|
|
200
|
+
commands: {
|
|
201
|
+
...this.state.commands,
|
|
202
|
+
errorMessage: undefined,
|
|
203
|
+
status: 'loading',
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
this.emit();
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const response = await fetch('/__proteum/commands', { cache: 'no-store' });
|
|
210
|
+
const body = (await response.json()) as {
|
|
211
|
+
commands?: TDevCommandDefinition[];
|
|
212
|
+
error?: string;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
throw new Error(body.error || 'Failed to load commands.');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.state = {
|
|
220
|
+
...this.state,
|
|
221
|
+
commands: {
|
|
222
|
+
...this.state.commands,
|
|
223
|
+
commands: Array.isArray(body.commands) ? body.commands.map(cloneCommand) : [],
|
|
224
|
+
errorMessage: undefined,
|
|
225
|
+
lastLoadedAt: nowIso(),
|
|
226
|
+
status: 'ready',
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
this.emit();
|
|
230
|
+
} catch (error) {
|
|
231
|
+
this.state = {
|
|
232
|
+
...this.state,
|
|
233
|
+
commands: {
|
|
234
|
+
...this.state.commands,
|
|
235
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
236
|
+
status: 'error',
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
this.emit();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public async runCommand(commandPath: string) {
|
|
244
|
+
this.state = {
|
|
245
|
+
...this.state,
|
|
246
|
+
commands: {
|
|
247
|
+
...this.state.commands,
|
|
248
|
+
errorMessage: undefined,
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
this.emit();
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const response = await fetch('/__proteum/commands/run', {
|
|
255
|
+
body: JSON.stringify({ path: commandPath }),
|
|
256
|
+
headers: { 'Content-Type': 'application/json' },
|
|
257
|
+
method: 'POST',
|
|
258
|
+
});
|
|
259
|
+
const body = (await response.json()) as {
|
|
260
|
+
error?: string;
|
|
261
|
+
execution?: TDevCommandExecution;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
this.state = {
|
|
265
|
+
...this.state,
|
|
266
|
+
commands: {
|
|
267
|
+
...this.state.commands,
|
|
268
|
+
errorMessage: response.ok ? undefined : body.error || 'Failed to run command.',
|
|
269
|
+
executions:
|
|
270
|
+
body.execution === undefined
|
|
271
|
+
? { ...this.state.commands.executions }
|
|
272
|
+
: {
|
|
273
|
+
...this.state.commands.executions,
|
|
274
|
+
[commandPath]: cloneCommandExecution(body.execution),
|
|
275
|
+
},
|
|
276
|
+
status: response.ok ? 'ready' : 'error',
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
this.emit();
|
|
280
|
+
|
|
281
|
+
if (response.ok) {
|
|
282
|
+
await this.refreshCommands();
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
this.state = {
|
|
286
|
+
...this.state,
|
|
287
|
+
commands: {
|
|
288
|
+
...this.state.commands,
|
|
289
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
290
|
+
status: 'error',
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
this.emit();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
public async refreshCronTasks() {
|
|
298
|
+
this.state = {
|
|
299
|
+
...this.state,
|
|
300
|
+
cron: {
|
|
301
|
+
...this.state.cron,
|
|
302
|
+
errorMessage: undefined,
|
|
303
|
+
status: 'loading',
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
this.emit();
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const response = await fetch('/__proteum/cron/tasks', { cache: 'no-store' });
|
|
310
|
+
const body = (await response.json()) as {
|
|
311
|
+
automaticExecution?: boolean;
|
|
312
|
+
error?: string;
|
|
313
|
+
tasks?: TProfilerCronTask[];
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
if (!response.ok) {
|
|
317
|
+
throw new Error(body.error || 'Failed to load cron tasks.');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
this.state = {
|
|
321
|
+
...this.state,
|
|
322
|
+
cron: {
|
|
323
|
+
automaticExecution: body.automaticExecution ?? false,
|
|
324
|
+
errorMessage: undefined,
|
|
325
|
+
lastLoadedAt: nowIso(),
|
|
326
|
+
status: 'ready',
|
|
327
|
+
tasks: Array.isArray(body.tasks) ? body.tasks.map(cloneCronTask) : [],
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
this.emit();
|
|
331
|
+
} catch (error) {
|
|
332
|
+
this.state = {
|
|
333
|
+
...this.state,
|
|
334
|
+
cron: {
|
|
335
|
+
...this.state.cron,
|
|
336
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
337
|
+
status: 'error',
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
this.emit();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
public async refreshDoctor() {
|
|
345
|
+
this.state = {
|
|
346
|
+
...this.state,
|
|
347
|
+
doctor: {
|
|
348
|
+
...this.state.doctor,
|
|
349
|
+
errorMessage: undefined,
|
|
350
|
+
status: 'loading',
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
this.emit();
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const response = await fetch('/__proteum/doctor', { cache: 'no-store' });
|
|
357
|
+
const body = (await response.json()) as TDoctorResponse & { error?: string };
|
|
358
|
+
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
throw new Error(body.error || 'Failed to load doctor diagnostics.');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.state = {
|
|
364
|
+
...this.state,
|
|
365
|
+
doctor: {
|
|
366
|
+
errorMessage: undefined,
|
|
367
|
+
lastLoadedAt: nowIso(),
|
|
368
|
+
response: cloneDoctorResponse(body),
|
|
369
|
+
status: 'ready',
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
this.emit();
|
|
373
|
+
} catch (error) {
|
|
374
|
+
this.state = {
|
|
375
|
+
...this.state,
|
|
376
|
+
doctor: {
|
|
377
|
+
...this.state.doctor,
|
|
378
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
379
|
+
status: 'error',
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
this.emit();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
public async refreshExplain() {
|
|
387
|
+
this.state = {
|
|
388
|
+
...this.state,
|
|
389
|
+
explain: {
|
|
390
|
+
...this.state.explain,
|
|
391
|
+
errorMessage: undefined,
|
|
392
|
+
status: 'loading',
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
this.emit();
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const response = await fetch('/__proteum/explain', { cache: 'no-store' });
|
|
399
|
+
const body = (await response.json()) as TProteumManifest & { error?: string };
|
|
400
|
+
|
|
401
|
+
if (!response.ok) {
|
|
402
|
+
throw new Error(body.error || 'Failed to load explain data.');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
this.state = {
|
|
406
|
+
...this.state,
|
|
407
|
+
explain: {
|
|
408
|
+
errorMessage: undefined,
|
|
409
|
+
lastLoadedAt: nowIso(),
|
|
410
|
+
manifest: cloneManifest(body),
|
|
411
|
+
status: 'ready',
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
this.emit();
|
|
415
|
+
} catch (error) {
|
|
416
|
+
this.state = {
|
|
417
|
+
...this.state,
|
|
418
|
+
explain: {
|
|
419
|
+
...this.state.explain,
|
|
420
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
421
|
+
status: 'error',
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
this.emit();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
public async runCronTask(name: string) {
|
|
429
|
+
this.state = {
|
|
430
|
+
...this.state,
|
|
431
|
+
cron: {
|
|
432
|
+
...this.state.cron,
|
|
433
|
+
errorMessage: undefined,
|
|
434
|
+
tasks: this.state.cron.tasks.map((task) => (task.name === name ? { ...task, running: true } : cloneCronTask(task))),
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
this.emit();
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
const response = await fetch('/__proteum/cron/tasks/run', {
|
|
441
|
+
body: JSON.stringify({ name }),
|
|
442
|
+
headers: { 'Content-Type': 'application/json' },
|
|
443
|
+
method: 'POST',
|
|
444
|
+
});
|
|
445
|
+
const body = (await response.json()) as {
|
|
446
|
+
error?: string;
|
|
447
|
+
task?: TProfilerCronTask;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const nextTasks = this.state.cron.tasks.map((task) =>
|
|
451
|
+
task.name === name && body.task ? cloneCronTask(body.task) : cloneCronTask(task),
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
this.state = {
|
|
455
|
+
...this.state,
|
|
456
|
+
cron: {
|
|
457
|
+
...this.state.cron,
|
|
458
|
+
errorMessage: response.ok ? undefined : body.error || 'Failed to run cron task.',
|
|
459
|
+
status: response.ok ? 'ready' : 'error',
|
|
460
|
+
tasks: nextTasks,
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
this.emit();
|
|
464
|
+
|
|
465
|
+
if (response.ok) {
|
|
466
|
+
await this.refreshCronTasks();
|
|
467
|
+
}
|
|
468
|
+
} catch (error) {
|
|
469
|
+
this.state = {
|
|
470
|
+
...this.state,
|
|
471
|
+
cron: {
|
|
472
|
+
...this.state.cron,
|
|
473
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
474
|
+
status: 'error',
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
this.emit();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
public ensureInitialSession(input: { path: string; requestId?: string; url: string }) {
|
|
482
|
+
if (this.state.sessions.some((session) => session.id === 'initial-ssr')) return;
|
|
483
|
+
|
|
484
|
+
const session: TProfilerNavigationSession = {
|
|
485
|
+
id: 'initial-ssr',
|
|
486
|
+
kind: 'initial-ssr',
|
|
487
|
+
label: input.path,
|
|
488
|
+
path: input.path,
|
|
489
|
+
url: input.url,
|
|
490
|
+
startedAt: nowIso(),
|
|
491
|
+
status: 'completed',
|
|
492
|
+
requestId: input.requestId,
|
|
493
|
+
steps: [this.createStep('Hydrate')],
|
|
494
|
+
traces: input.requestId
|
|
495
|
+
? [
|
|
496
|
+
{
|
|
497
|
+
id: this.nextTraceId(),
|
|
498
|
+
kind: 'initial-root',
|
|
499
|
+
label: 'Initial SSR request',
|
|
500
|
+
method: 'GET',
|
|
501
|
+
path: input.path,
|
|
502
|
+
requestId: input.requestId,
|
|
503
|
+
startedAt: nowIso(),
|
|
504
|
+
status: 'pending',
|
|
505
|
+
},
|
|
506
|
+
]
|
|
507
|
+
: [],
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
this.state = {
|
|
511
|
+
...this.state,
|
|
512
|
+
currentSessionId: session.id,
|
|
513
|
+
selectedSessionId: session.id,
|
|
514
|
+
sessions: [...this.state.sessions, session],
|
|
515
|
+
};
|
|
516
|
+
this.emit();
|
|
517
|
+
|
|
518
|
+
if (input.requestId) void this.attachTraceByRequestId(session.id, session.traces[0]?.id, input.requestId);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
public markInitialHydrated(meta: { chunkId?: string; title?: string }) {
|
|
522
|
+
const session = this.getSession('initial-ssr');
|
|
523
|
+
if (!session) return;
|
|
524
|
+
|
|
525
|
+
const hydrateStep = session.steps[0];
|
|
526
|
+
if (hydrateStep && hydrateStep.finishedAt === undefined) {
|
|
527
|
+
hydrateStep.finishedAt = nowIso();
|
|
528
|
+
hydrateStep.durationMs = durationMs(hydrateStep.startedAt, hydrateStep.finishedAt);
|
|
529
|
+
hydrateStep.status = 'completed';
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
session.routeChunkId = meta.chunkId || session.routeChunkId;
|
|
533
|
+
session.title = meta.title || session.title;
|
|
534
|
+
this.commitSession(session);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
public startNavigationSession(input: { path: string; url: string }) {
|
|
538
|
+
const sessionId = `nav:${++this.navigationCounter}`;
|
|
539
|
+
const session: TProfilerNavigationSession = {
|
|
540
|
+
id: sessionId,
|
|
541
|
+
kind: 'client-navigation',
|
|
542
|
+
label: `NAV ${input.path}`,
|
|
543
|
+
path: input.path,
|
|
544
|
+
url: input.url,
|
|
545
|
+
startedAt: nowIso(),
|
|
546
|
+
status: 'active',
|
|
547
|
+
steps: [this.createStep('Resolve route')],
|
|
548
|
+
traces: [],
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
this.state = {
|
|
552
|
+
...this.state,
|
|
553
|
+
currentSessionId: session.id,
|
|
554
|
+
selectedSessionId: session.id,
|
|
555
|
+
sessions: [...this.state.sessions, session],
|
|
556
|
+
};
|
|
557
|
+
this.emit();
|
|
558
|
+
|
|
559
|
+
return session.id;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
public completeResolveStep(meta: { chunkId?: string; routeLabel?: string; sessionId?: string }) {
|
|
563
|
+
const session = this.getSession(meta.sessionId || this.state.currentSessionId);
|
|
564
|
+
if (!session) return;
|
|
565
|
+
|
|
566
|
+
const resolveStep = session.steps.find((step) => step.label === 'Resolve route' && step.finishedAt === undefined);
|
|
567
|
+
if (resolveStep) {
|
|
568
|
+
resolveStep.finishedAt = nowIso();
|
|
569
|
+
resolveStep.durationMs = durationMs(resolveStep.startedAt, resolveStep.finishedAt);
|
|
570
|
+
resolveStep.status = 'completed';
|
|
571
|
+
resolveStep.details = {
|
|
572
|
+
...(resolveStep.details || {}),
|
|
573
|
+
...(meta.chunkId ? { chunkId: meta.chunkId } : {}),
|
|
574
|
+
...(meta.routeLabel ? { route: meta.routeLabel } : {}),
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (meta.chunkId) session.routeChunkId = meta.chunkId;
|
|
579
|
+
if (meta.routeLabel) session.routeLabel = meta.routeLabel;
|
|
580
|
+
this.commitSession(session);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
public startChunkStep(chunkId: string, sessionId?: string) {
|
|
584
|
+
const session = this.getSession(sessionId || this.state.currentSessionId);
|
|
585
|
+
if (!session) return undefined;
|
|
586
|
+
|
|
587
|
+
const step = this.createStep(`Load chunk ${chunkId}`, { chunkId });
|
|
588
|
+
session.steps.push(step);
|
|
589
|
+
this.commitSession(session);
|
|
590
|
+
return step.id;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
public finishStep(stepId: string | undefined, status: 'completed' | 'error' = 'completed', errorMessage?: string) {
|
|
594
|
+
if (!stepId) return;
|
|
595
|
+
|
|
596
|
+
for (const session of this.state.sessions) {
|
|
597
|
+
const step = session.steps.find((candidate) => candidate.id === stepId);
|
|
598
|
+
if (!step || step.finishedAt) continue;
|
|
599
|
+
|
|
600
|
+
step.finishedAt = nowIso();
|
|
601
|
+
step.durationMs = durationMs(step.startedAt, step.finishedAt);
|
|
602
|
+
step.status = status;
|
|
603
|
+
step.errorMessage = errorMessage;
|
|
604
|
+
this.commitSession(session);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
public startRenderStep(sessionId?: string) {
|
|
610
|
+
const session = this.getSession(sessionId || this.state.currentSessionId);
|
|
611
|
+
if (!session) return undefined;
|
|
612
|
+
|
|
613
|
+
const step = this.createStep('Render');
|
|
614
|
+
session.steps.push(step);
|
|
615
|
+
this.commitSession(session);
|
|
616
|
+
return step.id;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
public finishNavigation(meta: { chunkId?: string; routeLabel?: string; sessionId?: string; title?: string }) {
|
|
620
|
+
const session = this.getSession(meta.sessionId || this.state.currentSessionId);
|
|
621
|
+
if (!session) return;
|
|
622
|
+
|
|
623
|
+
const renderStep = [...session.steps].reverse().find((step) => step.label === 'Render' && step.finishedAt === undefined);
|
|
624
|
+
if (renderStep) {
|
|
625
|
+
renderStep.finishedAt = nowIso();
|
|
626
|
+
renderStep.durationMs = durationMs(renderStep.startedAt, renderStep.finishedAt);
|
|
627
|
+
renderStep.status = 'completed';
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
session.finishedAt = nowIso();
|
|
631
|
+
session.durationMs = durationMs(session.startedAt, session.finishedAt);
|
|
632
|
+
session.status = session.steps.some((step) => step.status === 'error') ? 'error' : 'completed';
|
|
633
|
+
session.routeChunkId = meta.chunkId || session.routeChunkId;
|
|
634
|
+
session.routeLabel = meta.routeLabel || session.routeLabel;
|
|
635
|
+
session.title = meta.title || session.title;
|
|
636
|
+
this.commitSession(session);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
public failNavigation(message: string, sessionId?: string) {
|
|
640
|
+
const session = this.getSession(sessionId || this.state.currentSessionId);
|
|
641
|
+
if (!session) return;
|
|
642
|
+
|
|
643
|
+
const pendingStep = [...session.steps].reverse().find((step) => step.finishedAt === undefined);
|
|
644
|
+
if (pendingStep) {
|
|
645
|
+
pendingStep.finishedAt = nowIso();
|
|
646
|
+
pendingStep.durationMs = durationMs(pendingStep.startedAt, pendingStep.finishedAt);
|
|
647
|
+
pendingStep.status = 'error';
|
|
648
|
+
pendingStep.errorMessage = message;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
session.finishedAt = nowIso();
|
|
652
|
+
session.durationMs = durationMs(session.startedAt, session.finishedAt);
|
|
653
|
+
session.status = 'error';
|
|
654
|
+
this.commitSession(session);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
public startTrace(kind: TProfilerSessionTraceKind, input: TStartTraceInput) {
|
|
658
|
+
const session = this.getSession(input.sessionId || this.getAttachmentSessionId());
|
|
659
|
+
if (!session) return undefined;
|
|
660
|
+
|
|
661
|
+
const trace: TProfilerSessionTrace = {
|
|
662
|
+
id: this.nextTraceId(),
|
|
663
|
+
kind,
|
|
664
|
+
label: input.label,
|
|
665
|
+
method: input.method,
|
|
666
|
+
path: input.path,
|
|
667
|
+
requestId: input.requestId,
|
|
668
|
+
fetcherIds: input.fetcherIds,
|
|
669
|
+
startedAt: nowIso(),
|
|
670
|
+
status: 'pending',
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
session.traces.push(trace);
|
|
674
|
+
this.commitSession(session);
|
|
675
|
+
return { sessionId: session.id, traceId: trace.id };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
public completeTrace(traceId: string | undefined, meta: { durationMs?: number; errorMessage?: string; status?: 'completed' | 'error' }) {
|
|
679
|
+
if (!traceId) return;
|
|
680
|
+
|
|
681
|
+
for (const session of this.state.sessions) {
|
|
682
|
+
const trace = session.traces.find((candidate) => candidate.id === traceId);
|
|
683
|
+
if (!trace) continue;
|
|
684
|
+
|
|
685
|
+
trace.finishedAt = nowIso();
|
|
686
|
+
trace.durationMs = meta.durationMs ?? durationMs(trace.startedAt, trace.finishedAt);
|
|
687
|
+
trace.status = meta.status || 'completed';
|
|
688
|
+
trace.errorMessage = meta.errorMessage;
|
|
689
|
+
this.commitSession(session);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
public async attachTraceByRequestId(sessionId: string | undefined, traceId: string | undefined, requestId: string | undefined) {
|
|
695
|
+
if (!sessionId || !traceId || !requestId) return;
|
|
696
|
+
|
|
697
|
+
const session = this.getSession(sessionId);
|
|
698
|
+
const traceRef = session?.traces.find((candidate) => candidate.id === traceId);
|
|
699
|
+
if (!session || !traceRef) return;
|
|
700
|
+
|
|
701
|
+
traceRef.requestId = requestId;
|
|
702
|
+
if (traceRef.kind !== 'async') session.requestId = session.requestId || requestId;
|
|
703
|
+
this.commitSession(session);
|
|
704
|
+
|
|
705
|
+
const trace = await this.fetchTrace(requestId);
|
|
706
|
+
if (!trace) return;
|
|
707
|
+
|
|
708
|
+
const nextSession = this.getSession(sessionId);
|
|
709
|
+
const nextTraceRef = nextSession?.traces.find((candidate) => candidate.id === traceId);
|
|
710
|
+
if (!nextSession || !nextTraceRef) return;
|
|
711
|
+
|
|
712
|
+
nextTraceRef.trace = trace;
|
|
713
|
+
nextTraceRef.method = trace.method;
|
|
714
|
+
nextTraceRef.path = trace.path;
|
|
715
|
+
nextTraceRef.startedAt = trace.startedAt;
|
|
716
|
+
nextTraceRef.finishedAt = trace.finishedAt;
|
|
717
|
+
nextTraceRef.durationMs = trace.durationMs;
|
|
718
|
+
nextTraceRef.status =
|
|
719
|
+
trace.errorMessage || (trace.statusCode !== undefined && trace.statusCode >= 400) ? 'error' : 'completed';
|
|
720
|
+
nextTraceRef.errorMessage = trace.errorMessage;
|
|
721
|
+
|
|
722
|
+
if (nextTraceRef.kind !== 'async') {
|
|
723
|
+
nextSession.requestId = trace.id;
|
|
724
|
+
nextSession.routeLabel = nextSession.routeLabel || this.findRouteLabel(trace);
|
|
725
|
+
nextSession.title = nextSession.title || this.findRenderTitle(trace);
|
|
726
|
+
nextSession.routeChunkId = nextSession.routeChunkId || this.findRouteChunkId(trace);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
this.commitSession(nextSession);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
public getRequestHeaders(origin: string) {
|
|
733
|
+
const session = this.getSession(this.getAttachmentSessionId());
|
|
734
|
+
if (!session) return {};
|
|
735
|
+
|
|
736
|
+
const headers: Record<string, string> = {
|
|
737
|
+
[profilerSessionIdHeader]: session.id,
|
|
738
|
+
[profilerOriginHeader]: origin,
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const parentRequestId = this.getParentRequestId(session);
|
|
742
|
+
if (parentRequestId) headers[profilerParentRequestIdHeader] = parentRequestId;
|
|
743
|
+
|
|
744
|
+
return headers;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private createStep(label: string, details?: TProfilerNavigationStep['details']): TProfilerNavigationStep {
|
|
748
|
+
return {
|
|
749
|
+
id: `step:${++this.stepCounter}`,
|
|
750
|
+
label,
|
|
751
|
+
startedAt: nowIso(),
|
|
752
|
+
status: 'pending',
|
|
753
|
+
details,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
private nextTraceId() {
|
|
758
|
+
return `trace:${++this.traceCounter}`;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private getAttachmentSessionId() {
|
|
762
|
+
return this.state.currentSessionId || this.state.selectedSessionId || this.state.sessions[this.state.sessions.length - 1]?.id;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private getParentRequestId(session: TProfilerNavigationSession) {
|
|
766
|
+
if (session.requestId) return session.requestId;
|
|
767
|
+
|
|
768
|
+
for (let index = this.state.sessions.length - 1; index >= 0; index -= 1) {
|
|
769
|
+
const candidate = this.state.sessions[index];
|
|
770
|
+
if (candidate.id === session.id) continue;
|
|
771
|
+
if (candidate.requestId) return candidate.requestId;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return undefined;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private getSession(sessionId?: string) {
|
|
778
|
+
if (!sessionId) return undefined;
|
|
779
|
+
return this.state.sessions.find((session) => session.id === sessionId);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
private commitSession(session: TProfilerNavigationSession) {
|
|
783
|
+
this.state = {
|
|
784
|
+
...this.state,
|
|
785
|
+
commands: cloneCommandsState(this.state.commands),
|
|
786
|
+
cron: cloneCronState(this.state.cron),
|
|
787
|
+
doctor: cloneDoctorState(this.state.doctor),
|
|
788
|
+
explain: cloneExplainState(this.state.explain),
|
|
789
|
+
sessions: this.state.sessions.map((candidate) => (candidate.id === session.id ? cloneSession(session) : candidate)),
|
|
790
|
+
};
|
|
791
|
+
this.emit();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
private emit() {
|
|
795
|
+
for (const listener of this.listeners) listener();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
private async fetchTrace(requestId: string) {
|
|
799
|
+
const existing = this.traceFetches.get(requestId);
|
|
800
|
+
if (existing) return existing;
|
|
801
|
+
|
|
802
|
+
const fetchPromise = fetch(`/__proteum/trace/requests/${requestId}`, { cache: 'no-store' })
|
|
803
|
+
.then(async (response) => {
|
|
804
|
+
if (!response.ok) return undefined;
|
|
805
|
+
const body = (await response.json()) as { request?: TRequestTrace };
|
|
806
|
+
return body.request;
|
|
807
|
+
})
|
|
808
|
+
.catch((_error) => undefined);
|
|
809
|
+
|
|
810
|
+
this.traceFetches.set(requestId, fetchPromise);
|
|
811
|
+
return fetchPromise;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
private findRenderTitle(trace: TRequestTrace) {
|
|
815
|
+
const title = trace.events.find((event) => event.type === 'render.start')?.details.title;
|
|
816
|
+
return typeof title === 'string' ? title : undefined;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
private findRouteChunkId(trace: TRequestTrace) {
|
|
820
|
+
const chunkId = trace.events.find((event) => event.type === 'render.start')?.details.chunkId;
|
|
821
|
+
return typeof chunkId === 'string' ? chunkId : undefined;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private findRouteLabel(trace: TRequestTrace) {
|
|
825
|
+
const routeEvent =
|
|
826
|
+
trace.events.find((event) => event.type === 'resolve.route-match') ||
|
|
827
|
+
trace.events.find((event) => event.type === 'resolve.controller-route');
|
|
828
|
+
if (!routeEvent) return undefined;
|
|
829
|
+
|
|
830
|
+
const routeId = routeEvent.details.routeId;
|
|
831
|
+
if (typeof routeId === 'string' && routeId) return routeId;
|
|
832
|
+
|
|
833
|
+
const routePath = routeEvent.details.routePath || routeEvent.details.path;
|
|
834
|
+
return typeof routePath === 'string' && routePath ? routePath : undefined;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export const profilerRuntime = new ProfilerRuntime();
|
|
839
|
+
|
|
840
|
+
export const readProfilerTraceRequestId = (response: Response) => response.headers.get(profilerTraceRequestIdHeader) || undefined;
|