@jay-framework/stack-server-runtime 0.15.6 → 0.16.0

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 (3) hide show
  1. package/dist/index.d.ts +68 -13
  2. package/dist/index.js +249 -15
  3. package/package.json +13 -13
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { AnyJayStackComponentDefinition, PageProps, AnySlowlyRenderResult, UrlParams, JayStackComponentDefinition, AnyFastRenderResult, HttpMethod, CacheOptions, JayAction, JayActionDefinition, HeadTag, ServiceMarker } from '@jay-framework/fullstack-component';
2
- import { JayComponentCore } from '@jay-framework/component';
1
+ import { AnyJayStackComponentDefinition, PageProps, AnySlowlyRenderResult, UrlParams, AnyFastRenderResult, HttpMethod, CacheOptions, JayAction, JayActionDefinition, JayStreamAction, JayStreamActionDefinition, HeadTag, ServiceMarker } from '@jay-framework/fullstack-component';
3
2
  import { ViteDevServer } from 'vite';
4
3
  import { JayRoute } from '@jay-framework/stack-route-scanner';
5
4
  import { WithValidations, JsonSchemaProperty, PluginManifest } from '@jay-framework/compiler-shared';
@@ -76,8 +75,11 @@ interface SlowlyChangingPhase {
76
75
  declare class DevSlowlyChangingPhase implements SlowlyChangingPhase {
77
76
  runSlowlyForPage(pageParams: UrlParams, pageProps: PageProps, parts: Array<DevServerPagePart>, discoveredInstances?: DiscoveredHeadlessInstance[], headlessInstanceComponents?: HeadlessInstanceComponent[], jayHtmlPath?: string): Promise<AnySlowlyRenderResult>;
78
77
  }
79
- declare function runLoadParams<Refs extends object, SlowVS extends object, FastVS extends object, InteractiveVS extends object, Services extends Array<any>, Contexts extends Array<any>, PropsT extends object, Params extends UrlParams, CompCore extends JayComponentCore<PropsT, InteractiveVS>>(compDefinition: JayStackComponentDefinition<Refs, SlowVS, FastVS, InteractiveVS, Services, Contexts, PropsT, Params, CompCore>, services: Services): Promise<void>;
80
- declare function runSlowlyChangingRender<Refs extends object, SlowVS extends object, FastVS extends object, InteractiveVS extends object, Services extends Array<any>, Contexts extends Array<any>, PropsT extends object, Params extends UrlParams, CompCore extends JayComponentCore<PropsT, InteractiveVS>>(compDefinition: JayStackComponentDefinition<Refs, SlowVS, FastVS, InteractiveVS, Services, Contexts, PropsT, Params, CompCore>): void;
78
+ /**
79
+ * Run loadParams for all parts (page + keyed headless components).
80
+ * Yields param batches from each part that has loadParams.
81
+ */
82
+ declare function runLoadParams(parts: DevServerPagePart[]): AsyncGenerator<Record<string, string>[]>;
81
83
 
82
84
  /**
83
85
  * Server-side slow render orchestration for headless component instances.
@@ -191,23 +193,43 @@ declare function resolveActionMetadataPath(actionPath: string, pluginDir: string
191
193
  */
192
194
 
193
195
  /**
194
- * Registered action entry with resolved metadata.
196
+ * Base fields shared by all registered action types.
195
197
  */
196
- interface RegisteredAction {
198
+ interface RegisteredActionBase {
197
199
  /** Unique action name */
198
200
  actionName: string;
199
201
  /** HTTP method */
200
202
  method: HttpMethod;
201
- /** Cache options (for GET requests) */
202
- cacheOptions?: CacheOptions;
203
203
  /** Service markers for dependency injection */
204
204
  services: any[];
205
- /** The handler function */
206
- handler: (input: any, ...services: any[]) => Promise<any>;
207
205
  /** Optional metadata from .jay-action file (description, input/output schemas).
208
206
  * Actions with metadata are exposed to AI agents; those without are not. */
209
207
  metadata?: ActionMetadata;
210
208
  }
209
+ /**
210
+ * Registered request-response action entry.
211
+ * Uses `isStreaming` as a discriminator for the union.
212
+ */
213
+ interface RegisteredAction extends RegisteredActionBase {
214
+ /** Discriminator: false or absent for regular actions */
215
+ isStreaming?: false;
216
+ /** Cache options (for GET requests) */
217
+ cacheOptions?: CacheOptions;
218
+ /** The handler function */
219
+ handler: (input: any, ...services: any[]) => Promise<any>;
220
+ }
221
+ /**
222
+ * Registered streaming action entry (DL#129).
223
+ */
224
+ interface RegisteredStreamAction extends RegisteredActionBase {
225
+ method: 'POST';
226
+ /** Discriminator: true for streaming actions */
227
+ isStreaming: true;
228
+ /** The generator handler function */
229
+ handler: (input: any, ...services: any[]) => AsyncIterable<any>;
230
+ }
231
+ /** Union of all registered action types, discriminated by `isStreaming`. */
232
+ type RegisteredActionEntry = RegisteredAction | RegisteredStreamAction;
211
233
  /**
212
234
  * Result of executing an action.
213
235
  */
@@ -258,7 +280,7 @@ declare class ActionRegistry {
258
280
  * @param actionName - The unique action name
259
281
  * @returns The registered action or undefined
260
282
  */
261
- get(actionName: string): RegisteredAction | undefined;
283
+ get(actionName: string): RegisteredActionEntry | undefined;
262
284
  /**
263
285
  * Checks if an action is registered.
264
286
  *
@@ -310,6 +332,18 @@ declare class ActionRegistry {
310
332
  * @returns Cache-Control header value or undefined
311
333
  */
312
334
  getCacheHeaders(actionName: string): string | undefined;
335
+ /**
336
+ * Register a streaming action.
337
+ */
338
+ registerStream<I, C, S extends any[]>(action: JayStreamAction<I, C> & JayStreamActionDefinition<I, C, S>): void;
339
+ /**
340
+ * Check if a registered action is a streaming action.
341
+ */
342
+ isStreaming(actionName: string): boolean;
343
+ /**
344
+ * Execute a streaming action, returning an async iterable of chunks.
345
+ */
346
+ executeStream(actionName: string, input: unknown): AsyncGenerator<any>;
313
347
  }
314
348
  /**
315
349
  * Default action registry instance.
@@ -325,7 +359,7 @@ declare function registerAction<I, O, S extends any[]>(action: JayAction<I, O> &
325
359
  * Retrieves a registered action by name from the default registry.
326
360
  * @deprecated Use actionRegistry.get() instead
327
361
  */
328
- declare function getRegisteredAction(actionName: string): RegisteredAction | undefined;
362
+ declare function getRegisteredAction(actionName: string): RegisteredActionEntry | undefined;
329
363
  /**
330
364
  * Checks if an action is registered in the default registry.
331
365
  * @deprecated Use actionRegistry.has() instead
@@ -575,6 +609,8 @@ interface GenerateClientScriptOptions {
575
609
  * so that AI/automation tools can see the complete page state.
576
610
  */
577
611
  slowViewState?: object;
612
+ /** Route pattern (e.g., /products/kitan{/:category}) for freeze entries */
613
+ routePattern?: string;
578
614
  }
579
615
  /**
580
616
  * Shared fragments generated by buildScriptFragments().
@@ -637,6 +673,18 @@ declare function generateSSRPageHtml(vite: ViteDevServer, jayHtmlContent: string
637
673
  sourceDir?: string,
638
674
  /** Head tags to inject into <head> during SSR (Design Log #127) */
639
675
  headTags?: HeadTag[]): Promise<string>;
