Framework updates
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
136
node_modules/@jqhtml/ssr/README.md
generated
vendored
136
node_modules/@jqhtml/ssr/README.md
generated
vendored
@@ -361,6 +361,142 @@ Or flush specific bundle:
|
||||
|
||||
---
|
||||
|
||||
## SSR Preload API (Hydration Acceleration)
|
||||
|
||||
The SSR Preload API eliminates redundant `on_load()` calls during client hydration. When the SSR server already fetched data for a component, the client can skip the identical fetch and use the captured data directly.
|
||||
|
||||
**Without preload:** SSR fetches data → client boots → client fetches same data again (wasted round-trip).
|
||||
|
||||
**With preload:** SSR fetches data → captures it → client receives captured data → client boots with preloaded data → no redundant fetch.
|
||||
|
||||
### Server-Side: Capturing Data
|
||||
|
||||
During SSR rendering, enable data capture to record each component's loaded data:
|
||||
|
||||
```javascript
|
||||
// 1. Enable capture before rendering
|
||||
jqhtml.start_data_capture();
|
||||
|
||||
// 2. Render the component (on_load() runs, data is captured automatically)
|
||||
// ... component renders normally ...
|
||||
|
||||
// 3. Retrieve captured data (one-shot: clears buffer after retrieval)
|
||||
const captured = jqhtml.get_captured_data();
|
||||
// Returns: [{ component: 'DashboardIndex', args: { user_id: 123 }, data: { items: [...] } }, ...]
|
||||
|
||||
// 4. Stop capture when done
|
||||
jqhtml.stop_data_capture();
|
||||
|
||||
// 5. Include captured data in SSR response
|
||||
return { html: renderedHtml, preload: captured };
|
||||
```
|
||||
|
||||
**Capture rules:**
|
||||
- Only components with `on_load()` are captured (static components excluded)
|
||||
- Deduplicates by component name + args (Load Coordinator aware)
|
||||
- `get_captured_data()` is one-shot: clears the buffer on retrieval
|
||||
- `start_data_capture()` is idempotent (safe to call multiple times)
|
||||
|
||||
### Client-Side: Injecting Preloaded Data
|
||||
|
||||
Before components boot on the client, seed the preload cache with SSR-captured data:
|
||||
|
||||
```javascript
|
||||
// 1. Inject preloaded data BEFORE components initialize
|
||||
jqhtml.set_preload_data(ssrPreloadData);
|
||||
|
||||
// 2. Components boot → _load() finds preload cache hit → skips on_load() entirely
|
||||
// ... components initialize normally, but with instant data ...
|
||||
|
||||
// 3. Clear unconsumed entries after hydration is complete
|
||||
jqhtml.clear_preload_data();
|
||||
```
|
||||
|
||||
**Preload rules:**
|
||||
- `set_preload_data(entries)` must be called before components boot
|
||||
- Each preload entry is consumed on first use (one-shot per key)
|
||||
- `set_preload_data(null)` or `set_preload_data([])` is a no-op
|
||||
- `clear_preload_data()` removes any unconsumed entries
|
||||
|
||||
### Complete Data Flow
|
||||
|
||||
```
|
||||
SSR Server:
|
||||
1. jqhtml.start_data_capture()
|
||||
2. Render component (on_load() fetches data → CAPTURED)
|
||||
3. captured = jqhtml.get_captured_data()
|
||||
4. Return { html, preload: captured }
|
||||
|
||||
Client:
|
||||
1. jqhtml.set_preload_data(ssr_preload)
|
||||
2. Components boot → _load() hits preload cache → skips on_load()
|
||||
3. jqhtml.clear_preload_data()
|
||||
```
|
||||
|
||||
### Updated PHP Integration Example (with Preload)
|
||||
|
||||
```php
|
||||
// Usage with preload data
|
||||
$ssr = new JqhtmlSSR();
|
||||
$result = $ssr->render('Dashboard_Index_Action', ['user_id' => 123], $bundles);
|
||||
|
||||
if ($result['status'] === 'success') {
|
||||
$html = $result['payload']['html'];
|
||||
$preload = json_encode($result['payload']['preload'] ?? []);
|
||||
|
||||
echo $html;
|
||||
echo "<script>jqhtml.set_preload_data({$preload});</script>";
|
||||
echo "<script src='/bundles/vendor.js'></script>";
|
||||
echo "<script src='/bundles/app.js'></script>";
|
||||
echo "<script>jqhtml.boot().then(() => jqhtml.clear_preload_data());</script>";
|
||||
}
|
||||
```
|
||||
|
||||
### Updated Node.js Client Example (with Preload)
|
||||
|
||||
```javascript
|
||||
const response = await sendSSRRequest(9876, {
|
||||
id: 'render-1',
|
||||
type: 'render',
|
||||
payload: {
|
||||
bundles: [
|
||||
{ id: 'vendor', content: vendorBundleContent },
|
||||
{ id: 'app', content: appBundleContent }
|
||||
],
|
||||
component: 'Dashboard_Index_Action',
|
||||
args: { user_id: 123 },
|
||||
options: { baseUrl: 'http://localhost:3000' }
|
||||
}
|
||||
});
|
||||
|
||||
const { html, preload } = response.payload;
|
||||
|
||||
// Embed preload data in the page for client-side hydration
|
||||
const pageHtml = `
|
||||
${html}
|
||||
<script>
|
||||
jqhtml.set_preload_data(${JSON.stringify(preload || [])});
|
||||
</script>
|
||||
<script src="/bundles/vendor.js"></script>
|
||||
<script src="/bundles/app.js"></script>
|
||||
<script>
|
||||
jqhtml.boot().then(() => jqhtml.clear_preload_data());
|
||||
</script>
|
||||
`;
|
||||
```
|
||||
|
||||
### API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `jqhtml.start_data_capture()` | Enable data capture mode (idempotent) |
|
||||
| `jqhtml.get_captured_data()` | Returns captured entries and clears buffer (one-shot) |
|
||||
| `jqhtml.stop_data_capture()` | Stops capture and clears all capture state |
|
||||
| `jqhtml.set_preload_data(entries)` | Seeds preload cache with SSR-captured data |
|
||||
| `jqhtml.clear_preload_data()` | Clears unconsumed preload entries |
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
|
||||
63
node_modules/@jqhtml/ssr/SPECIFICATION.md
generated
vendored
63
node_modules/@jqhtml/ssr/SPECIFICATION.md
generated
vendored
@@ -544,6 +544,69 @@ jqhtml-ssr --render \
|
||||
|
||||
---
|
||||
|
||||
## Preload Data Capture and Injection Protocol
|
||||
|
||||
The preload system captures component data during SSR and replays it on the client to skip redundant `on_load()` calls during hydration.
|
||||
|
||||
### Entry Format
|
||||
|
||||
Each captured entry has the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"component": "DashboardIndex",
|
||||
"args": { "user_id": 123 },
|
||||
"data": { "items": [...], "total": 42 }
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `component` | string | Component name |
|
||||
| `args` | object | Component arguments (as passed at creation) |
|
||||
| `data` | object | The component's `this.data` after `on_load()` completed |
|
||||
|
||||
### Key Generation
|
||||
|
||||
Preload entries are matched by a composite key of `component` + `args`. The key generation algorithm:
|
||||
|
||||
1. Component name is used as-is (string)
|
||||
2. Args are serialized deterministically (stable JSON stringification)
|
||||
3. Combined key: `component + ":" + serialized_args`
|
||||
|
||||
This means two components with the same name and identical args share a preload entry, which is consistent with Load Coordinator deduplication behavior.
|
||||
|
||||
### One-Shot Semantics
|
||||
|
||||
Both capture and preload use one-shot consumption:
|
||||
|
||||
- **`get_captured_data()`** — Returns all captured entries and clears the capture buffer. Calling again returns an empty array until new components are captured.
|
||||
- **Preload entries** — Each entry is consumed on first use. When `_load()` finds a preload cache hit, it removes that entry. A second component with the same key will not find a preload hit and will call `on_load()` normally.
|
||||
|
||||
### Integration Points in `_load()`
|
||||
|
||||
The preload system integrates into the component `_load()` method with the following priority:
|
||||
|
||||
1. **Preload cache check** (highest priority) — If `set_preload_data()` was called and an entry matches this component's name + args, apply the data via `_apply_load_result()` and skip `on_load()` entirely.
|
||||
2. **Load Coordinator check** — If another instance of the same component + args is already loading, wait for its result.
|
||||
3. **Normal `on_load()`** — Call the component's `on_load()` method.
|
||||
|
||||
### Capture Integration in `_apply_load_result()`
|
||||
|
||||
When data capture is enabled (`start_data_capture()` was called), `_apply_load_result()` records the component's data if the component has an `on_load()` method. Static components (those without `on_load()`) are excluded from capture.
|
||||
|
||||
### API Summary
|
||||
|
||||
| Method | Side | Description |
|
||||
|--------|------|-------------|
|
||||
| `start_data_capture()` | Server | Enable capture mode (idempotent) |
|
||||
| `get_captured_data()` | Server | Return and clear captured entries |
|
||||
| `stop_data_capture()` | Server | Stop capture, clear all state |
|
||||
| `set_preload_data(entries)` | Client | Seed preload cache; null/empty is no-op |
|
||||
| `clear_preload_data()` | Client | Clear unconsumed preload entries |
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Streaming SSR
|
||||
|
||||
2
node_modules/@jqhtml/ssr/package.json
generated
vendored
2
node_modules/@jqhtml/ssr/package.json
generated
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@jqhtml/ssr",
|
||||
"version": "2.3.41",
|
||||
"version": "2.3.42",
|
||||
"description": "Server-Side Rendering for JQHTML components - renders components to HTML for SEO",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
||||
66
node_modules/@jqhtml/ssr/src/environment.js
generated
vendored
66
node_modules/@jqhtml/ssr/src/environment.js
generated
vendored
@@ -149,6 +149,58 @@ class SSREnvironment {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute JavaScript code wrapped in an IIFE to prevent class declarations
|
||||
* from leaking into the global scope across requests.
|
||||
*
|
||||
* Without IIFE wrapping, `class Foo {}` in vm.runInThisContext() persists
|
||||
* in the global context, causing "Identifier already declared" errors on
|
||||
* subsequent requests that load the same bundles.
|
||||
*
|
||||
* @param {string} code - JavaScript code to execute
|
||||
* @param {string} filename - Filename for error reporting
|
||||
*/
|
||||
executeScoped(code, filename = 'scoped-bundle.js') {
|
||||
if (!this.initialized) {
|
||||
throw new Error('SSR environment not initialized. Call init() first.');
|
||||
}
|
||||
|
||||
const cleanCode = code.replace(/\/\/# sourceMappingURL=.*/g, '');
|
||||
|
||||
// Register Rsx on window inside the IIFE so boot() can access it.
|
||||
// Class declarations are block-scoped inside the IIFE, but closures
|
||||
// preserve the scope — so Rsx._rsx_core_boot() can still reference
|
||||
// Manifest, Ajax, etc. by name when called from outside.
|
||||
const registration = 'if (typeof Rsx !== "undefined") { window.Rsx = Rsx; }';
|
||||
const wrappedCode = `(function() {\n${cleanCode}\n${registration}\n})();`;
|
||||
|
||||
vm.runInThisContext(wrappedCode, {
|
||||
filename,
|
||||
displayErrors: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot the RSpade framework lifecycle.
|
||||
*
|
||||
* Calls Rsx._rsx_core_boot() which runs the full 10-phase initialization
|
||||
* sequence: Ajax init, component registration, jqhtml cache setup, etc.
|
||||
* Framework classes with browser-only APIs detect window.__SSR__ and skip
|
||||
* their initialization via Rsx.is_ssr() guards.
|
||||
*
|
||||
* Must be called after executeScoped() loads the bundles.
|
||||
*/
|
||||
async boot() {
|
||||
if (!this.initialized) {
|
||||
throw new Error('SSR environment not initialized. Call init() first.');
|
||||
}
|
||||
|
||||
const Rsx = global.Rsx || (global.window && global.window.Rsx);
|
||||
if (Rsx && typeof Rsx._rsx_core_boot === 'function') {
|
||||
await Rsx._rsx_core_boot();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a component to HTML
|
||||
* @param {string} componentName - Component name to render
|
||||
@@ -161,6 +213,12 @@ class SSREnvironment {
|
||||
}
|
||||
|
||||
const $ = global.$;
|
||||
const jqhtml = global.window.jqhtml;
|
||||
|
||||
// Start data capture for preload
|
||||
if (jqhtml && typeof jqhtml.start_data_capture === 'function') {
|
||||
jqhtml.start_data_capture();
|
||||
}
|
||||
|
||||
// Create container
|
||||
const $container = $('<div>');
|
||||
@@ -188,10 +246,16 @@ class SSREnvironment {
|
||||
// Get rendered HTML
|
||||
const html = component.$.prop('outerHTML');
|
||||
|
||||
// Get captured preload data
|
||||
let preload = [];
|
||||
if (jqhtml && typeof jqhtml.get_captured_data === 'function') {
|
||||
preload = jqhtml.get_captured_data();
|
||||
}
|
||||
|
||||
// Export cache state
|
||||
const cache = this.storage.exportAll();
|
||||
|
||||
return { html, cache };
|
||||
return { html, cache, preload };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
24
node_modules/@jqhtml/ssr/src/protocol.js
generated
vendored
24
node_modules/@jqhtml/ssr/src/protocol.js
generated
vendored
@@ -74,7 +74,7 @@ function parseRequest(message) {
|
||||
};
|
||||
}
|
||||
|
||||
const validTypes = ['render', 'render_spa', 'ping', 'flush_cache'];
|
||||
const validTypes = ['render', 'render_spa', 'ping', 'flush_cache', 'shutdown'];
|
||||
if (!validTypes.includes(parsed.type)) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -396,12 +396,12 @@ function errorResponse(id, code, message, stack) {
|
||||
* @param {object} timing - Timing info { total_ms, bundle_load_ms, render_ms }
|
||||
* @returns {string} JSON string with newline
|
||||
*/
|
||||
function renderResponse(id, html, cache, timing) {
|
||||
return successResponse(id, {
|
||||
html,
|
||||
cache,
|
||||
timing
|
||||
});
|
||||
function renderResponse(id, html, cache, timing, preload) {
|
||||
const payload = { html, cache, timing };
|
||||
if (preload && preload.length > 0) {
|
||||
payload.preload = preload;
|
||||
}
|
||||
return successResponse(id, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -424,6 +424,15 @@ function flushCacheResponse(id, flushed) {
|
||||
return successResponse(id, { flushed });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a shutdown response
|
||||
* @param {string} id - Request ID
|
||||
* @returns {string} JSON string with newline
|
||||
*/
|
||||
function shutdownResponse(id) {
|
||||
return successResponse(id, { result: 'shutting down' });
|
||||
}
|
||||
|
||||
/**
|
||||
* MessageBuffer - Handles newline-delimited message framing
|
||||
*
|
||||
@@ -483,5 +492,6 @@ module.exports = {
|
||||
renderSpaResponse,
|
||||
pingResponse,
|
||||
flushCacheResponse,
|
||||
shutdownResponse,
|
||||
MessageBuffer
|
||||
};
|
||||
|
||||
27
node_modules/@jqhtml/ssr/src/server.js
generated
vendored
27
node_modules/@jqhtml/ssr/src/server.js
generated
vendored
@@ -17,6 +17,7 @@ const {
|
||||
renderSpaResponse,
|
||||
pingResponse,
|
||||
flushCacheResponse,
|
||||
shutdownResponse,
|
||||
errorResponse,
|
||||
MessageBuffer
|
||||
} = require('./protocol.js');
|
||||
@@ -158,6 +159,12 @@ class SSRServer {
|
||||
response = await this._handleRenderSpa(request);
|
||||
break;
|
||||
|
||||
case 'shutdown':
|
||||
socket.write(shutdownResponse(request.id));
|
||||
await this.stop();
|
||||
process.exit(0);
|
||||
return;
|
||||
|
||||
default:
|
||||
response = errorResponse(
|
||||
request.id,
|
||||
@@ -253,11 +260,15 @@ class SSRServer {
|
||||
// Initialize environment
|
||||
env.init();
|
||||
|
||||
// Execute bundle code
|
||||
// Execute bundle code (scoped IIFE prevents class declaration leaks across requests)
|
||||
const execStartTime = Date.now();
|
||||
env.execute(bundleCode, `bundles:${cacheKey}`);
|
||||
env.executeScoped(bundleCode, `bundles:${cacheKey}`);
|
||||
bundleLoadMs += Date.now() - execStartTime;
|
||||
|
||||
// Boot the RSpade framework (runs full 10-phase lifecycle)
|
||||
// Framework classes detect window.__SSR__ and skip browser-only init
|
||||
await env.boot();
|
||||
|
||||
// Check if jqhtml loaded
|
||||
if (!env.isJqhtmlReady()) {
|
||||
throw new Error('jqhtml runtime not available after loading bundles');
|
||||
@@ -284,7 +295,7 @@ class SSRServer {
|
||||
total_ms: totalMs,
|
||||
bundle_load_ms: bundleLoadMs,
|
||||
render_ms: renderMs
|
||||
});
|
||||
}, result.preload);
|
||||
|
||||
} catch (err) {
|
||||
// Determine error type
|
||||
@@ -499,4 +510,14 @@ Options:
|
||||
await server.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGHUP', () => {});
|
||||
|
||||
process.on('unhandledRejection', (err) => {
|
||||
console.error('[SSR] Unhandled rejection:', err);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('[SSR] Uncaught exception:', err);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user