Skip to content

Commit 82c1d86

Browse files
committed
feat(ssr): implement dynamic HTML streaming and inline wasm bindings
- **HTML Streaming:** Introduced `renderPageStream` in `ProductionSSREngine` using `preact-render-to-string/stream` to send HTML chunks dynamically with `TransformStream`. - **Response Handling Update:** Modified `handleRequest` in the production server to return `jenResponse.body` directly instead of awaiting `.text()`, properly supporting streamed document delivery. - **WASM Bindings:** Updated the WASM-based router core (`jen_router.cjs`) to embed and initiate the WebAssembly module directly within the file rather than re-exporting from a separate background module.
1 parent 1ab2332 commit 82c1d86

4 files changed

Lines changed: 469 additions & 17 deletions

File tree

packages/jen/src/core/jen_router.cjs

Lines changed: 320 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,323 @@
11
/* @ts-self-types="./jen_router.d.ts" */
22

3-
import * as wasm from "./jen_router_bg.wasm";
4-
import { __wbg_set_wasm } from "./jen_router_bg.cjs";
5-
__wbg_set_wasm(wasm);
3+
/**
4+
* Route match result with parameters and file paths
5+
*/
6+
class RouteMatch {
7+
static __wrap(ptr) {
8+
ptr = ptr >>> 0;
9+
const obj = Object.create(RouteMatch.prototype);
10+
obj.__wbg_ptr = ptr;
11+
RouteMatchFinalization.register(obj, obj.__wbg_ptr, obj);
12+
return obj;
13+
}
14+
__destroy_into_raw() {
15+
const ptr = this.__wbg_ptr;
16+
this.__wbg_ptr = 0;
17+
RouteMatchFinalization.unregister(this);
18+
return ptr;
19+
}
20+
free() {
21+
const ptr = this.__destroy_into_raw();
22+
wasm.__wbg_routematch_free(ptr, 0);
23+
}
24+
/**
25+
* Gets the resolved `.jsx` file path, if any.
26+
* @returns {string}
27+
*/
28+
get filePathJsx() {
29+
let deferred1_0;
30+
let deferred1_1;
31+
try {
32+
const ret = wasm.routematch_filePathJsx(this.__wbg_ptr);
33+
deferred1_0 = ret[0];
34+
deferred1_1 = ret[1];
35+
return getStringFromWasm0(ret[0], ret[1]);
36+
} finally {
37+
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
38+
}
39+
}
40+
/**
41+
* Gets the resolved `.tsx` file path, if any.
42+
* @returns {string}
43+
*/
44+
get filePathTsx() {
45+
let deferred1_0;
46+
let deferred1_1;
47+
try {
48+
const ret = wasm.routematch_filePathTsx(this.__wbg_ptr);
49+
deferred1_0 = ret[0];
50+
deferred1_1 = ret[1];
51+
return getStringFromWasm0(ret[0], ret[1]);
52+
} finally {
53+
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
54+
}
55+
}
56+
/**
57+
* Returns true if the route was successfully matched.
58+
* @returns {boolean}
59+
*/
60+
get found() {
61+
const ret = wasm.routematch_found(this.__wbg_ptr);
62+
return ret !== 0;
63+
}
64+
/**
65+
* Creates a new RouteMatch instance.
66+
*
67+
* # Arguments
68+
*
69+
* * `found` - Whether the route was matched successfully
70+
* * `pathname` - The matched pathname
71+
* * `params` - A JSON-encoded string of route parameters
72+
* * `file_path_tsx` - The path to the resolved `.tsx` file
73+
* * `file_path_jsx` - The path to the resolved `.jsx` file
74+
* @param {boolean} found
75+
* @param {string} pathname
76+
* @param {string} params
77+
* @param {string} file_path_tsx
78+
* @param {string} file_path_jsx
79+
*/
80+
constructor(found, pathname, params, file_path_tsx, file_path_jsx) {
81+
const ptr0 = passStringToWasm0(pathname, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
82+
const len0 = WASM_VECTOR_LEN;
83+
const ptr1 = passStringToWasm0(params, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
84+
const len1 = WASM_VECTOR_LEN;
85+
const ptr2 = passStringToWasm0(file_path_tsx, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
86+
const len2 = WASM_VECTOR_LEN;
87+
const ptr3 = passStringToWasm0(file_path_jsx, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
88+
const len3 = WASM_VECTOR_LEN;
89+
const ret = wasm.routematch_new(found, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3);
90+
this.__wbg_ptr = ret >>> 0;
91+
RouteMatchFinalization.register(this, this.__wbg_ptr, this);
92+
return this;
93+
}
94+
/**
95+
* Gets the JSON string containing route parameters.
96+
* @returns {string}
97+
*/
98+
get params() {
99+
let deferred1_0;
100+
let deferred1_1;
101+
try {
102+
const ret = wasm.routematch_params(this.__wbg_ptr);
103+
deferred1_0 = ret[0];
104+
deferred1_1 = ret[1];
105+
return getStringFromWasm0(ret[0], ret[1]);
106+
} finally {
107+
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
108+
}
109+
}
110+
/**
111+
* Gets the normalized pathname that was matched.
112+
* @returns {string}
113+
*/
114+
get pathname() {
115+
let deferred1_0;
116+
let deferred1_1;
117+
try {
118+
const ret = wasm.routematch_pathname(this.__wbg_ptr);
119+
deferred1_0 = ret[0];
120+
deferred1_1 = ret[1];
121+
return getStringFromWasm0(ret[0], ret[1]);
122+
} finally {
123+
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
124+
}
125+
}
126+
}
127+
if (Symbol.dispose) RouteMatch.prototype[Symbol.dispose] = RouteMatch.prototype.free;
128+
exports.RouteMatch = RouteMatch;
129+
130+
/**
131+
* High-performance route matcher for dynamic and static routes
132+
*/
133+
class RouteMatcher {
134+
__destroy_into_raw() {
135+
const ptr = this.__wbg_ptr;
136+
this.__wbg_ptr = 0;
137+
RouteMatcherFinalization.unregister(this);
138+
return ptr;
139+
}
140+
free() {
141+
const ptr = this.__destroy_into_raw();
142+
wasm.__wbg_routematcher_free(ptr, 0);
143+
}
144+
/**
145+
* Clear all routes
146+
*/
147+
clear() {
148+
wasm.routematcher_clear(this.__wbg_ptr);
149+
}
150+
/**
151+
* Match a pathname against registered routes.
152+
*
153+
* First looks for exact static matches (O(1)), then falls back
154+
* to evaluating dynamic routes.
155+
*
156+
* # Arguments
157+
*
158+
* * `pathname` - The incoming URL pathname to match
159+
* @param {string} pathname
160+
* @returns {RouteMatch}
161+
*/
162+
match_route(pathname) {
163+
const ptr0 = passStringToWasm0(pathname, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
164+
const len0 = WASM_VECTOR_LEN;
165+
const ret = wasm.routematcher_match_route(this.__wbg_ptr, ptr0, len0);
166+
return RouteMatch.__wrap(ret);
167+
}
168+
/**
169+
* Create a new route matcher
170+
*/
171+
constructor() {
172+
const ret = wasm.routematcher_new();
173+
this.__wbg_ptr = ret >>> 0;
174+
RouteMatcherFinalization.register(this, this.__wbg_ptr, this);
175+
return this;
176+
}
177+
/**
178+
* Register a route pattern.
179+
*
180+
* # Arguments
181+
*
182+
* * `path` - The route pattern (can contain dynamic segments like `:id`)
183+
* * `file_path_tsx` - The associated `.tsx` file path
184+
* * `file_path_jsx` - The associated `.jsx` file path
185+
* @param {string} path
186+
* @param {string} file_path_tsx
187+
* @param {string} file_path_jsx
188+
*/
189+
register(path, file_path_tsx, file_path_jsx) {
190+
const ptr0 = passStringToWasm0(path, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
191+
const len0 = WASM_VECTOR_LEN;
192+
const ptr1 = passStringToWasm0(file_path_tsx, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
193+
const len1 = WASM_VECTOR_LEN;
194+
const ptr2 = passStringToWasm0(file_path_jsx, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
195+
const len2 = WASM_VECTOR_LEN;
196+
wasm.routematcher_register(this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2);
197+
}
198+
/**
199+
* Get count of registered routes
200+
* @returns {number}
201+
*/
202+
route_count() {
203+
const ret = wasm.routematcher_route_count(this.__wbg_ptr);
204+
return ret >>> 0;
205+
}
206+
/**
207+
* Set an optional base path to strip from incoming requests
208+
* @param {string} base_path
209+
*/
210+
set_base_path(base_path) {
211+
const ptr0 = passStringToWasm0(base_path, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
212+
const len0 = WASM_VECTOR_LEN;
213+
wasm.routematcher_set_base_path(this.__wbg_ptr, ptr0, len0);
214+
}
215+
}
216+
if (Symbol.dispose) RouteMatcher.prototype[Symbol.dispose] = RouteMatcher.prototype.free;
217+
exports.RouteMatcher = RouteMatcher;
218+
219+
function __wbg_get_imports() {
220+
const import0 = {
221+
__proto__: null,
222+
__wbg___wbindgen_throw_6ddd609b62940d55: function(arg0, arg1) {
223+
throw new Error(getStringFromWasm0(arg0, arg1));
224+
},
225+
__wbindgen_init_externref_table: function() {
226+
const table = wasm.__wbindgen_externrefs;
227+
const offset = table.grow(4);
228+
table.set(0, undefined);
229+
table.set(offset + 0, undefined);
230+
table.set(offset + 1, null);
231+
table.set(offset + 2, true);
232+
table.set(offset + 3, false);
233+
},
234+
};
235+
return {
236+
__proto__: null,
237+
"./jen_router_bg.js": import0,
238+
};
239+
}
240+
241+
const RouteMatchFinalization = (typeof FinalizationRegistry === 'undefined')
242+
? { register: () => {}, unregister: () => {} }
243+
: new FinalizationRegistry(ptr => wasm.__wbg_routematch_free(ptr >>> 0, 1));
244+
const RouteMatcherFinalization = (typeof FinalizationRegistry === 'undefined')
245+
? { register: () => {}, unregister: () => {} }
246+
: new FinalizationRegistry(ptr => wasm.__wbg_routematcher_free(ptr >>> 0, 1));
247+
248+
function getStringFromWasm0(ptr, len) {
249+
ptr = ptr >>> 0;
250+
return decodeText(ptr, len);
251+
}
252+
253+
let cachedUint8ArrayMemory0 = null;
254+
function getUint8ArrayMemory0() {
255+
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
256+
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
257+
}
258+
return cachedUint8ArrayMemory0;
259+
}
260+
261+
function passStringToWasm0(arg, malloc, realloc) {
262+
if (realloc === undefined) {
263+
const buf = cachedTextEncoder.encode(arg);
264+
const ptr = malloc(buf.length, 1) >>> 0;
265+
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
266+
WASM_VECTOR_LEN = buf.length;
267+
return ptr;
268+
}
269+
270+
let len = arg.length;
271+
let ptr = malloc(len, 1) >>> 0;
272+
273+
const mem = getUint8ArrayMemory0();
274+
275+
let offset = 0;
276+
277+
for (; offset < len; offset++) {
278+
const code = arg.charCodeAt(offset);
279+
if (code > 0x7F) break;
280+
mem[ptr + offset] = code;
281+
}
282+
if (offset !== len) {
283+
if (offset !== 0) {
284+
arg = arg.slice(offset);
285+
}
286+
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
287+
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
288+
const ret = cachedTextEncoder.encodeInto(arg, view);
289+
290+
offset += ret.written;
291+
ptr = realloc(ptr, len, offset, 1) >>> 0;
292+
}
293+
294+
WASM_VECTOR_LEN = offset;
295+
return ptr;
296+
}
297+
298+
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
299+
cachedTextDecoder.decode();
300+
function decodeText(ptr, len) {
301+
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
302+
}
303+
304+
const cachedTextEncoder = new TextEncoder();
305+
306+
if (!('encodeInto' in cachedTextEncoder)) {
307+
cachedTextEncoder.encodeInto = function (arg, view) {
308+
const buf = cachedTextEncoder.encode(arg);
309+
view.set(buf);
310+
return {
311+
read: arg.length,
312+
written: buf.length
313+
};
314+
};
315+
}
316+
317+
let WASM_VECTOR_LEN = 0;
318+
319+
const wasmPath = `${__dirname}/jen_router_bg.wasm`;
320+
const wasmBytes = require('fs').readFileSync(wasmPath);
321+
const wasmModule = new WebAssembly.Module(wasmBytes);
322+
let wasm = new WebAssembly.Instance(wasmModule, __wbg_get_imports()).exports;
6323
wasm.__wbindgen_start();
7-
export {
8-
RouteMatch, RouteMatcher
9-
} from "./jen_router_bg.cjs";
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { h, Component } from 'preact';
2+
3+
class MockPage extends Component {
4+
render() {
5+
return h('div', { id: 'mock-page' }, 'Hello Stream!');
6+
}
7+
}
8+
9+
export default MockPage;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, it, expect } from 'bun:test';
2+
import { ProductionSSREngine } from '../production.js';
3+
import path from 'node:path';
4+
5+
describe('ProductionSSREngine Streaming', () => {
6+
it('should stream the response correctly', async () => {
7+
// 1. Point to the mock page we just created
8+
const mockPagePath = path.resolve(__dirname, './mock-page.js');
9+
10+
// 2. Call the new stream streaming method
11+
const response = await ProductionSSREngine.renderPageStream(mockPagePath, 'en');
12+
13+
// 3. Verify we get a Response object
14+
expect(response).toBeInstanceOf(Response);
15+
expect(response.headers.get('Content-Type')).toContain('text/html');
16+
17+
// 4. Consume the stream
18+
const htmlText = await response.text();
19+
20+
// 5. Verify contents
21+
expect(htmlText).toContain('<!DOCTYPE html>');
22+
expect(htmlText).toContain('<html lang="en">');
23+
expect(htmlText).toContain('<div id="mock-page">Hello Stream!</div>');
24+
expect(htmlText).toContain('<script type="module">');
25+
});
26+
});

0 commit comments

Comments
 (0)