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

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 +70 -13
  2. package/dist/index.js +256 -16
  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,45 @@ 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;
208
+ /** Whether this action accepts file uploads (DL#131) */
209
+ acceptsFiles?: boolean;
210
210
  }
211
+ /**
212
+ * Registered request-response action entry.
213
+ * Uses `isStreaming` as a discriminator for the union.
214
+ */
215
+ interface RegisteredAction extends RegisteredActionBase {
216
+ /** Discriminator: false or absent for regular actions */
217
+ isStreaming?: false;
218
+ /** Cache options (for GET requests) */
219
+ cacheOptions?: CacheOptions;
220
+ /** The handler function */
221
+ handler: (input: any, ...services: any[]) => Promise<any>;
222
+ }
223
+ /**
224
+ * Registered streaming action entry (DL#129).
225
+ */
226
+ interface RegisteredStreamAction extends RegisteredActionBase {
227
+ method: 'POST';
228
+ /** Discriminator: true for streaming actions */
229
+ isStreaming: true;
230
+ /** The generator handler function */
231
+ handler: (input: any, ...services: any[]) => AsyncIterable<any>;
232
+ }
233
+ /** Union of all registered action types, discriminated by `isStreaming`. */
234
+ type RegisteredActionEntry = RegisteredAction | RegisteredStreamAction;
211
235
  /**
212
236
  * Result of executing an action.
213
237
  */
@@ -258,7 +282,7 @@ declare class ActionRegistry {
258
282
  * @param actionName - The unique action name
259
283
  * @returns The registered action or undefined
260
284
  */
261
- get(actionName: string): RegisteredAction | undefined;
285
+ get(actionName: string): RegisteredActionEntry | undefined;
262
286
  /**
263
287
  * Checks if an action is registered.
264
288
  *
@@ -310,6 +334,18 @@ declare class ActionRegistry {
310
334
  * @returns Cache-Control header value or undefined
311
335
  */
312
336
  getCacheHeaders(actionName: string): string | undefined;
337
+ /**
338
+ * Register a streaming action.
339
+ */
340
+ registerStream<I, C, S extends any[]>(action: JayStreamAction<I, C> & JayStreamActionDefinition<I, C, S>): void;
341
+ /**
342
+ * Check if a registered action is a streaming action.
343
+ */
344
+ isStreaming(actionName: string): boolean;
345
+ /**
346
+ * Execute a streaming action, returning an async iterable of chunks.
347
+ */
348
+ executeStream(actionName: string, input: unknown): AsyncGenerator<any>;
313
349
  }
314
350
  /**
315
351
  * Default action registry instance.
@@ -325,7 +361,7 @@ declare function registerAction<I, O, S extends any[]>(action: JayAction<I, O> &
325
361
  * Retrieves a registered action by name from the default registry.
326
362
  * @deprecated Use actionRegistry.get() instead
327
363
  */
328
- declare function getRegisteredAction(actionName: string): RegisteredAction | undefined;
364
+ declare function getRegisteredAction(actionName: string): RegisteredActionEntry | undefined;
329
365
  /**
330
366
  * Checks if an action is registered in the default registry.
331
367
  * @deprecated Use actionRegistry.has() instead
@@ -575,6 +611,8 @@ interface GenerateClientScriptOptions {
575
611
  * so that AI/automation tools can see the complete page state.
576
612
  */
577
613
  slowViewState?: object;
614
+ /** Route pattern (e.g., /products/kitan{/:category}) for freeze entries */
615
+ routePattern?: string;
578
616
  }
579
617
  /**
580
618
  * Shared fragments generated by buildScriptFragments().
@@ -637,6 +675,18 @@ declare function generateSSRPageHtml(vite: ViteDevServer, jayHtmlContent: string
637
675
  sourceDir?: string,
638
676
  /** Head tags to inject into <head> during SSR (Design Log #127) */
639
677
  headTags?: HeadTag[]): Promise<string>;
678
+ /**
679
+ * Generate a frozen page — pure SSR HTML with no client scripts (DL#127).
680
+ *
681
+ * Uses the same server element module as generateSSRPageHtml, but:
682
+ * - No hydration script
683
+ * - No Vite client
684
+ * - No component runtime
685
+ * - Just rendered HTML + CSS
686
+ *
687
+ * @param format - 'page' for full HTML document, 'fragment' for body-only (shadow DOM)
688
+ */
689
+ 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
690
 
