toiljs 0.0.115 → 0.0.117

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 (326) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +16 -7
  3. package/TYPESCRIPT_LAW.md +1946 -2073
  4. package/build/backend/.tsbuildinfo +1 -1
  5. package/build/cli/.tsbuildinfo +1 -1
  6. package/build/cli/index.js +2375 -258
  7. package/build/client/.tsbuildinfo +1 -1
  8. package/build/client/dev/devtools.js +1 -1
  9. package/build/client/dev/error-overlay.js +1 -1
  10. package/build/client/routing/mount.js +1 -1
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/auth-emails.js +7 -3
  13. package/build/compiler/emails.js +1 -1
  14. package/build/compiler/index.d.ts +1 -0
  15. package/build/compiler/index.js +25 -4
  16. package/build/compiler/pages.js +2 -3
  17. package/build/compiler/prerender.d.ts +3 -8
  18. package/build/compiler/prerender.js +88 -127
  19. package/build/compiler/ssg.js +3 -4
  20. package/build/compiler/template-build.js +3 -4
  21. package/build/compiler/toil-docs.generated.js +6 -5
  22. package/build/compiler/vite.js +3 -6
  23. package/build/devserver/.tsbuildinfo +1 -1
  24. package/build/devserver/db/database.js +7 -3
  25. package/build/devserver/db/types.js +1 -1
  26. package/build/devserver/production.js +4 -3
  27. package/build/devserver/runtime/crypto.d.ts +1 -1
  28. package/build/io/.tsbuildinfo +1 -1
  29. package/build/logger/.tsbuildinfo +1 -1
  30. package/build/shared/.tsbuildinfo +1 -1
  31. package/examples/basic/client/routes/analytics.tsx +7 -5
  32. package/examples/basic/server/tsconfig.json +1 -3
  33. package/package.json +45 -44
  34. package/presets/no-uint8array-tostring.js +120 -153
  35. package/presets/oxlint.d.ts +5 -0
  36. package/presets/oxlint.js +224 -0
  37. package/server/runtime/tsconfig.json +1 -3
  38. package/tsconfig.json +2 -11
  39. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  40. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
  41. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  42. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  43. package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
  44. package/.github/changelog-config.json +0 -45
  45. package/.github/dependabot.yml +0 -27
  46. package/.github/workflows/ci.yml +0 -222
  47. package/.vscode/settings.json +0 -10
  48. package/docs/README.md +0 -107
  49. package/docs/auth/README.md +0 -104
  50. package/docs/auth/configuration.md +0 -105
  51. package/docs/auth/emails.md +0 -92
  52. package/docs/auth/extending.md +0 -202
  53. package/docs/auth/how-it-works.md +0 -138
  54. package/docs/auth/usage.md +0 -188
  55. package/docs/backend/README.md +0 -143
  56. package/docs/backend/data.md +0 -351
  57. package/docs/backend/rest.md +0 -402
  58. package/docs/backend/rpc.md +0 -226
  59. package/docs/background/README.md +0 -114
  60. package/docs/background/daemons.md +0 -276
  61. package/docs/background/derive.md +0 -179
  62. package/docs/cli/README.md +0 -379
  63. package/docs/concepts/ai-guide.md +0 -132
  64. package/docs/concepts/config.md +0 -416
  65. package/docs/concepts/decorators.md +0 -127
  66. package/docs/concepts/security.md +0 -108
  67. package/docs/concepts/tiers.md +0 -166
  68. package/docs/concepts/types.md +0 -216
  69. package/docs/database/README.md +0 -143
  70. package/docs/database/capacity.md +0 -350
  71. package/docs/database/counters.md +0 -174
  72. package/docs/database/documents.md +0 -277
  73. package/docs/database/events.md +0 -307
  74. package/docs/database/membership.md +0 -246
  75. package/docs/database/setup.md +0 -155
  76. package/docs/database/unique.md +0 -216
  77. package/docs/database/views.md +0 -246
  78. package/docs/frontend/README.md +0 -104
  79. package/docs/frontend/components.md +0 -286
  80. package/docs/frontend/data-fetching.md +0 -260
  81. package/docs/frontend/images.md +0 -148
  82. package/docs/frontend/metadata.md +0 -344
  83. package/docs/frontend/navigation.md +0 -296
  84. package/docs/frontend/rendering.md +0 -236
  85. package/docs/frontend/routing.md +0 -346
  86. package/docs/frontend/scripts.md +0 -118
  87. package/docs/frontend/search.md +0 -191
  88. package/docs/frontend/styling.md +0 -147
  89. package/docs/frontend/toil-global.md +0 -216
  90. package/docs/getting-started/README.md +0 -81
  91. package/docs/getting-started/create-project.md +0 -131
  92. package/docs/getting-started/deploy.md +0 -101
  93. package/docs/getting-started/first-app.md +0 -215
  94. package/docs/getting-started/installation.md +0 -124
  95. package/docs/getting-started/migrating.md +0 -125
  96. package/docs/getting-started/project-structure.md +0 -163
  97. package/docs/introduction/README.md +0 -66
  98. package/docs/introduction/design-principles.md +0 -81
  99. package/docs/introduction/distributed.md +0 -91
  100. package/docs/introduction/how-it-works.md +0 -121
  101. package/docs/introduction/hyperscale.md +0 -70
  102. package/docs/introduction/modern-stack.md +0 -85
  103. package/docs/introduction/vs-other-frameworks.md +0 -64
  104. package/docs/introduction/why-toil.md +0 -99
  105. package/docs/llms.txt +0 -95
  106. package/docs/realtime/README.md +0 -102
  107. package/docs/realtime/channels.md +0 -211
  108. package/docs/realtime/streams.md +0 -369
  109. package/docs/services/README.md +0 -60
  110. package/docs/services/analytics.md +0 -279
  111. package/docs/services/caching.md +0 -175
  112. package/docs/services/cookies.md +0 -235
  113. package/docs/services/crypto.md +0 -209
  114. package/docs/services/email.md +0 -289
  115. package/docs/services/environment.md +0 -141
  116. package/docs/services/ratelimit.md +0 -174
  117. package/docs/services/time.md +0 -85
  118. package/eslint.config.js +0 -57
  119. package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
  120. package/presets/eslint.js +0 -88
  121. package/src/backend/index.ts +0 -204
  122. package/src/cli/configure.ts +0 -363
  123. package/src/cli/create.ts +0 -924
  124. package/src/cli/db.ts +0 -158
  125. package/src/cli/diagnostics.ts +0 -665
  126. package/src/cli/doctor.ts +0 -982
  127. package/src/cli/features.ts +0 -170
  128. package/src/cli/index.ts +0 -326
  129. package/src/cli/notify.ts +0 -120
  130. package/src/cli/proc.ts +0 -56
  131. package/src/cli/ui.ts +0 -169
  132. package/src/cli/update.ts +0 -248
  133. package/src/cli/updates.ts +0 -92
  134. package/src/cli/validate.ts +0 -31
  135. package/src/cli/version-check.ts +0 -125
  136. package/src/client/auth.ts +0 -869
  137. package/src/client/channel/channel.ts +0 -146
  138. package/src/client/components/Form.tsx +0 -65
  139. package/src/client/components/Image.tsx +0 -160
  140. package/src/client/components/Script.tsx +0 -113
  141. package/src/client/components/Slot.tsx +0 -21
  142. package/src/client/dev/devtools.tsx +0 -1089
  143. package/src/client/dev/error-overlay.tsx +0 -225
  144. package/src/client/errors.ts +0 -11
  145. package/src/client/head/head.ts +0 -182
  146. package/src/client/head/metadata.ts +0 -110
  147. package/src/client/index.ts +0 -130
  148. package/src/client/navigation/Link.tsx +0 -108
  149. package/src/client/navigation/NavLink.tsx +0 -86
  150. package/src/client/navigation/navigation.ts +0 -235
  151. package/src/client/navigation/prefetch.ts +0 -169
  152. package/src/client/navigation/scroll.ts +0 -53
  153. package/src/client/routing/Router.tsx +0 -216
  154. package/src/client/routing/action.ts +0 -122
  155. package/src/client/routing/error-boundary.tsx +0 -43
  156. package/src/client/routing/hooks.ts +0 -140
  157. package/src/client/routing/lazy.ts +0 -77
  158. package/src/client/routing/loader.ts +0 -381
  159. package/src/client/routing/match.ts +0 -47
  160. package/src/client/routing/mount.tsx +0 -90
  161. package/src/client/routing/params-context.ts +0 -10
  162. package/src/client/routing/slot-context.ts +0 -7
  163. package/src/client/rpc.ts +0 -97
  164. package/src/client/search/search.ts +0 -189
  165. package/src/client/search/use-page-search.ts +0 -73
  166. package/src/client/ssr/markers.tsx +0 -172
  167. package/src/client/stream/client.ts +0 -237
  168. package/src/client/types.ts +0 -73
  169. package/src/compiler/auth-emails.ts +0 -312
  170. package/src/compiler/config.ts +0 -369
  171. package/src/compiler/docs.ts +0 -127
  172. package/src/compiler/email-preview.ts +0 -314
  173. package/src/compiler/emails.ts +0 -404
  174. package/src/compiler/fonts.ts +0 -89
  175. package/src/compiler/generate.ts +0 -471
  176. package/src/compiler/image-blur.ts +0 -42
  177. package/src/compiler/image-report.ts +0 -88
  178. package/src/compiler/index.ts +0 -1362
  179. package/src/compiler/pages.ts +0 -49
  180. package/src/compiler/plugin.ts +0 -391
  181. package/src/compiler/prerender.ts +0 -263
  182. package/src/compiler/routes.ts +0 -170
  183. package/src/compiler/seo.ts +0 -415
  184. package/src/compiler/sri.ts +0 -144
  185. package/src/compiler/ssg.ts +0 -177
  186. package/src/compiler/ssr-codegen.ts +0 -85
  187. package/src/compiler/template-build.ts +0 -676
  188. package/src/compiler/template.ts +0 -262
  189. package/src/compiler/toil-docs.generated.ts +0 -77
  190. package/src/compiler/vite.ts +0 -245
  191. package/src/devserver/analytics/index.ts +0 -317
  192. package/src/devserver/config/dotenv.ts +0 -102
  193. package/src/devserver/config/env.ts +0 -30
  194. package/src/devserver/config/ratelimit.ts +0 -123
  195. package/src/devserver/daemon/catalog.ts +0 -120
  196. package/src/devserver/daemon/cron.ts +0 -87
  197. package/src/devserver/daemon/host.ts +0 -134
  198. package/src/devserver/daemon/index.ts +0 -369
  199. package/src/devserver/daemon/runtime.ts +0 -48
  200. package/src/devserver/db/catalog.ts +0 -109
  201. package/src/devserver/db/database.ts +0 -1834
  202. package/src/devserver/db/derives.ts +0 -121
  203. package/src/devserver/db/index.ts +0 -20
  204. package/src/devserver/db/routeKinds.ts +0 -191
  205. package/src/devserver/db/types.ts +0 -168
  206. package/src/devserver/email/caps.ts +0 -0
  207. package/src/devserver/email/config.ts +0 -141
  208. package/src/devserver/email/console.ts +0 -348
  209. package/src/devserver/email/index.ts +0 -155
  210. package/src/devserver/email/providers.ts +0 -131
  211. package/src/devserver/email/status.ts +0 -23
  212. package/src/devserver/email/validate.ts +0 -37
  213. package/src/devserver/email/wire.ts +0 -55
  214. package/src/devserver/http/cache.ts +0 -0
  215. package/src/devserver/http/envelope.ts +0 -157
  216. package/src/devserver/http/proxy.ts +0 -172
  217. package/src/devserver/http/runtime.ts +0 -387
  218. package/src/devserver/index.ts +0 -43
  219. package/src/devserver/production-ipc.ts +0 -63
  220. package/src/devserver/production-worker.ts +0 -83
  221. package/src/devserver/production.ts +0 -718
  222. package/src/devserver/runtime/crypto.ts +0 -587
  223. package/src/devserver/runtime/host.ts +0 -427
  224. package/src/devserver/runtime/module.ts +0 -442
  225. package/src/devserver/server.ts +0 -251
  226. package/src/devserver/ssr.ts +0 -208
  227. package/src/devserver/stream/catalog.ts +0 -106
  228. package/src/devserver/stream/host.ts +0 -42
  229. package/src/devserver/stream/index.ts +0 -301
  230. package/src/devserver/stream/manager.ts +0 -167
  231. package/src/devserver/stream/router.ts +0 -101
  232. package/src/devserver/stream/wire.ts +0 -58
  233. package/src/devserver/stream/ws.ts +0 -78
  234. package/src/devserver/wasm/sections.ts +0 -59
  235. package/src/devserver/wasm/surface.ts +0 -115
  236. package/src/io/FastMap.ts +0 -151
  237. package/src/io/FastSet.ts +0 -113
  238. package/src/io/codec.ts +0 -362
  239. package/src/io/index.ts +0 -10
  240. package/src/io/lengths.ts +0 -14
  241. package/src/io/types.ts +0 -19
  242. package/src/logger/index.ts +0 -22
  243. package/src/shared/index.ts +0 -46
  244. package/test/analytics-dev.test.ts +0 -113
  245. package/test/assembly/aspect-shim.ts +0 -33
  246. package/test/assembly/cookie.spec.ts +0 -303
  247. package/test/assembly/example.spec.ts +0 -22
  248. package/test/assembly/ssr.spec.ts +0 -95
  249. package/test/assembly.test.ts +0 -59
  250. package/test/auth-emails.test.ts +0 -118
  251. package/test/built-ssr.test.ts +0 -148
  252. package/test/channel.test.ts +0 -21
  253. package/test/config-host-port.test.ts +0 -59
  254. package/test/configure.test.ts +0 -94
  255. package/test/daemon-build.test.ts +0 -278
  256. package/test/daemon-catalog.test.ts +0 -349
  257. package/test/daemon-emulation.test.ts +0 -256
  258. package/test/db.test.ts +0 -0
  259. package/test/devserver-database.test.ts +0 -1102
  260. package/test/devserver-email.test.ts +0 -306
  261. package/test/devserver-pqauth.test.ts +0 -93
  262. package/test/devserver-secrets.test.ts +0 -63
  263. package/test/devserver.test.ts +0 -259
  264. package/test/doctor.test.ts +0 -298
  265. package/test/dom/Image.test.tsx +0 -144
  266. package/test/dom/Link.test.tsx +0 -67
  267. package/test/dom/NavLink.test.tsx +0 -37
  268. package/test/dom/Script.test.tsx +0 -48
  269. package/test/dom/action.test.tsx +0 -146
  270. package/test/dom/error-overlay.test.tsx +0 -44
  271. package/test/dom/loader.test.tsx +0 -121
  272. package/test/dom/navigation.test.ts +0 -59
  273. package/test/dom/revalidate.test.tsx +0 -38
  274. package/test/dom/route-head.test.tsx +0 -56
  275. package/test/dom/router-loading.test.tsx +0 -44
  276. package/test/dom/scroll.test.ts +0 -56
  277. package/test/dom/slot.test.tsx +0 -131
  278. package/test/dom/use-metadata.test.tsx +0 -58
  279. package/test/dom/view-transitions.test.tsx +0 -53
  280. package/test/dynamic-routing.test.ts +0 -132
  281. package/test/email-console.test.ts +0 -86
  282. package/test/email-preview.test.ts +0 -73
  283. package/test/emails.test.ts +0 -58
  284. package/test/errors.test.ts +0 -21
  285. package/test/example-guestbook.test.ts +0 -123
  286. package/test/features.test.ts +0 -149
  287. package/test/fixtures/bignum-wire/spec.ts +0 -25
  288. package/test/fixtures/daemon-app.ts +0 -65
  289. package/test/fixtures/stream-echo.ts +0 -26
  290. package/test/fixtures/stream-gate.ts +0 -28
  291. package/test/fixtures/stream-trap.ts +0 -18
  292. package/test/fixtures/stream-typed.ts +0 -41
  293. package/test/fonts.test.ts +0 -28
  294. package/test/global-setup.ts +0 -17
  295. package/test/head.test.ts +0 -42
  296. package/test/image-blur.test.ts +0 -50
  297. package/test/io.test.ts +0 -117
  298. package/test/metadata.test.ts +0 -40
  299. package/test/navlink.test.ts +0 -28
  300. package/test/pages.test.ts +0 -105
  301. package/test/placeholder.test.ts +0 -9
  302. package/test/pqauth-e2e.test.ts +0 -216
  303. package/test/pqauth-email.test.ts +0 -801
  304. package/test/prerender.test.ts +0 -116
  305. package/test/prettier-plugin.test.ts +0 -46
  306. package/test/routes.test.ts +0 -76
  307. package/test/rpc-bignum-wire.test.ts +0 -164
  308. package/test/rpc-dispatch.test.ts +0 -132
  309. package/test/rpc-kinds.test.ts +0 -18
  310. package/test/rpc.test.ts +0 -66
  311. package/test/search.test.ts +0 -114
  312. package/test/seo.test.ts +0 -175
  313. package/test/slot-layouts.test.ts +0 -69
  314. package/test/sri.test.ts +0 -129
  315. package/test/ssg.test.ts +0 -36
  316. package/test/ssr-hydration.test.tsx +0 -122
  317. package/test/ssr-render.test.ts +0 -197
  318. package/test/ssr-template.test.tsx +0 -482
  319. package/test/stream-emulation.test.ts +0 -479
  320. package/test/ui.test.ts +0 -40
  321. package/test/update.test.ts +0 -58
  322. package/test/validate.test.ts +0 -42
  323. package/test/version-check.test.ts +0 -130
  324. package/tests/data-parity/generated-parity.ts +0 -99
  325. package/tests/data-parity/parity.ts +0 -80
  326. package/tests/data-parity/spec.ts +0 -46