676
+ /**
677
+ * Generate a frozen page — pure SSR HTML with no client scripts (DL#127).
678
+ *
679
+ * Uses the same server element module as generateSSRPageHtml, but:
680
+ * - No hydration script
681
+ * - No Vite client
682
+ * - No component runtime
683
+ * - Just rendered HTML + CSS
684
+ *
685
+ * @param format - 'page' for full HTML document, 'fragment' for body-only (shadow DOM)
686
+ */
687
+ declare function generateFrozenPageHtml(vite: ViteDevServer, jayHtmlContent: string, jayHtmlFilename: string, jayHtmlDir: string, viewState: object, buildFolder: string, projectRoot: string, routeDir: string, tsConfigFilePath?: string, sourceDir?: string, format?: 'page' | 'fragment', freezeName?: string): Promise<string>;
640
688
 
641
689
  /**
642
690
  * Service registry for Jay Stack server-side dependency injection.
@@ -935,6 +983,11 @@ interface ContextIndexEntry {
935
983
  description?: string;
936
984
  doc?: string;
937
985
  }
986
+ /** Route entry in plugins-index.yaml (DL#130) */
987
+ interface RouteIndexEntry {
988
+ path: string;
989
+ description?: string;
990
+ }
938
991
  /** Entry for plugins-index.yaml (Design Log #85) */
