tova 0.3.4 → 0.3.6

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.
@@ -7,18 +7,24 @@ const VOID_ELEMENTS = new Set([
7
7
  'link', 'meta', 'param', 'source', 'track', 'wbr',
8
8
  ]);
9
9
 
10
+ const _ESC_HTML = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' };
11
+ const _RE_HTML = /[&<>"]/;
12
+ const _RE_HTML_G = /[&<>"]/g;
13
+
10
14
  function escapeHtml(str) {
11
15
  if (typeof str !== 'string') return String(str);
12
- return str
13
- .replace(/&/g, '&amp;')
14
- .replace(/</g, '&lt;')
15
- .replace(/>/g, '&gt;')
16
- .replace(/"/g, '&quot;');
16
+ if (!_RE_HTML.test(str)) return str; // fast path: no special chars
17
+ return str.replace(_RE_HTML_G, ch => _ESC_HTML[ch]);
17
18
  }
18
19
 
20
+ const _ESC_ATTR = { '&': '&amp;', '"': '&quot;' };
21
+ const _RE_ATTR = /[&"]/;
22
+ const _RE_ATTR_G = /[&"]/g;
23
+
19
24
  function escapeAttr(str) {
20
25
  if (typeof str !== 'string') return String(str);
21
- return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
26
+ if (!_RE_ATTR.test(str)) return str; // fast path
27
+ return str.replace(_RE_ATTR_G, ch => _ESC_ATTR[ch]);
22
28
  }
23
29
 
24
30
  // ─── SSR ID Counter for hydration markers ─────────────────
@@ -68,41 +74,68 @@ function renderPropsToString(props, vnode) {
68
74
 
69
75
  // Render a vnode tree to an HTML string
70
76
  export function renderToString(vnode) {
77
+ const parts = [];
78
+ _renderParts(vnode, parts);
79
+ return parts.join('');
80
+ }
81
+
82
+ function _renderParts(vnode, parts) {
71
83
  if (vnode === null || vnode === undefined) {
72
- return '';
84
+ return;
73
85
  }
74
86
 
75
87
  // Reactive function — evaluate it
76
88
  if (typeof vnode === 'function') {
77
- return renderToString(vnode());
89
+ _renderParts(vnode(), parts);
90
+ return;
78
91
  }
79
92
 
80
93
  // Primitives
81
- if (typeof vnode === 'string') return escapeHtml(vnode);
82
- if (typeof vnode === 'number' || typeof vnode === 'boolean') return escapeHtml(String(vnode));
94
+ if (typeof vnode === 'string') { parts.push(escapeHtml(vnode)); return; }
95
+ if (typeof vnode === 'number' || typeof vnode === 'boolean') { parts.push(escapeHtml(String(vnode))); return; }
83
96
 
84
97
  // Arrays
85
98
  if (Array.isArray(vnode)) {
86
- return vnode.map(renderToString).join('');
99
+ for (const child of vnode) _renderParts(child, parts);
100
+ return;
87
101
  }
88
102
 
89
103
  // Non-tova object
90
104
  if (!vnode.__tova) {
91
- return escapeHtml(String(vnode));
105
+ parts.push(escapeHtml(String(vnode)));
106
+ return;
92
107
  }
93
108
 
94
109
  // Fragment
95
110
  if (vnode.tag === '__fragment') {
96
- return flattenSSR(vnode.children).map(renderToString).join('');
111
+ const children = flattenSSR(vnode.children);
112
+ for (const child of children) _renderParts(child, parts);
113
+ return;
97
114
  }
98
115
 
99
- // Dynamic node (ErrorBoundary etc.)
116
+ // Dynamic node (ErrorBoundary, Suspense, etc.)
100
117
  if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {
101
118
  const id = nextSSRId();
102
119
  try {
103
120
  const inner = vnode.compute();
104
- const content = renderToString(inner);
105
- return `<!--tova-s:${id}-->${content}<!--/tova-s:${id}-->`;
121
+ // If inner is a Promise (async Suspense children), render fallback
122
+ if (inner && typeof inner.then === 'function') {
123
+ if (vnode._fallback) {
124
+ const fallbackContent = typeof vnode._fallback === 'function'
125
+ ? vnode._fallback()
126
+ : vnode._fallback;
127
+ parts.push(`<!--tova-s:${id}-->`);
128
+ _renderParts(fallbackContent, parts);
129
+ parts.push(`<!--/tova-s:${id}-->`);
130
+ return;
131
+ }
132
+ parts.push(`<!--tova-s:${id}--><!--/tova-s:${id}-->`);
133
+ return;
134
+ }
135
+ parts.push(`<!--tova-s:${id}-->`);
136
+ _renderParts(inner, parts);
137
+ parts.push(`<!--/tova-s:${id}-->`);
138
+ return;
106
139
  } catch (e) {
107
140
  // If this is an ErrorBoundary with a fallback, render fallback
108
141
  if (vnode._fallback) {
@@ -110,9 +143,11 @@ export function renderToString(vnode) {
110
143
  const fallbackContent = typeof vnode._fallback === 'function'
111
144
  ? vnode._fallback({ error: e, reset: () => {} })
112
145
  : vnode._fallback;
113
- return `<!--tova-s:${id}-->${renderToString(fallbackContent)}<!--/tova-s:${id}-->`;
146
+ parts.push(`<!--tova-s:${id}-->`);
147
+ _renderParts(fallbackContent, parts);
148
+ parts.push(`<!--/tova-s:${id}-->`);
149
+ return;
114
150
  } catch (fallbackError) {
115
- // Fallback also threw — re-throw
116
151
  throw fallbackError;
117
152
  }
118
153
  }
@@ -122,25 +157,24 @@ export function renderToString(vnode) {
122
157
 
123
158
  // Element
124
159
  const tag = vnode.tag;
125
- let html = `<${tag}`;
126
-
127
- html += renderPropsToString(vnode.props, vnode);
160
+ parts.push(`<${tag}`);
161
+ parts.push(renderPropsToString(vnode.props, vnode));
128
162
 
129
163
  // Self-closing
130
164
  if (VOID_ELEMENTS.has(tag)) {
131
- return html + ' />';
165
+ parts.push(' />');
166
+ return;
132
167
  }
133
168
 
134
- html += '>';
169
+ parts.push('>');
135
170
 
136
171
  // Children
137
172
  const children = flattenSSR(vnode.children || []);
138
173
  for (const child of children) {
139
- html += renderToString(child);
174
+ _renderParts(child, parts);
140
175
  }
141
176
 
142
- html += `</${tag}>`;
143
- return html;
177
+ parts.push(`</${tag}>`);
144
178
  }
145
179
 
146
180
  function flattenSSR(children) {
@@ -177,6 +211,36 @@ export function renderPage(component, { title = 'Tova App', head = '', scriptSrc
177
211
 
178
212
  // ─── Streaming SSR ─────────────────────────────────────────
179
213
 
214
+ // Buffered controller wrapper — batches small enqueue() calls into larger chunks
215
+ // to reduce the number of stream operations (N4 optimization)
216
+ class BufferedController {
217
+ constructor(controller, bufferSize = 4096) {
218
+ this._inner = controller;
219
+ this._buffer = '';
220
+ this._bufferSize = bufferSize;
221
+ }
222
+
223
+ enqueue(chunk) {
224
+ this._buffer += chunk;
225
+ if (this._buffer.length >= this._bufferSize) {
226
+ this._inner.enqueue(this._buffer);
227
+ this._buffer = '';
228
+ }
229
+ }
230
+
231
+ flush() {
232
+ if (this._buffer.length > 0) {
233
+ this._inner.enqueue(this._buffer);
234
+ this._buffer = '';
235
+ }
236
+ }
237
+
238
+ close() {
239
+ this.flush();
240
+ this._inner.close();
241
+ }
242
+ }
243
+
180
244
  // Stream a single vnode, writing chunks to the controller
181
245
  function streamVNode(vnode, controller) {
182
246
  if (vnode === null || vnode === undefined) {
@@ -267,38 +331,41 @@ function streamVNode(vnode, controller) {
267
331
 
268
332
  // Render a vnode tree to a Web ReadableStream
269
333
  export function renderToReadableStream(vnode, options = {}) {
270
- const { onError } = options;
334
+ const { onError, bufferSize } = options;
271
335
 
272
336
  return new ReadableStream({
273
337
  start(controller) {
338
+ const buf = new BufferedController(controller, bufferSize);
274
339
  try {
275
- streamVNode(vnode, controller);
340
+ streamVNode(vnode, buf);
276
341
  } catch (e) {
277
342
  if (onError) onError(e);
278
- controller.enqueue(`<!--tova-ssr-error-->`);
343
+ buf.enqueue(`<!--tova-ssr-error-->`);
279
344
  }
280
- controller.close();
345
+ buf.close();
281
346
  },
282
347
  });
283
348
  }
284
349
 
285
350
  // Render a full HTML page as a stream
286
351
  export function renderPageToStream(component, options = {}) {
287
- const { title = 'Tova App', head = '', scriptSrc = '/client.js', onError } = options;
352
+ const { title = 'Tova App', head = '', scriptSrc = '/client.js', onError, bufferSize } = options;
288
353
 
289
354
  return new ReadableStream({
290
355
  start(controller) {
291
- // Flush head immediately so CSS/JS start downloading
356
+ // Flush head immediately so CSS/JS start downloading (bypass buffer)
292
357
  controller.enqueue(`<!DOCTYPE html>\n<html>\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>${escapeHtml(title)}</title>\n ${head}\n</head>\n<body>\n <div id="app">`);
293
358
 
359
+ const buf = new BufferedController(controller, bufferSize);
294
360
  try {
295
361
  const vnode = typeof component === 'function' ? component() : component;
296
- streamVNode(vnode, controller);
362
+ streamVNode(vnode, buf);
297
363
  } catch (e) {
298
364
  if (onError) onError(e);
299
- controller.enqueue(`<!--tova-ssr-error-->`);
365
+ buf.enqueue(`<!--tova-ssr-error-->`);
300
366
  }
301
367
 
368
+ buf.flush();
302
369
  controller.enqueue(`</div>\n <script type="module" src="${escapeAttr(scriptSrc)}"></script>\n</body>\n</html>`);
303
370
  controller.close();
304
371
  },