@c9up/ream 0.2.28 → 0.2.30

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.
Files changed (293) hide show
  1. package/README.md +5 -6
  2. package/dist/Codemods.d.ts +24 -0
  3. package/dist/Codemods.d.ts.map +1 -1
  4. package/dist/Codemods.js +211 -1
  5. package/dist/Codemods.js.map +1 -1
  6. package/dist/HttpKernel.d.ts.map +1 -1
  7. package/dist/HttpKernel.js +17 -5
  8. package/dist/HttpKernel.js.map +1 -1
  9. package/dist/Ignitor.d.ts +15 -2
  10. package/dist/Ignitor.d.ts.map +1 -1
  11. package/dist/Ignitor.js +33 -23
  12. package/dist/Ignitor.js.map +1 -1
  13. package/dist/bodyparser/BodyParserMiddleware.d.ts +4 -4
  14. package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
  15. package/dist/bodyparser/BodyParserMiddleware.js +41 -11
  16. package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
  17. package/dist/bodyparser/MultipartFile.d.ts +21 -15
  18. package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
  19. package/dist/bodyparser/MultipartFile.js +40 -58
  20. package/dist/bodyparser/MultipartFile.js.map +1 -1
  21. package/dist/bodyparser/parseSize.d.ts +7 -4
  22. package/dist/bodyparser/parseSize.d.ts.map +1 -1
  23. package/dist/bodyparser/parseSize.js +12 -17
  24. package/dist/bodyparser/parseSize.js.map +1 -1
  25. package/dist/bodyparser/qsParse.d.ts +20 -1
  26. package/dist/bodyparser/qsParse.d.ts.map +1 -1
  27. package/dist/bodyparser/qsParse.js +78 -25
  28. package/dist/bodyparser/qsParse.js.map +1 -1
  29. package/dist/codemods/source.d.ts +57 -0
  30. package/dist/codemods/source.d.ts.map +1 -0
  31. package/dist/codemods/source.js +282 -0
  32. package/dist/codemods/source.js.map +1 -0
  33. package/dist/console/prompts.d.ts.map +1 -1
  34. package/dist/console/prompts.js +3 -1
  35. package/dist/console/prompts.js.map +1 -1
  36. package/dist/container/Container.d.ts.map +1 -1
  37. package/dist/container/Container.js +61 -71
  38. package/dist/container/Container.js.map +1 -1
  39. package/dist/events/Emitter.d.ts +12 -0
  40. package/dist/events/Emitter.d.ts.map +1 -1
  41. package/dist/events/Emitter.js +36 -9
  42. package/dist/events/Emitter.js.map +1 -1
  43. package/dist/events/helix/helpers.d.ts.map +1 -1
  44. package/dist/events/helix/helpers.js +19 -9
  45. package/dist/events/helix/helpers.js.map +1 -1
  46. package/dist/graphql/GraphQLEngine.d.ts +8 -0
  47. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  48. package/dist/graphql/GraphQLEngine.js +79 -12
  49. package/dist/graphql/GraphQLEngine.js.map +1 -1
  50. package/dist/helpers/MessageBuilder.d.ts +4 -2
  51. package/dist/helpers/MessageBuilder.d.ts.map +1 -1
  52. package/dist/helpers/MessageBuilder.js +4 -2
  53. package/dist/helpers/MessageBuilder.js.map +1 -1
  54. package/dist/helpers/errors.d.ts +11 -0
  55. package/dist/helpers/errors.d.ts.map +1 -0
  56. package/dist/helpers/errors.js +17 -0
  57. package/dist/helpers/errors.js.map +1 -0
  58. package/dist/helpers/napi-loader.d.ts.map +1 -1
  59. package/dist/helpers/napi-loader.js +3 -2
  60. package/dist/helpers/napi-loader.js.map +1 -1
  61. package/dist/http/Exception.d.ts +14 -1
  62. package/dist/http/Exception.d.ts.map +1 -1
  63. package/dist/http/Exception.js +10 -2
  64. package/dist/http/Exception.js.map +1 -1
  65. package/dist/http/HttpContext.d.ts +31 -2
  66. package/dist/http/HttpContext.d.ts.map +1 -1
  67. package/dist/http/HttpContext.js +12 -5
  68. package/dist/http/HttpContext.js.map +1 -1
  69. package/dist/http/RedirectBuilder.d.ts +9 -3
  70. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  71. package/dist/http/RedirectBuilder.js +43 -37
  72. package/dist/http/RedirectBuilder.js.map +1 -1
  73. package/dist/http/Request.d.ts +42 -17
  74. package/dist/http/Request.d.ts.map +1 -1
  75. package/dist/http/Request.js +80 -58
  76. package/dist/http/Request.js.map +1 -1
  77. package/dist/http/Response.d.ts +7 -4
  78. package/dist/http/Response.d.ts.map +1 -1
  79. package/dist/http/Response.js +14 -6
  80. package/dist/http/Response.js.map +1 -1
  81. package/dist/http/SseStream.js +6 -7
  82. package/dist/http/SseStream.js.map +1 -1
  83. package/dist/http/permissions.d.ts +25 -0
  84. package/dist/http/permissions.d.ts.map +1 -0
  85. package/dist/http/permissions.js +38 -0
  86. package/dist/http/permissions.js.map +1 -0
  87. package/dist/http-client/HttpClient.d.ts +111 -0
  88. package/dist/http-client/HttpClient.d.ts.map +1 -0
  89. package/dist/http-client/HttpClient.js +386 -0
  90. package/dist/http-client/HttpClient.js.map +1 -0
  91. package/dist/http-client/addresses.d.ts +14 -0
  92. package/dist/http-client/addresses.d.ts.map +1 -0
  93. package/dist/http-client/addresses.js +57 -0
  94. package/dist/http-client/addresses.js.map +1 -0
  95. package/dist/http-client/fake.d.ts +50 -0
  96. package/dist/http-client/fake.d.ts.map +1 -0
  97. package/dist/http-client/fake.js +95 -0
  98. package/dist/http-client/fake.js.map +1 -0
  99. package/dist/http-client/index.d.ts +7 -0
  100. package/dist/http-client/index.d.ts.map +1 -0
  101. package/dist/http-client/index.js +7 -0
  102. package/dist/http-client/index.js.map +1 -0
  103. package/dist/index.d.ts +4 -3
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +1 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/middleware/Pipeline.d.ts.map +1 -1
  108. package/dist/middleware/Pipeline.js +6 -3
  109. package/dist/middleware/Pipeline.js.map +1 -1
  110. package/dist/native/generated.d.ts +93 -36
  111. package/dist/native/generated.d.ts.map +1 -1
  112. package/dist/profiler/Profiler.d.ts.map +1 -1
  113. package/dist/profiler/Profiler.js +4 -1
  114. package/dist/profiler/Profiler.js.map +1 -1
  115. package/dist/profiler/client.generated.d.ts +5 -0
  116. package/dist/profiler/client.generated.d.ts.map +1 -0
  117. package/dist/profiler/client.generated.js +6 -0
  118. package/dist/profiler/client.generated.js.map +1 -0
  119. package/dist/profiler/collectors.d.ts +92 -13
  120. package/dist/profiler/collectors.d.ts.map +1 -1
  121. package/dist/profiler/collectors.js +106 -19
  122. package/dist/profiler/collectors.js.map +1 -1
  123. package/dist/profiler/endpoint.d.ts +2 -1
  124. package/dist/profiler/endpoint.d.ts.map +1 -1
  125. package/dist/profiler/endpoint.js +25 -1
  126. package/dist/profiler/endpoint.js.map +1 -1
  127. package/dist/profiler/index.d.ts +8 -4
  128. package/dist/profiler/index.d.ts.map +1 -1
  129. package/dist/profiler/index.js +11 -4
  130. package/dist/profiler/index.js.map +1 -1
  131. package/dist/profiler/middleware.d.ts.map +1 -1
  132. package/dist/profiler/middleware.js +76 -10
  133. package/dist/profiler/middleware.js.map +1 -1
  134. package/dist/profiler/preview.d.ts +17 -0
  135. package/dist/profiler/preview.d.ts.map +1 -0
  136. package/dist/profiler/preview.js +100 -0
  137. package/dist/profiler/preview.js.map +1 -0
  138. package/dist/profiler/toolbar.d.ts +15 -23
  139. package/dist/profiler/toolbar.d.ts.map +1 -1
  140. package/dist/profiler/toolbar.js +25 -91
  141. package/dist/profiler/toolbar.js.map +1 -1
  142. package/dist/profiler/types.d.ts +6 -1
  143. package/dist/profiler/types.d.ts.map +1 -1
  144. package/dist/router/Router.d.ts +6 -0
  145. package/dist/router/Router.d.ts.map +1 -1
  146. package/dist/router/Router.js +115 -12
  147. package/dist/router/Router.js.map +1 -1
  148. package/dist/rpc/RpcProvider.d.ts +6 -0
  149. package/dist/rpc/RpcProvider.d.ts.map +1 -1
  150. package/dist/rpc/RpcProvider.js +4 -1
  151. package/dist/rpc/RpcProvider.js.map +1 -1
  152. package/dist/rpc/RpcRouter.d.ts +27 -14
  153. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  154. package/dist/rpc/RpcRouter.js +97 -40
  155. package/dist/rpc/RpcRouter.js.map +1 -1
  156. package/dist/scheduler/Scheduler.js +1 -1
  157. package/dist/scheduler/Scheduler.js.map +1 -1
  158. package/dist/scheduler/locks/RedisLockBackend.d.ts.map +1 -1
  159. package/dist/scheduler/locks/RedisLockBackend.js +13 -2
  160. package/dist/scheduler/locks/RedisLockBackend.js.map +1 -1
  161. package/dist/security/CookieSigner.d.ts.map +1 -1
  162. package/dist/security/CookieSigner.js +32 -8
  163. package/dist/security/CookieSigner.js.map +1 -1
  164. package/dist/security/SignedUrl.d.ts.map +1 -1
  165. package/dist/security/SignedUrl.js +16 -19
  166. package/dist/security/SignedUrl.js.map +1 -1
  167. package/dist/server/Server.d.ts +44 -12
  168. package/dist/server/Server.d.ts.map +1 -1
  169. package/dist/server/Server.js +31 -10
  170. package/dist/server/Server.js.map +1 -1
  171. package/dist/server/nativeGrpc.d.ts +27 -0
  172. package/dist/server/nativeGrpc.d.ts.map +1 -0
  173. package/dist/server/nativeGrpc.js +47 -0
  174. package/dist/server/nativeGrpc.js.map +1 -0
  175. package/dist/server/nativeLimits.d.ts +57 -0
  176. package/dist/server/nativeLimits.d.ts.map +1 -0
  177. package/dist/server/nativeLimits.js +75 -0
  178. package/dist/server/nativeLimits.js.map +1 -0
  179. package/dist/session/Session.d.ts +12 -0
  180. package/dist/session/Session.d.ts.map +1 -1
  181. package/dist/session/Session.js +13 -4
  182. package/dist/session/Session.js.map +1 -1
  183. package/dist/session/SessionMiddleware.d.ts.map +1 -1
  184. package/dist/session/SessionMiddleware.js +28 -9
  185. package/dist/session/SessionMiddleware.js.map +1 -1
  186. package/dist/session/config.d.ts +5 -0
  187. package/dist/session/config.d.ts.map +1 -1
  188. package/dist/session/config.js +8 -0
  189. package/dist/session/config.js.map +1 -1
  190. package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
  191. package/dist/session/drivers/MemoryDriver.js +26 -7
  192. package/dist/session/drivers/MemoryDriver.js.map +1 -1
  193. package/dist/session/errors.d.ts +17 -0
  194. package/dist/session/errors.d.ts.map +1 -1
  195. package/dist/session/errors.js +21 -0
  196. package/dist/session/errors.js.map +1 -1
  197. package/dist/session/templateTags.d.ts +14 -0
  198. package/dist/session/templateTags.d.ts.map +1 -1
  199. package/dist/session/templateTags.js +45 -17
  200. package/dist/session/templateTags.js.map +1 -1
  201. package/dist/storage/StaticMiddleware.d.ts.map +1 -1
  202. package/dist/storage/StaticMiddleware.js +49 -27
  203. package/dist/storage/StaticMiddleware.js.map +1 -1
  204. package/dist/testing/ApiResponse.d.ts.map +1 -1
  205. package/dist/testing/ApiResponse.js +7 -1
  206. package/dist/testing/ApiResponse.js.map +1 -1
  207. package/dist/testing/TestClient.d.ts.map +1 -1
  208. package/dist/testing/TestClient.js +16 -4
  209. package/dist/testing/TestClient.js.map +1 -1
  210. package/dist/utils/Macroable.d.ts +1 -1
  211. package/dist/utils/Macroable.js +2 -2
  212. package/dist/utils/Macroable.js.map +1 -1
  213. package/events.darwin-arm64.node +0 -0
  214. package/events.darwin-x64.node +0 -0
  215. package/events.linux-arm64-gnu.node +0 -0
  216. package/events.linux-x64-gnu.node +0 -0
  217. package/events.win32-x64-msvc.node +0 -0
  218. package/index.darwin-arm64.node +0 -0
  219. package/index.darwin-x64.node +0 -0
  220. package/index.linux-arm64-gnu.node +0 -0
  221. package/index.linux-x64-gnu.node +0 -0
  222. package/index.win32-x64-msvc.node +0 -0
  223. package/package.json +11 -3
  224. package/scheduler.darwin-arm64.node +0 -0
  225. package/scheduler.darwin-x64.node +0 -0
  226. package/scheduler.linux-arm64-gnu.node +0 -0
  227. package/scheduler.linux-x64-gnu.node +0 -0
  228. package/scheduler.win32-x64-msvc.node +0 -0
  229. package/src/Codemods.ts +314 -3
  230. package/src/HttpKernel.ts +18 -7
  231. package/src/Ignitor.ts +58 -26
  232. package/src/bodyparser/BodyParserMiddleware.ts +45 -16
  233. package/src/bodyparser/MultipartFile.ts +46 -67
  234. package/src/bodyparser/parseSize.ts +17 -17
  235. package/src/bodyparser/qsParse.ts +96 -26
  236. package/src/codemods/source.ts +303 -0
  237. package/src/console/prompts.ts +3 -1
  238. package/src/container/Container.ts +97 -81
  239. package/src/events/Emitter.ts +37 -10
  240. package/src/events/helix/helpers.ts +24 -19
  241. package/src/graphql/GraphQLEngine.ts +91 -14
  242. package/src/helpers/MessageBuilder.ts +4 -2
  243. package/src/helpers/errors.ts +16 -0
  244. package/src/helpers/napi-loader.ts +3 -2
  245. package/src/http/Exception.ts +17 -2
  246. package/src/http/HttpContext.ts +34 -5
  247. package/src/http/RedirectBuilder.ts +51 -37
  248. package/src/http/Request.ts +102 -58
  249. package/src/http/Response.ts +15 -7
  250. package/src/http/SseStream.ts +6 -7
  251. package/src/http/permissions.ts +47 -0
  252. package/src/http-client/HttpClient.ts +505 -0
  253. package/src/http-client/addresses.ts +58 -0
  254. package/src/http-client/fake.ts +141 -0
  255. package/src/http-client/index.ts +21 -0
  256. package/src/index.ts +6 -1
  257. package/src/middleware/Pipeline.ts +7 -3
  258. package/src/native/generated.ts +106 -48
  259. package/src/profiler/Profiler.ts +4 -1
  260. package/src/profiler/client/toolbar.css +595 -0
  261. package/src/profiler/client/toolbar.js +1299 -0
  262. package/src/profiler/client.generated.ts +7 -0
  263. package/src/profiler/collectors.ts +156 -19
  264. package/src/profiler/endpoint.ts +27 -1
  265. package/src/profiler/index.ts +17 -3
  266. package/src/profiler/middleware.ts +74 -9
  267. package/src/profiler/preview.ts +104 -0
  268. package/src/profiler/toolbar.ts +28 -91
  269. package/src/profiler/types.ts +6 -1
  270. package/src/router/Router.ts +136 -13
  271. package/src/rpc/RpcProvider.ts +10 -1
  272. package/src/rpc/RpcRouter.ts +129 -45
  273. package/src/scheduler/Scheduler.ts +1 -1
  274. package/src/scheduler/locks/RedisLockBackend.ts +12 -4
  275. package/src/security/CookieSigner.ts +30 -8
  276. package/src/security/SignedUrl.ts +18 -17
  277. package/src/server/Server.ts +60 -12
  278. package/src/server/nativeGrpc.ts +59 -0
  279. package/src/server/nativeLimits.ts +111 -0
  280. package/src/session/Session.ts +29 -4
  281. package/src/session/SessionMiddleware.ts +30 -10
  282. package/src/session/config.ts +7 -0
  283. package/src/session/drivers/MemoryDriver.ts +26 -7
  284. package/src/session/errors.ts +31 -0
  285. package/src/session/templateTags.ts +57 -0
  286. package/src/storage/StaticMiddleware.ts +46 -27
  287. package/src/testing/ApiResponse.ts +7 -1
  288. package/src/testing/TestClient.ts +14 -5
  289. package/src/utils/Macroable.ts +2 -2
  290. package/scripts/build-napi-types.mjs +0 -69
  291. package/scripts/copy-napi.mjs +0 -121
  292. package/scripts/generate-mime.mjs +0 -119
  293. package/scripts/generate-napi-types.mjs +0 -187
