@textjs/app 0.0.0-stage → 1.0.3

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 (335) hide show
  1. package/README.md +34 -2
  2. package/bin/text.js +29 -0
  3. package/bin/text.ts +13 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/cli/build.d.ts +39 -0
  6. package/dist/cli/build.d.ts.map +1 -0
  7. package/dist/cli/build.js +157 -0
  8. package/dist/cli/build.js.map +1 -0
  9. package/dist/cli/config.d.ts +22 -0
  10. package/dist/cli/config.d.ts.map +1 -0
  11. package/dist/cli/config.js +73 -0
  12. package/dist/cli/config.js.map +1 -0
  13. package/dist/cli/dev.d.ts +12 -0
  14. package/dist/cli/dev.d.ts.map +1 -0
  15. package/dist/cli/dev.js +49 -0
  16. package/dist/cli/dev.js.map +1 -0
  17. package/dist/cli/index.d.ts +26 -0
  18. package/dist/cli/index.d.ts.map +1 -0
  19. package/dist/cli/index.js +192 -0
  20. package/dist/cli/index.js.map +1 -0
  21. package/dist/cli/run.d.ts +31 -0
  22. package/dist/cli/run.d.ts.map +1 -0
  23. package/dist/cli/run.js +111 -0
  24. package/dist/cli/run.js.map +1 -0
  25. package/dist/cli/start.d.ts +14 -0
  26. package/dist/cli/start.d.ts.map +1 -0
  27. package/dist/cli/start.js +183 -0
  28. package/dist/cli/start.js.map +1 -0
  29. package/dist/cli/ui.d.ts +35 -0
  30. package/dist/cli/ui.d.ts.map +1 -0
  31. package/dist/cli/ui.js +73 -0
  32. package/dist/cli/ui.js.map +1 -0
  33. package/dist/client.d.ts +34 -0
  34. package/dist/client.d.ts.map +1 -0
  35. package/dist/client.js +93 -0
  36. package/dist/client.js.map +1 -0
  37. package/dist/config.d.ts +143 -0
  38. package/dist/config.d.ts.map +1 -0
  39. package/dist/config.js +28 -0
  40. package/dist/config.js.map +1 -0
  41. package/dist/context.d.ts +44 -0
  42. package/dist/context.d.ts.map +1 -0
  43. package/dist/context.js +26 -0
  44. package/dist/context.js.map +1 -0
  45. package/dist/dev-errors.d.ts +68 -0
  46. package/dist/dev-errors.d.ts.map +1 -0
  47. package/dist/dev-errors.js +121 -0
  48. package/dist/dev-errors.js.map +1 -0
  49. package/dist/devtools.d.ts +64 -0
  50. package/dist/devtools.d.ts.map +1 -0
  51. package/dist/devtools.js +186 -0
  52. package/dist/devtools.js.map +1 -0
  53. package/dist/frame-mirror.d.ts +19 -0
  54. package/dist/frame-mirror.d.ts.map +1 -0
  55. package/dist/frame-mirror.js +141 -0
  56. package/dist/frame-mirror.js.map +1 -0
  57. package/dist/frame-protocol.d.ts +175 -0
  58. package/dist/frame-protocol.d.ts.map +1 -0
  59. package/dist/frame-protocol.js +369 -0
  60. package/dist/frame-protocol.js.map +1 -0
  61. package/dist/history.d.ts +54 -0
  62. package/dist/history.d.ts.map +1 -0
  63. package/dist/history.js +152 -0
  64. package/dist/history.js.map +1 -0
  65. package/dist/index.d.ts +9 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +8 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/link.d.ts +32 -0
  70. package/dist/link.d.ts.map +1 -0
  71. package/dist/link.js +54 -0
  72. package/dist/link.js.map +1 -0
  73. package/dist/live-player.d.ts +83 -0
  74. package/dist/live-player.d.ts.map +1 -0
  75. package/dist/live-player.js +413 -0
  76. package/dist/live-player.js.map +1 -0
  77. package/dist/live-protocol.d.ts +132 -0
  78. package/dist/live-protocol.d.ts.map +1 -0
  79. package/dist/live-protocol.js +278 -0
  80. package/dist/live-protocol.js.map +1 -0
  81. package/dist/live-stub.d.ts +10 -0
  82. package/dist/live-stub.d.ts.map +1 -0
  83. package/dist/live-stub.js +32 -0
  84. package/dist/live-stub.js.map +1 -0
  85. package/dist/mcp/cli.d.ts +22 -0
  86. package/dist/mcp/cli.d.ts.map +1 -0
  87. package/dist/mcp/cli.js +177 -0
  88. package/dist/mcp/cli.js.map +1 -0
  89. package/dist/mcp/frame.d.ts +58 -0
  90. package/dist/mcp/frame.d.ts.map +1 -0
  91. package/dist/mcp/frame.js +169 -0
  92. package/dist/mcp/frame.js.map +1 -0
  93. package/dist/mcp/index.d.ts +15 -0
  94. package/dist/mcp/index.d.ts.map +1 -0
  95. package/dist/mcp/index.js +15 -0
  96. package/dist/mcp/index.js.map +1 -0
  97. package/dist/mcp/inspect.d.ts +95 -0
  98. package/dist/mcp/inspect.d.ts.map +1 -0
  99. package/dist/mcp/inspect.js +386 -0
  100. package/dist/mcp/inspect.js.map +1 -0
  101. package/dist/mcp/keys.d.ts +20 -0
  102. package/dist/mcp/keys.d.ts.map +1 -0
  103. package/dist/mcp/keys.js +87 -0
  104. package/dist/mcp/keys.js.map +1 -0
  105. package/dist/mcp/logs.d.ts +63 -0
  106. package/dist/mcp/logs.d.ts.map +1 -0
  107. package/dist/mcp/logs.js +217 -0
  108. package/dist/mcp/logs.js.map +1 -0
  109. package/dist/mcp/server.d.ts +64 -0
  110. package/dist/mcp/server.d.ts.map +1 -0
  111. package/dist/mcp/server.js +485 -0
  112. package/dist/mcp/server.js.map +1 -0
  113. package/dist/mcp/session.d.ts +230 -0
  114. package/dist/mcp/session.d.ts.map +1 -0
  115. package/dist/mcp/session.js +742 -0
  116. package/dist/mcp/session.js.map +1 -0
  117. package/dist/navigation-errors.d.ts +28 -0
  118. package/dist/navigation-errors.d.ts.map +1 -0
  119. package/dist/navigation-errors.js +43 -0
  120. package/dist/navigation-errors.js.map +1 -0
  121. package/dist/navigation.d.ts +21 -0
  122. package/dist/navigation.d.ts.map +1 -0
  123. package/dist/navigation.js +33 -0
  124. package/dist/navigation.js.map +1 -0
  125. package/dist/router.d.ts +76 -0
  126. package/dist/router.d.ts.map +1 -0
  127. package/dist/router.js +296 -0
  128. package/dist/router.js.map +1 -0
  129. package/dist/routing.d.ts +101 -0
  130. package/dist/routing.d.ts.map +1 -0
  131. package/dist/routing.js +286 -0
  132. package/dist/routing.js.map +1 -0
  133. package/dist/server/adapters/build-step.d.ts +34 -0
  134. package/dist/server/adapters/build-step.d.ts.map +1 -0
  135. package/dist/server/adapters/build-step.js +112 -0
  136. package/dist/server/adapters/build-step.js.map +1 -0
  137. package/dist/server/adapters/bun.d.ts +8 -0
  138. package/dist/server/adapters/bun.d.ts.map +1 -0
  139. package/dist/server/adapters/bun.js +89 -0
  140. package/dist/server/adapters/bun.js.map +1 -0
  141. package/dist/server/adapters/bundle.d.ts +21 -0
  142. package/dist/server/adapters/bundle.d.ts.map +1 -0
  143. package/dist/server/adapters/bundle.js +86 -0
  144. package/dist/server/adapters/bundle.js.map +1 -0
  145. package/dist/server/adapters/cloudflare.d.ts +29 -0
  146. package/dist/server/adapters/cloudflare.d.ts.map +1 -0
  147. package/dist/server/adapters/cloudflare.js +105 -0
  148. package/dist/server/adapters/cloudflare.js.map +1 -0
  149. package/dist/server/adapters/index.d.ts +10 -0
  150. package/dist/server/adapters/index.d.ts.map +1 -0
  151. package/dist/server/adapters/index.js +24 -0
  152. package/dist/server/adapters/index.js.map +1 -0
  153. package/dist/server/adapters/runtime-node.d.ts +16 -0
  154. package/dist/server/adapters/runtime-node.d.ts.map +1 -0
  155. package/dist/server/adapters/runtime-node.js +29 -0
  156. package/dist/server/adapters/runtime-node.js.map +1 -0
  157. package/dist/server/adapters/runtime.d.ts +42 -0
  158. package/dist/server/adapters/runtime.d.ts.map +1 -0
  159. package/dist/server/adapters/runtime.js +115 -0
  160. package/dist/server/adapters/runtime.js.map +1 -0
  161. package/dist/server/adapters/shared.d.ts +66 -0
  162. package/dist/server/adapters/shared.d.ts.map +1 -0
  163. package/dist/server/adapters/shared.js +124 -0
  164. package/dist/server/adapters/shared.js.map +1 -0
  165. package/dist/server/adapters/static.d.ts +9 -0
  166. package/dist/server/adapters/static.d.ts.map +1 -0
  167. package/dist/server/adapters/static.js +70 -0
  168. package/dist/server/adapters/static.js.map +1 -0
  169. package/dist/server/adapters/vercel.d.ts +43 -0
  170. package/dist/server/adapters/vercel.d.ts.map +1 -0
  171. package/dist/server/adapters/vercel.js +119 -0
  172. package/dist/server/adapters/vercel.js.map +1 -0
  173. package/dist/server/bun-jsx.d.ts +18 -0
  174. package/dist/server/bun-jsx.d.ts.map +1 -0
  175. package/dist/server/bun-jsx.js +56 -0
  176. package/dist/server/bun-jsx.js.map +1 -0
  177. package/dist/server/cache-route.d.ts +38 -0
  178. package/dist/server/cache-route.d.ts.map +1 -0
  179. package/dist/server/cache-route.js +109 -0
  180. package/dist/server/cache-route.js.map +1 -0
  181. package/dist/server/cache.d.ts +131 -0
  182. package/dist/server/cache.d.ts.map +1 -0
  183. package/dist/server/cache.js +409 -0
  184. package/dist/server/cache.js.map +1 -0
  185. package/dist/server/frames.d.ts +45 -0
  186. package/dist/server/frames.d.ts.map +1 -0
  187. package/dist/server/frames.js +294 -0
  188. package/dist/server/frames.js.map +1 -0
  189. package/dist/server/http.d.ts +80 -0
  190. package/dist/server/http.d.ts.map +1 -0
  191. package/dist/server/http.js +453 -0
  192. package/dist/server/http.js.map +1 -0
  193. package/dist/server/jsx-dev-runtime.d.ts +5 -0
  194. package/dist/server/jsx-dev-runtime.d.ts.map +1 -0
  195. package/dist/server/jsx-dev-runtime.js +6 -0
  196. package/dist/server/jsx-dev-runtime.js.map +1 -0
  197. package/dist/server/jsx-runtime.d.ts +10 -0
  198. package/dist/server/jsx-runtime.d.ts.map +1 -0
  199. package/dist/server/jsx-runtime.js +11 -0
  200. package/dist/server/jsx-runtime.js.map +1 -0
  201. package/dist/server/live-app.d.ts +87 -0
  202. package/dist/server/live-app.d.ts.map +1 -0
  203. package/dist/server/live-app.js +307 -0
  204. package/dist/server/live-app.js.map +1 -0
  205. package/dist/server/live-build.d.ts +70 -0
  206. package/dist/server/live-build.d.ts.map +1 -0
  207. package/dist/server/live-build.js +222 -0
  208. package/dist/server/live-build.js.map +1 -0
  209. package/dist/server/live-dev.d.ts +62 -0
  210. package/dist/server/live-dev.d.ts.map +1 -0
  211. package/dist/server/live-dev.js +313 -0
  212. package/dist/server/live-dev.js.map +1 -0
  213. package/dist/server/live-manifest.d.ts +23 -0
  214. package/dist/server/live-manifest.d.ts.map +1 -0
  215. package/dist/server/live-manifest.js +7 -0
  216. package/dist/server/live-manifest.js.map +1 -0
  217. package/dist/server/live-prod.d.ts +30 -0
  218. package/dist/server/live-prod.d.ts.map +1 -0
  219. package/dist/server/live-prod.js +97 -0
  220. package/dist/server/live-prod.js.map +1 -0
  221. package/dist/server/live-routes.d.ts +72 -0
  222. package/dist/server/live-routes.d.ts.map +1 -0
  223. package/dist/server/live-routes.js +274 -0
  224. package/dist/server/live-routes.js.map +1 -0
  225. package/dist/server/live-web.d.ts +16 -0
  226. package/dist/server/live-web.d.ts.map +1 -0
  227. package/dist/server/live-web.js +159 -0
  228. package/dist/server/live-web.js.map +1 -0
  229. package/dist/server/live.d.ts +138 -0
  230. package/dist/server/live.d.ts.map +1 -0
  231. package/dist/server/live.js +542 -0
  232. package/dist/server/live.js.map +1 -0
  233. package/dist/server/middleware-node.d.ts +48 -0
  234. package/dist/server/middleware-node.d.ts.map +1 -0
  235. package/dist/server/middleware-node.js +167 -0
  236. package/dist/server/middleware-node.js.map +1 -0
  237. package/dist/server/middleware-runtime.d.ts +97 -0
  238. package/dist/server/middleware-runtime.d.ts.map +1 -0
  239. package/dist/server/middleware-runtime.js +447 -0
  240. package/dist/server/middleware-runtime.js.map +1 -0
  241. package/dist/server/middleware.d.ts +145 -0
  242. package/dist/server/middleware.d.ts.map +1 -0
  243. package/dist/server/middleware.js +365 -0
  244. package/dist/server/middleware.js.map +1 -0
  245. package/dist/server/ppr.d.ts +22 -0
  246. package/dist/server/ppr.d.ts.map +1 -0
  247. package/dist/server/ppr.js +58 -0
  248. package/dist/server/ppr.js.map +1 -0
  249. package/dist/server/scan.d.ts +48 -0
  250. package/dist/server/scan.d.ts.map +1 -0
  251. package/dist/server/scan.js +161 -0
  252. package/dist/server/scan.js.map +1 -0
  253. package/dist/server/server-components.d.ts +65 -0
  254. package/dist/server/server-components.d.ts.map +1 -0
  255. package/dist/server/server-components.js +215 -0
  256. package/dist/server/server-components.js.map +1 -0
  257. package/dist/terminal.d.ts +5 -0
  258. package/dist/terminal.d.ts.map +1 -0
  259. package/dist/terminal.js +5 -0
  260. package/dist/terminal.js.map +1 -0
  261. package/dist/testing.d.ts +5 -0
  262. package/dist/testing.d.ts.map +1 -0
  263. package/dist/testing.js +5 -0
  264. package/dist/testing.js.map +1 -0
  265. package/dist/vite-plugin.d.ts +51 -0
  266. package/dist/vite-plugin.d.ts.map +1 -0
  267. package/dist/vite-plugin.js +403 -0
  268. package/dist/vite-plugin.js.map +1 -0
  269. package/package.json +130 -4
  270. package/src/cli/build.ts +182 -0
  271. package/src/cli/config.ts +72 -0
  272. package/src/cli/dev.ts +54 -0
  273. package/src/cli/index.ts +185 -0
  274. package/src/cli/run.ts +124 -0
  275. package/src/cli/start.ts +194 -0
  276. package/src/cli/ui.ts +93 -0
  277. package/src/client.tsx +122 -0
  278. package/src/config.ts +155 -0
  279. package/src/context.ts +69 -0
  280. package/src/dev-errors.ts +167 -0
  281. package/src/devtools.tsx +419 -0
  282. package/src/frame-mirror.ts +145 -0
  283. package/src/frame-protocol.ts +435 -0
  284. package/src/history.ts +201 -0
  285. package/src/index.ts +9 -0
  286. package/src/link.tsx +108 -0
  287. package/src/live-player.ts +449 -0
  288. package/src/live-protocol.ts +317 -0
  289. package/src/live-stub.tsx +32 -0
  290. package/src/mcp/cli.ts +188 -0
  291. package/src/mcp/frame.ts +224 -0
  292. package/src/mcp/index.ts +15 -0
  293. package/src/mcp/inspect.ts +445 -0
  294. package/src/mcp/keys.ts +86 -0
  295. package/src/mcp/logs.ts +225 -0
  296. package/src/mcp/server.ts +595 -0
  297. package/src/mcp/session.ts +821 -0
  298. package/src/navigation-errors.ts +60 -0
  299. package/src/navigation.ts +44 -0
  300. package/src/router.tsx +497 -0
  301. package/src/routing.ts +344 -0
  302. package/src/server/adapters/build-step.ts +135 -0
  303. package/src/server/adapters/bun.ts +92 -0
  304. package/src/server/adapters/bundle.ts +97 -0
  305. package/src/server/adapters/cloudflare.ts +111 -0
  306. package/src/server/adapters/index.ts +29 -0
  307. package/src/server/adapters/runtime-node.ts +34 -0
  308. package/src/server/adapters/runtime.ts +145 -0
  309. package/src/server/adapters/shared.ts +159 -0
  310. package/src/server/adapters/static.ts +68 -0
  311. package/src/server/adapters/vercel.ts +149 -0
  312. package/src/server/bun-jsx.ts +65 -0
  313. package/src/server/cache-route.ts +139 -0
  314. package/src/server/cache.ts +498 -0
  315. package/src/server/frames.ts +326 -0
  316. package/src/server/http.ts +467 -0
  317. package/src/server/jsx-dev-runtime.ts +9 -0
  318. package/src/server/jsx-runtime.ts +13 -0
  319. package/src/server/live-app.tsx +374 -0
  320. package/src/server/live-build.ts +258 -0
  321. package/src/server/live-dev.ts +357 -0
  322. package/src/server/live-manifest.ts +25 -0
  323. package/src/server/live-prod.ts +120 -0
  324. package/src/server/live-routes.ts +298 -0
  325. package/src/server/live-web.ts +170 -0
  326. package/src/server/live.ts +551 -0
  327. package/src/server/middleware-node.ts +168 -0
  328. package/src/server/middleware-runtime.ts +478 -0
  329. package/src/server/middleware.ts +451 -0
  330. package/src/server/ppr.ts +62 -0
  331. package/src/server/scan.ts +190 -0
  332. package/src/server/server-components.ts +224 -0
  333. package/src/terminal.ts +5 -0
  334. package/src/testing.ts +5 -0
  335. package/src/vite-plugin.ts +413 -0