939
992
  interface PluginsIndexEntry {
940
993
  name: string;
@@ -946,6 +999,8 @@ interface PluginsIndexEntry {
946
999
  services?: ServiceIndexEntry[];
947
1000
  /** Client-side contexts provided by this plugin (DL#125) */
948
1001
  contexts?: ContextIndexEntry[];
1002
+ /** Plugin-provided routes (DL#130) */
1003
+ routes?: RouteIndexEntry[];
949
1004
  }
950
1005
  interface PluginsIndex {
951
1006
  plugins: PluginsIndexEntry[];
@@ -1202,4 +1257,4 @@ declare function mergeHeadTags(sources: HeadTag[][]): HeadTag[];
1202
1257
  */
1203
1258
  declare function serializeHeadTags(tags: HeadTag[]): string;
1204
1259
 
1205
- export { type ActionDiscoveryOptions, type ActionDiscoveryResult, type ActionErrorResponse, type ActionExecutionResult, type ActionIndexEntry, type ActionMetadata, ActionRegistry, type ActionSchema, type ContextIndexEntry, type DevServerPagePart, DevSlowlyChangingPhase, type GenerateClientScriptOptions, type HeadlessInstanceComponent, type InstancePhaseData, type InstanceSlowRenderResult, type LoadedPageParts, type MaterializeContractsOptions, type MaterializeResult, type PluginActionDiscoveryOptions, type PluginClientInitInfo, type PluginContractEntry, type PluginInitDiscoveryOptions, type PluginReferencesContext, type PluginReferencesHandler, type PluginReferencesResult, type PluginScanOptions, type PluginSetupContext, type PluginSetupHandler, type PluginSetupResult, type PluginWithInit, type PluginWithReferences, type PluginWithSetup, type PluginsIndex, type PluginsIndexEntry, type ProjectClientInitInfo, type RegisteredAction, type ScannedPlugin, type ScriptFragments, type ServiceIndexEntry, SlowRenderCache, type SlowRenderCacheEntry, type SlowlyChangingPhase, type ViteSSRLoader, actionRegistry, buildAutomationWrap, buildScriptFragments, clearActionRegistry, clearClientInitData, clearLifecycleCallbacks, clearServerElementCache, clearServiceRegistry, discoverAllPluginActions, discoverAndRegisterActions, discoverPluginActions, discoverPluginsWithInit, discoverPluginsWithReferences, discoverPluginsWithSetup, executeAction, executePluginReferences, executePluginServerInits, executePluginSetup, generateClientScript, generatePromiseReconstruction, generateSSRPageHtml, getActionCacheHeaders, getClientInitData, getClientInitDataForKey, getRegisteredAction, getRegisteredActionNames, getService, getServiceRegistry, hasAction, hasService, invalidateServerElementCache, listContracts, loadActionMetadata, loadPageParts, materializeContracts, mergeHeadTags, onInit, onShutdown, parseActionMetadata, preparePluginClientInits, registerAction, registerService, renderFastChangingData, resolveActionMetadataPath, resolveServices, resolveViewStatePromises, runInitCallbacks, runLoadParams, runShutdownCallbacks, runSlowlyChangingRender, scanPlugins, serializeHeadTags, setClientInitData, slowRenderInstances, sortPluginsByDependencies, tagIdentityKey, validateForEachInstances };
1260
+ export { type ActionDiscoveryOptions, type ActionDiscoveryResult, type ActionErrorResponse, type ActionExecutionResult, type ActionIndexEntry, type ActionMetadata, ActionRegistry, type ActionSchema, type ContextIndexEntry, type DevServerPagePart, DevSlowlyChangingPhase, type GenerateClientScriptOptions, type HeadlessInstanceComponent, type InstancePhaseData, type InstanceSlowRenderResult, type LoadedPageParts, type MaterializeContractsOptions, type MaterializeResult, type PluginActionDiscoveryOptions, type PluginClientInitInfo, type PluginContractEntry, type PluginInitDiscoveryOptions, type PluginReferencesContext, type PluginReferencesHandler, type PluginReferencesResult, type PluginScanOptions, type PluginSetupContext, type PluginSetupHandler, type PluginSetupResult, type PluginWithInit, type PluginWithReferences, type PluginWithSetup, type PluginsIndex, type PluginsIndexEntry, type ProjectClientInitInfo, type RegisteredAction, type RegisteredActionBase, type RegisteredActionEntry, type RegisteredStreamAction, type RouteIndexEntry, type ScannedPlugin, type ScriptFragments, type ServiceIndexEntry, SlowRenderCache, type SlowRenderCacheEntry, type SlowlyChangingPhase, type ViteSSRLoader, actionRegistry, buildAutomationWrap, buildScriptFragments, clearActionRegistry, clearClientInitData, clearLifecycleCallbacks, clearServerElementCache, clearServiceRegistry, discoverAllPluginActions, discoverAndRegisterActions, discoverPluginActions, discoverPluginsWithInit, discoverPluginsWithReferences, discoverPluginsWithSetup, executeAction, executePluginReferences, executePluginServerInits, executePluginSetup, generateClientScript, generateFrozenPageHtml, generatePromiseReconstruction, generateSSRPageHtml, getActionCacheHeaders, getClientInitData, getClientInitDataForKey, getRegisteredAction, getRegisteredActionNames, getService, getServiceRegistry, hasAction, hasService, invalidateServerElementCache, listContracts, loadActionMetadata, loadPageParts, materializeContracts, mergeHeadTags, onInit, onShutdown, parseActionMetadata, preparePluginClientInits, registerAction, registerService, renderFastChangingData, resolveActionMetadataPath, resolveServices, resolveViewStatePromises, runInitCallbacks, runLoadParams, runShutdownCallbacks, scanPlugins, serializeHeadTags, setClientInitData, slowRenderInstances, sortPluginsByDependencies, tagIdentityKey, validateForEachInstances };
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ var __publicField = (obj, key, value) => {
4
4
  __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
5
  return value;
6
6
  };
7
- import { phaseOutput, isJayAction } from "@jay-framework/fullstack-component";
7
+ import { phaseOutput, isJayAction, isJayStreamAction } from "@jay-framework/fullstack-component";
8
8
  import "prettier";
9
9
  import "js-beautify";
10
10
  import fs$1 from "fs";
@@ -178,10 +178,15 @@ class DevSlowlyChangingPhase {
178
178
  return phaseOutput(slowlyViewState, carryForward);
179
179
  }
180
180
  }
181
- async function runLoadParams(compDefinition, services) {
182
- compDefinition.loadParams(services);
183
- }
184
- function runSlowlyChangingRender(compDefinition) {
181
+ async function* runLoadParams(parts) {
182
+ for (const part of parts) {
183
+ if (part.compDefinition.loadParams) {
184
+ const services = resolveServices(part.compDefinition.services);
185
+ for await (const batch of part.compDefinition.loadParams(services)) {
186
+ yield batch;
187
+ }
188
+ }
189
+ }
185
190
  }
186
191
  var __defProp2 = Object.defineProperty;
187
192
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -403,11 +408,7 @@ async function renderFastChangingData(pageParams, pageProps, carryForward, parts
403
408
  metadata: contractInfo.metadata
404
409
  }
405
410
  };
406
- const fastRenderedPart = await compDefinition.fastRender(
407
- partProps,
408
- partSlowlyCarryForward,
409
- ...services
410
- );
411
+ const fastRenderedPart = compDefinition.slowlyRender ? await compDefinition.fastRender(partProps, partSlowlyCarryForward, ...services) : await compDefinition.fastRender(partProps, ...services);
411
412
  if (fastRenderedPart.kind === "PhaseOutput") {
412
413
  if (!key) {
413
414
  fastViewState = { ...fastViewState, ...fastRenderedPart.rendered };
@@ -576,6 +577,80 @@ ${parts.map((part) => " " + part.clientPart).join(",\n")}
576
577
  slowViewStateDecl
577
578
  };
578
579
  }
580
+ function buildFreezeScript(routePattern) {
581
+ const routePatternLiteral = routePattern ? `'${routePattern}'` : "undefined";
582
+ return `
583
+ // Page Freeze (DL#127, DL#128 iframe addendum)
584
+ // Sticky embed mode: URL param sets a session cookie so in-iframe navigation preserves it
585
+ if (new URLSearchParams(window.location.search).has('_jay_embed')) {
586
+ document.cookie = '_jay_embed=1;path=/;samesite=lax';
587
+ }
588
+ const __jayEmbedMode = document.cookie.split(';').some(c => c.trim().startsWith('_jay_embed='));
589
+
590
+ async function __jayDoFreeze() {
591
+ const automation = window.__jay?.automation;
592
+ if (!automation) return;
593
+
594
+ // Visual feedback: white flash
595
+ const flash = document.createElement('div');
596
+ flash.style.cssText = 'position:fixed;inset:0;background:white;z-index:999999;opacity:0.8;pointer-events:none;transition:opacity 0.3s';
597
+ document.body.appendChild(flash);
598
+ requestAnimationFrame(() => { flash.style.opacity = '0'; });
599
+ setTimeout(() => flash.remove(), 400);
600
+
601
+ // Audio feedback: camera shutter
602
+ try {
603
+ const ctx = new AudioContext();
604
+ const buf = ctx.createBuffer(1, ctx.sampleRate * 0.15, ctx.sampleRate);
605
+ const data = buf.getChannelData(0);
606
+ for (let i = 0; i < data.length; i++) {
607
+ data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.02));
608
+ }
609
+ const src = ctx.createBufferSource();
610
+ src.buffer = buf;
611
+ src.connect(ctx.destination);
612
+ src.start();
613
+ } catch {}
614
+
615
+ // Capture and save
616
+ try {
617
+ const state = automation.getPageState();
618
+ const route = window.location.pathname;
619
+ const routePattern = ${routePatternLiteral};
620
+ const resp = await fetch('/_jay/freeze', {
621
+ method: 'POST',
622
+ headers: { 'Content-Type': 'application/json' },
623
+ body: JSON.stringify({ route, routePattern, viewState: state.viewState }),
624
+ });
625
+ const { id } = await resp.json();
626
+ if (__jayEmbedMode) {
627
+ window.parent.postMessage({ type: 'jay:freeze', id, route }, '*');
628
+ } else {
629
+ window.open(route + '?_jay_freeze=' + id, '_blank');
630
+ }
631
+ } catch (err) {
632
+ console.error('[Freeze] Failed:', err);
633
+ }
634
+ }
635
+
636
+ if (__jayEmbedMode) {
637
+ // Notify parent of the current route on load (DL#128 route addendum)
638
+ window.parent.postMessage({ type: 'jay:route', route: window.location.pathname, routePattern: ${routePatternLiteral} }, '*');
639
+
640
+ // Embed mode: parent triggers freeze via postMessage
641
+ window.addEventListener('message', (e) => {
642
+ if (e.data?.type === 'jay:requestFreeze') __jayDoFreeze();
643
+ });
644
+ } else {
645
+ // Standalone: Alt+S / Option+S keyboard shortcut
646
+ document.addEventListener('keydown', (e) => {
647
+ if (e.altKey && e.code === 'KeyS') {
648
+ e.preventDefault();
649
+ __jayDoFreeze();
650
+ }
651
+ });
652
+ }`;
653
+ }
579
654
  function buildAutomationWrap(options, mode) {
580
655
  const { enableAutomation = true, slowViewState } = options;
581
656
  const hasSlowViewState = slowViewState && Object.keys(slowViewState).length > 0;
@@ -586,6 +661,7 @@ function buildAutomationWrap(options, mode) {
586
661
  }
587
662
  const appendLine = appendDom ? `
588
663
  target.appendChild(wrapped.element.dom);` : "";
664
+ const freezeScript = buildFreezeScript(options.routePattern);
589
665
  if (hasSlowViewState) {
590
666
  return `
591
667
  // Wrap with automation for dev tooling
@@ -595,7 +671,8 @@ function buildAutomationWrap(options, mode) {
595
671
  registerGlobalContext(AUTOMATION_CONTEXT, wrapped.automation);
596
672
  window.__jay = window.__jay || {};
597
673
  window.__jay.automation = wrapped.automation;
598
- window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}`;
674
+ window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}
675
+ ${freezeScript}`;
599
676
  }
600
677
  return `
601
678
  // Wrap with automation for dev tooling
@@ -603,7 +680,8 @@ function buildAutomationWrap(options, mode) {
603
680
  registerGlobalContext(AUTOMATION_CONTEXT, wrapped.automation);
604
681
  window.__jay = window.__jay || {};
605
682
  window.__jay.automation = wrapped.automation;
606
- window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}`;
683
+ window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}
684
+ ${freezeScript}`;
607
685
  }
608
686
  async function resolveViewStatePromises(viewState) {
609
687
  const entries = Object.entries(viewState);
@@ -830,6 +908,81 @@ ${titleHtml}${headExtras ? headExtras + "\n" : ""} </head>
830
908
  </body>
831
909
  </html>`;
832
910
  }
911
+ async function generateFrozenPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, viewState, buildFolder, projectRoot, routeDir, tsConfigFilePath, sourceDir, format = "page", freezeName) {
912
+ const jayHtmlPath = path__default.join(jayHtmlDir, jayHtmlFilename);
913
+ let cached = serverModuleCache.get(jayHtmlPath);
914
+ if (!cached) {
915
+ cached = await compileAndLoadServerElement(
916
+ vite,
917
+ jayHtmlContent,
918
+ jayHtmlFilename,
919
+ jayHtmlDir,
920
+ buildFolder,
921
+ projectRoot,
922
+ routeDir,
923
+ tsConfigFilePath,
924
+ sourceDir
925
+ );
926
+ serverModuleCache.set(jayHtmlPath, cached);
927
+ }
928
+ const htmlChunks = [];
929
+ const ctx = {
930
+ write: (chunk) => {
931
+ htmlChunks.push(chunk);
932
+ },
933
+ onAsync: () => {
934
+ }
935
+ };
936
+ cached.renderToStream(viewState, ctx);
937
+ const ssrHtml = htmlChunks.join("");
938
+ if (format === "fragment") {
939
+ let inlineCss = "";
940
+ if (cached.cssHref) {
941
+ try {
942
+ const cssPath = cached.cssHref.replace(/^\/@fs/, "").replace(/\?.*$/, "");
943
+ const cssContent = await fs$2.readFile(cssPath, "utf-8");
944
+ inlineCss = `<style>${cssContent}</style>`;
945
+ } catch {
946
+ inlineCss = `<link rel="stylesheet" href="${cached.cssHref}" />`;
947
+ }
948
+ }
949
+ return `${inlineCss}
950
+ ${ssrHtml}`;
951
+ }
952
+ const headLinksHtml = cached.headLinks.map((link) => {
953
+ const attrs = Object.entries(link.attributes).map(([k, v]) => ` ${k}="${v}"`).join("");
954
+ return ` <link rel="${link.rel}" href="${link.href}"${attrs} />`;
955
+ }).join("\n");
956
+ const cssLink = cached.cssHref ? ` <link rel="stylesheet" href="${cached.cssHref}" />` : "";
957
+ const headExtras = [headLinksHtml, cssLink].filter((_) => _).join("\n");
958
+ const label = freezeName ? ` — ${freezeName}` : "";
959
+ return `<!doctype html>
960
+ <html lang="en">
961
+ <head>
962
+ <meta charset="UTF-8" />
963
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
964
+ <title>Frozen${label}</title>
965
+ ${headExtras ? headExtras + "\n" : ""} <style>
966
+ body::before {
967
+ content: 'FROZEN${label ? `: ${freezeName}` : ""}';
968
+ position: fixed;
969
+ top: 0;
970
+ right: 0;
971
+ background: #1a1a2e;
972
+ color: #e0e0ff;
973
+ padding: 2px 10px;
974
+ font: 11px/1.6 system-ui;
975
+ z-index: 99999;
976
+ border-bottom-left-radius: 4px;
977
+ opacity: 0.8;
978
+ }
979
+ </style>
980
+ </head>
981
+ <body>
982
+ <div id="target">${ssrHtml}</div>
983
+ </body>
984
+ </html>`;
985
+ }
833
986
  function rebaseRelativeImports(code, fromDir, toDir) {
834
987
  return code.replace(/from "(\.\.\/[^"]+)"/g, (_match, relPath) => {
835
988
  const absolutePath = path__default.resolve(fromDir, relPath);
@@ -1226,6 +1379,16 @@ class ActionRegistry {
1226
1379
  }
1227
1380
  };
1228
1381
  }
1382
+ if (action.isStreaming) {
1383
+ return {
1384
+ success: false,
1385
+ error: {
1386
+ code: "STREAMING_ACTION",
1387
+ message: `Action '${actionName}' is a streaming action — use executeStream() instead`,
1388
+ isActionError: false
1389
+ }
1390
+ };
1391
+ }
1229
1392
  try {
1230
1393
  const services = resolveServices(action.services);
1231
1394
  const result = await action.handler(input, ...services);
@@ -1279,6 +1442,40 @@ class ActionRegistry {
1279
1442
  }
1280
1443
  return parts.length > 0 ? parts.join(", ") : void 0;
1281
1444
  }
1445
+ // --- Streaming actions (DL#129) ---
1446
+ /**
1447
+ * Register a streaming action.
1448
+ */
1449
+ registerStream(action) {
1450
+ const entry = {
1451
+ actionName: action.actionName,
1452
+ method: "POST",
1453
+ isStreaming: true,
1454
+ services: action.services,
1455
+ handler: action.handler
1456
+ };
1457
+ this.actions.set(action.actionName, entry);
1458
+ }
1459
+ /**
1460
+ * Check if a registered action is a streaming action.
1461
+ */
1462
+ isStreaming(actionName) {
1463
+ const action = this.actions.get(actionName);
1464
+ return !!action?.isStreaming;
1465
+ }
1466
+ /**
1467
+ * Execute a streaming action, returning an async iterable of chunks.
1468
+ */
1469
+ async *executeStream(actionName, input) {
1470
+ const action = this.actions.get(actionName);
1471
+ if (!action || !action.isStreaming) {
1472
+ throw new Error(`Streaming action '${actionName}' not found`);
1473
+ }
1474
+ const services = resolveServices(action.services);
1475
+ for await (const chunk of action.handler(input, ...services)) {
1476
+ yield chunk;
1477
+ }
1478
+ }
1282
1479
  }
1283
1480
  const actionRegistry = new ActionRegistry();
1284
1481
  function registerAction(action) {
@@ -1402,6 +1599,15 @@ async function discoverAndRegisterActions(options) {
1402
1599
  `[Actions] Registered: ${exportValue.actionName}`
1403
1600
  );
1404
1601
  }
1602
+ } else if (isJayStreamAction(exportValue)) {
1603
+ registry.registerStream(exportValue);
1604
+ result.actionNames.push(exportValue.actionName);
1605
+ result.actionCount++;
1606
+ if (verbose) {
1607
+ getLogger().info(
1608
+ `[Actions] Registered stream: ${exportValue.actionName}`
1609
+ );
1610
+ }
1405
1611
  }
1406
1612
  }
1407
1613
  } catch (error) {
@@ -1558,6 +1764,13 @@ async function registerNpmPluginActions(packageName, pluginConfig, pluginDir, re
1558
1764
  if (verbose) {
1559
1765
  getLogger().info(`[Actions] Registered NPM plugin action: ${registeredName}`);
1560
1766
  }
1767
+ } else if (actionExport && isJayStreamAction(actionExport)) {
1768
+ registry.registerStream(actionExport);
1769
+ const registeredName = actionExport.actionName;
1770
+ registeredActions.push(registeredName);
1771
+ if (verbose) {
1772
+ getLogger().info(`[Actions] Registered NPM plugin stream: ${registeredName}`);
1773
+ }
1561
1774
  } else {
1562
1775
  getLogger().warn(
1563
1776
  `[Actions] NPM plugin "${packageName}" declares action "${actionName}" but it's not exported or not a JayAction`
@@ -1626,6 +1839,13 @@ async function discoverPluginActions(pluginPath, projectRoot, registry = actionR
1626
1839
  if (verbose) {
1627
1840
  getLogger().info(`[Actions] Registered plugin action: ${registeredName}`);
1628
1841
  }
1842
+ } else if (actionExport && isJayStreamAction(actionExport)) {
1843
+ registry.registerStream(actionExport);
1844
+ const registeredName = actionExport.actionName;
1845
+ registeredActions.push(registeredName);
1846
+ if (verbose) {
1847
+ getLogger().info(`[Actions] Registered plugin stream: ${registeredName}`);
1848
+ }
1629
1849
  } else {
1630
1850
  getLogger().warn(
1631
1851
  `[Actions] Plugin "${pluginName}" declares action "${actionName}" but it's not exported or not a JayAction`
@@ -2324,6 +2544,12 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2324
2544
  };
2325
2545
  });
2326
2546
  }
