veryfront 0.1.74 → 0.1.75
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.
- package/esm/cli/commands/knowledge/command.d.ts +2 -0
- package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
- package/esm/cli/commands/knowledge/command.js +64 -1
- package/esm/deno.d.ts +2 -0
- package/esm/deno.js +3 -1
- package/esm/src/data/data-fetcher.d.ts +11 -1
- package/esm/src/data/data-fetcher.d.ts.map +1 -1
- package/esm/src/data/data-fetcher.js +5 -2
- package/esm/src/data/index.d.ts +1 -1
- package/esm/src/data/index.d.ts.map +1 -1
- package/esm/src/data/server-data-fetcher.d.ts +14 -1
- package/esm/src/data/server-data-fetcher.d.ts.map +1 -1
- package/esm/src/data/server-data-fetcher.js +49 -3
- package/esm/src/rendering/orchestrator/lifecycle.d.ts +4 -0
- package/esm/src/rendering/orchestrator/lifecycle.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/lifecycle.js +8 -0
- package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/pipeline.js +6 -1
- package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts +26 -1
- package/esm/src/rendering/orchestrator/ssr-orchestrator.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/ssr-orchestrator.js +77 -1
- package/esm/src/routing/api/handler.d.ts.map +1 -1
- package/esm/src/routing/api/handler.js +6 -2
- package/esm/src/routing/api/route-executor.d.ts +8 -2
- package/esm/src/routing/api/route-executor.d.ts.map +1 -1
- package/esm/src/routing/api/route-executor.js +131 -3
- package/esm/src/security/deno-permissions.d.ts +6 -0
- package/esm/src/security/deno-permissions.d.ts.map +1 -1
- package/esm/src/security/deno-permissions.js +10 -0
- package/esm/src/security/sandbox/project-worker.d.ts +61 -0
- package/esm/src/security/sandbox/project-worker.d.ts.map +1 -0
- package/esm/src/security/sandbox/project-worker.js +318 -0
- package/esm/src/security/sandbox/worker-permissions.d.ts +30 -0
- package/esm/src/security/sandbox/worker-permissions.d.ts.map +1 -0
- package/esm/src/security/sandbox/worker-permissions.js +60 -0
- package/esm/src/security/sandbox/worker-pool.d.ts +87 -0
- package/esm/src/security/sandbox/worker-pool.d.ts.map +1 -0
- package/esm/src/security/sandbox/worker-pool.js +356 -0
- package/esm/src/security/sandbox/worker-types.d.ts +165 -0
- package/esm/src/security/sandbox/worker-types.d.ts.map +1 -0
- package/esm/src/security/sandbox/worker-types.js +17 -0
- package/esm/src/server/project-env/storage.d.ts +6 -0
- package/esm/src/server/project-env/storage.d.ts.map +1 -1
- package/esm/src/server/project-env/storage.js +8 -0
- package/esm/src/server/runtime-handler/project-isolation.d.ts +5 -0
- package/esm/src/server/runtime-handler/project-isolation.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/project-isolation.js +44 -0
- package/esm/src/server/shared/renderer/memory/pressure.d.ts +7 -0
- package/esm/src/server/shared/renderer/memory/pressure.d.ts.map +1 -1
- package/esm/src/server/shared/renderer/memory/pressure.js +7 -0
- package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts +4 -4
- package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.d.ts.map +1 -1
- package/esm/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.js +15 -15
- package/esm/src/utils/index.d.ts +10 -1
- package/esm/src/utils/index.d.ts.map +1 -1
- package/esm/src/utils/index.js +9 -1
- package/esm/src/utils/logger/index.d.ts +1 -1
- package/esm/src/utils/logger/index.d.ts.map +1 -1
- package/esm/src/utils/logger/index.js +1 -1
- package/esm/src/utils/logger/logger.d.ts +14 -0
- package/esm/src/utils/logger/logger.d.ts.map +1 -1
- package/esm/src/utils/logger/logger.js +17 -0
- package/esm/src/workflow/claude-code/tool.d.ts +5 -5
- package/package.json +4 -1
- package/src/cli/commands/knowledge/command.ts +76 -1
- package/src/deno.js +3 -1
- package/src/src/data/data-fetcher.ts +18 -2
- package/src/src/data/index.ts +1 -1
- package/src/src/data/server-data-fetcher.ts +78 -3
- package/src/src/rendering/orchestrator/lifecycle.ts +11 -0
- package/src/src/rendering/orchestrator/pipeline.ts +7 -2
- package/src/src/rendering/orchestrator/ssr-orchestrator.ts +119 -0
- package/src/src/routing/api/handler.ts +16 -3
- package/src/src/routing/api/route-executor.ts +222 -1
- package/src/src/security/deno-permissions.ts +11 -0
- package/src/src/security/sandbox/project-worker.ts +416 -0
- package/src/src/security/sandbox/worker-permissions.ts +74 -0
- package/src/src/security/sandbox/worker-pool.ts +451 -0
- package/src/src/security/sandbox/worker-types.ts +209 -0
- package/src/src/server/project-env/storage.ts +9 -0
- package/src/src/server/runtime-handler/project-isolation.ts +53 -0
- package/src/src/server/shared/renderer/memory/pressure.ts +8 -0
- package/src/src/transforms/pipeline/stages/ssr-vf-modules/path-resolver.ts +18 -12
- package/src/src/utils/index.ts +11 -0
- package/src/src/utils/logger/index.ts +1 -0
- package/src/src/utils/logger/logger.ts +34 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as dntShim from "../../../_dnt.shims.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { createFileSystem } from "../../../src/platform/index.js";
|
|
3
|
+
import { createFileSystem, getEnv } from "../../../src/platform/index.js";
|
|
4
4
|
import { basename, extname, join, normalize, relative } from "../../../src/platform/compat/path/index.js";
|
|
5
5
|
import { withSpan } from "../../../src/observability/tracing/otlp-setup.js";
|
|
6
6
|
import { cliLogger } from "../../utils/index.js";
|
|
@@ -9,6 +9,7 @@ import type { ParsedArgs } from "../../shared/types.js";
|
|
|
9
9
|
import { downloadUploadToFile, listAllUploads, type UploadItem } from "../uploads/command.js";
|
|
10
10
|
import { putRemoteFileFromLocal } from "../files/command.js";
|
|
11
11
|
import { knowledgeIngestPythonSource } from "./parser-source.js";
|
|
12
|
+
import { createJobUserLogger, type Logger, serverLogger } from "../../../src/utils/index.js";
|
|
12
13
|
|
|
13
14
|
const SUPPORTED_EXTENSIONS = new Set([
|
|
14
15
|
".pdf",
|
|
@@ -59,6 +60,8 @@ type KnowledgeSource =
|
|
|
59
60
|
|
|
60
61
|
type DownloadResult = { uploadPath: string; localPath: string; bytes?: number };
|
|
61
62
|
|
|
63
|
+
const knowledgeJobLogger = serverLogger.component("knowledge-ingest");
|
|
64
|
+
|
|
62
65
|
const KnowledgeIngestArgsSchema = z.object({
|
|
63
66
|
projectSlug: z.string().optional(),
|
|
64
67
|
projectDir: z.string().optional(),
|
|
@@ -130,6 +133,27 @@ function printJson(value: unknown): void {
|
|
|
130
133
|
console.log(JSON.stringify(value, null, 2));
|
|
131
134
|
}
|
|
132
135
|
|
|
136
|
+
function createKnowledgeIngestEventLogger(): Logger | null {
|
|
137
|
+
const projectId = getEnv("TENANT_PROJECT_ID");
|
|
138
|
+
const jobId = getEnv("JOB_ID");
|
|
139
|
+
|
|
140
|
+
if (!projectId || !jobId) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return createJobUserLogger(knowledgeJobLogger, {
|
|
145
|
+
projectId,
|
|
146
|
+
jobId,
|
|
147
|
+
batchId: getEnv("JOB_BATCH_ID") ?? undefined,
|
|
148
|
+
jobTarget: getEnv("JOB_TARGET") ?? undefined,
|
|
149
|
+
task: "knowledge-ingest",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildKnowledgeSourceName(source: KnowledgeSource): string {
|
|
154
|
+
return basename(source.kind === "upload" ? source.uploadPath : source.localPath);
|
|
155
|
+
}
|
|
156
|
+
|
|
133
157
|
function showKnowledgeUsage(): void {
|
|
134
158
|
console.log(`
|
|
135
159
|
Veryfront Knowledge
|
|
@@ -484,6 +508,7 @@ export async function ingestResolvedSources(
|
|
|
484
508
|
outputDir: string;
|
|
485
509
|
runParser: typeof runKnowledgeParser;
|
|
486
510
|
uploadKnowledgeFile: (remotePath: string, localPath: string) => Promise<{ path: string }>;
|
|
511
|
+
eventLogger?: Logger | null;
|
|
487
512
|
},
|
|
488
513
|
): Promise<KnowledgeIngestFileResult[]> {
|
|
489
514
|
if (options.slug && sources.length !== 1) {
|
|
@@ -494,6 +519,13 @@ export async function ingestResolvedSources(
|
|
|
494
519
|
const results: KnowledgeIngestFileResult[] = [];
|
|
495
520
|
|
|
496
521
|
for (const [index, source] of sources.entries()) {
|
|
522
|
+
deps.eventLogger?.info("Processing knowledge source", {
|
|
523
|
+
phase: "file_processing",
|
|
524
|
+
progress_current: index + 1,
|
|
525
|
+
progress_total: sources.length,
|
|
526
|
+
source_name: buildKnowledgeSourceName(source),
|
|
527
|
+
});
|
|
528
|
+
|
|
497
529
|
const parser = await deps.runParser({
|
|
498
530
|
filePath: source.localPath,
|
|
499
531
|
outputDir: deps.outputDir,
|
|
@@ -507,6 +539,26 @@ export async function ingestResolvedSources(
|
|
|
507
539
|
options.knowledgePath,
|
|
508
540
|
);
|
|
509
541
|
const uploaded = await deps.uploadKnowledgeFile(remotePath, parser.sandbox_output_path);
|
|
542
|
+
|
|
543
|
+
deps.eventLogger?.info("Knowledge source ingested", {
|
|
544
|
+
phase: "file_completed",
|
|
545
|
+
progress_current: index + 1,
|
|
546
|
+
progress_total: sources.length,
|
|
547
|
+
source_name: buildKnowledgeSourceName(source),
|
|
548
|
+
remote_path: uploaded.path,
|
|
549
|
+
warning_count: parser.warnings.length,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
if (parser.warnings.length > 0) {
|
|
553
|
+
deps.eventLogger?.warn("Knowledge source emitted warnings", {
|
|
554
|
+
phase: "file_warning",
|
|
555
|
+
progress_current: index + 1,
|
|
556
|
+
progress_total: sources.length,
|
|
557
|
+
source_name: buildKnowledgeSourceName(source),
|
|
558
|
+
warning_count: parser.warnings.length,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
510
562
|
results.push(
|
|
511
563
|
createKnowledgeIngestResult({
|
|
512
564
|
source: buildSourceReference(source),
|
|
@@ -545,8 +597,14 @@ export async function knowledgeCommand(args: ParsedArgs): Promise<void> {
|
|
|
545
597
|
const outputDir = options.outputDir ?? await defaultOutputRoot();
|
|
546
598
|
const shouldCleanupOutputDir = options.outputDir === undefined;
|
|
547
599
|
const downloadOutputDir = resolveKnowledgeDownloadOutputDir(outputDir);
|
|
600
|
+
const eventLogger = createKnowledgeIngestEventLogger();
|
|
548
601
|
|
|
549
602
|
try {
|
|
603
|
+
eventLogger?.info("Starting knowledge ingest", {
|
|
604
|
+
phase: "started",
|
|
605
|
+
mode: options.path ? "path_prefix" : "explicit_sources",
|
|
606
|
+
});
|
|
607
|
+
|
|
550
608
|
const sources = await collectKnowledgeSources(options, {
|
|
551
609
|
client,
|
|
552
610
|
projectSlug: config.projectSlug,
|
|
@@ -558,15 +616,27 @@ export async function knowledgeCommand(args: ParsedArgs): Promise<void> {
|
|
|
558
616
|
),
|
|
559
617
|
});
|
|
560
618
|
|
|
619
|
+
eventLogger?.info("Resolved knowledge sources", {
|
|
620
|
+
phase: "sources_resolved",
|
|
621
|
+
progress_total: sources.length,
|
|
622
|
+
});
|
|
623
|
+
|
|
561
624
|
const results = await ingestResolvedSources(sources, options, {
|
|
562
625
|
client,
|
|
563
626
|
projectSlug: config.projectSlug,
|
|
564
627
|
outputDir,
|
|
565
628
|
runParser: runKnowledgeParser,
|
|
629
|
+
eventLogger,
|
|
566
630
|
uploadKnowledgeFile: (remotePath, localPath) =>
|
|
567
631
|
putRemoteFileFromLocal(client, config.projectSlug, remotePath, localPath),
|
|
568
632
|
});
|
|
569
633
|
|
|
634
|
+
eventLogger?.info("Completed knowledge ingest", {
|
|
635
|
+
phase: "completed",
|
|
636
|
+
progress_current: results.length,
|
|
637
|
+
progress_total: results.length,
|
|
638
|
+
});
|
|
639
|
+
|
|
570
640
|
if (options.json) {
|
|
571
641
|
printJson(results);
|
|
572
642
|
return;
|
|
@@ -578,6 +648,11 @@ export async function knowledgeCommand(args: ParsedArgs): Promise<void> {
|
|
|
578
648
|
cliLogger.info(` ${result.summary}`);
|
|
579
649
|
}
|
|
580
650
|
}
|
|
651
|
+
} catch (error) {
|
|
652
|
+
eventLogger?.error("Knowledge ingest failed", {
|
|
653
|
+
phase: "failed",
|
|
654
|
+
});
|
|
655
|
+
throw error;
|
|
581
656
|
} finally {
|
|
582
657
|
if (shouldCleanupOutputDir) {
|
|
583
658
|
await Promise.all([
|
package/src/deno.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
"name": "veryfront",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.75",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"nodeModulesDir": "auto",
|
|
6
6
|
"exclude": [
|
|
@@ -34,6 +34,7 @@ export default {
|
|
|
34
34
|
"./resource": "./src/resource/index.ts",
|
|
35
35
|
"./mcp": "./src/mcp/index.ts",
|
|
36
36
|
"./middleware": "./src/middleware/index.ts",
|
|
37
|
+
"./utils": "./src/utils/index.ts",
|
|
37
38
|
"./oauth": "./src/oauth/index.ts",
|
|
38
39
|
"./provider": "./src/provider/index.ts",
|
|
39
40
|
"./fs": "./src/fs/index.ts",
|
|
@@ -71,6 +72,7 @@ export default {
|
|
|
71
72
|
"veryfront/workflow/claude-code": "./src/workflow/claude-code/index.ts",
|
|
72
73
|
"veryfront/workflow/claude-code/react": "./src/workflow/claude-code/react/index.ts",
|
|
73
74
|
"veryfront/workflow/discovery": "./src/workflow/discovery/index.ts",
|
|
75
|
+
"veryfront/utils": "./src/utils/index.ts",
|
|
74
76
|
"veryfront/utils/box": "./src/utils/box.ts",
|
|
75
77
|
"veryfront/utils/case-utils": "./src/utils/case-utils.ts",
|
|
76
78
|
"veryfront/utils/constants/server": "./src/utils/constants/server.ts",
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { withSpan } from "../observability/tracing/otlp-setup.js";
|
|
2
2
|
import { SpanNames } from "../observability/tracing/span-names.js";
|
|
3
3
|
import { CacheManager } from "./data-fetching-cache.js";
|
|
4
|
-
import { ServerDataFetcher } from "./server-data-fetcher.js";
|
|
4
|
+
import { ServerDataFetcher, type ServerDataFetchOptions } from "./server-data-fetcher.js";
|
|
5
5
|
import { StaticDataFetcher } from "./static-data-fetcher.js";
|
|
6
6
|
import { StaticPathsFetcher } from "./static-paths-fetcher.js";
|
|
7
7
|
import type { DataContext, DataResult, PageWithData, StaticPathsResult } from "./types.js";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Options for isolated data fetching. Passed through to ServerDataFetcher
|
|
11
|
+
* when worker isolation is enabled.
|
|
12
|
+
*/
|
|
13
|
+
export interface FetchDataOptions {
|
|
14
|
+
/** Absolute path to the module containing getServerData */
|
|
15
|
+
modulePath?: string;
|
|
16
|
+
/** Project directory for worker scoping */
|
|
17
|
+
projectDir?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
export class DataFetcher {
|
|
10
21
|
private cacheManager: CacheManager;
|
|
11
22
|
private serverFetcher: ServerDataFetcher;
|
|
@@ -23,6 +34,7 @@ export class DataFetcher {
|
|
|
23
34
|
pageModule: PageWithData,
|
|
24
35
|
context: DataContext,
|
|
25
36
|
mode: "development" | "production" = "development",
|
|
37
|
+
options?: FetchDataOptions,
|
|
26
38
|
): Promise<DataResult> {
|
|
27
39
|
const preferServerData = mode === "development" || !pageModule.getStaticData;
|
|
28
40
|
const useServer = preferServerData && !!pageModule.getServerData;
|
|
@@ -34,10 +46,14 @@ export class DataFetcher {
|
|
|
34
46
|
? "static"
|
|
35
47
|
: "none";
|
|
36
48
|
|
|
49
|
+
const isolationOptions: ServerDataFetchOptions | undefined = options
|
|
50
|
+
? { modulePath: options.modulePath, projectDir: options.projectDir }
|
|
51
|
+
: undefined;
|
|
52
|
+
|
|
37
53
|
return withSpan(
|
|
38
54
|
SpanNames.DATA_FETCH,
|
|
39
55
|
() => {
|
|
40
|
-
if (useServer) return this.serverFetcher.fetch(pageModule, context);
|
|
56
|
+
if (useServer) return this.serverFetcher.fetch(pageModule, context, isolationOptions);
|
|
41
57
|
if (useStatic) return this.staticFetcher.fetch(pageModule, context);
|
|
42
58
|
return Promise.resolve({ props: {} });
|
|
43
59
|
},
|
package/src/src/data/index.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
|
+
import * as dntShim from "../../_dnt.shims.js";
|
|
1
2
|
import type { DataContext, DataResult, PageWithData } from "./types.js";
|
|
2
3
|
import { serverLogger } from "../utils/index.js";
|
|
3
4
|
import { DATA_FETCH_TIMEOUT_MS } from "../config/defaults.js";
|
|
4
5
|
import { TimeoutError, withTimeoutThrow } from "../rendering/utils/stream-utils.js";
|
|
5
6
|
import { withSpan } from "../observability/tracing/otlp-setup.js";
|
|
6
7
|
import { CircuitBreakerOpen, getCircuitBreaker } from "../utils/circuit-breaker.js";
|
|
8
|
+
import { getWorkerPool, isDataIsolationEnabled } from "../security/sandbox/worker-pool.js";
|
|
9
|
+
import type { WorkerResponse } from "../security/sandbox/worker-types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options for isolated data fetching through Worker pool.
|
|
13
|
+
*/
|
|
14
|
+
export interface ServerDataFetchOptions {
|
|
15
|
+
/** Absolute path to the module containing getServerData */
|
|
16
|
+
modulePath?: string;
|
|
17
|
+
/** Project directory for worker scoping */
|
|
18
|
+
projectDir?: string;
|
|
19
|
+
}
|
|
7
20
|
|
|
8
21
|
export class ServerDataFetcher {
|
|
9
|
-
fetch(
|
|
22
|
+
fetch(
|
|
23
|
+
pageModule: PageWithData,
|
|
24
|
+
context: DataContext,
|
|
25
|
+
options?: ServerDataFetchOptions,
|
|
26
|
+
): Promise<DataResult> {
|
|
10
27
|
if (typeof pageModule.getServerData !== "function") {
|
|
11
28
|
return Promise.resolve({ props: {} });
|
|
12
29
|
}
|
|
@@ -20,6 +37,11 @@ export class ServerDataFetcher {
|
|
|
20
37
|
successThreshold: 2,
|
|
21
38
|
});
|
|
22
39
|
|
|
40
|
+
// Choose isolated or direct execution
|
|
41
|
+
const useIsolation = isDataIsolationEnabled() &&
|
|
42
|
+
!!options?.modulePath &&
|
|
43
|
+
!!options?.projectDir;
|
|
44
|
+
|
|
23
45
|
return withSpan(
|
|
24
46
|
"data.fetch_server",
|
|
25
47
|
async () => {
|
|
@@ -28,7 +50,9 @@ export class ServerDataFetcher {
|
|
|
28
50
|
try {
|
|
29
51
|
const result = await circuitBreaker.execute(() =>
|
|
30
52
|
withTimeoutThrow(
|
|
31
|
-
|
|
53
|
+
useIsolation
|
|
54
|
+
? this.fetchIsolated(options!.modulePath!, options!.projectDir!, context)
|
|
55
|
+
: Promise.resolve(pageModule.getServerData!(context)),
|
|
32
56
|
DATA_FETCH_TIMEOUT_MS,
|
|
33
57
|
`getServerData for ${pathname}`,
|
|
34
58
|
)
|
|
@@ -59,7 +83,11 @@ export class ServerDataFetcher {
|
|
|
59
83
|
throw error;
|
|
60
84
|
}
|
|
61
85
|
|
|
62
|
-
this.logError("DATA_FETCH_ERROR getServerData failed", error, {
|
|
86
|
+
this.logError("DATA_FETCH_ERROR getServerData failed", error, {
|
|
87
|
+
pathname,
|
|
88
|
+
durationMs,
|
|
89
|
+
isolated: useIsolation,
|
|
90
|
+
});
|
|
63
91
|
throw error;
|
|
64
92
|
}
|
|
65
93
|
},
|
|
@@ -68,8 +96,55 @@ export class ServerDataFetcher {
|
|
|
68
96
|
"data.pathname": pathname,
|
|
69
97
|
"data.timeout_ms": DATA_FETCH_TIMEOUT_MS,
|
|
70
98
|
"data.project_id": projectId,
|
|
99
|
+
"data.isolated": useIsolation,
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Execute getServerData in a per-project Worker.
|
|
106
|
+
*/
|
|
107
|
+
private async fetchIsolated(
|
|
108
|
+
modulePath: string,
|
|
109
|
+
projectDir: string,
|
|
110
|
+
context: DataContext,
|
|
111
|
+
): Promise<DataResult> {
|
|
112
|
+
const pool = getWorkerPool();
|
|
113
|
+
const body = context.request?.body ? new Uint8Array(await context.request.arrayBuffer()) : null;
|
|
114
|
+
|
|
115
|
+
const workerResponse: WorkerResponse = await pool.execute(
|
|
116
|
+
projectDir,
|
|
117
|
+
[projectDir],
|
|
118
|
+
{
|
|
119
|
+
type: "fetch-data",
|
|
120
|
+
id: dntShim.crypto.randomUUID(),
|
|
121
|
+
modulePath,
|
|
122
|
+
context: {
|
|
123
|
+
params: context.params,
|
|
124
|
+
query: context.query?.toString() ?? "",
|
|
125
|
+
request: {
|
|
126
|
+
url: context.request?.url ?? context.url?.toString() ?? "http://localhost",
|
|
127
|
+
method: context.request?.method ?? "GET",
|
|
128
|
+
headers: context.request ? [...context.request.headers.entries()] : [],
|
|
129
|
+
body,
|
|
130
|
+
},
|
|
131
|
+
url: context.url?.toString() ?? "http://localhost",
|
|
132
|
+
},
|
|
71
133
|
},
|
|
72
134
|
);
|
|
135
|
+
|
|
136
|
+
if (workerResponse.type === "error") {
|
|
137
|
+
const err = new Error(workerResponse.error.message);
|
|
138
|
+
err.name = workerResponse.error.name;
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (workerResponse.type === "data-result") {
|
|
143
|
+
return workerResponse.result as DataResult;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Unexpected response type — shouldn't happen but be defensive
|
|
147
|
+
throw new Error(`Unexpected worker response type: ${workerResponse.type}`);
|
|
73
148
|
}
|
|
74
149
|
|
|
75
150
|
/**
|
|
@@ -38,6 +38,8 @@ export interface LifecycleOptions {
|
|
|
38
38
|
projectId?: string;
|
|
39
39
|
/** Content source identifier for cache isolation (branch or release) */
|
|
40
40
|
contentSourceId?: string;
|
|
41
|
+
/** Injectable factory for testing — bypasses real service construction */
|
|
42
|
+
servicesFactory?: (adapter: RuntimeAdapter) => RendererServices;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export interface RendererServices {
|
|
@@ -62,6 +64,7 @@ export class RendererLifecycle {
|
|
|
62
64
|
private contentSourceId?: string;
|
|
63
65
|
private services?: RendererServices;
|
|
64
66
|
private adapter!: RuntimeAdapter;
|
|
67
|
+
private servicesFactory?: (adapter: RuntimeAdapter) => RendererServices;
|
|
65
68
|
|
|
66
69
|
constructor(options: LifecycleOptions) {
|
|
67
70
|
this.configManager = options.configManager;
|
|
@@ -69,6 +72,7 @@ export class RendererLifecycle {
|
|
|
69
72
|
this.moduleServerUrl = options.moduleServerUrl;
|
|
70
73
|
this.projectId = options.projectId;
|
|
71
74
|
this.contentSourceId = options.contentSourceId;
|
|
75
|
+
this.servicesFactory = options.servicesFactory;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
async initialize(): Promise<RendererServices> {
|
|
@@ -83,6 +87,13 @@ export class RendererLifecycle {
|
|
|
83
87
|
this.adapter = await runtime.get();
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
// Allow tests to bypass the full service graph construction
|
|
91
|
+
if (this.servicesFactory) {
|
|
92
|
+
this.services = this.servicesFactory(this.adapter);
|
|
93
|
+
logger.debug("Renderer services initialized via injected factory");
|
|
94
|
+
return this.services;
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
const projectDir = this.configManager.getProjectDir();
|
|
87
98
|
const mode = this.configManager.getMode();
|
|
88
99
|
const debugMode = this.configManager.isDebugMode();
|
|
@@ -36,7 +36,7 @@ import type { PageResolver } from "../page-resolution/index.js";
|
|
|
36
36
|
import type { LayoutOrchestrator } from "./layout.js";
|
|
37
37
|
import type { SSROrchestrator } from "./ssr-orchestrator.js";
|
|
38
38
|
import type { PageDataResponse, RenderOptions, RenderResult } from "./types.js";
|
|
39
|
-
import { DataFetcher } from "../../data/index.js";
|
|
39
|
+
import { DataFetcher, type FetchDataOptions } from "../../data/index.js";
|
|
40
40
|
import type { DataContext, PageWithData } from "../../data/types.js";
|
|
41
41
|
import { clearSSRModuleCacheForProject } from "../../modules/react-loader/index.js";
|
|
42
42
|
import { setupSSRGlobals } from "../ssr-globals.js";
|
|
@@ -303,8 +303,13 @@ export class RenderPipeline {
|
|
|
303
303
|
Promise.all(
|
|
304
304
|
dataJobs.map(async (job) => {
|
|
305
305
|
try {
|
|
306
|
+
const jobPath = (job as LoadedModule & { path?: string }).path;
|
|
307
|
+
const fetchOptions: FetchDataOptions = {
|
|
308
|
+
modulePath: jobPath,
|
|
309
|
+
projectDir: this.config.projectDir,
|
|
310
|
+
};
|
|
306
311
|
const result = await this.dataFetcher
|
|
307
|
-
.fetchData(job.mod as PageWithData, dataContext, this.config.mode);
|
|
312
|
+
.fetchData(job.mod as PageWithData, dataContext, this.config.mode, fetchOptions);
|
|
308
313
|
return { ...job, result, error: null as Error | null };
|
|
309
314
|
} catch (error) {
|
|
310
315
|
return { ...job, result: null, error: error as Error };
|
|
@@ -10,6 +10,8 @@ import { computeHash } from "../utils/index.js";
|
|
|
10
10
|
import type { HTMLGenerationContext, HTMLGenerator } from "./html.js";
|
|
11
11
|
import type { RenderOptions } from "./types.js";
|
|
12
12
|
import { runWithHeadCollector } from "../../react/head-collector.js";
|
|
13
|
+
import { getWorkerPool, isSSRIsolationEnabled } from "../../security/sandbox/worker-pool.js";
|
|
14
|
+
import type { WorkerResponse } from "../../security/sandbox/worker-types.js";
|
|
13
15
|
|
|
14
16
|
const logger = rendererLogger.component("ssr-orchestrator");
|
|
15
17
|
|
|
@@ -27,6 +29,24 @@ export interface SSRRenderingResult {
|
|
|
27
29
|
ssrHash: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Options for isolated SSR rendering through the Worker pool.
|
|
34
|
+
* When provided and SSR isolation is enabled, the rendering happens
|
|
35
|
+
* in a per-project Worker instead of the main process.
|
|
36
|
+
*/
|
|
37
|
+
export interface SSRIsolationOptions {
|
|
38
|
+
/** Temp file path for the page component module */
|
|
39
|
+
pageModulePath: string;
|
|
40
|
+
/** Ordered layout module temp paths (innermost → outermost) */
|
|
41
|
+
layoutModulePaths: string[];
|
|
42
|
+
/** Page component props */
|
|
43
|
+
pageProps: Record<string, unknown>;
|
|
44
|
+
/** Layout props (one entry per layout, matching layoutModulePaths order) */
|
|
45
|
+
layoutProps: Record<string, unknown>[];
|
|
46
|
+
/** Project directory for worker scoping */
|
|
47
|
+
projectDir: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
30
50
|
function getElementTypeName(el: React.ReactElement | null | undefined): string {
|
|
31
51
|
if (!el?.type) return "unknown";
|
|
32
52
|
if (typeof el.type === "string") return el.type;
|
|
@@ -46,7 +66,18 @@ export class SSROrchestrator {
|
|
|
46
66
|
pageElement: React.ReactElement,
|
|
47
67
|
generationContext: Omit<HTMLGenerationContext, "html" | "ssrHash">,
|
|
48
68
|
options?: RenderOptions,
|
|
69
|
+
isolationOptions?: SSRIsolationOptions,
|
|
49
70
|
): Promise<SSRRenderingResult> {
|
|
71
|
+
// Isolated SSR path: render in per-project Worker
|
|
72
|
+
if (
|
|
73
|
+
isSSRIsolationEnabled() &&
|
|
74
|
+
isolationOptions?.pageModulePath &&
|
|
75
|
+
isolationOptions?.projectDir
|
|
76
|
+
) {
|
|
77
|
+
return this.performIsolatedSSR(generationContext, options, isolationOptions);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Default path: render in main process
|
|
50
81
|
logger.debug("performSSRRendering called", {
|
|
51
82
|
elementType: getElementTypeName(pageElement),
|
|
52
83
|
hasChildren: !!(pageElement.props as Record<string, unknown>)?.children,
|
|
@@ -133,6 +164,94 @@ export class SSROrchestrator {
|
|
|
133
164
|
};
|
|
134
165
|
}
|
|
135
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Perform SSR rendering in an isolated per-project Worker.
|
|
169
|
+
*
|
|
170
|
+
* The Worker imports user modules from their temp file paths,
|
|
171
|
+
* constructs the React element tree, and renders to HTML.
|
|
172
|
+
* For streaming, the Worker sends chunks via postMessage.
|
|
173
|
+
*/
|
|
174
|
+
private async performIsolatedSSR(
|
|
175
|
+
generationContext: Omit<HTMLGenerationContext, "html" | "ssrHash">,
|
|
176
|
+
options: RenderOptions | undefined,
|
|
177
|
+
isolation: SSRIsolationOptions,
|
|
178
|
+
): Promise<SSRRenderingResult> {
|
|
179
|
+
const wantsStream = options?.delivery === "stream";
|
|
180
|
+
const pool = getWorkerPool();
|
|
181
|
+
const requestId = dntShim.crypto.randomUUID();
|
|
182
|
+
|
|
183
|
+
return withSpan(
|
|
184
|
+
"ssr.isolated_render",
|
|
185
|
+
async () => {
|
|
186
|
+
const worker = pool.getOrCreateWorker(isolation.projectDir, [isolation.projectDir]);
|
|
187
|
+
|
|
188
|
+
if (wantsStream) {
|
|
189
|
+
// Streaming mode: get a ReadableStream of chunks from the Worker
|
|
190
|
+
const stream = worker.executeStream({
|
|
191
|
+
type: "render-ssr",
|
|
192
|
+
id: requestId,
|
|
193
|
+
pageModulePath: isolation.pageModulePath,
|
|
194
|
+
layoutModulePaths: isolation.layoutModulePaths,
|
|
195
|
+
pageProps: isolation.pageProps,
|
|
196
|
+
layoutProps: isolation.layoutProps,
|
|
197
|
+
delivery: "stream",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const ssrHash = `stream-isolated-${Date.now()}`;
|
|
201
|
+
|
|
202
|
+
// Generate HTML stream using the framework's HTML generator
|
|
203
|
+
const finalStream = await this.config.htmlGenerator.generateHTMLStream(stream, {
|
|
204
|
+
...generationContext,
|
|
205
|
+
ssrHash,
|
|
206
|
+
options: { ...generationContext.options, ...options },
|
|
207
|
+
collectedHead: undefined,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return { fullHtml: "", finalStream, ssrHash };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// String mode: render to HTML in Worker, get result back
|
|
214
|
+
const workerResponse: WorkerResponse = await worker.execute({
|
|
215
|
+
type: "render-ssr",
|
|
216
|
+
id: requestId,
|
|
217
|
+
pageModulePath: isolation.pageModulePath,
|
|
218
|
+
layoutModulePaths: isolation.layoutModulePaths,
|
|
219
|
+
pageProps: isolation.pageProps,
|
|
220
|
+
layoutProps: isolation.layoutProps,
|
|
221
|
+
delivery: "string",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (workerResponse.type === "error") {
|
|
225
|
+
const err = new Error(workerResponse.error.message);
|
|
226
|
+
err.name = workerResponse.error.name;
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (workerResponse.type !== "ssr-result") {
|
|
231
|
+
throw new Error(`Unexpected worker response type: ${workerResponse.type}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const html = workerResponse.html;
|
|
235
|
+
const ssrHash = await computeHash(html);
|
|
236
|
+
|
|
237
|
+
const fullHtml = await this.config.htmlGenerator.generateFullHTML({
|
|
238
|
+
...generationContext,
|
|
239
|
+
html,
|
|
240
|
+
ssrHash,
|
|
241
|
+
options: { ...generationContext.options, ...options },
|
|
242
|
+
collectedHead: undefined,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return { fullHtml, finalStream: null, ssrHash };
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"ssr.isolated": true,
|
|
249
|
+
"ssr.wants_stream": wantsStream,
|
|
250
|
+
"ssr.project_dir": isolation.projectDir,
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
136
255
|
private createStream(html: string): ReadableStream | null {
|
|
137
256
|
try {
|
|
138
257
|
return new dntShim.Response(html).body ?? null;
|
|
@@ -13,7 +13,7 @@ import { ApiRouteMatcher, type RouteMatch } from "./api-route-matcher.js";
|
|
|
13
13
|
import type { APIRoute } from "./module-loader/types.js";
|
|
14
14
|
import { loadHandlerModule } from "./module-loader/loader.js";
|
|
15
15
|
import { discoverAppRoutes, discoverPagesRoutes } from "./route-discovery.js";
|
|
16
|
-
import { executeAppRoute, executePagesRoute } from "./route-executor.js";
|
|
16
|
+
import { executeAppRoute, executePagesRoute, type ExecuteRouteOptions } from "./route-executor.js";
|
|
17
17
|
import { withSpan } from "../../observability/tracing/otlp-setup.js";
|
|
18
18
|
|
|
19
19
|
/** Max entries in the loaded-handler LRU cache */
|
|
@@ -188,9 +188,22 @@ export class APIRouteHandler {
|
|
|
188
188
|
// Note: Cannot use path-based detection (/app/) as projectDir may be '/app' in production
|
|
189
189
|
const isAppRoute = /\/route\.(ts|js|tsx|jsx)$/.test(match.route.page);
|
|
190
190
|
|
|
191
|
+
const isolationOptions: ExecuteRouteOptions = {
|
|
192
|
+
modulePath: match.route.page,
|
|
193
|
+
projectDir: this.projectDir,
|
|
194
|
+
};
|
|
195
|
+
|
|
191
196
|
const response = isAppRoute
|
|
192
|
-
? await executeAppRoute(handler, request, match, pathname, adapter)
|
|
193
|
-
: await executePagesRoute(
|
|
197
|
+
? await executeAppRoute(handler, request, match, pathname, adapter, isolationOptions)
|
|
198
|
+
: await executePagesRoute(
|
|
199
|
+
handler,
|
|
200
|
+
request,
|
|
201
|
+
match,
|
|
202
|
+
pathname,
|
|
203
|
+
adapter,
|
|
204
|
+
this.projectDir,
|
|
205
|
+
isolationOptions,
|
|
206
|
+
);
|
|
194
207
|
|
|
195
208
|
const corsResponse = await applyCORSHeaders({
|
|
196
209
|
request,
|