641
691
  /**
642
692
  * Service registry for Jay Stack server-side dependency injection.
@@ -935,6 +985,11 @@ interface ContextIndexEntry {
935
985
  description?: string;
936
986
  doc?: string;
937
987
  }
988
+ /** Route entry in plugins-index.yaml (DL#130) */
989
+ interface RouteIndexEntry {
990
+ path: string;
991
+ description?: string;
992
+ }
938
993
  /** Entry for plugins-index.yaml (Design Log #85) */
939
994
  interface PluginsIndexEntry {
940
995
  name: string;
@@ -946,6 +1001,8 @@ interface PluginsIndexEntry {
946
1001
  services?: ServiceIndexEntry[];
947
1002
  /** Client-side contexts provided by this plugin (DL#125) */
948
1003
  contexts?: ContextIndexEntry[];
1004
+ /** Plugin-provided routes (DL#130) */
1005
+ routes?: RouteIndexEntry[];
949
1006
  }
950
1007
  interface PluginsIndex {
951
1008
  plugins: PluginsIndexEntry[];
@@ -1202,4 +1259,4 @@ declare function mergeHeadTags(sources: HeadTag[][]): HeadTag[];
1202
1259
  */
1203
1260
  declare function serializeHeadTags(tags: HeadTag[]): string;
1204
1261
 
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 };
1262
+ 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;
@@ -199,6 +204,7 @@ new JayAtomicType("string");
199
204
  new JayAtomicType("number");
200
205
  new JayAtomicType("boolean");
201
206
  new JayAtomicType("Date");
207
+ new JayAtomicType("file");
202
208
  new JayAtomicType("Unknown");
203
209
  class JayObjectType {
204
210
  constructor(name, props) {
@@ -242,6 +248,9 @@ function jayTypeToJsonSchema(type) {
242
248
  if (name === "string" || name === "number" || name === "boolean") {
243
249
  return { type: name };
244
250
  }
251
+ if (name === "file") {
252
+ return { type: "string", description: "Binary file upload (JayFile)" };
253
+ }
245
254
  return { type: "string" };
246
255
  }
247
256
  if (isEnumType(type)) {
@@ -403,11 +412,7 @@ async function renderFastChangingData(pageParams, pageProps, carryForward, parts
403
412
  metadata: contractInfo.metadata
404
413
  }
405
414
  };
406
- const fastRenderedPart = await compDefinition.fastRender(
407
- partProps,
408
- partSlowlyCarryForward,
409
- ...services
410
- );
415
+ const fastRenderedPart = compDefinition.slowlyRender ? await compDefinition.fastRender(partProps, partSlowlyCarryForward, ...services) : await compDefinition.fastRender(partProps, ...services);
411
416
  if (fastRenderedPart.kind === "PhaseOutput") {
412
417
  if (!key) {
413
418
  fastViewState = { ...fastViewState, ...fastRenderedPart.rendered };
@@ -576,6 +581,80 @@ ${parts.map((part) => " " + part.clientPart).join(",\n")}
576
581
  slowViewStateDecl
577
582
  };
578
583
  }
584
+ function buildFreezeScript(routePattern) {
585
+ const routePatternLiteral = routePattern ? `'${routePattern}'` : "undefined";
586
+ return `
587
+ // Page Freeze (DL#127, DL#128 iframe addendum)
588
+ // Sticky embed mode: URL param sets a session cookie so in-iframe navigation preserves it
589
+ if (new URLSearchParams(window.location.search).has('_jay_embed')) {
590
+ document.cookie = '_jay_embed=1;path=/;samesite=lax';
591
+ }
592
+ const __jayEmbedMode = document.cookie.split(';').some(c => c.trim().startsWith('_jay_embed='));
593
+
594
+ async function __jayDoFreeze() {
595
+ const automation = window.__jay?.automation;
596
+ if (!automation) return;
597
+
598
+ // Visual feedback: white flash
599
+ const flash = document.createElement('div');
600
+ flash.style.cssText = 'position:fixed;inset:0;background:white;z-index:999999;opacity:0.8;pointer-events:none;transition:opacity 0.3s';
601
+ document.body.appendChild(flash);
602
+ requestAnimationFrame(() => { flash.style.opacity = '0'; });
603
+ setTimeout(() => flash.remove(), 400);
604
+
605
+ // Audio feedback: camera shutter
606
+ try {
607
+ const ctx = new AudioContext();
608
+ const buf = ctx.createBuffer(1, ctx.sampleRate * 0.15, ctx.sampleRate);
609
+ const data = buf.getChannelData(0);
610
+ for (let i = 0; i < data.length; i++) {
611
+ data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.02));
612
+ }
613
+ const src = ctx.createBufferSource();
614
+ src.buffer = buf;
615
+ src.connect(ctx.destination);
616
+ src.start();
617
+ } catch {}
618
+
619
+ // Capture and save
620
+ try {
621
+ const state = automation.getPageState();
622
+ const route = window.location.pathname;
623
+ const routePattern = ${routePatternLiteral};
624
+ const resp = await fetch('/_jay/freeze', {
625
+ method: 'POST',
626
+ headers: { 'Content-Type': 'application/json' },
627
+ body: JSON.stringify({ route, routePattern, viewState: state.viewState }),
628
+ });
629
+ const { id } = await resp.json();
630
+ if (__jayEmbedMode) {
631
+ window.parent.postMessage({ type: 'jay:freeze', id, route }, '*');
632
+ } else {
633
+ window.open(route + '?_jay_freeze=' + id, '_blank');
634
+ }
635
+ } catch (err) {
636
+ console.error('[Freeze] Failed:', err);
637
+ }
638
+ }
639
+
640
+ if (__jayEmbedMode) {
641
+ // Notify parent of the current route on load (DL#128 route addendum)
642
+ window.parent.postMessage({ type: 'jay:route', route: window.location.pathname, routePattern: ${routePatternLiteral} }, '*');
643
+
644
+ // Embed mode: parent triggers freeze via postMessage
645
+ window.addEventListener('message', (e) => {
646
+ if (e.data?.type === 'jay:requestFreeze') __jayDoFreeze();
647
+ });
648
+ } else {
649
+ // Standalone: Alt+S / Option+S keyboard shortcut
650
+ document.addEventListener('keydown', (e) => {
651
+ if (e.altKey && e.code === 'KeyS') {
652
+ e.preventDefault();
653
+ __jayDoFreeze();
654
+ }
655
+ });
656
+ }`;
657
+ }
579
658
  function buildAutomationWrap(options, mode) {
580
659
  const { enableAutomation = true, slowViewState } = options;
581
660
  const hasSlowViewState = slowViewState && Object.keys(slowViewState).length > 0;
@@ -586,6 +665,7 @@ function buildAutomationWrap(options, mode) {
586
665
  }
587
666
  const appendLine = appendDom ? `
588
667
  target.appendChild(wrapped.element.dom);` : "";
668
+ const freezeScript = buildFreezeScript(options.routePattern);
589
669
  if (hasSlowViewState) {
590
670
  return `
591
671
  // Wrap with automation for dev tooling
@@ -595,7 +675,8 @@ function buildAutomationWrap(options, mode) {
595
675
  registerGlobalContext(AUTOMATION_CONTEXT, wrapped.automation);
596
676
  window.__jay = window.__jay || {};
597
677
  window.__jay.automation = wrapped.automation;
598
- window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}`;
678
+ window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}
679
+ ${freezeScript}`;
599
680
  }
600
681
  return `
601
682
  // Wrap with automation for dev tooling
@@ -603,7 +684,8 @@ function buildAutomationWrap(options, mode) {
603
684
  registerGlobalContext(AUTOMATION_CONTEXT, wrapped.automation);
604
685
  window.__jay = window.__jay || {};
605
686
  window.__jay.automation = wrapped.automation;
606
- window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}`;
687
+ window.dispatchEvent(new Event('jay:automation-ready'));${appendLine}
688
+ ${freezeScript}`;
607
689
  }
608
690
  async function resolveViewStatePromises(viewState) {
609
691
  const entries = Object.entries(viewState);
@@ -830,6 +912,81 @@ ${titleHtml}${headExtras ? headExtras + "\n" : ""} </head>
830
912
  </body>
831
913
  </html>`;
832
914
  }
915
+ async function generateFrozenPageHtml(vite, jayHtmlContent, jayHtmlFilename, jayHtmlDir, viewState, buildFolder, projectRoot, routeDir, tsConfigFilePath, sourceDir, format = "page", freezeName) {
916
+ const jayHtmlPath = path__default.join(jayHtmlDir, jayHtmlFilename);
917
+ let cached = serverModuleCache.get(jayHtmlPath);
918
+ if (!cached) {
919
+ cached = await compileAndLoadServerElement(
920
+ vite,
921
+ jayHtmlContent,
922
+ jayHtmlFilename,
923
+ jayHtmlDir,
924
+ buildFolder,
925
+ projectRoot,
926
+ routeDir,
927
+ tsConfigFilePath,
928
+ sourceDir
929
+ );
930
+ serverModuleCache.set(jayHtmlPath, cached);
931
+ }
932
+ const htmlChunks = [];
933
+ const ctx = {
934
+ write: (chunk) => {
935
+ htmlChunks.push(chunk);
936
+ },
937
+ onAsync: () => {
938
+ }
939
+ };
940
+ cached.renderToStream(viewState, ctx);
941
+ const ssrHtml = htmlChunks.join("");
942
+ if (format === "fragment") {
943
+ let inlineCss = "";
944
+ if (cached.cssHref) {
945
+ try {
946
+ const cssPath = cached.cssHref.replace(/^\/@fs/, "").replace(/\?.*$/, "");
947
+ const cssContent = await fs$2.readFile(cssPath, "utf-8");
948
+ inlineCss = `<style>${cssContent}</style>`;
949
+ } catch {
950
+ inlineCss = `<link rel="stylesheet" href="${cached.cssHref}" />`;
951
+ }
952
+ }
953
+ return `${inlineCss}
954
+ ${ssrHtml}`;
955
+ }
956
+ const headLinksHtml = cached.headLinks.map((link) => {
957
+ const attrs = Object.entries(link.attributes).map(([k, v]) => ` ${k}="${v}"`).join("");
958
+ return ` <link rel="${link.rel}" href="${link.href}"${attrs} />`;
959
+ }).join("\n");
960
+ const cssLink = cached.cssHref ? ` <link rel="stylesheet" href="${cached.cssHref}" />` : "";
961
+ const headExtras = [headLinksHtml, cssLink].filter((_) => _).join("\n");
962
+ const label = freezeName ? ` — ${freezeName}` : "";
963
+ return `<!doctype html>
964
+ <html lang="en">
965
+ <head>
966
+ <meta charset="UTF-8" />
967
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
968
+ <title>Frozen${label}</title>
969
+ ${headExtras ? headExtras + "\n" : ""} <style>
970
+ body::before {
971
+ content: 'FROZEN${label ? `: ${freezeName}` : ""}';
972
+ position: fixed;
973
+ top: 0;
974
+ right: 0;
975
+ background: #1a1a2e;
976
+ color: #e0e0ff;
977
+ padding: 2px 10px;
978
+ font: 11px/1.6 system-ui;
979
+ z-index: 99999;
980
+ border-bottom-left-radius: 4px;
981
+ opacity: 0.8;
982
+ }
983
+ </style>
984
+ </head>
985
+ <body>
986
+ <div id="target">${ssrHtml}</div>
987
+ </body>
988
+ </html>`;
989
+ }
833
990
  function rebaseRelativeImports(code, fromDir, toDir) {
834
991
  return code.replace(/from "(\.\.\/[^"]+)"/g, (_match, relPath) => {
835
992
  const absolutePath = path__default.resolve(fromDir, relPath);
@@ -1142,7 +1299,8 @@ class ActionRegistry {
1142
1299
  method: action.method,
1143
1300
  cacheOptions: action.cacheOptions,
1144
1301
  services: action.services,
1145
- handler: action.handler
1302
+ handler: action.handler,
1303
+ ...action.acceptsFiles && { acceptsFiles: true }
1146
1304
  };
1147
1305
  this.actions.set(action.actionName, entry);
1148
1306
  }
@@ -1226,6 +1384,16 @@ class ActionRegistry {
1226
1384
  }
1227
1385
  };
1228
1386
  }
1387
+ if (action.isStreaming) {
1388
+ return {
1389
+ success: false,
1390
+ error: {
1391
+ code: "STREAMING_ACTION",
1392
+ message: `Action '${actionName}' is a streaming action — use executeStream() instead`,
1393
+ isActionError: false
1394
+ }
1395
+ };
1396
+ }
1229
1397
  try {
1230
1398
  const services = resolveServices(action.services);
1231
1399
  const result = await action.handler(input, ...services);
@@ -1279,6 +1447,41 @@ class ActionRegistry {
1279
1447
  }
1280
1448
  return parts.length > 0 ? parts.join(", ") : void 0;
1281
1449
  }
1450
+ // --- Streaming actions (DL#129) ---
1451
+ /**
1452
+ * Register a streaming action.
1453
+ */
1454
+ registerStream(action) {
1455
+ const entry = {
1456
+ actionName: action.actionName,
1457
+ method: "POST",
1458
+ isStreaming: true,
1459
+ services: action.services,
1460
+ handler: action.handler,
1461
+ ...action.acceptsFiles && { acceptsFiles: true }
1462
+ };
1463
+ this.actions.set(action.actionName, entry);
1464
+ }
1465
+ /**
1466
+ * Check if a registered action is a streaming action.
1467
+ */
1468
+ isStreaming(actionName) {
1469
+ const action = this.actions.get(actionName);
1470
+ return !!action?.isStreaming;
1471
+ }
1472
+ /**
1473
+ * Execute a streaming action, returning an async iterable of chunks.
1474
+ */
1475
+ async *executeStream(actionName, input) {
1476
+ const action = this.actions.get(actionName);
1477
+ if (!action || !action.isStreaming) {
1478
+ throw new Error(`Streaming action '${actionName}' not found`);
1479
+ }
1480
+ const services = resolveServices(action.services);
1481
+ for await (const chunk of action.handler(input, ...services)) {
1482
+ yield chunk;
1483
+ }
1484
+ }
1282
1485
  }
1283
1486
  const actionRegistry = new ActionRegistry();
1284
1487
  function registerAction(action) {
@@ -1402,6 +1605,15 @@ async function discoverAndRegisterActions(options) {
1402
1605
  `[Actions] Registered: ${exportValue.actionName}`
1403
1606
  );
1404
1607
  }
1608
+ } else if (isJayStreamAction(exportValue)) {
1609
+ registry.registerStream(exportValue);
1610
+ result.actionNames.push(exportValue.actionName);
1611
+ result.actionCount++;
1612
+ if (verbose) {
1613
+ getLogger().info(
1614
+ `[Actions] Registered stream: ${exportValue.actionName}`
1615
+ );
1616
+ }
1405
1617
  }
1406
1618
  }
1407
1619
  } catch (error) {
@@ -1558,6 +1770,13 @@ async function registerNpmPluginActions(packageName, pluginConfig, pluginDir, re
1558
1770
  if (verbose) {
1559
1771
  getLogger().info(`[Actions] Registered NPM plugin action: ${registeredName}`);
1560
1772
  }
1773
+ } else if (actionExport && isJayStreamAction(actionExport)) {
1774
+ registry.registerStream(actionExport);
1775
+ const registeredName = actionExport.actionName;
1776
+ registeredActions.push(registeredName);
1777
+ if (verbose) {
1778
+ getLogger().info(`[Actions] Registered NPM plugin stream: ${registeredName}`);
1779
+ }
1561
1780
  } else {
1562
1781
  getLogger().warn(
1563
1782
  `[Actions] NPM plugin "${packageName}" declares action "${actionName}" but it's not exported or not a JayAction`
@@ -1626,6 +1845,13 @@ async function discoverPluginActions(pluginPath, projectRoot, registry = actionR
1626
1845
  if (verbose) {
1627
1846
  getLogger().info(`[Actions] Registered plugin action: ${registeredName}`);
1628
1847
  }
1848
+ } else if (actionExport && isJayStreamAction(actionExport)) {
1849
+ registry.registerStream(actionExport);
1850
+ const registeredName = actionExport.actionName;
1851
+ registeredActions.push(registeredName);
1852
+ if (verbose) {
1853
+ getLogger().info(`[Actions] Registered plugin stream: ${registeredName}`);
1854
+ }
1629
1855
  } else {
1630
1856
  getLogger().warn(
1631
1857
  `[Actions] Plugin "${pluginName}" declares action "${actionName}" but it's not exported or not a JayAction`
@@ -2324,6 +2550,12 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2324
2550
  };
2325
2551
  });
2326
2552
  }
2553
+ if (manifest.routes?.length) {
2554
+ entry.routes = manifest.routes.map((r) => ({
2555
+ path: r.path,
2556
+ ...r.description && { description: r.description }
2557
+ }));
2558
+ }
2327
2559
  pluginsIndexMap.set(plugin.name, entry);
2328
2560
  }
2329
2561
  if (!dynamicOnly && manifest.contracts) {
@@ -2448,7 +2680,8 @@ async function materializeContracts(options, services = /* @__PURE__ */ new Map(
2448
2680
  contracts: data.contracts,
2449
2681
  ...data.actions && data.actions.length > 0 && { actions: data.actions },
2450
2682
  ...data.services?.length && { services: data.services },
2451
- ...data.contexts?.length && { contexts: data.contexts }
2683
+ ...data.contexts?.length && { contexts: data.contexts },
2684
+ ...data.routes?.length && { routes: data.routes }
2452
2685
  }))
2453
2686
  };