2547
+ if (manifest.routes?.length) {
2548
+ entry.routes = manifest.routes.map((r) => ({
2549
+ path: r.path,
2550
+ ...r.description && { description: r.description }
2551
+ }));
2552
+ }
2327
2553
  pluginsIndexMap.set(plugin.name, entry);
2328
2554
  }
2329
2555
  if (!dynamicOnly && manifest.contracts) {
@@ -2448,7 +2674,8 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2448
2674
  contracts: data.contracts,
2449
2675
  ...data.actions && data.actions.length > 0 && { actions: data.actions },
2450
2676
  ...data.services?.length && { services: data.services },
2451
- ...data.contexts?.length && { contexts: data.contexts }
2677
+ ...data.contexts?.length && { contexts: data.contexts },
2678
+ ...data.routes?.length && { routes: data.routes }
2452
2679
  }))
2453
2680
  };
2454
2681
  fs.mkdirSync(outputDir, { recursive: true });
@@ -2505,6 +2732,12 @@ async function listContracts(options) {
2505
2732
  };
2506
2733
  });
2507
2734
  }
2735
+ if (manifest.routes?.length) {
2736
+ entry.routes = manifest.routes.map((r) => ({
2737
+ path: r.path,
2738
+ ...r.description && { description: r.description }
2739
+ }));
2740
+ }
2508
2741
  pluginsMap.set(plugin.name, entry);