@@ -0,0 +1,742 @@
1
+ /**
2
+ * A headless Text.js app session for `text mcp`: the `app/` directory mounted
3
+ * in-process (same route manifest and in-process route handlers as `text run`,
4
+ * memory history), rendered into an offscreen cell grid and driven with keys.
5
+ *
6
+ * Frame pacing is deterministic for agents: every input waits for the app to
7
+ * go idle — no scheduled frame, no in-flight `fetch`, and no commit or frame
8
+ * for `quietMs` — before the frame is read.
9
+ */
10
+ import { AsyncLocalStorage } from "node:async_hooks";
11
+ import { existsSync, realpathSync } from "node:fs";
12
+ import { dirname, relative, resolve, sep } from "node:path";
13
+ import { fileURLToPath, pathToFileURL } from "node:url";
14
+ import { createElement } from "react";
15
+ import { ATTR_INVERSE, ATTR_WIDE_CONT, initYoga, InputEngine } from "@textjs/core";
16
+ import { createConsoleRootSync } from "@textjs/react";
17
+ import { createMemoryHistory } from "../history.js";
18
+ import { isNavigationError } from "../navigation-errors.js";
19
+ import { AppRouter } from "../router.js";
20
+ import { resolveMetadata, resolveRoute, searchParamsToRecord } from "../routing.js";
21
+ import { listApiRoutes, listPages, matchApiRoute, scanApp, toApiManifest } from "../server/scan.js";
22
+ import { runRouteHandlerWithCache } from "../server/cache-route.js";
23
+ import { loadRuntimeManifest } from "../cli/run.js";
24
+ import { installServerJsxRuntime } from "../server/bun-jsx.js";
25
+ import { prepareServerManifest } from "../server/live-app.js";
26
+ import { ServerComponentCache, ServerComponentsProvider } from "../server/server-components.js";
27
+ import { loadTextConfig } from "../cli/config.js";
28
+ import { clampRegion, frameContains, frameText, styledRows } from "./frame.js";
29
+ import { FiberProbe, inspect } from "./inspect.js";
30
+ import { parseKeyInput, textToKeyEvents } from "./keys.js";
31
+ import { LogBuffer, txCodeOf } from "./logs.js";
32
+ /** A tool-facing error with an optional `TX###` code. */
33
+ export class SessionError extends Error {
34
+ code;
35
+ constructor(message, code) {
36
+ super(message);
37
+ this.name = "SessionError";
38
+ this.code = code;
39
+ }
40
+ }
41
+ const nullConsole = { clear() { }, log() { } };
42
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
43
+ const MIN_FRAME_GAP_MS = 8;
44
+ /**
45
+ * React throttles revealing resolved Suspense boundaries (300ms after the
46
+ * last fallback commit), so a settled server-component promise may commit
47
+ * that much later: stay busy until it can have.
48
+ */
49
+ const SUSPENSE_REVEAL_MS = 350;
50
+ /** `setTimeout`-driven frames, with the pending set visible for idle detection. */
51
+ class TrackedScheduler {
52
+ onActivity;
53
+ pending = new Set();
54
+ lastRun = 0;
55
+ constructor(onActivity) {
56
+ this.onActivity = onActivity;
57
+ }
58
+ request(cb) {
59
+ this.onActivity();
60
+ // A small gap caps runaway render loops at ~120fps without slowing input.
61
+ const delay = Math.max(0, this.lastRun + MIN_FRAME_GAP_MS - Date.now());
62
+ const h = setTimeout(() => {
63
+ this.pending.delete(h);
64
+ this.lastRun = Date.now();
65
+ cb();
66
+ }, delay);
67
+ this.pending.add(h);
68
+ return h;
69
+ }
70
+ cancel(handle) {
71
+ clearTimeout(handle);
72
+ this.pending.delete(handle);
73
+ }
74
+ dispose() {
75
+ for (const h of this.pending)
76
+ clearTimeout(h);
77
+ this.pending.clear();
78
+ }
79
+ }
80
+ function errorMessage(error) {
81
+ if (error instanceof Error)
82
+ return error.message;
83
+ return String(error);
84
+ }
85
+ /** `@textjs/app` sources (`src/` or `dist/`): never purged on reload. */
86
+ const PACKAGE_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
87
+ export class HeadlessSession {
88
+ logs;
89
+ rootDir;
90
+ options;
91
+ scan;
92
+ manifest = null;
93
+ appRoot = null;
94
+ scheduler = null;
95
+ probe = null;
96
+ history;
97
+ theme;
98
+ cols = 100;
99
+ rows = 32;
100
+ resized = false;
101
+ generation = 0;
102
+ frameSeq = 0;
103
+ lastActivity = Date.now();
104
+ /** In-flight `fetch` calls per mount generation (a stale app's hung request never blocks idleness). */
105
+ inflight = new Map();
106
+ errorCursor = 0;
107
+ /** Normalized `options.origin` (no trailing slash). */
108
+ origin;
109
+ status = "failed";
110
+ loadError = null;
111
+ disposed = false;
112
+ /** Async Server Components' promises (pending ones keep the session busy). */
113
+ serverCache;
114
+ quietMs;
115
+ constructor(options) {
116
+ this.options = options;
117
+ this.rootDir = resolve(options.root);
118
+ this.logs = options.logs ?? new LogBuffer();
119
+ this.history = createMemoryHistory(options.path ?? "/");
120
+ this.quietMs = options.quietMs ?? 50;
121
+ this.origin = new URL(options.origin ?? "http://localhost:3000").origin;
122
+ this.serverCache = new ServerComponentCache({ onActivity: () => this.touch() });
123
+ this.history.listen(() => this.touch());
124
+ }
125
+ /** Load `text.config.ts`, import every route module and mount the app. */
126
+ static async start(options) {
127
+ const session = new HeadlessSession(options);
128
+ await initYoga();
129
+ await session.loadConfig();
130
+ await session.mount();
131
+ if (session.status === "failed") {
132
+ const err = session.loadError;
133
+ await session.dispose();
134
+ throw err instanceof Error ? err : new SessionError(errorMessage(err));
135
+ }
136
+ await session.settle();
137
+ session.errorCursor = session.logs.cursor;
138
+ return session;
139
+ }
140
+ touch() {
141
+ this.lastActivity = Date.now();
142
+ }
143
+ async loadConfig() {
144
+ let cfgCols;
145
+ let cfgRows;
146
+ let appDir = "app";
147
+ if (!this.options.skipConfig) {
148
+ const { config } = await loadTextConfig(this.rootDir, "serve");
149
+ const vp = config.viewport;
150
+ if (typeof vp?.width === "number")
151
+ cfgCols = vp.width;
152
+ if (typeof vp?.height === "number")
153
+ cfgRows = vp.height;
154
+ if (typeof vp?.theme === "string" && this.options.theme === undefined)
155
+ this.theme = vp.theme;
156
+ if (typeof config.appDir === "string")
157
+ appDir = config.appDir;
158
+ }
159
+ if (this.options.theme !== undefined)
160
+ this.theme = this.options.theme;
161
+ if (!this.resized) {
162
+ this.cols = clampCols(this.options.cols ?? cfgCols ?? 100);
163
+ this.rows = clampRows(this.options.rows ?? cfgRows ?? 32);
164
+ }
165
+ this.scan = scanApp(this.rootDir, appDir);
166
+ if (!existsSync(this.scan.appDir))
167
+ throw new SessionError(`No app/ directory found in ${this.rootDir}.`, "TX018");
168
+ }
169
+ trackFetch(gen, delta) {
170
+ const n = (this.inflight.get(gen) ?? 0) + delta;
171
+ if (n > 0)
172
+ this.inflight.set(gen, n);
173
+ else
174
+ this.inflight.delete(gen);
175
+ this.touch();
176
+ }
177
+ /**
178
+ * The app's `fetch`: same-origin URLs (`"/api/health"`,
179
+ * `"http://localhost:3000/api/x"`) run `app/**\/route.ts` handlers in-process
180
+ * exactly like `text dev`; everything else goes to the network.
181
+ */
182
+ async handleFetch(input, init) {
183
+ const gen = this.generation;
184
+ this.trackFetch(gen, 1);
185
+ try {
186
+ const raw = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
187
+ const url = sameOriginUrl(raw, this.origin);
188
+ if (!url)
189
+ return await networkFetch()(input, init);
190
+ const hit = matchApiRoute(toApiManifest(this.scan), url.pathname);
191
+ if (!hit)
192
+ return new Response("Not Found", { status: 404 });
193
+ const mod = (await import(pathToFileURL(hit.file).href));
194
+ const request = input instanceof Request ? new Request(url, init ? new Request(input, init) : input) : new Request(url, init);
195
+ return await runRouteHandlerWithCache(mod, request, { params: hit.params, pattern: hit.pattern, pathname: url.pathname, cache: false });
196
+ }
197
+ finally {
198
+ this.trackFetch(gen, -1);
199
+ }
200
+ }
201
+ recordError(source, error, componentStack) {
202
+ if (isNavigationError(error))
203
+ return;
204
+ const args = [error];
205
+ if (componentStack)
206
+ args.push(`\nComponent stack:${componentStack}`);
207
+ this.logs.push("error", source, args);
208
+ }
209
+ async mount() {
210
+ attachFetch(this);
211
+ const gen = ++this.generation;
212
+ // `async function` components (e.g. `runtime = "server"` pages) render like in `text dev`.
213
+ installServerJsxRuntime(this.rootDir);
214
+ try {
215
+ this.manifest = await loadRuntimeManifest(this.scan);
216
+ }
217
+ catch (error) {
218
+ this.status = "failed";
219
+ this.loadError = error;
220
+ this.logs.push("error", "server", ["Failed to load app/ modules:", error]);
221
+ return;
222
+ }
223
+ const scheduler = new TrackedScheduler(() => this.touch());
224
+ this.scheduler = scheduler;
225
+ const root = createConsoleRootSync({
226
+ width: this.cols,
227
+ height: this.rows,
228
+ console: nullConsole,
229
+ input: new InputEngine({ document: null }),
230
+ registerGlobal: false,
231
+ scheduler,
232
+ minFrameInterval: 0,
233
+ theme: this.theme,
234
+ onFrame: (f) => {
235
+ if (f.skipped)
236
+ return;
237
+ this.frameSeq++;
238
+ this.touch();
239
+ },
240
+ onUncaughtError: (error) => {
241
+ this.recordError("react", error);
242
+ if (gen === this.generation)
243
+ this.status = "crashed";
244
+ },
245
+ onCaughtError: (error, info) => this.recordError("react", error, info.componentStack),
246
+ onRecoverableError: (error) => this.logs.push("warn", "react", [error]),
247
+ });
248
+ this.appRoot = root;
249
+ void root.waitUntilExit().then(() => {
250
+ if (gen === this.generation && !this.disposed && this.status === "running")
251
+ this.status = "exited";
252
+ });
253
+ this.status = "running";
254
+ this.loadError = null;
255
+ const probeRef = (p) => {
256
+ if (gen === this.generation)
257
+ this.probe = p;
258
+ };
259
+ const tree = (createElement(ServerComponentsProvider, { cache: this.serverCache }, createElement(FiberProbe, { ref: probeRef }, createElement(AppRouter, { routes: prepareServerManifest(this.manifest), history: this.history, theme: this.theme, documentTitle: false, errorStore: null }))));
260
+ fetchScope.run(this, () => {
261
+ root.render(tree);
262
+ root.flushSyncRender();
263
+ });
264
+ }
265
+ unmountApp() {
266
+ this.generation++;
267
+ this.scheduler?.dispose();
268
+ this.scheduler = null;
269
+ this.serverCache.clear();
270
+ const root = this.appRoot;
271
+ this.appRoot = null;
272
+ this.probe = null;
273
+ if (root && !root.unmounted) {
274
+ try {
275
+ root.unmount();
276
+ }
277
+ catch (error) {
278
+ this.logs.push("error", "server", ["Error while unmounting:", error]);
279
+ }
280
+ }
281
+ }
282
+ requireRoot() {
283
+ if (this.disposed)
284
+ throw new SessionError("The session is closed.");
285
+ const root = this.appRoot;
286
+ if (!root || this.status === "failed") {
287
+ throw new SessionError(`The app failed to load: ${errorMessage(this.loadError)}\nFix the error and call \`reload\`.`, txCodeOf(this.loadError));
288
+ }
289
+ if (root.unmounted || this.status === "exited")
290
+ throw new SessionError("The app exited (it called exit()). Call `reload` to start it again.");
291
+ return root;
292
+ }
293
+ /** Throw a tool error unless the app is mounted and running (not exited, crashed or failed to load). */
294
+ assertRunning() {
295
+ this.requireRoot();
296
+ if (this.status === "crashed")
297
+ throw new SessionError("The app crashed with an uncaught error (see `logs`). Fix it and call `reload`.");
298
+ }
299
+ /** Whether the app is idle right now (see the module comment). */
300
+ isIdle(quietMs = this.quietMs) {
301
+ if (!this.appRoot)
302
+ return true;
303
+ const pending = this.scheduler?.pending.size ?? 0;
304
+ const now = Date.now();
305
+ const server = this.serverCache.pending === 0 && now - this.serverCache.lastSettled >= SUSPENSE_REVEAL_MS;
306
+ return pending === 0 && (this.inflight.get(this.generation) ?? 0) === 0 && server && now - this.lastActivity >= quietMs;
307
+ }
308
+ /**
309
+ * Wait until the app is idle or `timeout` elapses. On timeout (a spinner or
310
+ * live chart that never stops), the pending frame is flushed so reads still
311
+ * see the latest committed tree.
312
+ */
313
+ async settle(options = {}) {
314
+ const quietMs = options.quietMs ?? this.quietMs;
315
+ const timeout = options.timeout ?? 1500;
316
+ const start = Date.now();
317
+ for (;;) {
318
+ throwIfAborted(options.signal);
319
+ if (this.isIdle(quietMs))
320
+ return { idle: true, waitedMs: Date.now() - start };
321
+ if (Date.now() - start >= timeout)
322
+ break;
323
+ await sleep(Math.min(10, Math.max(1, quietMs / 4)));
324
+ }
325
+ const root = this.appRoot;
326
+ if (root && !root.unmounted && (this.scheduler?.pending.size ?? 0) > 0)
327
+ root.flushSyncRender();
328
+ return { idle: false, waitedMs: Date.now() - start };
329
+ }
330
+ /**
331
+ * Send events in order, letting each one's effects and follow-up commits
332
+ * land before the next. Stops if the app exits or `signal` aborts (a timed
333
+ * out tool call must not keep typing into the next call's frame).
334
+ */
335
+ async dispatch(events, signal) {
336
+ const root = this.requireRoot();
337
+ for (const e of events) {
338
+ throwIfAborted(signal);
339
+ if (root.unmounted || root !== this.appRoot)
340
+ break;
341
+ this.touch();
342
+ fetchScope.run(this, () => {
343
+ try {
344
+ if (e.kind === "key")
345
+ root.input.emit("key", e.event);
346
+ else if (e.kind === "wheel")
347
+ root.input.emit("wheel", e.event);
348
+ else
349
+ root.input.handlePaste(e.text);
350
+ }
351
+ catch (error) {
352
+ this.logs.push("error", "react", ["Error in input handler:", error]);
353
+ }
354
+ });
355
+ await sleep(0);
356
+ }
357
+ }
358
+ async press(keys, signal) {
359
+ const events = parseKeyInput(keys);
360
+ await this.dispatch(events.map((event) => ({ kind: "key", event })), signal);
361
+ return events.length;
362
+ }
363
+ async type(text, signal) {
364
+ const events = textToKeyEvents(text);
365
+ await this.dispatch(events.map((event) => ({ kind: "key", event })), signal);
366
+ return events.length;
367
+ }
368
+ async paste(text, signal) {
369
+ await this.dispatch([{ kind: "paste", text }], signal);
370
+ }
371
+ /**
372
+ * Mouse-wheel scroll by `rows` (positive scrolls down) and `cols`, sent in
373
+ * one-row steps like a wheel. Wheel input is not positional: it reaches the
374
+ * focused `<ScrollBox>` first, else every wheel-enabled one.
375
+ */
376
+ async scroll(rows, cols = 0, signal) {
377
+ const steps = Math.max(Math.abs(Math.trunc(rows)), Math.abs(Math.trunc(cols)), 1);
378
+ const event = (dy, dx) => ({ type: "wheel", deltaX: dx, deltaY: dy, rows: dy, cols: dx, ctrl: false, meta: false, shift: false, alt: false });
379
+ const events = Array.from({ length: steps }, (_, i) => {
380
+ const dy = i < Math.abs(Math.trunc(rows)) ? Math.sign(rows) : 0;
381
+ const dx = i < Math.abs(Math.trunc(cols)) ? Math.sign(cols) : 0;
382
+ return { kind: "wheel", event: event(dy, dx) };
383
+ }).filter((e) => e.event.rows !== 0 || e.event.cols !== 0);
384
+ await this.dispatch(events, signal);
385
+ return events.length;
386
+ }
387
+ resize(cols, rows) {
388
+ const root = this.requireRoot();
389
+ this.resized = true;
390
+ this.cols = clampCols(cols);
391
+ this.rows = clampRows(rows);
392
+ this.touch();
393
+ root.renderer.resize(this.cols, this.rows);
394
+ return { cols: this.cols, rows: this.rows };
395
+ }
396
+ navigate(target) {
397
+ this.requireRoot();
398
+ this.touch();
399
+ if (target === "back")
400
+ this.history.back();
401
+ else if (target === "forward")
402
+ this.history.forward();
403
+ else if (target.replace)
404
+ this.history.replace(target.path);
405
+ else
406
+ this.history.push(target.path);
407
+ }
408
+ get href() {
409
+ return this.history.location.href;
410
+ }
411
+ routes() {
412
+ return {
413
+ pages: listPages(this.scan),
414
+ api: listApiRoutes(this.scan).map((r) => ({ pattern: r.pattern, file: toProjectPath(this.rootDir, r.file) })),
415
+ current: this.href,
416
+ };
417
+ }
418
+ /** Read the current frame. Errors logged since the previous read are counted in `newErrors`. */
419
+ frame(options = {}) {
420
+ const root = this.appRoot;
421
+ const loc = this.history.location;
422
+ let route = loc.pathname;
423
+ let title;
424
+ if (this.manifest) {
425
+ const match = resolveRoute(this.manifest, loc.pathname);
426
+ route = match.pattern;
427
+ title = resolveMetadata(match, searchParamsToRecord(loc.search)).title;
428
+ }
429
+ const newErrors = this.logs.errorsSince(this.errorCursor);
430
+ if (!options.peek)
431
+ this.errorCursor = this.logs.cursor;
432
+ const base = { href: loc.href, route, ...(title !== undefined ? { title } : {}), frame: this.frameSeq, newErrors, status: this.status };
433
+ if (!root)
434
+ return { ...base, text: "", cols: this.cols, rows: this.rows, idle: true, focus: null, cursor: null };
435
+ const buffer = root.renderer.buffer;
436
+ const focus = this.focusSummary();
437
+ return {
438
+ ...base,
439
+ text: frameText(buffer, options.region, options.trim),
440
+ ...(options.region ? { region: clampRegion(buffer, options.region) } : {}),
441
+ cols: buffer.width,
442
+ rows: buffer.height,
443
+ idle: this.isIdle(),
444
+ focus,
445
+ cursor: focus?.bounds ? findCursor(root, focus.bounds) : null,
446
+ ...(options.styles ? { styles: styledRows(buffer, options.region) } : {}),
447
+ };
448
+ }
449
+ contains(text) {
450
+ const root = this.appRoot;
451
+ return root ? frameContains(root.renderer.buffer, text) : false;
452
+ }
453
+ get buffer() {
454
+ return this.appRoot?.renderer.buffer ?? null;
455
+ }
456
+ get fiberProbe() {
457
+ return this.probe;
458
+ }
459
+ /** The mounted scene and grid size, for paint-order hit testing. */
460
+ get sceneGrid() {
461
+ const root = this.appRoot;
462
+ if (!root || root.unmounted)
463
+ return null;
464
+ return { root: root.rootNode, cols: root.renderer.buffer.width, rows: root.renderer.buffer.height };
465
+ }
466
+ inspect(options = {}) {
467
+ const root = this.requireRoot();
468
+ return inspect(this.probe, root.focus, options);
469
+ }
470
+ focusSummary() {
471
+ const root = this.appRoot;
472
+ if (!root)
473
+ return null;
474
+ const id = root.focus.getActiveId();
475
+ if (id === null)
476
+ return null;
477
+ const info = inspect(this.probe, root.focus, { hosts: false }).focusables.find((f) => f.id === id);
478
+ return { id, ...(info?.component ? { component: info.component } : {}), ...(info?.bounds ? { bounds: info.bounds } : {}) };
479
+ }
480
+ /**
481
+ * Call an `app/**\/route.ts` handler in-process. The body is read up to
482
+ * `maxBytes` (then cancelled); event streams are read for `streamMs` and
483
+ * cancelled, so an SSE route returns its first events instead of hanging.
484
+ */
485
+ async request(path, init = {}, signal) {
486
+ const url = path.startsWith("/") ? sameOriginUrl(path, this.origin) : null;
487
+ if (!url)
488
+ throw new SessionError(`fetch only calls the app's own route handlers; pass a path like "/api/health" (got ${JSON.stringify(path)}).`);
489
+ attachFetch(this);
490
+ const hit = matchApiRoute(toApiManifest(this.scan), url.pathname);
491
+ const method = (init.method ?? "GET").toUpperCase();
492
+ const response = await abortable(fetchScope.run(this, () => this.handleFetch(url.href, { method, headers: init.headers, body: method === "GET" || method === "HEAD" ? undefined : init.body, signal })), signal);
493
+ const maxBytes = init.maxBytes ?? 64 * 1024;
494
+ const headers = {};
495
+ response.headers.forEach((v, k) => {
496
+ headers[k] = v;
497
+ });
498
+ const type = response.headers.get("content-type") ?? "";
499
+ const stream = /event-stream/i.test(type);
500
+ const read = await readBody(response, maxBytes, stream ? (init.streamMs ?? 1000) : Number.POSITIVE_INFINITY, signal);
501
+ const textual = type === "" || /^text\/|json|xml|javascript|ndjson|x-www-form-urlencoded|event-stream/i.test(type);
502
+ const body = textual ? new TextDecoder().decode(read.bytes) : `<${read.bytes.length}${read.truncated ? "+" : ""} bytes of ${type}>`;
503
+ return {
504
+ status: response.status,
505
+ statusText: response.statusText,
506
+ headers,
507
+ body,
508
+ truncated: read.truncated,
509
+ ...(stream && read.open ? { streaming: true } : {}),
510
+ route: hit ? hit.pattern : null,
511
+ };
512
+ }
513
+ /**
514
+ * Re-import the app after file changes: unmount, purge every module under
515
+ * the project root from the module cache, re-scan `app/`, re-import and
516
+ * remount at the current location (history is kept).
517
+ */
518
+ async reload() {
519
+ if (this.disposed)
520
+ throw new SessionError("The session is closed.");
521
+ this.unmountApp();
522
+ const purged = purgeModuleCache(this.rootDir);
523
+ try {
524
+ await this.loadConfig();
525
+ }
526
+ catch (error) {
527
+ this.status = "failed";
528
+ this.loadError = error;
529
+ this.logs.push("error", "server", ["Failed to load the app:", error]);
530
+ throw new SessionError(`Reload failed: ${errorMessage(error)}\nFix the error and call \`reload\`.`, txCodeOf(error));
531
+ }
532
+ await this.mount();
533
+ if (this.status === "failed") {
534
+ throw new SessionError(`Reload failed: ${errorMessage(this.loadError)}`, txCodeOf(this.loadError));
535
+ }
536
+ this.touch();
537
+ return { purged };
538
+ }
539
+ async dispose() {
540
+ if (this.disposed)
541
+ return;
542
+ this.disposed = true;
543
+ this.unmountApp();
544
+ detachFetch(this);
545
+ this.history.dispose?.();
546
+ }
547
+ }
548
+ function clampCols(n) {
549
+ return Math.max(10, Math.min(1000, Math.floor(Number.isFinite(n) ? n : 100)));
550
+ }
551
+ function clampRows(n) {
552
+ return Math.max(3, Math.min(500, Math.floor(Number.isFinite(n) ? n : 32)));
553
+ }
554
+ function toProjectPath(root, file) {
555
+ return relative(root, file).split(sep).join("/");
556
+ }
557
+ /** A single inverse-video cell inside the focused component is the block cursor of an `<Input>` / `<Textarea>`. */
558
+ function findCursor(root, b) {
559
+ const buf = root.renderer.buffer;
560
+ let hit = null;
561
+ for (let y = b.y; y < b.y + b.height; y++) {
562
+ for (let x = b.x; x < b.x + b.width; x++) {
563
+ const c = buf.getCell(x, y);
564
+ // The second cell of a wide grapheme under the cursor is the same cursor.
565
+ if (!c || (c.attrs & ATTR_INVERSE) === 0 || (c.attrs & ATTR_WIDE_CONT) !== 0)
566
+ continue;
567
+ if (hit)
568
+ return null;
569
+ hit = { x, y };
570
+ }
571
+ }
572
+ return hit;
573
+ }
574
+ function realpath(p) {
575
+ try {
576
+ return realpathSync(p);
577
+ }
578
+ catch {
579
+ return p;
580
+ }
581
+ }
582
+ /**
583
+ * Delete every cached module under `root` (except dependencies and Text.js
584
+ * itself) so the next `import()` re-evaluates it. Bun's `require.cache`
585
+ * covers ES modules too.
586
+ */
587
+ export function purgeModuleCache(root) {
588
+ const cache = (typeof require !== "undefined" ? require.cache : undefined);
589
+ if (!cache)
590
+ return 0;
591
+ const roots = [resolve(root), realpath(resolve(root))].map((r) => (r.endsWith(sep) ? r : r + sep));
592
+ const pkg = [PACKAGE_DIR, realpath(PACKAGE_DIR)].map((r) => r + sep);
593
+ let n = 0;
594
+ for (const key of Object.keys(cache)) {
595
+ const file = key.startsWith("file://") ? fileURLToPath(key) : key;
596
+ if (!roots.some((r) => file.startsWith(r)))
597
+ continue;
598
+ if (file.includes(`${sep}node_modules${sep}`) || pkg.some((p) => file.startsWith(p)))
599
+ continue;
600
+ delete cache[key];
601
+ n++;
602
+ }
603
+ return n;
604
+ }
605
+ /** Which session an app `fetch()` belongs to, when async context propagates (input dispatch, renders, `request`). */
606
+ const fetchScope = new AsyncLocalStorage();
607
+ /** Sessions routing `globalThis.fetch`, oldest first (the newest handles calls without a scope). */
608
+ const fetchSessions = [];
609
+ let nativeFetch = null;
610
+ let routedFetch = null;
611
+ /** The `fetch` that existed before any session installed its router. */
612
+ function networkFetch() {
613
+ return nativeFetch ?? globalThis.fetch;
614
+ }
615
+ /**
616
+ * Route `globalThis.fetch` through the session registry. One router serves
617
+ * every in-process session, and the original `fetch` comes back only when
618
+ * the last session detaches, whatever the dispose order.
619
+ */
620
+ function attachFetch(session) {
621
+ const g = globalThis;
622
+ if (!routedFetch || g.fetch !== routedFetch) {
623
+ nativeFetch = g.fetch;
624
+ const router = (input, init) => {
625
+ const owner = fetchScope.getStore() ?? fetchSessions[fetchSessions.length - 1];
626
+ return owner ? owner.handleFetch(input, init) : nativeFetch(input, init);
627
+ };
628
+ routedFetch = Object.assign(router, { preconnect: () => undefined });
629
+ g.fetch = routedFetch;
630
+ }
631
+ if (!fetchSessions.includes(session))
632
+ fetchSessions.push(session);
633
+ }
634
+ function detachFetch(session) {
635
+ const i = fetchSessions.indexOf(session);
636
+ if (i >= 0)
637
+ fetchSessions.splice(i, 1);
638
+ if (fetchSessions.length > 0)
639
+ return;
640
+ const g = globalThis;
641
+ if (routedFetch && g.fetch === routedFetch && nativeFetch)
642
+ g.fetch = nativeFetch;
643
+ routedFetch = null;
644
+ nativeFetch = null;
645
+ }
646
+ /**
647
+ * `raw` as a URL on `origin`, or `null` when it targets anything else.
648
+ * Paths like `//evil.com` and `/\evil.com` (a backslash is a slash in
649
+ * special-scheme URLs) are protocol-relative, not same-origin.
650
+ */
651
+ export function sameOriginUrl(raw, origin) {
652
+ if (raw.startsWith("/") && /^\/[\\/]/.test(raw))
653
+ return null;
654
+ let url;
655
+ try {
656
+ url = new URL(raw, origin);
657
+ }
658
+ catch {
659
+ return null;
660
+ }
661
+ return url.origin === origin ? url : null;
662
+ }
663
+ function abortReason(signal) {
664
+ return signal.reason instanceof Error ? signal.reason : new SessionError("The operation was cancelled.");
665
+ }
666
+ function throwIfAborted(signal) {
667
+ if (signal?.aborted)
668
+ throw abortReason(signal);
669
+ }
670
+ /** `promise`, rejecting as soon as `signal` aborts. */
671
+ function abortable(promise, signal) {
672
+ if (!signal)
673
+ return promise;
674
+ if (signal.aborted)
675
+ return Promise.reject(abortReason(signal));
676
+ return new Promise((resolve, reject) => {
677
+ const onAbort = () => reject(abortReason(signal));
678
+ signal.addEventListener("abort", onAbort, { once: true });
679
+ promise.then((v) => {
680
+ signal.removeEventListener("abort", onAbort);
681
+ resolve(v);
682
+ }, (e) => {
683
+ signal.removeEventListener("abort", onAbort);
684
+ reject(e);
685
+ });
686
+ });
687
+ }
688
+ /**
689
+ * Read at most `maxBytes` of a response body, for at most `windowMs`, then
690
+ * cancel it. `open` is set when the body was still streaming.
691
+ */
692
+ export async function readBody(response, maxBytes, windowMs, signal) {
693
+ const body = response.body;
694
+ if (!body)
695
+ return { bytes: new Uint8Array(0), truncated: false, open: false };
696
+ const reader = body.getReader();
697
+ const chunks = [];
698
+ let total = 0;
699
+ let truncated = false;
700
+ let open = true;
701
+ const deadline = Date.now() + windowMs;
702
+ const TIMEOUT = Symbol("timeout");
703
+ try {
704
+ while (total <= maxBytes) {
705
+ const remaining = deadline - Date.now();
706
+ if (remaining <= 0) {
707
+ truncated = true;
708
+ break;
709
+ }
710
+ let timer;
711
+ const tick = Number.isFinite(remaining) ? new Promise((r) => (timer = setTimeout(() => r(TIMEOUT), remaining))) : null;
712
+ const next = await abortable(tick ? Promise.race([reader.read(), tick]) : reader.read(), signal).finally(() => clearTimeout(timer));
713
+ if (next === TIMEOUT) {
714
+ truncated = true;
715
+ break;
716
+ }
717
+ if (next.done) {
718
+ open = false;
719
+ break;
720
+ }
721
+ chunks.push(next.value);
722
+ total += next.value.length;
723
+ }
724
+ if (total > maxBytes)
725
+ truncated = true;
726
+ }
727
+ finally {
728
+ if (open)
729
+ await reader.cancel().catch(() => undefined);
730
+ }
731
+ const all = new Uint8Array(Math.min(total, maxBytes));
732
+ let offset = 0;
733
+ for (const c of chunks) {
734
+ if (offset >= all.length)
735
+ break;
736
+ const part = c.subarray(0, all.length - offset);
737
+ all.set(part, offset);
738
+ offset += part.length;
739
+ }
740
+ return { bytes: all, truncated, open: open && truncated };
741
+ }
742
+ //# sourceMappingURL=session.js.map