@warlock.js/devtools 5.25.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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/cjs/index.cjs +694 -0
- package/cjs/index.cjs.map +1 -0
- package/cjs/server-DHYSVive.cjs +286 -0
- package/cjs/server-DHYSVive.cjs.map +1 -0
- package/esm/caller-location.mjs +33 -0
- package/esm/caller-location.mjs.map +1 -0
- package/esm/collector.mjs +185 -0
- package/esm/collector.mjs.map +1 -0
- package/esm/current-request-id.mjs +24 -0
- package/esm/current-request-id.mjs.map +1 -0
- package/esm/devtools-connector.d.mts +26 -0
- package/esm/devtools-connector.d.mts.map +1 -0
- package/esm/devtools-connector.mjs +112 -0
- package/esm/devtools-connector.mjs.map +1 -0
- package/esm/index.d.mts +3 -0
- package/esm/index.mjs +3 -0
- package/esm/n-plus-one.mjs +36 -0
- package/esm/n-plus-one.mjs.map +1 -0
- package/esm/options.mjs +19 -0
- package/esm/options.mjs.map +1 -0
- package/esm/query-shape.mjs +27 -0
- package/esm/query-shape.mjs.map +1 -0
- package/esm/safe-value.mjs +63 -0
- package/esm/safe-value.mjs.map +1 -0
- package/esm/server/background-routes.mjs +9 -0
- package/esm/server/background-routes.mjs.map +1 -0
- package/esm/server/explain-route.mjs +37 -0
- package/esm/server/explain-route.mjs.map +1 -0
- package/esm/server/guard.mjs +30 -0
- package/esm/server/guard.mjs.map +1 -0
- package/esm/server/index.mjs +37 -0
- package/esm/server/index.mjs.map +1 -0
- package/esm/server/log-routes.mjs +12 -0
- package/esm/server/log-routes.mjs.map +1 -0
- package/esm/server/mail-routes.mjs +27 -0
- package/esm/server/mail-routes.mjs.map +1 -0
- package/esm/server/requests-routes.mjs +21 -0
- package/esm/server/requests-routes.mjs.map +1 -0
- package/esm/server/route-routes.mjs +9 -0
- package/esm/server/route-routes.mjs.map +1 -0
- package/esm/server/stream-route.mjs +32 -0
- package/esm/server/stream-route.mjs.map +1 -0
- package/esm/server/ui-assets.mjs +38 -0
- package/esm/server/ui-assets.mjs.map +1 -0
- package/esm/sources/cache-source.mjs +51 -0
- package/esm/sources/cache-source.mjs.map +1 -0
- package/esm/sources/log-source.mjs +46 -0
- package/esm/sources/log-source.mjs.map +1 -0
- package/esm/sources/mail-source.mjs +48 -0
- package/esm/sources/mail-source.mjs.map +1 -0
- package/esm/sources/query-source.mjs +52 -0
- package/esm/sources/query-source.mjs.map +1 -0
- package/esm/sources/tracing-source.mjs +47 -0
- package/esm/sources/tracing-source.mjs.map +1 -0
- package/esm/types.d.mts +213 -0
- package/esm/types.d.mts.map +1 -0
- package/esm/ui-root.mjs +40 -0
- package/esm/ui-root.mjs.map +1 -0
- package/llms-full.txt +78 -0
- package/llms.txt +9 -0
- package/package.json +52 -0
- package/skills/devtools-overview/SKILL.md +68 -0
- package/ui/app.css +354 -0
- package/ui/app.js +520 -0
- package/ui/index.html +24 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { detectNPlusOne } from "./n-plus-one.mjs";
|
|
2
|
+
import { queryShape } from "./query-shape.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../devtools/src/collector.ts
|
|
5
|
+
const mailLimit = 100;
|
|
6
|
+
const logLimit = 1e3;
|
|
7
|
+
const backgroundLimit = 500;
|
|
8
|
+
/**
|
|
9
|
+
* Creates the in-memory devtools store.
|
|
10
|
+
*
|
|
11
|
+
* Read methods return shallow structural copies, including copied arrays, so consumers
|
|
12
|
+
* cannot mutate collector-owned arrays. The individual nested entries are retained.
|
|
13
|
+
*/
|
|
14
|
+
function createCollector(options) {
|
|
15
|
+
const requests = /* @__PURE__ */ new Map();
|
|
16
|
+
const mails = /* @__PURE__ */ new Map();
|
|
17
|
+
const logs = [];
|
|
18
|
+
const background = {
|
|
19
|
+
queries: [],
|
|
20
|
+
cache: [],
|
|
21
|
+
logs: []
|
|
22
|
+
};
|
|
23
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
24
|
+
let querySequence = 0;
|
|
25
|
+
const emit = (event) => {
|
|
26
|
+
for (const listener of listeners) try {
|
|
27
|
+
listener(event);
|
|
28
|
+
} catch {}
|
|
29
|
+
};
|
|
30
|
+
const addBackground = (list, value) => {
|
|
31
|
+
list.push(value);
|
|
32
|
+
if (list.length > backgroundLimit) list.shift();
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
startRequest(request) {
|
|
36
|
+
requests.delete(request.id);
|
|
37
|
+
requests.set(request.id, {
|
|
38
|
+
...request,
|
|
39
|
+
phases: [],
|
|
40
|
+
queries: [],
|
|
41
|
+
cache: [],
|
|
42
|
+
logs: [],
|
|
43
|
+
mails: [],
|
|
44
|
+
warnings: []
|
|
45
|
+
});
|
|
46
|
+
while (requests.size > options.maxRequests) requests.delete(requests.keys().next().value);
|
|
47
|
+
},
|
|
48
|
+
endRequest(id, result) {
|
|
49
|
+
const request = requests.get(id);
|
|
50
|
+
if (!request) return;
|
|
51
|
+
request.status = result.status;
|
|
52
|
+
request.durationMs = result.durationMs;
|
|
53
|
+
request.error = result.error;
|
|
54
|
+
if (result.route !== void 0) request.route = result.route;
|
|
55
|
+
request.warnings = detectNPlusOne(request.queries, options.nPlusOneThreshold);
|
|
56
|
+
emit({
|
|
57
|
+
type: "request",
|
|
58
|
+
data: summary(request)
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
addPhase(requestId, phase) {
|
|
62
|
+
requests.get(requestId)?.phases.push(phase);
|
|
63
|
+
},
|
|
64
|
+
addQuery(requestId, query) {
|
|
65
|
+
const stored = {
|
|
66
|
+
...query,
|
|
67
|
+
id: `q${++querySequence}`,
|
|
68
|
+
shape: queryShape(query)
|
|
69
|
+
};
|
|
70
|
+
const request = requestId ? requests.get(requestId) : void 0;
|
|
71
|
+
if (request) request.queries.push(stored);
|
|
72
|
+
else addBackground(background.queries, stored);
|
|
73
|
+
return stored;
|
|
74
|
+
},
|
|
75
|
+
addCacheOperation(requestId, operation) {
|
|
76
|
+
const request = requestId ? requests.get(requestId) : void 0;
|
|
77
|
+
if (request) request.cache.push(operation);
|
|
78
|
+
else addBackground(background.cache, operation);
|
|
79
|
+
},
|
|
80
|
+
addLog(requestId, entry) {
|
|
81
|
+
const request = requestId ? requests.get(requestId) : void 0;
|
|
82
|
+
if (request) request.logs.push(entry);
|
|
83
|
+
else addBackground(background.logs, entry);
|
|
84
|
+
logs.unshift(entry);
|
|
85
|
+
if (logs.length > logLimit) logs.pop();
|
|
86
|
+
emit({
|
|
87
|
+
type: "log",
|
|
88
|
+
data: entry
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
addMail(mail) {
|
|
92
|
+
mails.delete(mail.id);
|
|
93
|
+
mails.set(mail.id, mail);
|
|
94
|
+
while (mails.size > mailLimit) mails.delete(mails.keys().next().value);
|
|
95
|
+
if (mail.requestId) requests.get(mail.requestId)?.mails.push(mail.id);
|
|
96
|
+
emit({
|
|
97
|
+
type: "mail",
|
|
98
|
+
data: mailSummary(mail)
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
listRequests() {
|
|
102
|
+
return [...requests.values()].reverse().map(summary);
|
|
103
|
+
},
|
|
104
|
+
getRequest(id) {
|
|
105
|
+
const request = requests.get(id);
|
|
106
|
+
return request && copyRequest(request);
|
|
107
|
+
},
|
|
108
|
+
listMails() {
|
|
109
|
+
return [...mails.values()].reverse().map(mailSummary);
|
|
110
|
+
},
|
|
111
|
+
getMail(id) {
|
|
112
|
+
const mail = mails.get(id);
|
|
113
|
+
return mail && {
|
|
114
|
+
...mail,
|
|
115
|
+
to: [...mail.to],
|
|
116
|
+
cc: [...mail.cc],
|
|
117
|
+
bcc: [...mail.bcc],
|
|
118
|
+
headers: { ...mail.headers },
|
|
119
|
+
attachments: [...mail.attachments]
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
listLogs(filter = {}) {
|
|
123
|
+
const module = filter.module?.toLowerCase();
|
|
124
|
+
return logs.filter((entry) => (!filter.level || entry.level === filter.level) && (!module || entry.module.toLowerCase().includes(module))).map((entry) => ({ ...entry }));
|
|
125
|
+
},
|
|
126
|
+
getBackground() {
|
|
127
|
+
return {
|
|
128
|
+
queries: [...background.queries],
|
|
129
|
+
cache: [...background.cache],
|
|
130
|
+
logs: [...background.logs]
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
subscribe(listener) {
|
|
134
|
+
listeners.add(listener);
|
|
135
|
+
return () => listeners.delete(listener);
|
|
136
|
+
},
|
|
137
|
+
clear() {
|
|
138
|
+
requests.clear();
|
|
139
|
+
mails.clear();
|
|
140
|
+
logs.length = 0;
|
|
141
|
+
background.queries.length = 0;
|
|
142
|
+
background.cache.length = 0;
|
|
143
|
+
background.logs.length = 0;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function summary(request) {
|
|
148
|
+
const { id, method, path, route, status, startedAt, durationMs } = request;
|
|
149
|
+
return {
|
|
150
|
+
id,
|
|
151
|
+
method,
|
|
152
|
+
path,
|
|
153
|
+
route,
|
|
154
|
+
status,
|
|
155
|
+
startedAt,
|
|
156
|
+
durationMs,
|
|
157
|
+
queryCount: request.queries.length,
|
|
158
|
+
warningCount: request.warnings.length
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function mailSummary(mail) {
|
|
162
|
+
const { id, requestId, capturedAt, to, subject } = mail;
|
|
163
|
+
return {
|
|
164
|
+
id,
|
|
165
|
+
requestId,
|
|
166
|
+
capturedAt,
|
|
167
|
+
to: [...to],
|
|
168
|
+
subject
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function copyRequest(request) {
|
|
172
|
+
return {
|
|
173
|
+
...request,
|
|
174
|
+
phases: [...request.phases],
|
|
175
|
+
queries: [...request.queries],
|
|
176
|
+
cache: [...request.cache],
|
|
177
|
+
logs: [...request.logs],
|
|
178
|
+
mails: [...request.mails],
|
|
179
|
+
warnings: [...request.warnings]
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
//#endregion
|
|
184
|
+
export { createCollector };
|
|
185
|
+
//# sourceMappingURL=collector.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collector.mjs","names":[],"sources":["../../../../../../devtools/src/collector.ts"],"sourcesContent":["import type {\n DevtoolsBackground,\n DevtoolsCacheOperation,\n DevtoolsCollector,\n DevtoolsDisposer,\n DevtoolsLogEntry,\n DevtoolsMail,\n DevtoolsMailSummary,\n DevtoolsPhase,\n DevtoolsQuery,\n DevtoolsRequest,\n DevtoolsRequestSummary,\n DevtoolsStreamEvent,\n ResolvedDevtoolsOptions,\n} from \"./types\";\nimport { detectNPlusOne } from \"./n-plus-one\";\nimport { queryShape } from \"./query-shape\";\n\nconst mailLimit = 100;\nconst logLimit = 1000;\nconst backgroundLimit = 500;\n\n/**\n * Creates the in-memory devtools store.\n *\n * Read methods return shallow structural copies, including copied arrays, so consumers\n * cannot mutate collector-owned arrays. The individual nested entries are retained.\n */\nexport function createCollector(options: ResolvedDevtoolsOptions): DevtoolsCollector {\n const requests = new Map<string, DevtoolsRequest>();\n const mails = new Map<string, DevtoolsMail>();\n const logs: DevtoolsLogEntry[] = [];\n const background: DevtoolsBackground = { queries: [], cache: [], logs: [] };\n const listeners = new Set<(event: DevtoolsStreamEvent) => void>();\n let querySequence = 0;\n\n const emit = (event: DevtoolsStreamEvent) => {\n for (const listener of listeners) {\n try {\n listener(event);\n } catch {\n /* Subscribers must not affect collection. */\n }\n }\n };\n\n const addBackground = <T>(list: T[], value: T) => {\n list.push(value);\n if (list.length > backgroundLimit) list.shift();\n };\n\n return {\n startRequest(request) {\n requests.delete(request.id);\n requests.set(request.id, {\n ...request,\n phases: [],\n queries: [],\n cache: [],\n logs: [],\n mails: [],\n warnings: [],\n });\n while (requests.size > options.maxRequests)\n requests.delete(requests.keys().next().value as string);\n },\n endRequest(id, result) {\n const request = requests.get(id);\n if (!request) return;\n request.status = result.status;\n request.durationMs = result.durationMs;\n request.error = result.error;\n if (result.route !== undefined) request.route = result.route;\n request.warnings = detectNPlusOne(request.queries, options.nPlusOneThreshold);\n emit({ type: \"request\", data: summary(request) });\n },\n addPhase(requestId, phase) {\n requests.get(requestId)?.phases.push(phase);\n },\n addQuery(requestId, query) {\n const stored: DevtoolsQuery = {\n ...query,\n id: `q${++querySequence}`,\n shape: queryShape(query),\n };\n const request = requestId ? requests.get(requestId) : undefined;\n if (request) request.queries.push(stored);\n else addBackground(background.queries, stored);\n return stored;\n },\n addCacheOperation(requestId, operation) {\n const request = requestId ? requests.get(requestId) : undefined;\n if (request) request.cache.push(operation);\n else addBackground(background.cache, operation);\n },\n addLog(requestId, entry) {\n const request = requestId ? requests.get(requestId) : undefined;\n if (request) request.logs.push(entry);\n else addBackground(background.logs, entry);\n logs.unshift(entry);\n if (logs.length > logLimit) logs.pop();\n emit({ type: \"log\", data: entry });\n },\n addMail(mail) {\n mails.delete(mail.id);\n mails.set(mail.id, mail);\n while (mails.size > mailLimit) mails.delete(mails.keys().next().value as string);\n if (mail.requestId) requests.get(mail.requestId)?.mails.push(mail.id);\n emit({ type: \"mail\", data: mailSummary(mail) });\n },\n listRequests() {\n return [...requests.values()].reverse().map(summary);\n },\n getRequest(id) {\n const request = requests.get(id);\n return request && copyRequest(request);\n },\n listMails() {\n return [...mails.values()].reverse().map(mailSummary);\n },\n getMail(id) {\n const mail = mails.get(id);\n return (\n mail && {\n ...mail,\n to: [...mail.to],\n cc: [...mail.cc],\n bcc: [...mail.bcc],\n headers: { ...mail.headers },\n attachments: [...mail.attachments],\n }\n );\n },\n listLogs(filter = {}) {\n const module = filter.module?.toLowerCase();\n return logs\n .filter(\n (entry) =>\n (!filter.level || entry.level === filter.level) &&\n (!module || entry.module.toLowerCase().includes(module)),\n )\n .map((entry) => ({ ...entry }));\n },\n getBackground() {\n return {\n queries: [...background.queries],\n cache: [...background.cache],\n logs: [...background.logs],\n };\n },\n subscribe(listener): DevtoolsDisposer {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n clear() {\n requests.clear();\n mails.clear();\n logs.length = 0;\n background.queries.length = 0;\n background.cache.length = 0;\n background.logs.length = 0;\n },\n };\n}\n\nfunction summary(request: DevtoolsRequest): DevtoolsRequestSummary {\n const { id, method, path, route, status, startedAt, durationMs } = request;\n return {\n id,\n method,\n path,\n route,\n status,\n startedAt,\n durationMs,\n queryCount: request.queries.length,\n warningCount: request.warnings.length,\n };\n}\n\nfunction mailSummary(mail: DevtoolsMail): DevtoolsMailSummary {\n const { id, requestId, capturedAt, to, subject } = mail;\n return { id, requestId, capturedAt, to: [...to], subject };\n}\n\nfunction copyRequest(request: DevtoolsRequest): DevtoolsRequest {\n return {\n ...request,\n phases: [...request.phases],\n queries: [...request.queries],\n cache: [...request.cache],\n logs: [...request.logs],\n mails: [...request.mails],\n warnings: [...request.warnings],\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { requestContext } from "@warlock.js/core";
|
|
2
|
+
|
|
3
|
+
//#region ../devtools/src/current-request-id.ts
|
|
4
|
+
/**
|
|
5
|
+
* The current request's id (core's `request.id`, threaded through
|
|
6
|
+
* `AsyncLocalStorage`), or `undefined` outside a request — sources feed
|
|
7
|
+
* devtools' background bucket in that case.
|
|
8
|
+
*
|
|
9
|
+
* Only ever reached through `./sources/*`, which the connector imports
|
|
10
|
+
* dynamically inside `boot()` — by then core is already running, so this
|
|
11
|
+
* static import never drags core's runtime graph into config-load time (see
|
|
12
|
+
* `devtools-connector.ts`).
|
|
13
|
+
*/
|
|
14
|
+
function currentRequestId() {
|
|
15
|
+
try {
|
|
16
|
+
return requestContext.getRequest()?.id;
|
|
17
|
+
} catch {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { currentRequestId };
|
|
24
|
+
//# sourceMappingURL=current-request-id.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"current-request-id.mjs","names":[],"sources":["../../../../../../devtools/src/current-request-id.ts"],"sourcesContent":["import { requestContext } from \"@warlock.js/core\";\n\n/**\n * The current request's id (core's `request.id`, threaded through\n * `AsyncLocalStorage`), or `undefined` outside a request — sources feed\n * devtools' background bucket in that case.\n *\n * Only ever reached through `./sources/*`, which the connector imports\n * dynamically inside `boot()` — by then core is already running, so this\n * static import never drags core's runtime graph into config-load time (see\n * `devtools-connector.ts`).\n */\nexport function currentRequestId(): string | undefined {\n try {\n return requestContext.getRequest()?.id;\n } catch {\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAgB,mBAAuC;CACrD,IAAI;EACF,OAAO,eAAe,WAAW,CAAC,EAAE;CACtC,QAAQ;EACN;CACF;AACF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DevtoolsOptions } from "./types.mjs";
|
|
2
|
+
import { Connector } from "@warlock.js/core";
|
|
3
|
+
|
|
4
|
+
//#region ../devtools/src/devtools-connector.d.ts
|
|
5
|
+
/** Options accepted by `devtoolsConnector()`. */
|
|
6
|
+
type DevtoolsConnectorOptions = DevtoolsOptions;
|
|
7
|
+
/**
|
|
8
|
+
* Construct the devtools connector.
|
|
9
|
+
*
|
|
10
|
+
* Refuses to run outside development: `boot()` checks
|
|
11
|
+
* `Application.runtimeStrategy` and, when it isn't `"development"`, logs one
|
|
12
|
+
* warning and returns without mounting a route or subscribing to anything.
|
|
13
|
+
* In development it attaches every source (tracing, queries, cache, mail,
|
|
14
|
+
* logs), then mounts the dashboard's routes on the HTTP server — before it
|
|
15
|
+
* starts listening, the only point in the boot sequence where mounting is
|
|
16
|
+
* possible (see `queue/src/dashboard-boot.ts` for the same constraint).
|
|
17
|
+
*
|
|
18
|
+
* Apps don't call this: `warlock dev` registers it automatically when the
|
|
19
|
+
* package is installed (`warlock add devtools`). Keep it OUT of
|
|
20
|
+
* `warlock.config.ts` — devtools is a dev dependency, so a production
|
|
21
|
+
* install doesn't have it and a config import of it would fail there.
|
|
22
|
+
*/
|
|
23
|
+
declare function devtoolsConnector(options?: DevtoolsConnectorOptions): Connector;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { devtoolsConnector };
|
|
26
|
+
//# sourceMappingURL=devtools-connector.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools-connector.d.mts","names":[],"sources":["../../../../../../devtools/src/devtools-connector.ts"],"mappings":";;;;AA8CoF;AAAA,KAlBxE,wBAAA,GAA2B,eAAe;;;;;;;;;;;;;;;;;iBAkBtC,iBAAA,CAAkB,OAAA,GAAS,wBAAA,GAAgC,SAAS"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createCollector } from "./collector.mjs";
|
|
2
|
+
import { resolveDevtoolsOptions } from "./options.mjs";
|
|
3
|
+
import { attachCacheSource } from "./sources/cache-source.mjs";
|
|
4
|
+
import { attachLogSource } from "./sources/log-source.mjs";
|
|
5
|
+
import { attachMailSource } from "./sources/mail-source.mjs";
|
|
6
|
+
import { attachQuerySource } from "./sources/query-source.mjs";
|
|
7
|
+
import { attachTracingSource } from "./sources/tracing-source.mjs";
|
|
8
|
+
|
|
9
|
+
//#region ../devtools/src/devtools-connector.ts
|
|
10
|
+
/**
|
|
11
|
+
* Boots after the queue connector (priority 11), which is the last
|
|
12
|
+
* built-in/framework connector to claim a priority; devtools has nothing
|
|
13
|
+
* that depends on it, so it simply goes last.
|
|
14
|
+
*/
|
|
15
|
+
const DEVTOOLS_CONNECTOR_PRIORITY = 12;
|
|
16
|
+
/**
|
|
17
|
+
* Construct the devtools connector.
|
|
18
|
+
*
|
|
19
|
+
* Refuses to run outside development: `boot()` checks
|
|
20
|
+
* `Application.runtimeStrategy` and, when it isn't `"development"`, logs one
|
|
21
|
+
* warning and returns without mounting a route or subscribing to anything.
|
|
22
|
+
* In development it attaches every source (tracing, queries, cache, mail,
|
|
23
|
+
* logs), then mounts the dashboard's routes on the HTTP server — before it
|
|
24
|
+
* starts listening, the only point in the boot sequence where mounting is
|
|
25
|
+
* possible (see `queue/src/dashboard-boot.ts` for the same constraint).
|
|
26
|
+
*
|
|
27
|
+
* Apps don't call this: `warlock dev` registers it automatically when the
|
|
28
|
+
* package is installed (`warlock add devtools`). Keep it OUT of
|
|
29
|
+
* `warlock.config.ts` — devtools is a dev dependency, so a production
|
|
30
|
+
* install doesn't have it and a config import of it would fail there.
|
|
31
|
+
*/
|
|
32
|
+
function devtoolsConnector(options = {}) {
|
|
33
|
+
let active = false;
|
|
34
|
+
const disposers = [];
|
|
35
|
+
const connector = {
|
|
36
|
+
name: "devtools",
|
|
37
|
+
priority: 12,
|
|
38
|
+
lifecyclePhase: "late",
|
|
39
|
+
isActive: () => active,
|
|
40
|
+
async boot() {
|
|
41
|
+
const { Application, getHttpServer } = await import("@warlock.js/core");
|
|
42
|
+
const { log } = await import("@warlock.js/logger");
|
|
43
|
+
if (Application.runtimeStrategy !== "development") {
|
|
44
|
+
log.warn("devtools", "disabled", "@warlock.js/devtools only runs in development; it is not mounted");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const resolved = resolveDevtoolsOptions(options);
|
|
48
|
+
const collector = createCollector(resolved);
|
|
49
|
+
await attachSource(log, "tracing", () => attachTracingSource(collector, resolved));
|
|
50
|
+
await attachSource(log, "query", () => attachQuerySource(collector));
|
|
51
|
+
await attachSource(log, "cache", () => attachCacheSource(collector));
|
|
52
|
+
await attachSource(log, "mail", () => attachMailSource(collector));
|
|
53
|
+
await attachSource(log, "log", () => attachLogSource(collector));
|
|
54
|
+
const { mountDevtoolsRoutes } = await import("./server/index.mjs");
|
|
55
|
+
mountDevtoolsRoutes(getHttpServer(), {
|
|
56
|
+
collector,
|
|
57
|
+
options: resolved,
|
|
58
|
+
isDevelopment: () => Application.runtimeStrategy === "development",
|
|
59
|
+
explain: (query) => explainQuery(query),
|
|
60
|
+
listRoutes: () => listRoutes()
|
|
61
|
+
});
|
|
62
|
+
disposers.push(collector.subscribe((event) => {
|
|
63
|
+
if (event.type !== "request" || event.data.warningCount === 0) return;
|
|
64
|
+
for (const warning of collector.getRequest(event.data.id)?.warnings ?? []) {
|
|
65
|
+
const where = warning.caller ? ` — ${warning.caller.file}:${warning.caller.line}` : "";
|
|
66
|
+
log.warn("devtools", "n+1", `${warning.message}${where} (${event.data.method} ${event.data.path})`);
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
69
|
+
active = true;
|
|
70
|
+
log.info("devtools", "ready", `devtools ready at ${resolved.path}`);
|
|
71
|
+
},
|
|
72
|
+
async start() {},
|
|
73
|
+
async restart() {
|
|
74
|
+
await connector.shutdown();
|
|
75
|
+
await connector.start();
|
|
76
|
+
},
|
|
77
|
+
async shutdown() {
|
|
78
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
79
|
+
active = false;
|
|
80
|
+
},
|
|
81
|
+
shouldRestart: () => false
|
|
82
|
+
};
|
|
83
|
+
/** Attaches one source, logging and continuing when it fails to attach. */
|
|
84
|
+
async function attachSource(logger, name, attach) {
|
|
85
|
+
try {
|
|
86
|
+
disposers.push(await attach());
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logger.warn("devtools", "source-failed", `devtools' ${name} source failed to attach: ${error instanceof Error ? error.message : String(error)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return connector;
|
|
92
|
+
}
|
|
93
|
+
/** Runs `EXPLAIN (FORMAT JSON)` for a stored Postgres query on its own connection. */
|
|
94
|
+
async function explainQuery(query) {
|
|
95
|
+
const { dataSourceRegistry } = await import("@warlock.js/cascade");
|
|
96
|
+
const { rows } = await dataSourceRegistry.get(query.connection).raw(`EXPLAIN (FORMAT JSON) ${query.sql}`, query.bindings);
|
|
97
|
+
return rows;
|
|
98
|
+
}
|
|
99
|
+
/** Lists every registered HTTP route — the same source `warlock routes --json` reads. */
|
|
100
|
+
async function listRoutes() {
|
|
101
|
+
const { router } = await import("@warlock.js/core");
|
|
102
|
+
return router.list().map((route) => ({
|
|
103
|
+
method: route.method.toUpperCase(),
|
|
104
|
+
path: route.path,
|
|
105
|
+
name: route.name,
|
|
106
|
+
middleware: route.middleware?.map((middleware) => middleware.name || "anonymous")
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
//#endregion
|
|
111
|
+
export { devtoolsConnector };
|
|
112
|
+
//# sourceMappingURL=devtools-connector.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools-connector.mjs","names":[],"sources":["../../../../../../devtools/src/devtools-connector.ts"],"sourcesContent":["/**\n * The devtools connector for `warlock.config.ts > connectors`.\n *\n * Deliberately a plain object with TYPE-ONLY imports from core: the config\n * file that constructs it must not drag core's runtime graph in at\n * config-load time. Core is imported lazily inside `boot()`, where the app\n * has already loaded it — see `queue/src/queue-connector.ts`, the pattern\n * this mirrors.\n */\nimport type { Connector, ConnectorLifecyclePhase } from \"@warlock.js/core\";\nimport { createCollector } from \"./collector\";\nimport { resolveDevtoolsOptions } from \"./options\";\nimport type { DevtoolsRouteRow } from \"./server\";\nimport { attachCacheSource } from \"./sources/cache-source\";\nimport { attachLogSource } from \"./sources/log-source\";\nimport { attachMailSource } from \"./sources/mail-source\";\nimport { attachQuerySource } from \"./sources/query-source\";\nimport { attachTracingSource } from \"./sources/tracing-source\";\nimport type { DevtoolsDisposer, DevtoolsOptions, DevtoolsQuery } from \"./types\";\n\n/**\n * Boots after the queue connector (priority 11), which is the last\n * built-in/framework connector to claim a priority; devtools has nothing\n * that depends on it, so it simply goes last.\n */\nexport const DEVTOOLS_CONNECTOR_PRIORITY = 12;\n\n/** Options accepted by `devtoolsConnector()`. */\nexport type DevtoolsConnectorOptions = DevtoolsOptions;\n\n/**\n * Construct the devtools connector.\n *\n * Refuses to run outside development: `boot()` checks\n * `Application.runtimeStrategy` and, when it isn't `\"development\"`, logs one\n * warning and returns without mounting a route or subscribing to anything.\n * In development it attaches every source (tracing, queries, cache, mail,\n * logs), then mounts the dashboard's routes on the HTTP server — before it\n * starts listening, the only point in the boot sequence where mounting is\n * possible (see `queue/src/dashboard-boot.ts` for the same constraint).\n *\n * Apps don't call this: `warlock dev` registers it automatically when the\n * package is installed (`warlock add devtools`). Keep it OUT of\n * `warlock.config.ts` — devtools is a dev dependency, so a production\n * install doesn't have it and a config import of it would fail there.\n */\nexport function devtoolsConnector(options: DevtoolsConnectorOptions = {}): Connector {\n let active = false;\n const disposers: DevtoolsDisposer[] = [];\n\n const connector: Connector = {\n name: \"devtools\",\n priority: DEVTOOLS_CONNECTOR_PRIORITY,\n // Core's `ConnectorLifecyclePhase.Late`; spelled out so this module stays\n // free of a runtime import of core.\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\n isActive: () => active,\n async boot() {\n const { Application, getHttpServer } = await import(\"@warlock.js/core\");\n const { log } = await import(\"@warlock.js/logger\");\n\n if (Application.runtimeStrategy !== \"development\") {\n log.warn(\n \"devtools\",\n \"disabled\",\n \"@warlock.js/devtools only runs in development; it is not mounted\",\n );\n return;\n }\n\n const resolved = resolveDevtoolsOptions(options);\n const collector = createCollector(resolved);\n\n await attachSource(log, \"tracing\", () => attachTracingSource(collector, resolved));\n await attachSource(log, \"query\", () => attachQuerySource(collector));\n await attachSource(log, \"cache\", () => attachCacheSource(collector));\n await attachSource(log, \"mail\", () => attachMailSource(collector));\n await attachSource(log, \"log\", () => attachLogSource(collector));\n\n const { mountDevtoolsRoutes } = await import(\"./server\");\n\n mountDevtoolsRoutes(getHttpServer(), {\n collector,\n options: resolved,\n isDevelopment: () => Application.runtimeStrategy === \"development\",\n explain: (query) => explainQuery(query),\n listRoutes: () => listRoutes(),\n });\n\n // An N+1 is worth a terminal line, not just a badge the developer has\n // to go looking for: one warning per offending query shape, with the\n // line that issued it.\n disposers.push(\n collector.subscribe((event) => {\n if (event.type !== \"request\" || event.data.warningCount === 0) return;\n\n for (const warning of collector.getRequest(event.data.id)?.warnings ?? []) {\n const where = warning.caller ? ` — ${warning.caller.file}:${warning.caller.line}` : \"\";\n\n log.warn(\n \"devtools\",\n \"n+1\",\n `${warning.message}${where} (${event.data.method} ${event.data.path})`,\n );\n }\n }),\n );\n\n active = true;\n\n log.info(\"devtools\", \"ready\", `devtools ready at ${resolved.path}`);\n },\n async start() {},\n async restart() {\n await connector.shutdown();\n await connector.start();\n },\n async shutdown() {\n for (const dispose of disposers.splice(0)) dispose();\n active = false;\n },\n shouldRestart: () => false,\n };\n\n /** Attaches one source, logging and continuing when it fails to attach. */\n async function attachSource(\n logger: { warn: (module: string, action: string, message: string) => unknown },\n name: string,\n attach: () => Promise<DevtoolsDisposer>,\n ): Promise<void> {\n try {\n disposers.push(await attach());\n } catch (error) {\n logger.warn(\n \"devtools\",\n \"source-failed\",\n `devtools' ${name} source failed to attach: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n return connector;\n}\n\n/** Runs `EXPLAIN (FORMAT JSON)` for a stored Postgres query on its own connection. */\nasync function explainQuery(query: DevtoolsQuery): Promise<unknown> {\n const { dataSourceRegistry } = await import(\"@warlock.js/cascade\");\n const dataSource = dataSourceRegistry.get(query.connection);\n const { rows } = await dataSource.raw(`EXPLAIN (FORMAT JSON) ${query.sql}`, query.bindings);\n\n return rows;\n}\n\n/** Lists every registered HTTP route — the same source `warlock routes --json` reads. */\nasync function listRoutes(): Promise<DevtoolsRouteRow[]> {\n const { router } = await import(\"@warlock.js/core\");\n\n return router.list().map((route) => ({\n method: route.method.toUpperCase(),\n path: route.path,\n name: route.name,\n middleware: route.middleware?.map((middleware) => middleware.name || \"anonymous\"),\n }));\n}\n"],"mappings":";;;;;;;;;;;;;;AAyBA,MAAa,8BAA8B;;;;;;;;;;;;;;;;;AAqB3C,SAAgB,kBAAkB,UAAoC,CAAC,GAAc;CACnF,IAAI,SAAS;CACb,MAAM,YAAgC,CAAC;CAEvC,MAAM,YAAuB;EAC3B,MAAM;EACN;EAGA,gBAAgB;EAChB,gBAAgB;EAChB,MAAM,OAAO;GACX,MAAM,EAAE,aAAa,kBAAkB,MAAM,OAAO;GACpD,MAAM,EAAE,QAAQ,MAAM,OAAO;GAE7B,IAAI,YAAY,oBAAoB,eAAe;IACjD,IAAI,KACF,YACA,YACA,kEACF;IACA;GACF;GAEA,MAAM,WAAW,uBAAuB,OAAO;GAC/C,MAAM,YAAY,gBAAgB,QAAQ;GAE1C,MAAM,aAAa,KAAK,iBAAiB,oBAAoB,WAAW,QAAQ,CAAC;GACjF,MAAM,aAAa,KAAK,eAAe,kBAAkB,SAAS,CAAC;GACnE,MAAM,aAAa,KAAK,eAAe,kBAAkB,SAAS,CAAC;GACnE,MAAM,aAAa,KAAK,cAAc,iBAAiB,SAAS,CAAC;GACjE,MAAM,aAAa,KAAK,aAAa,gBAAgB,SAAS,CAAC;GAE/D,MAAM,EAAE,wBAAwB,MAAM,OAAO;GAE7C,oBAAoB,cAAc,GAAG;IACnC;IACA,SAAS;IACT,qBAAqB,YAAY,oBAAoB;IACrD,UAAU,UAAU,aAAa,KAAK;IACtC,kBAAkB,WAAW;GAC/B,CAAC;GAKD,UAAU,KACR,UAAU,WAAW,UAAU;IAC7B,IAAI,MAAM,SAAS,aAAa,MAAM,KAAK,iBAAiB,GAAG;IAE/D,KAAK,MAAM,WAAW,UAAU,WAAW,MAAM,KAAK,EAAE,CAAC,EAAE,YAAY,CAAC,GAAG;KACzE,MAAM,QAAQ,QAAQ,SAAS,MAAM,QAAQ,OAAO,KAAK,GAAG,QAAQ,OAAO,SAAS;KAEpF,IAAI,KACF,YACA,OACA,GAAG,QAAQ,UAAU,MAAM,IAAI,MAAM,KAAK,OAAO,GAAG,MAAM,KAAK,KAAK,EACtE;IACF;GACF,CAAC,CACH;GAEA,SAAS;GAET,IAAI,KAAK,YAAY,SAAS,qBAAqB,SAAS,MAAM;EACpE;EACA,MAAM,QAAQ,CAAC;EACf,MAAM,UAAU;GACd,MAAM,UAAU,SAAS;GACzB,MAAM,UAAU,MAAM;EACxB;EACA,MAAM,WAAW;GACf,KAAK,MAAM,WAAW,UAAU,OAAO,CAAC,GAAG,QAAQ;GACnD,SAAS;EACX;EACA,qBAAqB;CACvB;;CAGA,eAAe,aACb,QACA,MACA,QACe;EACf,IAAI;GACF,UAAU,KAAK,MAAM,OAAO,CAAC;EAC/B,SAAS,OAAO;GACd,OAAO,KACL,YACA,iBACA,aAAa,KAAK,4BAA4B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACrG;EACF;CACF;CAEA,OAAO;AACT;;AAGA,eAAe,aAAa,OAAwC;CAClE,MAAM,EAAE,uBAAuB,MAAM,OAAO;CAE5C,MAAM,EAAE,SAAS,MADE,mBAAmB,IAAI,MAAM,UAChB,CAAC,CAAC,IAAI,yBAAyB,MAAM,OAAO,MAAM,QAAQ;CAE1F,OAAO;AACT;;AAGA,eAAe,aAA0C;CACvD,MAAM,EAAE,WAAW,MAAM,OAAO;CAEhC,OAAO,OAAO,KAAK,CAAC,CAAC,KAAK,WAAW;EACnC,QAAQ,MAAM,OAAO,YAAY;EACjC,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,YAAY,MAAM,YAAY,KAAK,eAAe,WAAW,QAAQ,WAAW;CAClF,EAAE;AACJ"}
|
package/esm/index.d.mts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { DevtoolsBackground, DevtoolsCacheOperation, DevtoolsCallerLocation, DevtoolsCollector, DevtoolsDisposer, DevtoolsLogEntry, DevtoolsMail, DevtoolsMailAttachment, DevtoolsMailSummary, DevtoolsOptions, DevtoolsPhase, DevtoolsQuery, DevtoolsRequest, DevtoolsRequestSummary, DevtoolsStreamEvent, DevtoolsWarning, ResolvedDevtoolsOptions } from "./types.mjs";
|
|
2
|
+
import { devtoolsConnector } from "./devtools-connector.mjs";
|
|
3
|
+
export { type DevtoolsBackground, type DevtoolsCacheOperation, type DevtoolsCallerLocation, type DevtoolsCollector, type DevtoolsDisposer, type DevtoolsLogEntry, type DevtoolsMail, type DevtoolsMailAttachment, type DevtoolsMailSummary, type DevtoolsOptions, type DevtoolsPhase, type DevtoolsQuery, type DevtoolsRequest, type DevtoolsRequestSummary, type DevtoolsStreamEvent, type DevtoolsWarning, type ResolvedDevtoolsOptions, devtoolsConnector };
|
package/esm/index.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region ../devtools/src/n-plus-one.ts
|
|
2
|
+
/** Finds repeated query shapes whose occurrence count reaches the supplied threshold. */
|
|
3
|
+
function detectNPlusOne(queries, threshold) {
|
|
4
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5
|
+
for (const query of queries) groups.set(query.shape, [...groups.get(query.shape) ?? [], query]);
|
|
6
|
+
return [...groups.entries()].filter(([, grouped]) => grouped.length >= threshold).sort(([, left], [, right]) => right.length - left.length).map(([shape, grouped]) => ({
|
|
7
|
+
type: "n+1",
|
|
8
|
+
shape,
|
|
9
|
+
count: grouped.length,
|
|
10
|
+
message: `N+1: ${grouped.length}× "${truncate(shape, 120)}"`,
|
|
11
|
+
...mostFrequentCaller(grouped) ? { caller: mostFrequentCaller(grouped) } : {}
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
function truncate(value, maximum) {
|
|
15
|
+
return value.length > maximum ? `${value.slice(0, maximum - 1)}…` : value;
|
|
16
|
+
}
|
|
17
|
+
function mostFrequentCaller(queries) {
|
|
18
|
+
const callers = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const query of queries) {
|
|
20
|
+
if (!query.caller) continue;
|
|
21
|
+
const key = `${query.caller.file}:${query.caller.line}`;
|
|
22
|
+
const found = callers.get(key);
|
|
23
|
+
callers.set(key, found ? {
|
|
24
|
+
...found,
|
|
25
|
+
count: found.count + 1
|
|
26
|
+
} : {
|
|
27
|
+
caller: query.caller,
|
|
28
|
+
count: 1
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return [...callers.values()].sort((left, right) => right.count - left.count)[0]?.caller;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
export { detectNPlusOne };
|
|
36
|
+
//# sourceMappingURL=n-plus-one.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"n-plus-one.mjs","names":[],"sources":["../../../../../../devtools/src/n-plus-one.ts"],"sourcesContent":["import type { DevtoolsCallerLocation, DevtoolsQuery, DevtoolsWarning } from \"./types\";\n\n/** Finds repeated query shapes whose occurrence count reaches the supplied threshold. */\nexport function detectNPlusOne(queries: DevtoolsQuery[], threshold: number): DevtoolsWarning[] {\n const groups = new Map<string, DevtoolsQuery[]>();\n for (const query of queries) groups.set(query.shape, [...(groups.get(query.shape) ?? []), query]);\n\n return [...groups.entries()]\n .filter(([, grouped]) => grouped.length >= threshold)\n .sort(([, left], [, right]) => right.length - left.length)\n .map(([shape, grouped]) => ({\n type: \"n+1\" as const,\n shape,\n count: grouped.length,\n message: `N+1: ${grouped.length}× \"${truncate(shape, 120)}\"`,\n ...(mostFrequentCaller(grouped) ? { caller: mostFrequentCaller(grouped) } : {}),\n }));\n}\n\nfunction truncate(value: string, maximum: number): string {\n return value.length > maximum ? `${value.slice(0, maximum - 1)}…` : value;\n}\n\nfunction mostFrequentCaller(queries: DevtoolsQuery[]): DevtoolsCallerLocation | undefined {\n const callers = new Map<string, { caller: DevtoolsCallerLocation; count: number }>();\n for (const query of queries) {\n if (!query.caller) continue;\n const key = `${query.caller.file}:${query.caller.line}`;\n const found = callers.get(key);\n callers.set(\n key,\n found ? { ...found, count: found.count + 1 } : { caller: query.caller, count: 1 },\n );\n }\n return [...callers.values()].sort((left, right) => right.count - left.count)[0]?.caller;\n}\n"],"mappings":";;AAGA,SAAgB,eAAe,SAA0B,WAAsC;CAC7F,MAAM,yBAAS,IAAI,IAA6B;CAChD,KAAK,MAAM,SAAS,SAAS,OAAO,IAAI,MAAM,OAAO,CAAC,GAAI,OAAO,IAAI,MAAM,KAAK,KAAK,CAAC,GAAI,KAAK,CAAC;CAEhG,OAAO,CAAC,GAAG,OAAO,QAAQ,CAAC,CAAC,CACzB,QAAQ,GAAG,aAAa,QAAQ,UAAU,SAAS,CAAC,CACpD,MAAM,GAAG,OAAO,GAAG,WAAW,MAAM,SAAS,KAAK,MAAM,CAAC,CACzD,KAAK,CAAC,OAAO,cAAc;EAC1B,MAAM;EACN;EACA,OAAO,QAAQ;EACf,SAAS,QAAQ,QAAQ,OAAO,KAAK,SAAS,OAAO,GAAG,EAAE;EAC1D,GAAI,mBAAmB,OAAO,IAAI,EAAE,QAAQ,mBAAmB,OAAO,EAAE,IAAI,CAAC;CAC/E,EAAE;AACN;AAEA,SAAS,SAAS,OAAe,SAAyB;CACxD,OAAO,MAAM,SAAS,UAAU,GAAG,MAAM,MAAM,GAAG,UAAU,CAAC,EAAE,KAAK;AACtE;AAEA,SAAS,mBAAmB,SAA8D;CACxF,MAAM,0BAAU,IAAI,IAA+D;CACnF,KAAK,MAAM,SAAS,SAAS;EAC3B,IAAI,CAAC,MAAM,QAAQ;EACnB,MAAM,MAAM,GAAG,MAAM,OAAO,KAAK,GAAG,MAAM,OAAO;EACjD,MAAM,QAAQ,QAAQ,IAAI,GAAG;EAC7B,QAAQ,IACN,KACA,QAAQ;GAAE,GAAG;GAAO,OAAO,MAAM,QAAQ;EAAE,IAAI;GAAE,QAAQ,MAAM;GAAQ,OAAO;EAAE,CAClF;CACF;CACA,OAAO,CAAC,GAAG,QAAQ,OAAO,CAAC,CAAC,CAAC,MAAM,MAAM,UAAU,MAAM,QAAQ,KAAK,KAAK,CAAC,CAAC,EAAE,EAAE;AACnF"}
|
package/esm/options.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region ../devtools/src/options.ts
|
|
2
|
+
/** Resolves dashboard options and rejects invalid capacity or URL-prefix values. */
|
|
3
|
+
function resolveDevtoolsOptions(options = {}) {
|
|
4
|
+
const path = (options.path ?? "/__warlock").replace(/\/+$/, "");
|
|
5
|
+
const maxRequests = options.maxRequests ?? 200;
|
|
6
|
+
const nPlusOneThreshold = options.nPlusOneThreshold ?? 5;
|
|
7
|
+
if (!path || !path.startsWith("/")) throw new Error("Devtools option \"path\" must be a non-root path starting with \"/\".");
|
|
8
|
+
if (maxRequests < 1) throw new Error("Devtools option \"maxRequests\" must be at least 1.");
|
|
9
|
+
if (nPlusOneThreshold < 2) throw new Error("Devtools option \"nPlusOneThreshold\" must be at least 2.");
|
|
10
|
+
return {
|
|
11
|
+
path,
|
|
12
|
+
maxRequests,
|
|
13
|
+
nPlusOneThreshold
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { resolveDevtoolsOptions };
|
|
19
|
+
//# sourceMappingURL=options.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.mjs","names":[],"sources":["../../../../../../devtools/src/options.ts"],"sourcesContent":["import type { DevtoolsOptions, ResolvedDevtoolsOptions } from \"./types\";\n\n/** Resolves dashboard options and rejects invalid capacity or URL-prefix values. */\nexport function resolveDevtoolsOptions(options: DevtoolsOptions = {}): ResolvedDevtoolsOptions {\n const path = (options.path ?? \"/__warlock\").replace(/\\/+$/, \"\");\n const maxRequests = options.maxRequests ?? 200;\n const nPlusOneThreshold = options.nPlusOneThreshold ?? 5;\n\n if (!path || !path.startsWith(\"/\")) {\n throw new Error('Devtools option \"path\" must be a non-root path starting with \"/\".');\n }\n\n if (maxRequests < 1) {\n throw new Error('Devtools option \"maxRequests\" must be at least 1.');\n }\n\n if (nPlusOneThreshold < 2) {\n throw new Error('Devtools option \"nPlusOneThreshold\" must be at least 2.');\n }\n\n return { path, maxRequests, nPlusOneThreshold };\n}\n"],"mappings":";;AAGA,SAAgB,uBAAuB,UAA2B,CAAC,GAA4B;CAC7F,MAAM,QAAQ,QAAQ,QAAQ,aAAY,CAAE,QAAQ,QAAQ,EAAE;CAC9D,MAAM,cAAc,QAAQ,eAAe;CAC3C,MAAM,oBAAoB,QAAQ,qBAAqB;CAEvD,IAAI,CAAC,QAAQ,CAAC,KAAK,WAAW,GAAG,GAC/B,MAAM,IAAI,MAAM,uEAAmE;CAGrF,IAAI,cAAc,GAChB,MAAM,IAAI,MAAM,qDAAmD;CAGrE,IAAI,oBAAoB,GACtB,MAAM,IAAI,MAAM,2DAAyD;CAG3E,OAAO;EAAE;EAAM;EAAa;CAAkB;AAChD"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region ../devtools/src/query-shape.ts
|
|
2
|
+
const numericLiteral = /(?<![\w$])[-+]?(?:\d+(?:\.\d+)?|\.\d+)(?![\w$])/g;
|
|
3
|
+
const quotedLiteral = /'(?:''|[^'])*'/g;
|
|
4
|
+
/** Returns a binding-independent shape for a database query. */
|
|
5
|
+
function queryShape(query) {
|
|
6
|
+
if (query.driver === "postgres") return (query.sql ?? "").replace(quotedLiteral, "?").replace(numericLiteral, "?").replace(/\s+/g, " ").trim();
|
|
7
|
+
return `${query.collection ?? ""}.${query.command ?? ""} ${JSON.stringify(normalizeMongoValue(query.pipeline))}`;
|
|
8
|
+
}
|
|
9
|
+
function normalizeMongoValue(value) {
|
|
10
|
+
if (isMongoLeaf(value)) return "?";
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
if (value.every(isMongoLeaf)) return ["?"];
|
|
13
|
+
return value.map(normalizeMongoValue);
|
|
14
|
+
}
|
|
15
|
+
const record = value;
|
|
16
|
+
return Object.fromEntries(Object.keys(record).map((key) => [key, normalizeMongoValue(record[key])]));
|
|
17
|
+
}
|
|
18
|
+
function isMongoLeaf(value) {
|
|
19
|
+
if (value === null || typeof value !== "object") return true;
|
|
20
|
+
if (value instanceof Date) return true;
|
|
21
|
+
const record = value;
|
|
22
|
+
return typeof record.toHexString === "function" || typeof record.$oid === "string";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { queryShape };
|
|
27
|
+
//# sourceMappingURL=query-shape.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-shape.mjs","names":[],"sources":["../../../../../../devtools/src/query-shape.ts"],"sourcesContent":["import type { DevtoolsQuery } from \"./types\";\n\ntype QueryShapeInput = Pick<\n DevtoolsQuery,\n \"driver\" | \"sql\" | \"collection\" | \"command\" | \"pipeline\"\n>;\n\nconst numericLiteral = /(?<![\\w$])[-+]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?![\\w$])/g;\nconst quotedLiteral = /'(?:''|[^'])*'/g;\n\n/** Returns a binding-independent shape for a database query. */\nexport function queryShape(query: QueryShapeInput): string {\n if (query.driver === \"postgres\") {\n return (query.sql ?? \"\")\n .replace(quotedLiteral, \"?\")\n .replace(numericLiteral, \"?\")\n .replace(/\\s+/g, \" \")\n .trim();\n }\n\n return `${query.collection ?? \"\"}.${query.command ?? \"\"} ${JSON.stringify(normalizeMongoValue(query.pipeline))}`;\n}\n\nfunction normalizeMongoValue(value: unknown): unknown {\n if (isMongoLeaf(value)) return \"?\";\n\n if (Array.isArray(value)) {\n if (value.every(isMongoLeaf)) return [\"?\"];\n return value.map(normalizeMongoValue);\n }\n\n const record = value as Record<string, unknown>;\n return Object.fromEntries(\n Object.keys(record).map((key) => [key, normalizeMongoValue(record[key])]),\n );\n}\n\nfunction isMongoLeaf(value: unknown): boolean {\n if (value === null || typeof value !== \"object\") return true;\n if (value instanceof Date) return true;\n const record = value as Record<string, unknown>;\n return typeof record.toHexString === \"function\" || typeof record.$oid === \"string\";\n}\n"],"mappings":";AAOA,MAAM,iBAAiB;AACvB,MAAM,gBAAgB;;AAGtB,SAAgB,WAAW,OAAgC;CACzD,IAAI,MAAM,WAAW,YACnB,QAAQ,MAAM,OAAO,GAAE,CACpB,QAAQ,eAAe,GAAG,CAAC,CAC3B,QAAQ,gBAAgB,GAAG,CAAC,CAC5B,QAAQ,QAAQ,GAAG,CAAC,CACpB,KAAK;CAGV,OAAO,GAAG,MAAM,cAAc,GAAG,GAAG,MAAM,WAAW,GAAG,GAAG,KAAK,UAAU,oBAAoB,MAAM,QAAQ,CAAC;AAC/G;AAEA,SAAS,oBAAoB,OAAyB;CACpD,IAAI,YAAY,KAAK,GAAG,OAAO;CAE/B,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxB,IAAI,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG;EACzC,OAAO,MAAM,IAAI,mBAAmB;CACtC;CAEA,MAAM,SAAS;CACf,OAAO,OAAO,YACZ,OAAO,KAAK,MAAM,CAAC,CAAC,KAAK,QAAQ,CAAC,KAAK,oBAAoB,OAAO,IAAI,CAAC,CAAC,CAC1E;AACF;AAEA,SAAS,YAAY,OAAyB;CAC5C,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CACxD,IAAI,iBAAiB,MAAM,OAAO;CAClC,MAAM,SAAS;CACf,OAAO,OAAO,OAAO,gBAAgB,cAAc,OAAO,OAAO,SAAS;AAC5E"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//#region ../devtools/src/safe-value.ts
|
|
2
|
+
const MAX_DEPTH = 6;
|
|
3
|
+
/**
|
|
4
|
+
* A JSON-safe snapshot of a value devtools keeps for later display.
|
|
5
|
+
*
|
|
6
|
+
* Log contexts and phase attributes are app data: they can hold live objects
|
|
7
|
+
* (a request, its socket) whose graphs are circular and which must not be kept
|
|
8
|
+
* alive by the collector anyway. Plain objects and arrays are copied; anything
|
|
9
|
+
* with a `toJSON()` is serialized through it; other class instances collapse
|
|
10
|
+
* to `[ClassName]`, cycles to `[Circular]`, and nesting past a fixed depth to
|
|
11
|
+
* `[Object]` / `[Array]`.
|
|
12
|
+
*/
|
|
13
|
+
function toSafeValue(value) {
|
|
14
|
+
return snapshot(value, 0, /* @__PURE__ */ new WeakSet());
|
|
15
|
+
}
|
|
16
|
+
function snapshot(value, depth, seen) {
|
|
17
|
+
if (value === null || value === void 0) return value;
|
|
18
|
+
switch (typeof value) {
|
|
19
|
+
case "string":
|
|
20
|
+
case "number":
|
|
21
|
+
case "boolean": return value;
|
|
22
|
+
case "bigint": return value.toString();
|
|
23
|
+
case "symbol": return value.toString();
|
|
24
|
+
case "function": return;
|
|
25
|
+
}
|
|
26
|
+
const object = value;
|
|
27
|
+
if (seen.has(object)) return "[Circular]";
|
|
28
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
|
|
29
|
+
if (value instanceof Error) return {
|
|
30
|
+
name: value.name,
|
|
31
|
+
message: value.message
|
|
32
|
+
};
|
|
33
|
+
if (depth >= MAX_DEPTH) return Array.isArray(value) ? "[Array]" : "[Object]";
|
|
34
|
+
seen.add(object);
|
|
35
|
+
try {
|
|
36
|
+
if (Array.isArray(value)) return value.map((item) => snapshot(item, depth + 1, seen));
|
|
37
|
+
const prototype = Object.getPrototypeOf(object);
|
|
38
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
39
|
+
const toJSON = object.toJSON;
|
|
40
|
+
if (typeof toJSON === "function") try {
|
|
41
|
+
return snapshot(toJSON.call(object), depth + 1, seen);
|
|
42
|
+
} catch {
|
|
43
|
+
return `[${constructorName(object)}]`;
|
|
44
|
+
}
|
|
45
|
+
return `[${constructorName(object)}]`;
|
|
46
|
+
}
|
|
47
|
+
const copy = {};
|
|
48
|
+
for (const [key, entry] of Object.entries(object)) {
|
|
49
|
+
const safe = snapshot(entry, depth + 1, seen);
|
|
50
|
+
if (safe !== void 0) copy[key] = safe;
|
|
51
|
+
}
|
|
52
|
+
return copy;
|
|
53
|
+
} finally {
|
|
54
|
+
seen.delete(object);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function constructorName(object) {
|
|
58
|
+
return object.constructor?.name || "Object";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { toSafeValue };
|
|
63
|
+
//# sourceMappingURL=safe-value.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-value.mjs","names":[],"sources":["../../../../../../devtools/src/safe-value.ts"],"sourcesContent":["const MAX_DEPTH = 6;\n\n/**\n * A JSON-safe snapshot of a value devtools keeps for later display.\n *\n * Log contexts and phase attributes are app data: they can hold live objects\n * (a request, its socket) whose graphs are circular and which must not be kept\n * alive by the collector anyway. Plain objects and arrays are copied; anything\n * with a `toJSON()` is serialized through it; other class instances collapse\n * to `[ClassName]`, cycles to `[Circular]`, and nesting past a fixed depth to\n * `[Object]` / `[Array]`.\n */\nexport function toSafeValue(value: unknown): unknown {\n return snapshot(value, 0, new WeakSet());\n}\n\nfunction snapshot(value: unknown, depth: number, seen: WeakSet<object>): unknown {\n if (value === null || value === undefined) return value;\n\n switch (typeof value) {\n case \"string\":\n case \"number\":\n case \"boolean\":\n return value;\n case \"bigint\":\n return value.toString();\n case \"symbol\":\n return value.toString();\n case \"function\":\n return undefined;\n }\n\n const object = value as object;\n\n if (seen.has(object)) return \"[Circular]\";\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? \"Invalid Date\" : value.toISOString();\n if (value instanceof Error) return { name: value.name, message: value.message };\n\n if (depth >= MAX_DEPTH) return Array.isArray(value) ? \"[Array]\" : \"[Object]\";\n\n seen.add(object);\n\n try {\n if (Array.isArray(value)) return value.map((item) => snapshot(item, depth + 1, seen));\n\n const prototype = Object.getPrototypeOf(object);\n\n if (prototype !== Object.prototype && prototype !== null) {\n const toJSON = (object as { toJSON?: unknown }).toJSON;\n\n if (typeof toJSON === \"function\") {\n try {\n return snapshot(toJSON.call(object), depth + 1, seen);\n } catch {\n return `[${constructorName(object)}]`;\n }\n }\n\n return `[${constructorName(object)}]`;\n }\n\n const copy: Record<string, unknown> = {};\n\n for (const [key, entry] of Object.entries(object)) {\n const safe = snapshot(entry, depth + 1, seen);\n if (safe !== undefined) copy[key] = safe;\n }\n\n return copy;\n } finally {\n seen.delete(object);\n }\n}\n\nfunction constructorName(object: object): string {\n return (object as { constructor?: { name?: string } }).constructor?.name || \"Object\";\n}\n"],"mappings":";AAAA,MAAM,YAAY;;;;;;;;;;;AAYlB,SAAgB,YAAY,OAAyB;CACnD,OAAO,SAAS,OAAO,mBAAG,IAAI,QAAQ,CAAC;AACzC;AAEA,SAAS,SAAS,OAAgB,OAAe,MAAgC;CAC/E,IAAI,UAAU,QAAQ,UAAU,QAAW,OAAO;CAElD,QAAQ,OAAO,OAAf;EACE,KAAK;EACL,KAAK;EACL,KAAK,WACH,OAAO;EACT,KAAK,UACH,OAAO,MAAM,SAAS;EACxB,KAAK,UACH,OAAO,MAAM,SAAS;EACxB,KAAK,YACH;CACJ;CAEA,MAAM,SAAS;CAEf,IAAI,KAAK,IAAI,MAAM,GAAG,OAAO;CAC7B,IAAI,iBAAiB,MAAM,OAAO,OAAO,MAAM,MAAM,QAAQ,CAAC,IAAI,iBAAiB,MAAM,YAAY;CACrG,IAAI,iBAAiB,OAAO,OAAO;EAAE,MAAM,MAAM;EAAM,SAAS,MAAM;CAAQ;CAE9E,IAAI,SAAS,WAAW,OAAO,MAAM,QAAQ,KAAK,IAAI,YAAY;CAElE,KAAK,IAAI,MAAM;CAEf,IAAI;EACF,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,KAAK,SAAS,SAAS,MAAM,QAAQ,GAAG,IAAI,CAAC;EAEpF,MAAM,YAAY,OAAO,eAAe,MAAM;EAE9C,IAAI,cAAc,OAAO,aAAa,cAAc,MAAM;GACxD,MAAM,SAAU,OAAgC;GAEhD,IAAI,OAAO,WAAW,YACpB,IAAI;IACF,OAAO,SAAS,OAAO,KAAK,MAAM,GAAG,QAAQ,GAAG,IAAI;GACtD,QAAQ;IACN,OAAO,IAAI,gBAAgB,MAAM,EAAE;GACrC;GAGF,OAAO,IAAI,gBAAgB,MAAM,EAAE;EACrC;EAEA,MAAM,OAAgC,CAAC;EAEvC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;GACjD,MAAM,OAAO,SAAS,OAAO,QAAQ,GAAG,IAAI;GAC5C,IAAI,SAAS,QAAW,KAAK,OAAO;EACtC;EAEA,OAAO;CACT,UAAU;EACR,KAAK,OAAO,MAAM;CACpB;AACF;AAEA,SAAS,gBAAgB,QAAwB;CAC/C,OAAQ,OAA+C,aAAa,QAAQ;AAC9E"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region ../devtools/src/server/background-routes.ts
|
|
2
|
+
/** Registers the route that returns work recorded outside any request. */
|
|
3
|
+
function registerBackgroundRoutes(server, deps) {
|
|
4
|
+
server.get("/api/background", async () => deps.collector.getBackground());
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
//#endregion
|
|
8
|
+
export { registerBackgroundRoutes };
|
|
9
|
+
//# sourceMappingURL=background-routes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"background-routes.mjs","names":[],"sources":["../../../../../../../devtools/src/server/background-routes.ts"],"sourcesContent":["import type { FastifyInstance } from \"fastify\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\n\n/** Registers the route that returns work recorded outside any request. */\nexport function registerBackgroundRoutes(\n server: FastifyInstance,\n deps: MountDevtoolsRoutesDeps,\n): void {\n server.get(\"/api/background\", async () => deps.collector.getBackground());\n}\n"],"mappings":";;AAIA,SAAgB,yBACd,QACA,MACM;CACN,OAAO,IAAI,mBAAmB,YAAY,KAAK,UAAU,cAAc,CAAC;AAC1E"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region ../devtools/src/server/explain-route.ts
|
|
2
|
+
/**
|
|
3
|
+
* The value the logger's redaction substitutes for a censored leaf
|
|
4
|
+
* (`logger/src/redact/redact.ts`, `DEFAULT_CENSOR`). Not exported from
|
|
5
|
+
* `@warlock.js/logger`, so matched by literal here.
|
|
6
|
+
*/
|
|
7
|
+
const REDACTED_PLACEHOLDER = "[REDACTED]";
|
|
8
|
+
function isExplainableSql(sql) {
|
|
9
|
+
return /^\s*(select|with)\b/i.test(sql ?? "");
|
|
10
|
+
}
|
|
11
|
+
/** Registers the EXPLAIN route: Postgres `SELECT`/`WITH` queries only. */
|
|
12
|
+
function registerExplainRoute(server, deps) {
|
|
13
|
+
server.post("/api/requests/:requestId/queries/:queryId/explain", async (request, reply) => {
|
|
14
|
+
const query = deps.collector.getRequest(request.params.requestId)?.queries.find((item) => item.id === request.params.queryId);
|
|
15
|
+
if (!query) {
|
|
16
|
+
reply.code(404).send();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (query.driver !== "postgres" || !isExplainableSql(query.sql) || !deps.explain) {
|
|
20
|
+
reply.code(400).send({ error: "Only Postgres SELECT or WITH queries can be explained" });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (query.bindings?.some((binding) => binding === REDACTED_PLACEHOLDER)) {
|
|
24
|
+
reply.code(400).send({ error: "bindings redacted" });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return { plan: await deps.explain(query) };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
reply.code(500).send({ error: error instanceof Error ? error.message : String(error) });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { registerExplainRoute };
|
|
37
|
+
//# sourceMappingURL=explain-route.mjs.map
|