2509
2742
  }
2510
2743
  if (!dynamicOnly && manifest.contracts) {
@@ -2551,7 +2784,8 @@ async function listContracts(options) {
2551
2784
  path: data.path,
2552
2785
  contracts: data.contracts,
2553
2786
  ...data.services?.length && { services: data.services },
2554
- ...data.contexts?.length && { contexts: data.contexts }
2787
+ ...data.contexts?.length && { contexts: data.contexts },
2788
+ ...data.routes?.length && { routes: data.routes }
2555
2789
  }))
2556
2790
  };
2557
2791
  }
@@ -2705,6 +2939,7 @@ export {
2705
2939
  executePluginServerInits,
2706
2940
  executePluginSetup,
2707
2941
  generateClientScript,
2942
+ generateFrozenPageHtml,
2708
2943
  generatePromiseReconstruction,
2709
2944
  generateSSRPageHtml,
2710
2945
  getActionCacheHeaders,
@@ -2735,7 +2970,6 @@ export {
2735
2970
  runInitCallbacks,
2736
2971
  runLoadParams,
2737
2972
  runShutdownCallbacks,
2738
- runSlowlyChangingRender,
2739
2973
  scanPlugins,
2740
2974
  serializeHeadTags,
2741
2975
  setClientInitData,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/stack-server-runtime",
3
- "version": "0.15.6",
3
+ "version": "0.16.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.mts",
@@ -26,21 +26,21 @@
26
26
  "test:watch": "vitest"
27
27
  },
28
28
  "dependencies": {
29
- "@jay-framework/compiler-jay-html": "^0.15.6",
30
- "@jay-framework/compiler-shared": "^0.15.6",
31
- "@jay-framework/component": "^0.15.6",
32
- "@jay-framework/fullstack-component": "^0.15.6",
33
- "@jay-framework/logger": "^0.15.6",
34
- "@jay-framework/runtime": "^0.15.6",
35
- "@jay-framework/ssr-runtime": "^0.15.6",
36
- "@jay-framework/stack-route-scanner": "^0.15.6",
37
- "@jay-framework/view-state-merge": "^0.15.6",
29
+ "@jay-framework/compiler-jay-html": "^0.16.0",
30
+ "@jay-framework/compiler-shared": "^0.16.0",
31
+ "@jay-framework/component": "^0.16.0",
32
+ "@jay-framework/fullstack-component": "^0.16.0",
33
+ "@jay-framework/logger": "^0.16.0",
34
+ "@jay-framework/runtime": "^0.16.0",
35
+ "@jay-framework/ssr-runtime": "^0.16.0",
36
+ "@jay-framework/stack-route-scanner": "^0.16.0",
37
+ "@jay-framework/view-state-merge": "^0.16.0",
38
38
  "yaml": "^2.3.4"
39
39
  },
40
40
  "devDependencies": {
41
- "@jay-framework/dev-environment": "^0.15.6",
42
- "@jay-framework/jay-cli": "^0.15.6",
43
- "@jay-framework/stack-client-runtime": "^0.15.6",
41
+ "@jay-framework/dev-environment": "^0.16.0",
42
+ "@jay-framework/jay-cli": "^0.16.0",
43
+ "@jay-framework/stack-client-runtime": "^0.16.0",
44
44
  "@types/express": "^5.0.2",
45
45
  "@types/node": "^22.15.21",
46
46
  "nodemon": "^3.0.3",