@@ -0,0 +1,1299 @@
1
+ /*
2
+ * The debug toolbar, as it runs in the browser.
3
+ *
4
+ * Served from /__ream/profiler/_assets/toolbar.js and loaded by a <script src>
5
+ * that carries the request's profile token in data attributes: no inline code,
6
+ * so a `script-src 'self'` policy runs it, and a nonce-only one runs it through
7
+ * the nonce the server stamps on the tag.
8
+ *
9
+ * Built with the DOM API and `textContent` only. Everything it shows came from
10
+ * a request — a URL, SQL, a log message — and none of it is ever parsed as
11
+ * markup.
12
+ *
13
+ * Plain readable JS, on purpose: someone will open the inspector and land
14
+ * here.
15
+ */
16
+ ;(() => {
17
+ const script = document.currentScript
18
+ if (!script || window.__reamProfiler) return
19
+ const base = script.dataset.path || '/__ream/profiler'
20
+ const pageToken = script.dataset.token || ''
21
+ const nonce = script.nonce || script.getAttribute('nonce') || ''
22
+ const TOKEN = /^[A-Za-z0-9_-]{1,64}$/
23
+ const STORE = 'ream/profiler/toolbar'
24
+ const BAR = 34
25
+
26
+ // ── State ───────────────────────────────────────────────
27
+
28
+ const saved = read()
29
+ const state = {
30
+ closed: saved.closed === true,
31
+ open: saved.open === true,
32
+ tab: typeof saved.tab === 'string' ? saved.tab : 'request',
33
+ height: clampHeight(typeof saved.height === 'number' ? saved.height : 320),
34
+ /** Every request this page knows: itself first, then each fetch/XHR. */
35
+ requests: [],
36
+ /** The one the bar shows. */
37
+ current: null,
38
+ filters: {
39
+ query: '',
40
+ onlyDuplicates: false,
41
+ sortBySlowest: false,
42
+ levels: {},
43
+ logText: '',
44
+ eventText: '',
45
+ },
46
+ }
47
+ window.__reamProfiler = { state }
48
+
49
+ function read() {
50
+ try {
51
+ const value = JSON.parse(localStorage.getItem(STORE) || '{}')
52
+ return value && typeof value === 'object' ? value : {}
53
+ } catch {
54
+ return {}
55
+ }
56
+ }
57
+
58
+ function persist() {
59
+ try {
60
+ localStorage.setItem(
61
+ STORE,
62
+ JSON.stringify({
63
+ closed: state.closed,
64
+ open: state.open,
65
+ tab: state.tab,
66
+ height: state.height,
67
+ }),
68
+ )
69
+ } catch {
70
+ // Private mode or storage disabled: the bar still works, it just forgets.
71
+ }
72
+ }
73
+
74
+ function clampHeight(value) {
75
+ const max = Math.max(160, window.innerHeight - BAR - 60)
76
+ return Math.min(Math.max(value, 120), max)
77
+ }
78
+
79
+ // ── Requests made by the page after it loaded ───────────
80
+
81
+ function track(entry) {
82
+ state.requests.push(entry)
83
+ if (state.requests.length > 100) state.requests.splice(1, 1)
84
+ if (entry.token) load(entry)
85
+ else render()
86
+ }
87
+
88
+ function own(url) {
89
+ try {
90
+ return new URL(url, location.href).pathname.startsWith(base)
91
+ } catch {
92
+ return false
93
+ }
94
+ }
95
+
96
+ const originalFetch = window.fetch
97
+ if (typeof originalFetch === 'function') {
98
+ window.fetch = function (...args) {
99
+ const [input, init] = args
100
+ const url = typeof input === 'string' ? input : input?.url || String(input)
101
+ const method = (init?.method || input?.method || 'GET').toUpperCase()
102
+ const started = performance.now()
103
+ const promise = originalFetch.apply(this, args)
104
+ if (own(url)) return promise
105
+ promise.then(
106
+ (response) =>
107
+ track({
108
+ kind: 'fetch',
109
+ method,
110
+ url,
111
+ status: response.status,
112
+ duration: performance.now() - started,
113
+ token: response.headers.get('x-debug-token') || '',
114
+ }),
115
+ () =>
116
+ track({
117
+ kind: 'fetch',
118
+ method,
119
+ url,
120
+ status: 0,
121
+ duration: performance.now() - started,
122
+ token: '',
123
+ }),
124
+ )
125
+ return promise
126
+ }
127
+ }
128
+
129
+ const xhrOpen = XMLHttpRequest.prototype.open
130
+ XMLHttpRequest.prototype.open = function (...args) {
131
+ const [method, url] = args
132
+ if (!own(String(url))) {
133
+ const started = performance.now()
134
+ this.addEventListener('loadend', () => {
135
+ // Only read a header the response actually exposes: asking for one it
136
+ // does not prints "Refused to get unsafe header" in the console.
137
+ const has = /^x-debug-token:/im.test(this.getAllResponseHeaders())
138
+ track({
139
+ kind: 'xhr',
140
+ method: String(method).toUpperCase(),
141
+ url: String(url),
142
+ status: this.status,
143
+ duration: performance.now() - started,
144
+ token: has ? this.getResponseHeader('x-debug-token') || '' : '',
145
+ })
146
+ })
147
+ }
148
+ return xhrOpen.apply(this, args)
149
+ }
150
+
151
+ /** Fetch one profile; the server may still be saving it, so retry a little. */
152
+ function load(entry, attempt = 0) {
153
+ if (!TOKEN.test(entry.token)) return render()
154
+ originalFetch
155
+ .call(window, `${base}/${entry.token}.json`, { headers: { accept: 'application/json' } })
156
+ .then((response) => {
157
+ if (response.status === 404 && attempt < 5) {
158
+ setTimeout(() => load(entry, attempt + 1), 120 * (attempt + 1))
159
+ return null
160
+ }
161
+ return response.ok ? response.json() : null
162
+ })
163
+ .then((profile) => {
164
+ if (profile) {
165
+ entry.profile = profile
166
+ entry.status = profile.status
167
+ // The page's own load is timed by the server, not by an interceptor.
168
+ if (entry.kind === 'page') entry.duration = profile.duration
169
+ if (state.current === null && entry.kind === 'page') state.current = entry
170
+ }
171
+ render()
172
+ })
173
+ .catch(() => render())
174
+ }
175
+
176
+ // ── DOM helpers ─────────────────────────────────────────
177
+
178
+ function h(tag, props, ...children) {
179
+ const node = document.createElement(tag)
180
+ for (const [key, value] of Object.entries(props || {})) {
181
+ if (value === undefined || value === null || value === false) continue
182
+ if (key === 'text') node.textContent = String(value)
183
+ else if (key === 'class') node.className = value
184
+ // Through the CSSOM: a `style` attribute is inline style, which a
185
+ // `style-src` policy without 'unsafe-inline' silently drops.
186
+ else if (key === 'style') node.style.cssText = value
187
+ else if (key.startsWith('on')) node.addEventListener(key.slice(2), value)
188
+ else if (key === 'dataset') Object.assign(node.dataset, value)
189
+ else node.setAttribute(key, value === true ? '' : String(value))
190
+ }
191
+ for (const child of children.flat(Infinity)) {
192
+ if (child === null || child === undefined || child === false) continue
193
+ node.append(child instanceof Node ? child : document.createTextNode(String(child)))
194
+ }
195
+ return node
196
+ }
197
+
198
+ function ms(value) {
199
+ const n = Number(value) || 0
200
+ if (n < 1) return `${n.toFixed(2)} ms`
201
+ if (n < 100) return `${n.toFixed(1)} ms`
202
+ return `${Math.round(n)} ms`
203
+ }
204
+
205
+ function mb(value) {
206
+ return `${(Number(value) || 0).toFixed(1)} MB`
207
+ }
208
+
209
+ function json(value) {
210
+ try {
211
+ return JSON.stringify(value, null, 2)
212
+ } catch {
213
+ return String(value)
214
+ }
215
+ }
216
+
217
+ function statusTone(status) {
218
+ if (!status || status >= 500) return 'bad'
219
+ if (status >= 400) return 'warn'
220
+ if (status >= 300) return 'warn'
221
+ return 'ok'
222
+ }
223
+
224
+ function row(label, value) {
225
+ if (value === undefined || value === null || value === '') return null
226
+ return h('div', { class: 'row' }, h('b', { text: label }), h('span', { text: String(value) }))
227
+ }
228
+
229
+ function copy(text, button) {
230
+ const done = () => {
231
+ button.textContent = 'copied'
232
+ setTimeout(() => {
233
+ button.textContent = 'copy'
234
+ }, 1500)
235
+ }
236
+ if (navigator.clipboard?.writeText) navigator.clipboard.writeText(text).then(done, () => {})
237
+ }
238
+
239
+ // ── What each panel derives from a profile ──────────────
240
+
241
+ function panelOf(profile, name) {
242
+ const value = profile?.data?.[name]
243
+ return value && typeof value === 'object' ? value : {}
244
+ }
245
+
246
+ function queriesOf(profile) {
247
+ const db = panelOf(profile, 'database')
248
+ const queries = Array.isArray(db.queries) ? db.queries : []
249
+ const counts = new Map()
250
+ for (const query of queries) {
251
+ const key = `${query.sql}\u0000${JSON.stringify(query.bindings)}\u0000${query.connection || ''}`
252
+ query.key = key
253
+ counts.set(key, (counts.get(key) || 0) + 1)
254
+ }
255
+ let duplicates = 0
256
+ for (const query of queries) {
257
+ query.copies = counts.get(query.key) || 1
258
+ if (query.copies > 1) duplicates += 1
259
+ }
260
+ return {
261
+ total: Number(db.total) || 0,
262
+ failed: Number(db.failed) || 0,
263
+ duration: Number(db.duration) || 0,
264
+ capped: db.capped === true,
265
+ queries,
266
+ duplicates,
267
+ unique: counts.size,
268
+ slowest: queries.reduce((max, query) => Math.max(max, Number(query.duration) || 0), 0),
269
+ }
270
+ }
271
+
272
+ function eventsOf(profile) {
273
+ const panel = panelOf(profile, 'events')
274
+ return Array.isArray(panel.events) ? panel.events : []
275
+ }
276
+
277
+ function cacheOf(profile) {
278
+ const ops = eventsOf(profile).filter((event) => event.name.startsWith('cache:'))
279
+ const count = (suffix) => ops.filter((event) => event.name === `cache:${suffix}`).length
280
+ return { ops, hits: count('hit'), misses: count('miss'), writes: count('written') }
281
+ }
282
+
283
+ function mailsOf(profile) {
284
+ return eventsOf(profile).filter((event) => /^mail[:.]/.test(event.name))
285
+ }
286
+
287
+ function logsOf(profile) {
288
+ const panel = panelOf(profile, 'logs')
289
+ return {
290
+ total: Number(panel.total) || 0,
291
+ errors: Number(panel.errors) || 0,
292
+ warnings: Number(panel.warnings) || 0,
293
+ lines: Array.isArray(panel.lines) ? panel.lines : [],
294
+ }
295
+ }
296
+
297
+ // ── The shell ───────────────────────────────────────────
298
+
299
+ const host = h('div', { 'data-ream-profiler': '' })
300
+ host.style.cssText = 'all:initial'
301
+ const root = host.attachShadow({ mode: 'open' })
302
+ root.append(
303
+ h('link', {
304
+ rel: 'stylesheet',
305
+ href: `${base}/_assets/toolbar.css`,
306
+ nonce: nonce || undefined,
307
+ }),
308
+ )
309
+ const shell = h('div', { class: 'rp', role: 'region', 'aria-label': 'Debug toolbar' })
310
+ const panel = h('div', { class: 'panel' })
311
+ const grip = h('div', { class: 'grip', title: 'Drag to resize' })
312
+ const tabs = h('nav', { class: 'tabs' })
313
+ const body = h('div', { class: 'body' })
314
+ const bar = h('div', { class: 'bar', role: 'toolbar' })
315
+ const restore = h('button', {
316
+ class: 'restore',
317
+ text: 'R',
318
+ title: 'Show the debug toolbar',
319
+ onclick: () => {
320
+ state.closed = false
321
+ persist()
322
+ render()
323
+ },
324
+ })
325
+ panel.append(grip, tabs, body)
326
+ shell.append(panel, bar, restore)
327
+ root.append(shell)
328
+
329
+ // ── Keeping the page visible ────────────────────────────
330
+
331
+ // The page keeps its own bottom padding; the bar adds its height to it, so
332
+ // nothing at the bottom of the page ends up underneath.
333
+ let originalPadding = null
334
+ function offsetPage() {
335
+ if (!document.body) return
336
+ if (originalPadding === null) {
337
+ originalPadding = Number.parseFloat(getComputedStyle(document.body).paddingBottom) || 0
338
+ }
339
+ const covered = state.closed ? 0 : shell.getBoundingClientRect().height
340
+ document.body.style.paddingBottom = covered > 0 ? `${originalPadding + covered}px` : ''
341
+ }
342
+
343
+ // ── Resizing ────────────────────────────────────────────
344
+
345
+ grip.addEventListener('mousedown', (start) => {
346
+ start.preventDefault()
347
+ const startY = start.clientY
348
+ const startHeight = state.height
349
+ const move = (event) => {
350
+ state.height = clampHeight(startHeight + (startY - event.clientY))
351
+ panel.style.height = `${state.height}px`
352
+ offsetPage()
353
+ }
354
+ const up = () => {
355
+ document.removeEventListener('mousemove', move)
356
+ document.removeEventListener('mouseup', up)
357
+ persist()
358
+ }
359
+ document.addEventListener('mousemove', move)
360
+ document.addEventListener('mouseup', up)
361
+ })
362
+
363
+ document.addEventListener('keydown', (event) => {
364
+ if (event.key === 'Escape' && state.open) {
365
+ state.open = false
366
+ persist()
367
+ render()
368
+ }
369
+ })
370
+
371
+ function openTab(name) {
372
+ if (state.open && state.tab === name) state.open = false
373
+ else {
374
+ state.open = true
375
+ state.tab = name
376
+ }
377
+ persist()
378
+ render()
379
+ }
380
+
381
+ // ── The bar ─────────────────────────────────────────────
382
+
383
+ function item(opts) {
384
+ return h(
385
+ 'div',
386
+ {
387
+ class: 'item',
388
+ tabindex: '0',
389
+ role: 'button',
390
+ dataset: {
391
+ tone: opts.tone || '',
392
+ ...(state.open && state.tab === opts.tab ? { active: '' } : {}),
393
+ },
394
+ onclick: () => opts.tab && openTab(opts.tab),
395
+ onkeydown: (event) => {
396
+ if (event.key === 'Enter' || event.key === ' ') {
397
+ event.preventDefault()
398
+ if (opts.tab) openTab(opts.tab)
399
+ }
400
+ },
401
+ },
402
+ opts.lead || null,
403
+ opts.label ? h('span', { class: 'label', text: opts.label }) : null,
404
+ h('span', { class: 'value', text: opts.value }),
405
+ opts.info
406
+ ? h('div', { class: 'info', onclick: (event) => event.stopPropagation() }, opts.info)
407
+ : null,
408
+ )
409
+ }
410
+
411
+ function renderBar(profile, entry) {
412
+ const items = []
413
+ const request = panelOf(profile, 'request')
414
+ const status = Number(profile.status) || 0
415
+ items.push(
416
+ item({
417
+ tab: 'request',
418
+ lead: h('span', {
419
+ class: 'status',
420
+ dataset: { tone: statusTone(status) },
421
+ text: String(status),
422
+ }),
423
+ value: `${profile.method} ${request.route || request.path || profile.url}`,
424
+ info: [
425
+ row('Status', status),
426
+ row('Method', profile.method),
427
+ row('URL', profile.url),
428
+ row('Route', request.route),
429
+ row('Pattern', request.pattern),
430
+ row('Controller', request.controller),
431
+ row('User', request.userId),
432
+ row('Session', request.session ? 'yes' : 'no'),
433
+ row('Content type', request.response?.contentType),
434
+ ],
435
+ }),
436
+ )
437
+
438
+ const duration = Number(profile.duration) || 0
439
+ const q = queriesOf(profile)
440
+ items.push(
441
+ item({
442
+ tab: 'timeline',
443
+ label: 'time',
444
+ value: ms(duration),
445
+ tone: duration > 1000 ? 'warn' : '',
446
+ info: [
447
+ row('Total time', ms(duration)),
448
+ row(
449
+ 'In SQL',
450
+ `${ms(q.duration)} (${duration > 0 ? Math.round((q.duration / duration) * 100) : 0}%)`,
451
+ ),
452
+ row('Profile', profile.token),
453
+ ],
454
+ }),
455
+ )
456
+
457
+ const memory = panelOf(profile, 'memory')
458
+ items.push(
459
+ item({
460
+ tab: 'timeline',
461
+ label: 'mem',
462
+ value: mb(memory.usedMb),
463
+ tone: memory.usedMb > 512 ? 'warn' : '',
464
+ info: [
465
+ row('Heap used', mb(memory.usedMb)),
466
+ row('Heap total', mb(memory.totalMb)),
467
+ row('Resident', mb(memory.rssMb)),
468
+ row('During request', `${memory.deltaMb >= 0 ? '+' : ''}${mb(memory.deltaMb)}`),
469
+ ],
470
+ }),
471
+ )
472
+
473
+ items.push(
474
+ item({
475
+ tab: 'queries',
476
+ label: 'db',
477
+ value: `${q.total} · ${ms(q.duration)}`,
478
+ tone: q.failed > 0 ? 'bad' : q.duplicates > 0 || q.total >= 50 ? 'warn' : '',
479
+ info: [
480
+ row('Statements', q.total),
481
+ row('Duplicated', q.duplicates > 0 ? `${q.duplicates} (${q.unique} unique)` : 'none'),
482
+ row('Failed', q.failed || 'none'),
483
+ row('Total time', ms(q.duration)),
484
+ row('Slowest', q.total > 0 ? ms(q.slowest) : ''),
485
+ ],
486
+ }),
487
+ )
488
+
489
+ const logs = logsOf(profile)
490
+ if (logs.total > 0) {
491
+ items.push(
492
+ item({
493
+ tab: 'logs',
494
+ label: 'logs',
495
+ value: String(logs.errors || logs.warnings || logs.total),
496
+ tone: logs.errors > 0 ? 'bad' : logs.warnings > 0 ? 'warn' : '',
497
+ info: [
498
+ row('Errors', logs.errors),
499
+ row('Warnings', logs.warnings),
500
+ row('Lines', logs.total),
501
+ ],
502
+ }),
503
+ )
504
+ }
505
+
506
+ const events = eventsOf(profile)
507
+ if (events.length > 0) {
508
+ const names = new Map()
509
+ for (const event of events) names.set(event.name, (names.get(event.name) || 0) + 1)
510
+ items.push(
511
+ item({
512
+ tab: 'events',
513
+ label: 'events',
514
+ value: String(panelOf(profile, 'events').total || events.length),
515
+ info: [...names.entries()].slice(0, 12).map(([name, count]) => row(name, `× ${count}`)),
516
+ }),
517
+ )
518
+ }
519
+
520
+ const cache = cacheOf(profile)
521
+ if (cache.ops.length > 0) {
522
+ const reads = cache.hits + cache.misses
523
+ items.push(
524
+ item({
525
+ tab: 'cache',
526
+ label: 'cache',
527
+ value: String(cache.ops.length),
528
+ info: [
529
+ row('Operations', cache.ops.length),
530
+ row(
531
+ 'Hits',
532
+ reads > 0
533
+ ? `${cache.hits} / ${reads} (${Math.round((cache.hits / reads) * 100)}%)`
534
+ : 0,
535
+ ),
536
+ row('Misses', cache.misses),
537
+ row('Writes', cache.writes),
538
+ ],
539
+ }),
540
+ )
541
+ }
542
+
543
+ const mails = mailsOf(profile)
544
+ if (mails.length > 0) {
545
+ items.push(item({ tab: 'mail', label: 'mail', value: String(mails.length) }))
546
+ }
547
+
548
+ const exception = panelOf(profile, 'exception')
549
+ if (typeof exception.message === 'string') {
550
+ items.push(
551
+ item({
552
+ tab: 'exception',
553
+ tone: 'bad',
554
+ value: `${exception.name || 'Error'}`,
555
+ info: [row('Message', exception.message), row('Code', exception.code)],
556
+ }),
557
+ )
558
+ }
559
+
560
+ const ajax = state.requests.filter((r) => r.kind !== 'page')
561
+ if (ajax.length > 0) {
562
+ const failed = ajax.slice(-4).some((r) => !r.status || r.status >= 400)
563
+ items.push(
564
+ item({
565
+ tab: 'ajax',
566
+ label: 'ajax',
567
+ value: String(ajax.length),
568
+ tone: failed ? 'bad' : '',
569
+ info: ajax
570
+ .slice(-8)
571
+ .reverse()
572
+ .map((r) => row(`${r.method} ${r.status || '—'}`, `${r.url} · ${ms(r.duration)}`)),
573
+ }),
574
+ )
575
+ }
576
+
577
+ const spacer = h('div', { class: 'spacer' }, profile.url || '')
578
+ if (entry && entry.kind !== 'page') {
579
+ spacer.append(
580
+ h('button', {
581
+ class: 'viewing',
582
+ text: `viewing ${entry.kind} · back to page`,
583
+ onclick: () => {
584
+ state.current = state.requests[0] || null
585
+ render()
586
+ },
587
+ }),
588
+ )
589
+ }
590
+
591
+ const app = panelOf(profile, 'app')
592
+ const appItem = item({
593
+ tab: 'app',
594
+ value: app.ream ? `ream ${app.ream}` : 'ream',
595
+ info: [
596
+ row('ream', app.ream),
597
+ row('Node.js', app.node),
598
+ row('Environment', app.environment),
599
+ row('Platform', app.platform),
600
+ row('Process', app.pid),
601
+ row('Uptime', app.uptime !== undefined ? `${app.uptime} s` : ''),
602
+ ],
603
+ })
604
+
605
+ const full = h('a', {
606
+ class: 'tool',
607
+ href: TOKEN.test(profile.token) ? `${base}/${profile.token}` : base,
608
+ target: '_blank',
609
+ rel: 'noopener',
610
+ title: 'Open the full profile',
611
+ text: '↗',
612
+ })
613
+ const toggle = h('button', {
614
+ class: 'tool',
615
+ title: state.open ? 'Collapse the panel' : 'Expand the panel',
616
+ text: state.open ? '▾' : '▴',
617
+ onclick: () => openTab(state.tab),
618
+ })
619
+ const close = h('button', {
620
+ class: 'tool',
621
+ title: 'Hide the toolbar',
622
+ text: '✕',
623
+ onclick: () => {
624
+ state.closed = true
625
+ state.open = false
626
+ persist()
627
+ render()
628
+ },
629
+ })
630
+
631
+ bar.replaceChildren(...items, spacer, appItem, full, toggle, close)
632
+ }
633
+
634
+ // ── The panel ───────────────────────────────────────────
635
+
636
+ const TABS = [
637
+ { name: 'request', label: 'Request' },
638
+ { name: 'queries', label: 'Queries', count: (p) => queriesOf(p).total, tone: (p) => dbTone(p) },
639
+ { name: 'logs', label: 'Logs', count: (p) => logsOf(p).total, tone: (p) => logTone(p) },
640
+ { name: 'events', label: 'Events', count: (p) => eventsOf(p).length },
641
+ { name: 'timeline', label: 'Timeline' },
642
+ { name: 'cache', label: 'Cache', count: (p) => cacheOf(p).ops.length, hideEmpty: true },
643
+ { name: 'mail', label: 'Mail', count: (p) => mailsOf(p).length, hideEmpty: true },
644
+ {
645
+ name: 'exception',
646
+ label: 'Exception',
647
+ count: (p) => (typeof panelOf(p, 'exception').message === 'string' ? 1 : 0),
648
+ tone: () => 'bad',
649
+ hideEmpty: true,
650
+ },
651
+ { name: 'ajax', label: 'AJAX', count: () => state.requests.length - 1, hideEmpty: true },
652
+ { name: 'app', label: 'App' },
653
+ ]
654
+
655
+ function dbTone(profile) {
656
+ const q = queriesOf(profile)
657
+ return q.failed > 0 ? 'bad' : q.duplicates > 0 ? 'warn' : ''
658
+ }
659
+
660
+ function logTone(profile) {
661
+ const logs = logsOf(profile)
662
+ return logs.errors > 0 ? 'bad' : logs.warnings > 0 ? 'warn' : ''
663
+ }
664
+
665
+ function renderPanel(profile) {
666
+ panel.style.height = `${state.height}px`
667
+ const visible = TABS.filter((tab) => !tab.hideEmpty || (tab.count?.(profile) || 0) > 0)
668
+ if (!visible.some((tab) => tab.name === state.tab)) state.tab = 'request'
669
+ tabs.replaceChildren(
670
+ ...visible.map((tab) => {
671
+ const count = tab.count ? tab.count(profile) : undefined
672
+ return h(
673
+ 'button',
674
+ {
675
+ class: 'tab',
676
+ dataset: state.tab === tab.name ? { active: '' } : {},
677
+ onclick: () => {
678
+ state.tab = tab.name
679
+ persist()
680
+ render()
681
+ },
682
+ },
683
+ tab.label,
684
+ count
685
+ ? h('span', {
686
+ class: 'count',
687
+ dataset: { tone: tab.tone?.(profile) || '' },
688
+ text: count,
689
+ })
690
+ : null,
691
+ )
692
+ }),
693
+ )
694
+ const view = VIEWS[state.tab] || VIEWS.request
695
+ // replaceChildren would print a null as the text "null".
696
+ body.replaceChildren(
697
+ ...[view(profile)].flat(Infinity).filter((node) => node !== null && node !== undefined),
698
+ )
699
+ }
700
+
701
+ function table(rows) {
702
+ const present = rows.filter(
703
+ ([, value]) => value !== undefined && value !== null && value !== '',
704
+ )
705
+ if (present.length === 0) return h('div', { class: 'empty', text: 'Nothing.' })
706
+ return h(
707
+ 'table',
708
+ null,
709
+ present.map(([key, value]) =>
710
+ h(
711
+ 'tr',
712
+ null,
713
+ h('td', { class: 'key', text: key }),
714
+ h(
715
+ 'td',
716
+ null,
717
+ value instanceof Node
718
+ ? value
719
+ : typeof value === 'object'
720
+ ? h('pre', { text: json(value) })
721
+ : h('span', { text: String(value) }),
722
+ ),
723
+ ),
724
+ ),
725
+ )
726
+ }
727
+
728
+ function objectTable(value) {
729
+ if (!value || typeof value !== 'object' || Object.keys(value).length === 0) {
730
+ return h('div', { class: 'empty', text: 'Empty.' })
731
+ }
732
+ return table(Object.entries(value))
733
+ }
734
+
735
+ function searchBox(value, onInput, placeholder) {
736
+ const input = h('input', { type: 'search', placeholder, value })
737
+ input.addEventListener('input', () => {
738
+ onInput(input.value)
739
+ const at = input.selectionStart
740
+ render()
741
+ const again = body.querySelector('input[type=search]')
742
+ if (again) {
743
+ again.focus()
744
+ again.setSelectionRange(at, at)
745
+ }
746
+ })
747
+ return input
748
+ }
749
+
750
+ const VIEWS = {
751
+ request(profile) {
752
+ const r = panelOf(profile, 'request')
753
+ return [
754
+ h('h3', { text: 'Route' }),
755
+ table([
756
+ ['Name', r.route],
757
+ ['Pattern', r.pattern],
758
+ ['Controller', r.controller],
759
+ ['Params', r.params && Object.keys(r.params).length ? r.params : undefined],
760
+ ]),
761
+ h('h3', { text: 'Request' }),
762
+ table([
763
+ ['Method', profile.method],
764
+ ['URL', profile.url],
765
+ ['Path', r.path],
766
+ ['IP', r.ip],
767
+ ['Request id', r.requestId],
768
+ ['AJAX', r.ajax ? 'yes' : undefined],
769
+ ['Time', new Date(profile.time).toLocaleString()],
770
+ ]),
771
+ h('h3', { text: 'Query string' }),
772
+ objectTable(r.query),
773
+ h('h3', { text: 'Body' }),
774
+ r.body === undefined
775
+ ? h('div', { class: 'empty', text: 'No body.' })
776
+ : h('pre', { text: json(r.body) }),
777
+ h('h3', { text: 'Headers' }),
778
+ objectTable(r.headers),
779
+ h('h3', { text: 'Cookies' }),
780
+ r.cookies?.length
781
+ ? h(
782
+ 'div',
783
+ null,
784
+ r.cookies.map((name) => [h('span', { class: 'chip', text: name }), ' ']),
785
+ )
786
+ : h('div', { class: 'empty', text: 'None.' }),
787
+ h('h3', { text: 'Session' }),
788
+ r.session === undefined
789
+ ? h('div', { class: 'empty', text: 'No session.' })
790
+ : objectTable(r.session),
791
+ h('h3', { text: 'User' }),
792
+ r.user === undefined
793
+ ? h('div', { class: 'empty', text: 'Guest.' })
794
+ : h('pre', { text: json(r.user) }),
795
+ h('h3', { text: 'Response' }),
796
+ table([
797
+ ['Status', profile.status],
798
+ ['Content type', r.response?.contentType],
799
+ ['Duration', ms(profile.duration)],
800
+ ]),
801
+ r.response ? objectTable(r.response.headers) : null,
802
+ ]
803
+ },
804
+
805
+ queries(profile) {
806
+ const q = queriesOf(profile)
807
+ const f = state.filters
808
+ if (q.total === 0) {
809
+ return h('div', {
810
+ class: 'empty',
811
+ text: 'No query reached the profiler for this request. A data layer reports statements as `db:query` on the application emitter.',
812
+ })
813
+ }
814
+ let rows = q.queries.slice()
815
+ if (f.onlyDuplicates) rows = rows.filter((query) => query.copies > 1)
816
+ if (f.query) {
817
+ const needle = f.query.toLowerCase()
818
+ rows = rows.filter((query) =>
819
+ `${query.sql} ${JSON.stringify(query.bindings)}`.toLowerCase().includes(needle),
820
+ )
821
+ }
822
+ if (f.sortBySlowest) rows.sort((a, b) => b.duration - a.duration)
823
+ const max = Math.max(q.slowest, 0.0001)
824
+ return [
825
+ h(
826
+ 'div',
827
+ { class: 'toolbar' },
828
+ h('span', {
829
+ text: `${q.total} statements · ${q.unique} unique · ${q.duplicates} duplicated · ${q.failed} failed · ${ms(q.duration)}`,
830
+ }),
831
+ q.capped
832
+ ? h('span', {
833
+ class: 'chip',
834
+ dataset: { tone: 'warn' },
835
+ text: `listing stopped at ${q.queries.length}`,
836
+ })
837
+ : null,
838
+ searchBox(
839
+ f.query,
840
+ (value) => {
841
+ f.query = value
842
+ },
843
+ 'Filter SQL',
844
+ ),
845
+ h('button', {
846
+ class: 'btn',
847
+ dataset: f.onlyDuplicates ? { on: '' } : {},
848
+ text: 'Only duplicates',
849
+ onclick: () => {
850
+ f.onlyDuplicates = !f.onlyDuplicates
851
+ render()
852
+ },
853
+ }),
854
+ h('button', {
855
+ class: 'btn',
856
+ dataset: f.sortBySlowest ? { on: '' } : {},
857
+ text: 'Slowest first',
858
+ onclick: () => {
859
+ f.sortBySlowest = !f.sortBySlowest
860
+ render()
861
+ },
862
+ }),
863
+ ),
864
+ h(
865
+ 'div',
866
+ { class: 'list' },
867
+ rows.map((query) => {
868
+ const copyButton = h('button', {
869
+ class: 'btn',
870
+ text: 'copy',
871
+ onclick: (event) => {
872
+ event.stopPropagation()
873
+ copy(query.sql, copyButton)
874
+ },
875
+ })
876
+ return h(
877
+ 'div',
878
+ {
879
+ class: 'entry',
880
+ dataset: { tone: query.error ? 'bad' : query.copies > 1 ? 'warn' : '' },
881
+ onclick: (event) => {
882
+ if (getSelection()?.type === 'Range') return
883
+ event.currentTarget.toggleAttribute('data-expanded')
884
+ },
885
+ },
886
+ h(
887
+ 'div',
888
+ { class: 'num' },
889
+ ms(query.duration),
890
+ h('div', {
891
+ class: 'meter',
892
+ style: `width:${Math.max(4, (query.duration / max) * 100)}%`,
893
+ }),
894
+ ),
895
+ h(
896
+ 'div',
897
+ null,
898
+ h('div', { class: 'sql', text: query.sql }),
899
+ query.error ? h('div', { class: 'bad', text: query.error }) : null,
900
+ h(
901
+ 'div',
902
+ { class: 'meta' },
903
+ query.bindings?.length
904
+ ? h('span', { class: 'mono', text: JSON.stringify(query.bindings) })
905
+ : null,
906
+ ),
907
+ ),
908
+ h(
909
+ 'div',
910
+ null,
911
+ query.copies > 1
912
+ ? [
913
+ h('span', {
914
+ class: 'chip',
915
+ dataset: { tone: 'warn' },
916
+ text: `× ${query.copies}`,
917
+ }),
918
+ ' ',
919
+ ]
920
+ : null,
921
+ query.model ? [h('span', { class: 'chip', text: query.model }), ' '] : null,
922
+ query.method ? [h('span', { class: 'chip', text: query.method }), ' '] : null,
923
+ query.inTransaction ? [h('span', { class: 'chip', text: 'trx' }), ' '] : null,
924
+ query.connection
925
+ ? [h('span', { class: 'chip', text: query.connection }), ' ']
926
+ : null,
927
+ copyButton,
928
+ ),
929
+ h(
930
+ 'div',
931
+ { class: 'extra' },
932
+ table([
933
+ ['Started at', `+${ms(query.at)}`],
934
+ ['Bindings', query.bindings],
935
+ ['Copies', query.copies > 1 ? query.copies : undefined],
936
+ ]),
937
+ ),
938
+ )
939
+ }),
940
+ ),
941
+ ]
942
+ },
943
+
944
+ logs(profile) {
945
+ const logs = logsOf(profile)
946
+ const f = state.filters
947
+ if (logs.total === 0)
948
+ return h('div', { class: 'empty', text: 'Nothing was logged during this request.' })
949
+ const levels = [...new Set(logs.lines.map((line) => line.level))]
950
+ let lines = logs.lines.filter((line) => f.levels[line.level] !== false)
951
+ if (f.logText) {
952
+ const needle = f.logText.toLowerCase()
953
+ lines = lines.filter((line) => line.message.toLowerCase().includes(needle))
954
+ }
955
+ const tone = (level) =>
956
+ level === 'error' || level === 'fatal' ? 'bad' : level === 'warn' ? 'warn' : ''
957
+ return [
958
+ h(
959
+ 'div',
960
+ { class: 'toolbar' },
961
+ searchBox(
962
+ f.logText,
963
+ (value) => {
964
+ f.logText = value
965
+ },
966
+ 'Filter messages',
967
+ ),
968
+ levels.map((level) =>
969
+ h('button', {
970
+ class: 'chip',
971
+ dataset: { tone: tone(level), ...(f.levels[level] === false ? { off: '' } : {}) },
972
+ text: level,
973
+ onclick: () => {
974
+ f.levels[level] = f.levels[level] === false
975
+ render()
976
+ },
977
+ }),
978
+ ),
979
+ ),
980
+ h(
981
+ 'div',
982
+ { class: 'list' },
983
+ lines.map((line) =>
984
+ h(
985
+ 'div',
986
+ {
987
+ class: 'entry',
988
+ dataset: { tone: tone(line.level) },
989
+ onclick: (event) => {
990
+ if (getSelection()?.type === 'Range') return
991
+ event.currentTarget.toggleAttribute('data-expanded')
992
+ },
993
+ },
994
+ h('div', { class: 'num', text: `+${ms(line.at)}` }),
995
+ h('div', { class: 'sql', text: line.message }),
996
+ h('span', { class: 'chip', dataset: { tone: tone(line.level) }, text: line.level }),
997
+ line.context
998
+ ? h('div', { class: 'extra' }, h('pre', { text: json(line.context) }))
999
+ : null,
1000
+ ),
1001
+ ),
1002
+ ),
1003
+ ]
1004
+ },
1005
+
1006
+ events(profile) {
1007
+ const events = eventsOf(profile)
1008
+ const f = state.filters
1009
+ if (events.length === 0)
1010
+ return h('div', { class: 'empty', text: 'No event was emitted during this request.' })
1011
+ const shown = f.eventText
1012
+ ? events.filter((event) => event.name.toLowerCase().includes(f.eventText.toLowerCase()))
1013
+ : events
1014
+ return [
1015
+ h(
1016
+ 'div',
1017
+ { class: 'toolbar' },
1018
+ h('span', { text: `${panelOf(profile, 'events').total || events.length} events` }),
1019
+ searchBox(
1020
+ f.eventText,
1021
+ (value) => {
1022
+ f.eventText = value
1023
+ },
1024
+ 'Filter by name',
1025
+ ),
1026
+ ),
1027
+ h(
1028
+ 'div',
1029
+ { class: 'list' },
1030
+ shown.map((event) =>
1031
+ h(
1032
+ 'div',
1033
+ {
1034
+ class: 'entry',
1035
+ onclick: (e) => {
1036
+ if (getSelection()?.type === 'Range') return
1037
+ e.currentTarget.toggleAttribute('data-expanded')
1038
+ },
1039
+ },
1040
+ h('div', { class: 'num', text: `+${ms(event.at)}` }),
1041
+ h('div', { class: 'mono', text: event.name }),
1042
+ h('span', null),
1043
+ h('div', { class: 'extra' }, h('pre', { text: json(event.data) })),
1044
+ ),
1045
+ ),
1046
+ ),
1047
+ ]
1048
+ },
1049
+
1050
+ timeline(profile) {
1051
+ const total = Math.max(Number(profile.duration) || 0, 0.001)
1052
+ const lanes = [
1053
+ { kind: 'request', name: `${profile.method} ${profile.url}`, at: 0, duration: total },
1054
+ ]
1055
+ for (const query of queriesOf(profile).queries) {
1056
+ lanes.push({ kind: 'query', name: query.sql, at: query.at, duration: query.duration })
1057
+ }
1058
+ for (const line of logsOf(profile).lines) {
1059
+ lanes.push({
1060
+ kind: 'log',
1061
+ name: `${line.level}: ${line.message}`,
1062
+ at: line.at,
1063
+ duration: 0,
1064
+ })
1065
+ }
1066
+ for (const event of eventsOf(profile)) {
1067
+ lanes.push({ kind: 'event', name: event.name, at: event.at, duration: 0 })
1068
+ }
1069
+ lanes.sort((a, b) => a.at - b.at || (a.kind === 'request' ? -1 : 1))
1070
+ return [
1071
+ h(
1072
+ 'div',
1073
+ { class: 'toolbar' },
1074
+ ['request', 'query', 'log', 'event'].map((kind) =>
1075
+ h('span', { class: 'chip legend', dataset: { kind }, text: kind }),
1076
+ ),
1077
+ h('span', { text: `total ${ms(total)}` }),
1078
+ ),
1079
+ lanes.map((lane) =>
1080
+ h(
1081
+ 'div',
1082
+ { class: 'lane' },
1083
+ h('div', {
1084
+ class: lane.kind === 'query' ? 'name mono' : 'name',
1085
+ text: lane.name,
1086
+ title: lane.name,
1087
+ }),
1088
+ h(
1089
+ 'div',
1090
+ { class: 'track' },
1091
+ h('div', {
1092
+ class: 'span',
1093
+ dataset: { kind: lane.kind },
1094
+ style: `left:${Math.min(100, (lane.at / total) * 100)}%;width:${Math.min(100, (lane.duration / total) * 100)}%`,
1095
+ }),
1096
+ ),
1097
+ h('div', {
1098
+ class: 'num meta',
1099
+ text: lane.duration > 0 ? ms(lane.duration) : `+${ms(lane.at)}`,
1100
+ }),
1101
+ ),
1102
+ ),
1103
+ ]
1104
+ },
1105
+
1106
+ cache(profile) {
1107
+ const cache = cacheOf(profile)
1108
+ return [
1109
+ h(
1110
+ 'div',
1111
+ { class: 'toolbar' },
1112
+ h('span', {
1113
+ text: `${cache.hits} hits · ${cache.misses} misses · ${cache.writes} writes`,
1114
+ }),
1115
+ ),
1116
+ h(
1117
+ 'table',
1118
+ null,
1119
+ h(
1120
+ 'tr',
1121
+ null,
1122
+ h('th', { text: 'At' }),
1123
+ h('th', { text: 'Operation' }),
1124
+ h('th', { text: 'Key' }),
1125
+ h('th', { text: 'Store' }),
1126
+ ),
1127
+ cache.ops.map((op) =>
1128
+ h(
1129
+ 'tr',
1130
+ null,
1131
+ h('td', { class: 'meta', text: `+${ms(op.at)}` }),
1132
+ h('td', { text: op.name.slice('cache:'.length) }),
1133
+ h('td', { class: 'mono', text: op.data?.key ?? '' }),
1134
+ h('td', { class: 'meta', text: op.data?.store ?? '' }),
1135
+ ),
1136
+ ),
1137
+ ),
1138
+ ]
1139
+ },
1140
+
1141
+ mail(profile) {
1142
+ return h(
1143
+ 'div',
1144
+ { class: 'list' },
1145
+ mailsOf(profile).map((event) =>
1146
+ h(
1147
+ 'div',
1148
+ { class: 'entry', onclick: (e) => e.currentTarget.toggleAttribute('data-expanded') },
1149
+ h('div', { class: 'num', text: `+${ms(event.at)}` }),
1150
+ h('div', { class: 'mono', text: event.name }),
1151
+ h('span', null),
1152
+ h('div', { class: 'extra' }, h('pre', { text: json(event.data) })),
1153
+ ),
1154
+ ),
1155
+ )
1156
+ },
1157
+
1158
+ exception(profile) {
1159
+ const e = panelOf(profile, 'exception')
1160
+ return [
1161
+ h('h3', { text: e.name || 'Error' }),
1162
+ h('div', { class: 'bad', text: e.message || '' }),
1163
+ e.code ? h('div', { class: 'meta', text: `code ${e.code}` }) : null,
1164
+ e.stack ? h('pre', { text: e.stack }) : null,
1165
+ ]
1166
+ },
1167
+
1168
+ ajax() {
1169
+ return [
1170
+ h(
1171
+ 'div',
1172
+ { class: 'toolbar' },
1173
+ h('span', { text: `${state.requests.length - 1} requests made by this page` }),
1174
+ h('button', {
1175
+ class: 'btn',
1176
+ text: 'Clear',
1177
+ onclick: () => {
1178
+ state.requests.splice(1)
1179
+ state.current = state.requests[0] || null
1180
+ render()
1181
+ },
1182
+ }),
1183
+ ),
1184
+ h(
1185
+ 'table',
1186
+ null,
1187
+ h(
1188
+ 'tr',
1189
+ null,
1190
+ ['#', 'Method', 'URL', 'Status', 'Time', 'Type', 'Profile'].map((label) =>
1191
+ h('th', { text: label }),
1192
+ ),
1193
+ ),
1194
+ state.requests.map((r, index) =>
1195
+ h(
1196
+ 'tr',
1197
+ {
1198
+ class: r.profile ? 'pick' : '',
1199
+ dataset: state.current === r ? { active: '' } : {},
1200
+ onclick: () => {
1201
+ if (!r.profile) return
1202
+ state.current = r
1203
+ state.tab = 'request'
1204
+ render()
1205
+ },
1206
+ },
1207
+ h('td', { class: 'meta', text: index === 0 ? 'page' : String(index) }),
1208
+ h('td', { text: r.method }),
1209
+ h('td', { class: 'mono', text: r.url }),
1210
+ h(
1211
+ 'td',
1212
+ null,
1213
+ h('span', {
1214
+ class: 'chip',
1215
+ dataset: { tone: statusTone(r.status) },
1216
+ text: r.status || '—',
1217
+ }),
1218
+ ),
1219
+ h('td', { class: 'meta', text: ms(r.duration) }),
1220
+ h('td', { class: 'meta', text: r.kind }),
1221
+ h(
1222
+ 'td',
1223
+ null,
1224
+ TOKEN.test(r.token || '')
1225
+ ? h('a', {
1226
+ href: `${base}/${r.token}`,
1227
+ target: '_blank',
1228
+ rel: 'noopener',
1229
+ text: r.token,
1230
+ onclick: (e) => e.stopPropagation(),
1231
+ })
1232
+ : h('span', { class: 'meta', text: 'n/a' }),
1233
+ ),
1234
+ ),
1235
+ ),
1236
+ ),
1237
+ ]
1238
+ },
1239
+
1240
+ app(profile) {
1241
+ const app = panelOf(profile, 'app')
1242
+ const memory = panelOf(profile, 'memory')
1243
+ return [
1244
+ h('h3', { text: 'Application' }),
1245
+ table([
1246
+ ['ream', app.ream],
1247
+ ['Environment', app.environment],
1248
+ ['Profile token', profile.token],
1249
+ ['All profiles', h('a', { href: base, target: '_blank', rel: 'noopener', text: base })],
1250
+ ]),
1251
+ h('h3', { text: 'Runtime' }),
1252
+ table([
1253
+ ['Node.js', app.node],
1254
+ ['Platform', app.platform],
1255
+ ['Process id', app.pid],
1256
+ ['Uptime', app.uptime !== undefined ? `${app.uptime} s` : undefined],
1257
+ ['Heap used', mb(memory.usedMb)],
1258
+ ['Heap total', mb(memory.totalMb)],
1259
+ ['Resident', mb(memory.rssMb)],
1260
+ ]),
1261
+ ]
1262
+ },
1263
+ }
1264
+
1265
+ // ── Render ──────────────────────────────────────────────
1266
+
1267
+ function render() {
1268
+ const entry = state.current
1269
+ const profile = entry?.profile
1270
+ // Nothing to show until the page's profile has arrived.
1271
+ shell.style.display = profile || state.closed ? '' : 'none'
1272
+ shell.toggleAttribute('data-closed', state.closed)
1273
+ shell.toggleAttribute('data-open', Boolean(state.open && profile && !state.closed))
1274
+ if (profile) {
1275
+ renderBar(profile, entry)
1276
+ if (state.open && !state.closed) renderPanel(profile)
1277
+ }
1278
+ offsetPage()
1279
+ }
1280
+
1281
+ function mount() {
1282
+ document.body.append(host)
1283
+ state.requests.unshift({
1284
+ kind: 'page',
1285
+ method: 'GET',
1286
+ url: location.pathname + location.search,
1287
+ token: pageToken,
1288
+ duration: 0,
1289
+ })
1290
+ load(state.requests[0])
1291
+ window.addEventListener('resize', () => {
1292
+ state.height = clampHeight(state.height)
1293
+ render()
1294
+ })
1295
+ }
1296
+
1297
+ if (document.body) mount()
1298
+ else document.addEventListener('DOMContentLoaded', mount, { once: true })
1299
+ })()