@@ -1,208 +0,0 @@
1
- /**
2
- * Dev-server edge SSR: splice the guest `render` values into a route's
3
- * template-with-holes and serve real server-rendered HTML, mirroring the
4
- * production edge (`toil-backend/src/host/template/assemble.rs`).
5
- *
6
- * The templates are extracted once at dev startup against the LIVE (Vite-
7
- * transformed) dev shell (see `compiler/template-build.ts extractDevSsrTemplates`)
8
- * so the served markup boots the dev client and hydrates in place. At request
9
- * time the dev server runs the real `render` export (`WasmServerModule.
10
- * dispatchRender`), this module decodes the values envelope and splices each
11
- * value at its manifest-fixed offset. The hash-coherence guard the prod edge
12
- * enforces is skipped in dev: the guest and the template are built together
13
- * here, so there is no deploy skew to catch — only fail-safe envelopes (status
14
- * >= 500 or no slots) fall back to client rendering.
15
- */
16
-
17
- /** One SSR route's spliceable template + its slot insertion points. */
18
- export interface DevSsrTemplate {
19
- pattern: string;
20
- name: string;
21
- tmpl: Uint8Array;
22
- entries: { id: number; offset: number }[];
23
- /** Optional deployed template hash. Present for built/self-host SSR, omitted in dev. */
24
- hash?: Uint8Array;
25
- }
26
-
27
- /** A matchable SSR route. */
28
- export interface SsrRoute {
29
- /** Matches a request pathname (no query) to this route's template. */
30
- test: (pathname: string) => boolean;
31
- tmpl: Uint8Array;
32
- entries: { id: number; offset: number }[];
33
- /** Optional deployed template hash. When present, guest values must match it. */
34
- hash?: Uint8Array;
35
- }
36
-
37
- /** The pathname of a request URL (strip the query string). */
38
- export function pathnameOf(url: string): string {
39
- const q = url.indexOf('?');
40
- return q < 0 ? url : url.slice(0, q);
41
- }
42
-
43
- /** Compile a route pattern (`/hello`, `/u/:name`, `/blog/[id]`, `/files/[...path]`,
44
- * `/*`) to a pathname matcher. Dynamic segments match one path segment; catch-all
45
- * (`[...x]` / `*`) matches the rest. Trailing slashes are ignored. */
46
- function patternToTest(pattern: string): (pathname: string) => boolean {
47
- const norm = (p: string): string => (p.length > 1 && p.endsWith('/') ? p.slice(0, -1) : p);
48
- let re = '';
49
- // Tokenise into literal runs and dynamic/catch-all holes.
50
- let i = 0;
51
- while (i < pattern.length) {
52
- const ch = pattern[i];
53
- if (ch === ':') {
54
- // `:name` — one segment.
55
- i++;
56
- while (i < pattern.length && /[A-Za-z0-9_]/.test(pattern[i])) i++;
57
- re += '[^/]+';
58
- } else if (ch === '*') {
59
- re += '.*';
60
- i++;
61
- } else if (ch === '[') {
62
- const end = pattern.indexOf(']', i);
63
- const inner = end < 0 ? '' : pattern.slice(i + 1, end);
64
- re += inner.startsWith('...') ? '.*' : '[^/]+';
65
- i = end < 0 ? pattern.length : end + 1;
66
- } else {
67
- // Literal char, regex-escaped.
68
- re += ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
69
- i++;
70
- }
71
- }
72
- const compiled = new RegExp(`^${re}$`);
73
- return (pathname: string): boolean => compiled.test(norm(pathname));
74
- }
75
-
76
- /** Build matchable SSR routes from the extracted dev templates. */
77
- export function buildSsrRoutes(templates: readonly DevSsrTemplate[]): SsrRoute[] {
78
- return templates.map((t) => ({
79
- test: patternToTest(t.pattern),
80
- tmpl: t.tmpl,
81
- entries: t.entries,
82
- hash: t.hash,
83
- }));
84
- }
85
-
86
- /** A decoded guest values envelope. */
87
- interface DecodedValues {
88
- status: number;
89
- hash: Uint8Array;
90
- headers: [string, string][];
91
- /** Slot value bytes keyed by numeric slot id. */
92
- values: Map<number, Uint8Array>;
93
- }
94
-
95
- /** Decode the guest values envelope (mirrors the prod host `decode_values`). All
96
- * fields little-endian, no padding. Returns null on a malformed/short buffer. */
97
- function decodeValues(buf: Uint8Array): DecodedValues | null {
98
- const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
99
- let o = 0;
100
- const need = (n: number): boolean => o + n <= buf.byteLength;
101
- try {
102
- if (!need(2 + 32 + 2)) return null;
103
- const status = dv.getUint16(o, true);
104
- o += 2;
105
- const hash = buf.subarray(o, o + 32);
106
- o += 32;
107
- const nHeaders = dv.getUint16(o, true);
108
- o += 2;
109
- const headers: [string, string][] = [];
110
- const dec = new TextDecoder();
111
- for (let i = 0; i < nHeaders; i++) {
112
- if (!need(4)) return null;
113
- const nameLen = dv.getUint16(o, true);
114
- const valLen = dv.getUint16(o + 2, true);
115
- o += 4;
116
- if (!need(nameLen + valLen)) return null;
117
- const name = dec.decode(buf.subarray(o, o + nameLen));
118
- o += nameLen;
119
- const val = dec.decode(buf.subarray(o, o + valLen));
120
- o += valLen;
121
- headers.push([name, val]);
122
- }
123
- if (!need(2)) return null;
124
- const nSlots = dv.getUint16(o, true);
125
- o += 2;
126
- const values = new Map<number, Uint8Array>();
127
- for (let i = 0; i < nSlots; i++) {
128
- if (!need(2 + 1 + 4)) return null;
129
- const id = dv.getUint16(o, true);
130
- o += 2;
131
- o += 1; // kind (the splice is kind-agnostic; the guest pre-escaped/stamped)
132
- const len = dv.getUint32(o, true);
133
- o += 4;
134
- if (!need(len)) return null;
135
- values.set(id, buf.subarray(o, o + len));
136
- o += len;
137
- }
138
- return { status, hash, headers, values };
139
- } catch {
140
- return null;
141
- }
142
- }
143
-
144
- function sameBytes(a: Uint8Array, b: Uint8Array): boolean {
145
- if (a.byteLength !== b.byteLength) return false;
146
- for (let i = 0; i < a.byteLength; i++) {
147
- if (a[i] !== b[i]) return false;
148
- }
149
- return true;
150
- }
151
-
152
- /** Splice ascending-offset inserts into the template (mirrors the host `assemble`). */
153
- function splice(tmpl: Uint8Array, inserts: { offset: number; value: Uint8Array }[]): Uint8Array {
154
- const parts: Uint8Array[] = [];
155
- let prev = 0;
156
- for (const ins of inserts) {
157
- if (ins.offset > prev) parts.push(tmpl.subarray(prev, ins.offset));
158
- if (ins.value.length > 0) parts.push(ins.value);
159
- prev = ins.offset;
160
- }
161
- if (tmpl.length > prev) parts.push(tmpl.subarray(prev));
162
- return Buffer.concat(parts.map((p) => Buffer.from(p.buffer, p.byteOffset, p.byteLength)));
163
- }
164
-
165
- /** A spliced SSR response. */
166
- export interface SsrResult {
167
- status: number;
168
- headers: [string, string][];
169
- html: Uint8Array;
170
- }
171
-
172
- /** The internal header a guest's `SlotValues.setTitle` rides in: the host splices its value into the
173
- * document `<title>` and strips it, so a per-request title never reaches the client as a real header. */
174
- const SSR_TITLE_HEADER = 'x-toil-ssr-title';
175
-
176
- /** Replace the document `<title>` content (the guest set a per-request title; the value is already
177
- * React-escaped). Mirrors the host `assemble` so dev and prod agree. */
178
- function replaceTitle(html: Uint8Array, title: string): Uint8Array {
179
- const out = new TextDecoder()
180
- .decode(html)
181
- .replace(/<title>[\s\S]*?<\/title>/i, `<title>${title}</title>`);
182
- return new TextEncoder().encode(out);
183
- }
184
-
185
- /**
186
- * Decode the guest envelope and splice it into `route`'s template. Returns null
187
- * to fall back to client rendering: a fail-safe envelope (status >= 500, e.g. no
188
- * renderer matched), no slots, or a decode error.
189
- */
190
- export function assembleSsr(route: SsrRoute, envelope: Uint8Array): SsrResult | null {
191
- const decoded = decodeValues(envelope);
192
- if (decoded === null) return null;
193
- if (decoded.status >= 500 || decoded.values.size === 0) return null;
194
- if (route.hash !== undefined && !sameBytes(decoded.hash, route.hash)) return null;
195
- const inserts = route.entries
196
- .map((e) => ({ offset: e.offset, value: decoded.values.get(e.id) ?? new Uint8Array(0) }))
197
- .sort((a, b) => a.offset - b.offset);
198
- let html = splice(route.tmpl, inserts);
199
- // A guest-set per-request <title> (SlotValues.setTitle) rides in an internal header: splice it into
200
- // the <title> and strip the header so it never reaches the client.
201
- let headers = decoded.headers;
202
- const ti = headers.findIndex(([k]) => k.toLowerCase() === SSR_TITLE_HEADER);
203
- if (ti >= 0) {
204
- html = replaceTitle(html, headers[ti][1]);
205
- headers = headers.filter((_, i) => i !== ti);
206
- }
207
- return { status: decoded.status, headers, html };
208
- }
@@ -1,106 +0,0 @@
1
- /**
2
- * Parse a compiled HOT server wasm's `toilstream.catalog` custom section into the dev stream router's
3
- * route table (doc 08 section 3.1; emitted by the toilscript hot pass per RECONCILIATION Part 5 when
4
- * any `@stream` class exists). The dev router keys on `route` (the `@stream` class's fixed mount path)
5
- * to intercept matching WebSocket upgrades (section 4.1/4.2) and ignore the rest (Vite HMR).
6
- *
7
- * Byte layout (Part 5, all little-endian; mirrors the toilscript emitter + the edge decoder):
8
- *
9
- * u16 format_version (= 1)
10
- * u16 n_streams
11
- * per stream:
12
- * str name (the @stream class name)
13
- * str route (the mount path, e.g. "/echo")
14
- * u8 hook_presence_bitmask (bit0 connect, bit1 message, bit2 close, bit3 disconnect)
15
- * u8 declared_scope (0 = L2 regional, 1 = L3 continental)
16
- * u8 message_mode (0 = raw bytes default, 1 = @data-typed)
17
- * u32 max_frame_bytes
18
- * u32 ingress_ring_bytes
19
- * u32 message_value_data_id (0 when message_mode = 0)
20
- * u32 message_schema_version (0 when message_mode = 0)
21
- * u16 stream_index
22
- *
23
- * Fails closed via `DataReader.ok`: a short read mid-record stops the loop and yields the cleanly
24
- * decoded prefix; an absent/unparseable section yields an empty map (the dev router serves no stream).
25
- */
26
-
27
- import { DataReader } from 'toiljs/io';
28
-
29
- import { customSection } from '../wasm/sections.js';
30
-
31
- /** One `@stream` class's catalog entry (doc 08 section 3.1). */
32
- export interface StreamDef {
33
- readonly name: string;
34
- readonly route: string;
35
- readonly hooks: {
36
- readonly connect: boolean;
37
- readonly message: boolean;
38
- readonly close: boolean;
39
- readonly disconnect: boolean;
40
- };
41
- readonly scope: 'regional' | 'continental';
42
- readonly messageMode: 'raw' | 'data';
43
- readonly maxFrameBytes: number;
44
- readonly ingressRingBytes: number;
45
- readonly messageValueDataId: number;
46
- readonly messageSchemaVersion: number;
47
- readonly streamIndex: number;
48
- }
49
-
50
- /** The dev router's route table, keyed by the `@stream` mount path. */
51
- export type StreamCatalog = Map<string, StreamDef>;
52
-
53
- /** Parse `toilstream.catalog` into a route table. Absent/unparseable -> empty (serve no stream). */
54
- export function parseStreamCatalog(wasm: Buffer): StreamCatalog {
55
- const out: StreamCatalog = new Map();
56
- let sec: Buffer | null;
57
- try {
58
- sec = customSection(wasm, 'toilstream.catalog');
59
- } catch {
60
- return out;
61
- }
62
- if (sec === null) return out;
63
- const r = new DataReader(sec);
64
- r.readU16(); // format_version
65
- const n = r.readU16();
66
- for (let i = 0; i < n && r.ok; i++) {
67
- const name = r.readString();
68
- const route = r.readString();
69
- const bits = r.readU8();
70
- const scope = r.readU8() === 1 ? 'continental' : 'regional';
71
- const messageMode = r.readU8() === 1 ? 'data' : 'raw';
72
- const maxFrameBytes = r.readU32();
73
- const ingressRingBytes = r.readU32();
74
- const messageValueDataId = r.readU32();
75
- const messageSchemaVersion = r.readU32();
76
- const streamIndex = r.readU16();
77
- if (!r.ok) break;
78
- out.set(route, {
79
- name,
80
- route,
81
- hooks: {
82
- connect: (bits & 1) !== 0,
83
- message: (bits & 2) !== 0,
84
- close: (bits & 4) !== 0,
85
- disconnect: (bits & 8) !== 0,
86
- },
87
- scope,
88
- messageMode,
89
- maxFrameBytes,
90
- ingressRingBytes,
91
- messageValueDataId,
92
- messageSchemaVersion,
93
- streamIndex,
94
- });
95
- }
96
- return out;
97
- }
98
-
99
- /** Exact-path route match (doc 08 section 4.2): strip the query string, then look the path up in the
100
- * catalog. Returns the matched `@stream` def, or `null` when the path is not a stream route (the dev
101
- * router then proxies the upgrade to Vite). */
102
- export function matchStreamRoute(catalog: StreamCatalog, path: string): StreamDef | null {
103
- const q = path.indexOf('?');
104
- const exact = q >= 0 ? path.slice(0, q) : path;
105
- return catalog.get(exact) ?? null;
106
- }
@@ -1,42 +0,0 @@
1
- /**
2
- * Host-import surface for the dev STREAM (hot, L2/L3) box, mirroring the production edge's stream box
3
- * (`toil-backend` `src/wasm/stream`). A `@stream` box is a HOT artifact: it imports the same request
4
- * `env.*` runtime surface (so the ToilScript runtime + `@data`/crypto/env/`Date.now` code runs
5
- * unchanged) and drives its lifecycle through the `stream_dispatch` export + the ingress/egress ring
6
- * bridge ({@link ../index.js}), NOT through `handle`.
7
- *
8
- * The `stream.*` namespace (`stream.send` / `@channel`) is DEFERRED: the raw `@message` ring bridge -
9
- * the only stream feature wired so far - needs NO host imports (the guest reads/writes the rings in
10
- * its own linear memory). A box that does not use `stream.*` instantiates against `env.*` alone.
11
- */
12
-
13
- import { buildDatabaseImports, type DbDevState, freshDbState } from '../db/index.js';
14
- import { buildCryptoImports, type CryptoState, freshCryptoState } from '../runtime/crypto.js';
15
- import { buildEnvImports, type MemoryRef } from '../runtime/host.js';
16
-
17
- /** Per-stream-box host scratch (DB + crypto), analogous to the daemon's `DaemonState`. Each resident
18
- * connection box carries its own, so two connections never share `@data` transaction scratch. */
19
- export interface StreamBoxState {
20
- crypto: CryptoState;
21
- db: DbDevState;
22
- }
23
-
24
- export function freshStreamBoxState(): StreamBoxState {
25
- return { crypto: freshCryptoState(), db: freshDbState() };
26
- }
27
-
28
- /**
29
- * The full `env` import object for a dev stream box: the request-surface `env.*` runtime (built by
30
- * {@link buildEnvImports}) plus the crypto and `@data` namespaces. The response/stream `env.*`
31
- * functions a box must not have (`set_status`/`respond_file`/...) are excluded by `buildEnvImports`
32
- * itself, exactly as for the daemon cold box.
33
- */
34
- export function buildStreamImports(ref: MemoryRef, state: StreamBoxState): WebAssembly.Imports {
35
- return {
36
- env: {
37
- ...buildEnvImports(ref, state),
38
- ...buildCryptoImports(ref, state.crypto),
39
- ...buildDatabaseImports(ref, state.db),
40
- },
41
- };
42
- }
@@ -1,301 +0,0 @@
1
- /**
2
- * Dev STREAM (L2/L3) emulation: load a `release-stream.wasm` hot artifact into a resident per-connection
3
- * box and drive its lifecycle (`@connect` / `@message` / `@close` / `@disconnect`) through the
4
- * `stream_dispatch` export + the ingress/egress RING BRIDGE - the dev-side port of the production
5
- * `StreamBox` (`toil-backend` `src/wasm/stream/runtime.rs`). The ring wire format (RingControl 32B +
6
- * RingFrame 12B + drained-reset + the packed-i64 reject bridge) AND the `@connect` info-block ABI are
7
- * replicated BYTE-FOR-BYTE so a `@stream` app behaves identically under `npm run dev` and at the edge.
8
- *
9
- * RESIDENT: one box per connection, NEVER reset between events (linear memory persists across
10
- * connect -> message -> close); a fresh `WebAssembly.Instance` per connection IS that residency.
11
- *
12
- * DEV LIMITATION vs the edge: Node's `WebAssembly` has NO gas-metering middleware, so a runaway guest
13
- * loop is NOT gas-killed in dev (it hangs) - only a genuine wasm TRAP (unreachable / OOB / abort)
14
- * surfaces, which the caller turns into a `STREAM_HOOK_TRAPPED` close. Packet gas, chunk reassembly,
15
- * `@channel`, and the `stream.*` host imports are deferred exactly as on the edge.
16
- */
17
-
18
- import { parseSurface } from '../wasm/surface.js';
19
- import { buildStreamImports, freshStreamBoxState, type StreamBoxState } from './host.js';
20
-
21
- // --- the stream_dispatch ABI (toil-backend src/wasm/stream/section.rs) ---
22
- const EVENT_CONNECT = 1;
23
- const EVENT_MESSAGE = 2;
24
- const EVENT_CLOSE = 3;
25
- const EVENT_DISCONNECT = 4;
26
-
27
- // --- ring wire format (05 sections 5-6; toil-backend src/wasm/stream/runtime.rs) ---
28
- const RING_CTRL_BYTES = 32; // RingControl header
29
- const RING_FRAME_HEADER = 12; // RingFrame header
30
- const RING_MAGIC = 0x3147_4e52; // "RNG1" little-endian
31
- const RING_VERSION = 1;
32
- const RC_WRITE = 12; // write_cursor offset in RingControl
33
- const RC_READ = 16; // read_cursor offset
34
- const FRAME_TYPE_DATA_RELIABLE = 1;
35
- const MAX_STREAM_FRAME_LEN = 65536; // 64 KiB per egress frame (05 6.1)
36
-
37
- // --- @connect info-block ABI (05 section 2.2; toil-backend runtime.rs write_connect_info) ---
38
- // Layout: [u64 stream_id][u8 transport][u8 _][u16 auth_len][u16 path_len][u16 _] then auth + path UTF8.
39
- const SI_TRANSPORT = 8;
40
- const SI_AUTH_LEN = 10;
41
- const SI_PATH_LEN = 12;
42
- const SI_RESERVED2 = 14;
43
- const SI_BODY = 16;
44
- const SI_TRANSPORT_WEBTRANSPORT = 1; // the v1 transport (the dev emulates it)
45
-
46
- /**
47
- * Decode a guest's NEGATIVE `stream_dispatch` return into a `0x02xx` reject/close code (the Part-3
48
- * bridge `-(0x10000 + code)`), clamped to the stream range `0x0200..=0x02FF`; an out-of-range value
49
- * normalizes to `0x0208 STREAM_REJECTED`, mirroring `toil-backend` `decode_reject_code`.
50
- */
51
- export function decodeRejectCode(rc: bigint): number {
52
- const raw = Number((-rc - 0x10000n) & 0xffffn);
53
- return raw >= 0x0200 && raw <= 0x02ff ? raw : 0x0208;
54
- }
55
-
56
- interface StreamExports {
57
- readonly memory: WebAssembly.Memory;
58
- readonly stream_dispatch: (eventKind: number, lo: number, hi: number) => bigint;
59
- readonly stream_ring_offset?: () => number;
60
- readonly stream_ring_capacity?: () => number;
61
- readonly stream_egress_offset?: () => number;
62
- readonly stream_egress_capacity?: () => number;
63
- readonly stream_info_offset?: () => number;
64
- readonly stream_info_capacity?: () => number;
65
- }
66
-
67
- interface Rings {
68
- readonly ingressOff: number;
69
- readonly ingressCap: number;
70
- readonly egressOff: number;
71
- readonly egressCap: number;
72
- }
73
-
74
- interface StreamInfo {
75
- readonly offset: number;
76
- readonly cap: number;
77
- }
78
-
79
- /** A `@message` dispatch outcome: the drained egress reply frames, or a guest reject code. */
80
- export type StreamMessageOutcome =
81
- | { readonly kind: 'reply'; readonly frames: Buffer[] }
82
- | { readonly kind: 'reject'; readonly code: number };
83
-
84
- /** A `@connect` outcome: accepted/rejected plus any immediate frames staged during the hook. */
85
- export type StreamConnectOutcome =
86
- | { readonly kind: 'accept'; readonly initialEgress: Buffer[] }
87
- | { readonly kind: 'reject'; readonly code: number; readonly initialEgress: Buffer[] };
88
-
89
- export class DevStreamBox {
90
- private constructor(
91
- private readonly exports: StreamExports,
92
- private readonly _state: StreamBoxState,
93
- private readonly rings: Rings | null,
94
- private readonly streamInfo: StreamInfo | null,
95
- ) {}
96
-
97
- /** Compile + instantiate a resident stream box from a HOT `release-stream.wasm`. Fails closed: a
98
- * cold artifact, a missing `stream_dispatch`/`memory`, or a bad module throws. */
99
- static load(wasm: Buffer): DevStreamBox {
100
- const surface = parseSurface(wasm);
101
- if (surface === 'invalid' || surface.targetMode !== 'hot' || !surface.flags.stream) {
102
- throw new Error('stream box requires a hot stream artifact with a valid toil.surface');
103
- }
104
- const ref: { memory: WebAssembly.Memory | null } = { memory: null };
105
- const state = freshStreamBoxState();
106
- const module = new WebAssembly.Module(new Uint8Array(wasm));
107
- const instance = new WebAssembly.Instance(module, buildStreamImports(ref, state));
108
- const exports = instance.exports as unknown as StreamExports;
109
- if (
110
- typeof exports.stream_dispatch !== 'function' ||
111
- !(exports.memory instanceof WebAssembly.Memory)
112
- ) {
113
- throw new Error("stream artifact must export 'stream_dispatch' + 'memory'");
114
- }
115
- ref.memory = exports.memory;
116
- const rings = DevStreamBox.resolveRings(exports);
117
- const streamInfo = DevStreamBox.resolveStreamInfo(exports);
118
- const box = new DevStreamBox(exports, state, rings, streamInfo);
119
- if (rings) box.stampRings();
120
- return box;
121
- }
122
-
123
- /** Whether the box carries the ring runtime (a pre-bridge fixture omits the exports). */
124
- get hasRings(): boolean {
125
- return this.rings !== null;
126
- }
127
-
128
- /** Whether the box carries the `@connect` info-block bridge (a `@connect(StreamInbound)` hook). */
129
- get hasConnectBridge(): boolean {
130
- return this.streamInfo !== null;
131
- }
132
-
133
- /**
134
- * Fire `@connect`: write the connect-info block (stream id + transport + authority + path) into the
135
- * guest's info region, dispatch `EVENT_CONNECT`, decode the `StreamOutbound` accept/reject, and
136
- * drain any staged egress as initial frames. A box without the bridge runs `@connect` context-free
137
- * (the write is a no-op) and always accepts unless it returns a negative code.
138
- */
139
- onConnect(streamId: bigint, authority: string, path: string): StreamConnectOutcome {
140
- this.writeConnectInfo(streamId, authority, path);
141
- const rc = this.dispatch(EVENT_CONNECT, streamId);
142
- const initialEgress = this.egressDrain();
143
- if (rc < 0n) return { kind: 'reject', code: decodeRejectCode(rc), initialEgress };
144
- return { kind: 'accept', initialEgress };
145
- }
146
-
147
- /** Fire `@close` (graceful close). */
148
- onClose(streamId: bigint): bigint {
149
- return this.dispatch(EVENT_CLOSE, streamId);
150
- }
151
-
152
- /** Fire `@disconnect` (abrupt teardown). */
153
- onDisconnect(streamId: bigint): bigint {
154
- return this.dispatch(EVENT_DISCONNECT, streamId);
155
- }
156
-
157
- /** Drive the raw-bytes `@message` bridge: write `inbound` as one DATA_RELIABLE frame into the
158
- * ingress ring, fire `@message`, then drain the egress ring (reply) or surface the reject code.
159
- * A genuine wasm trap propagates (the caller maps it to a STREAM_HOOK_TRAPPED close). */
160
- onMessage(streamId: bigint, inbound: Buffer): StreamMessageOutcome {
161
- if (!this.rings) {
162
- throw new Error('stream box has no ring runtime; the message bridge is unavailable');
163
- }
164
- this.ingressWrite(inbound);
165
- const ret = this.dispatch(EVENT_MESSAGE, streamId);
166
- if (ret < 0n) return { kind: 'reject', code: decodeRejectCode(ret) };
167
- return { kind: 'reply', frames: this.egressDrain() };
168
- }
169
-
170
- private dispatch(eventKind: number, streamId: bigint): bigint {
171
- const lo = Number(streamId & 0xffff_ffffn) | 0;
172
- const hi = Number((streamId >> 32n) & 0xffff_ffffn) | 0;
173
- return this.exports.stream_dispatch(eventKind, lo, hi);
174
- }
175
-
176
- private static resolveRings(e: StreamExports): Rings | null {
177
- if (
178
- typeof e.stream_ring_offset !== 'function' ||
179
- typeof e.stream_ring_capacity !== 'function' ||
180
- typeof e.stream_egress_offset !== 'function' ||
181
- typeof e.stream_egress_capacity !== 'function'
182
- ) {
183
- return null;
184
- }
185
- return {
186
- ingressOff: e.stream_ring_offset() >>> 0,
187
- ingressCap: e.stream_ring_capacity() >>> 0,
188
- egressOff: e.stream_egress_offset() >>> 0,
189
- egressCap: e.stream_egress_capacity() >>> 0,
190
- };
191
- }
192
-
193
- private static resolveStreamInfo(e: StreamExports): StreamInfo | null {
194
- if (
195
- typeof e.stream_info_offset !== 'function' ||
196
- typeof e.stream_info_capacity !== 'function'
197
- ) {
198
- return null;
199
- }
200
- return { offset: e.stream_info_offset() >>> 0, cap: e.stream_info_capacity() >>> 0 };
201
- }
202
-
203
- private stampRings(): void {
204
- const rings = this.rings;
205
- if (!rings) return;
206
- this.stampOne(rings.ingressOff, rings.ingressCap);
207
- this.stampOne(rings.egressOff, rings.egressCap);
208
- }
209
-
210
- private stampOne(base: number, cap: number): void {
211
- const dv = new DataView(this.exports.memory.buffer);
212
- dv.setUint32(base + 0, RING_MAGIC, true);
213
- dv.setUint16(base + 4, RING_VERSION, true);
214
- dv.setUint16(base + 6, 0, true); // flags
215
- dv.setUint32(base + 8, cap, true); // capacity
216
- dv.setUint32(base + RC_WRITE, 0, true);
217
- dv.setUint32(base + RC_READ, 0, true);
218
- }
219
-
220
- /** Write the `@connect` info block into the guest's info region (no-op if the box carries none).
221
- * Authority + path are bounded into `[SI_BODY, cap)` - truncated to fit, never an OOB write. */
222
- private writeConnectInfo(streamId: bigint, authority: string, path: string): void {
223
- const info = this.streamInfo;
224
- if (!info) return;
225
- const base = info.offset;
226
- const body = Math.max(0, info.cap - SI_BODY);
227
- const authBytes = Buffer.from(authority, 'utf8');
228
- const authLen = Math.min(authBytes.length, 0xffff, body);
229
- const pathBytes = Buffer.from(path, 'utf8');
230
- const pathLen = Math.min(pathBytes.length, 0xffff, body - authLen);
231
- const dv = new DataView(this.exports.memory.buffer);
232
- dv.setBigUint64(base + 0, streamId, true);
233
- dv.setUint8(base + SI_TRANSPORT, SI_TRANSPORT_WEBTRANSPORT);
234
- dv.setUint8(base + SI_TRANSPORT + 1, 0); // reserved
235
- dv.setUint16(base + SI_AUTH_LEN, authLen, true);
236
- dv.setUint16(base + SI_PATH_LEN, pathLen, true);
237
- dv.setUint16(base + SI_RESERVED2, 0, true); // reserved
238
- const memU8 = new Uint8Array(this.exports.memory.buffer);
239
- if (authLen > 0) memU8.set(authBytes.subarray(0, authLen), base + SI_BODY);
240
- if (pathLen > 0) memU8.set(pathBytes.subarray(0, pathLen), base + SI_BODY + authLen);
241
- }
242
-
243
- /** Host (producer) writes ONE inbound RingFrame into the ingress ring with the drained-reset
244
- * (host owns write_cursor; reset read_cursor only when the guest has drained). */
245
- private ingressWrite(inbound: Buffer): void {
246
- const rings = this.rings;
247
- if (!rings) throw new Error('ingressWrite: no ring runtime');
248
- const { ingressOff: base, ingressCap: cap } = rings;
249
- const dv = new DataView(this.exports.memory.buffer);
250
- const n = inbound.length;
251
- const frameLen = RING_FRAME_HEADER + n;
252
- if (frameLen > cap) {
253
- throw new Error(`inbound frame (${String(frameLen)} B) exceeds ingress capacity`);
254
- }
255
- const w0 = dv.getUint32(base + RC_WRITE, true);
256
- const r0 = dv.getUint32(base + RC_READ, true);
257
- let w: number;
258
- if (r0 === w0) {
259
- dv.setUint32(base + RC_WRITE, 0, true);
260
- dv.setUint32(base + RC_READ, 0, true);
261
- w = 0;
262
- } else {
263
- w = w0;
264
- }
265
- if (w + frameLen > cap) throw new Error('ingress frame would not fit (v1 is no-wrap)');
266
- const f = base + RING_CTRL_BYTES + w;
267
- dv.setUint8(f + 0, RING_VERSION);
268
- dv.setUint8(f + 1, FRAME_TYPE_DATA_RELIABLE);
269
- dv.setUint16(f + 2, 0, true); // flags
270
- dv.setUint32(f + 4, n, true); // length
271
- dv.setUint32(f + 8, 0, true); // msg_seq
272
- if (n > 0)
273
- new Uint8Array(this.exports.memory.buffer, f + RING_FRAME_HEADER, n).set(inbound);
274
- dv.setUint32(base + RC_WRITE, w + frameLen, true);
275
- }
276
-
277
- /** Host (consumer) drains every egress RingFrame the guest staged, copying each payload out and
278
- * advancing the host-owned read_cursor. The guest does the drained-reset on its next write. */
279
- private egressDrain(): Buffer[] {
280
- const rings = this.rings;
281
- if (!rings) return [];
282
- const { egressOff: base, egressCap: cap } = rings;
283
- const dv = new DataView(this.exports.memory.buffer);
284
- const w = dv.getUint32(base + RC_WRITE, true);
285
- let r = dv.getUint32(base + RC_READ, true);
286
- const frames: Buffer[] = [];
287
- while (r < w) {
288
- const f = base + RING_CTRL_BYTES + r;
289
- if (r + RING_FRAME_HEADER > cap) break; // header must fit the frame region
290
- const len = dv.getUint32(f + 4, true);
291
- if (len > MAX_STREAM_FRAME_LEN) break; // over-cap frame (contained, not over-read)
292
- const span = RING_FRAME_HEADER + len;
293
- if (r + span > cap) break; // payload must fit
294
- const payloadOff = f + RING_FRAME_HEADER;
295
- frames.push(Buffer.from(new Uint8Array(this.exports.memory.buffer, payloadOff, len)));
296
- r += span;
297
- }
298
- dv.setUint32(base + RC_READ, r, true);
299
- return frames;
300
- }
301
- }