2454
2687
  fs.mkdirSync(outputDir, { recursive: true });
@@ -2505,6 +2738,12 @@ async function listContracts(options) {
2505
2738
  };
2506
2739
  });
2507
2740
  }
2741
+ if (manifest.routes?.length) {
2742
+ entry.routes = manifest.routes.map((r) => ({
2743
+ path: r.path,
2744
+ ...r.description && { description: r.description }
2745
+ }));
2746
+ }
2508
2747
  pluginsMap.set(plugin.name, entry);
2509
2748
  }
2510
2749
  if (!dynamicOnly && manifest.contracts) {
@@ -2551,7 +2790,8 @@ async function listContracts(options) {
2551
2790
  path: data.path,
2552
2791
  contracts: data.contracts,
2553
2792
  ...data.services?.length && { services: data.services },
2554
- ...data.contexts?.length && { contexts: data.contexts }
2793
+ ...data.contexts?.length && { contexts: data.contexts },
2794
+ ...data.routes?.length && { routes: data.routes }
2555
2795
  }))
2556
2796
  };
2557
2797
  }
@@ -2705,6 +2945,7 @@ export {
2705
2945
  executePluginServerInits,
2706
2946
  executePluginSetup,
2707
2947
  generateClientScript,
2948
+ generateFrozenPageHtml,
2708
2949
  generatePromiseReconstruction,
2709
2950
  generateSSRPageHtml,
2710
2951
  getActionCacheHeaders,
@@ -2735,7 +2976,6 @@ export {
2735
2976
  runInitCallbacks,
2736
2977
  runLoadParams,
2737
2978
  runShutdownCallbacks,
2738
- runSlowlyChangingRender,
2739
2979
  scanPlugins,
2740
2980
  serializeHeadTags,
2741
2981
  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.1",
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.1",
30
+ "@jay-framework/compiler-shared": "^0.16.1",
31
+ "@jay-framework/component": "^0.16.1",
32
+ "@jay-framework/fullstack-component": "^0.16.1",
33
+ "@jay-framework/logger": "^0.16.1",
34
+ "@jay-framework/runtime": "^0.16.1",
35
+ "@jay-framework/ssr-runtime": "^0.16.1",
36
+ "@jay-framework/stack-route-scanner": "^0.16.1",
37
+ "@jay-framework/view-state-merge": "^0.16.1",
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.1",
42
+ "@jay-framework/jay-cli": "^0.16.1",
43
+ "@jay-framework/stack-client-runtime": "^0.16.1",
44
44
  "@types/express": "^5.0.2",
45
45
  "@types/node": "^22.15.21",
46
46
  "nodemon": "^3.0.3",