veryfront 0.1.73 → 0.1.74
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-help.d.ts.map +1 -1
- package/esm/cli/commands/knowledge/command-help.js +3 -1
- package/esm/cli/commands/knowledge/command.d.ts +32 -5
- package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
- package/esm/cli/commands/knowledge/command.js +87 -21
- package/esm/cli/commands/knowledge/parser-source.d.ts.map +1 -1
- package/esm/cli/commands/knowledge/parser-source.js +110 -5
- package/esm/deno.js +1 -1
- package/esm/src/server/handlers/request/ssr/ssr.handler.d.ts +2 -0
- package/esm/src/server/handlers/request/ssr/ssr.handler.d.ts.map +1 -1
- package/esm/src/server/handlers/request/ssr/ssr.handler.js +6 -2
- package/esm/src/server/runtime-handler/adapter-factory.d.ts +3 -0
- package/esm/src/server/runtime-handler/adapter-factory.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/adapter-factory.js +6 -5
- package/esm/src/server/runtime-handler/index.d.ts +33 -0
- package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/index.js +103 -37
- package/esm/src/server/runtime-handler/local-project-discovery.d.ts +32 -4
- package/esm/src/server/runtime-handler/local-project-discovery.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/local-project-discovery.js +46 -16
- package/esm/src/server/services/rendering/ssr.service.d.ts +19 -1
- package/esm/src/server/services/rendering/ssr.service.d.ts.map +1 -1
- package/esm/src/server/services/rendering/ssr.service.js +9 -1
- package/esm/src/server/shared/renderer/adapter.d.ts +25 -0
- package/esm/src/server/shared/renderer/adapter.d.ts.map +1 -1
- package/esm/src/server/shared/renderer/adapter.js +83 -10
- package/esm/src/server/shared/renderer/index.d.ts +1 -1
- package/esm/src/server/shared/renderer/index.d.ts.map +1 -1
- package/esm/src/server/shared/renderer/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/commands/knowledge/command-help.ts +3 -1
- package/src/cli/commands/knowledge/command.ts +104 -21
- package/src/cli/commands/knowledge/parser-source.ts +110 -5
- package/src/deno.js +1 -1
- package/src/src/server/handlers/request/ssr/ssr.handler.ts +11 -2
- package/src/src/server/runtime-handler/adapter-factory.ts +13 -5
- package/src/src/server/runtime-handler/index.ts +132 -39
- package/src/src/server/runtime-handler/local-project-discovery.ts +51 -17
- package/src/src/server/services/rendering/ssr.service.ts +34 -3
- package/src/src/server/shared/renderer/adapter.ts +107 -8
- package/src/src/server/shared/renderer/index.ts +7 -1
|
@@ -17,24 +17,56 @@ const baseLogger = getBaseLogger("SERVER");
|
|
|
17
17
|
|
|
18
18
|
const logger = baseLogger.component("runtime-handler");
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Injectable cache container for project discovery state.
|
|
22
|
+
*
|
|
23
|
+
* Wraps both the project-path cache (slug → absolute path) and the
|
|
24
|
+
* adapter cache (project dir → RuntimeAdapter) so that callers — especially
|
|
25
|
+
* tests — can supply an isolated instance instead of sharing global state.
|
|
26
|
+
*/
|
|
27
|
+
export class ProjectDiscoveryCache {
|
|
28
|
+
/** Cache of discovered local project paths by slug */
|
|
29
|
+
readonly projects: LRUCache<string, string>;
|
|
30
|
+
/** Cache of local adapters by project directory */
|
|
31
|
+
readonly adapters: LRUCache<string, RuntimeAdapter>;
|
|
32
|
+
|
|
33
|
+
constructor(opts?: { maxProjects?: number; maxAdapters?: number }) {
|
|
34
|
+
this.projects = new LRUCache<string, string>({
|
|
35
|
+
maxEntries: opts?.maxProjects ?? 100,
|
|
36
|
+
});
|
|
37
|
+
this.adapters = new LRUCache<string, RuntimeAdapter>({
|
|
38
|
+
maxEntries: opts?.maxAdapters ?? 50,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
/** Clear both caches */
|
|
43
|
+
clear(): void {
|
|
44
|
+
this.projects.clear();
|
|
45
|
+
this.adapters.clear();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
27
48
|
|
|
28
|
-
/**
|
|
29
|
-
export const
|
|
49
|
+
/** Default module-level cache instance (backward-compatible singleton) */
|
|
50
|
+
export const defaultDiscoveryCache = new ProjectDiscoveryCache();
|
|
30
51
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
});
|
|
52
|
+
// Register the default caches for monitoring
|
|
53
|
+
registerLRUCache("local-project-cache", defaultDiscoveryCache.projects);
|
|
54
|
+
registerLRUCache("local-adapter-cache", defaultDiscoveryCache.adapters);
|
|
35
55
|
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
/**
|
|
57
|
+
* @deprecated Use `defaultDiscoveryCache.adapters` instead.
|
|
58
|
+
* Kept for backward compatibility with existing consumers.
|
|
59
|
+
*/
|
|
60
|
+
export const localAdapterCache = defaultDiscoveryCache.adapters;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @deprecated Use `defaultDiscoveryCache.projects` instead.
|
|
64
|
+
* Kept for backward compatibility with existing consumers.
|
|
65
|
+
*/
|
|
66
|
+
export const localProjectCache = defaultDiscoveryCache.projects;
|
|
67
|
+
|
|
68
|
+
/** Standard directories to search for local projects */
|
|
69
|
+
export const standardProjectDirs = ["data/projects", "projects"];
|
|
38
70
|
|
|
39
71
|
function isNotFoundError(error: unknown): boolean {
|
|
40
72
|
if (!(error instanceof Error)) return false;
|
|
@@ -78,12 +110,14 @@ async function isValidLocalProjectPath(path: string, adapter: RuntimeAdapter): P
|
|
|
78
110
|
* @param slug - The project slug to find
|
|
79
111
|
* @param adapter - The runtime adapter to use for filesystem operations
|
|
80
112
|
* @param headerPath - Optional path from x-project-path header (takes precedence)
|
|
113
|
+
* @param cache - Optional cache instance (defaults to module-level singleton)
|
|
81
114
|
* @returns The absolute path to the project, or undefined if not found
|
|
82
115
|
*/
|
|
83
116
|
export async function findLocalProjectPath(
|
|
84
117
|
slug: string,
|
|
85
118
|
adapter: RuntimeAdapter,
|
|
86
119
|
headerPath?: string,
|
|
120
|
+
cache: ProjectDiscoveryCache = defaultDiscoveryCache,
|
|
87
121
|
): Promise<string | undefined> {
|
|
88
122
|
if (headerPath) {
|
|
89
123
|
try {
|
|
@@ -92,7 +126,7 @@ export async function findLocalProjectPath(
|
|
|
92
126
|
const absolutePath = normalizedPath.startsWith("/")
|
|
93
127
|
? normalizedPath
|
|
94
128
|
: `${cwd()}/${normalizedPath}`;
|
|
95
|
-
|
|
129
|
+
cache.projects.set(slug, absolutePath);
|
|
96
130
|
return absolutePath;
|
|
97
131
|
}
|
|
98
132
|
logger.warn("Ignoring invalid x-project-path override", {
|
|
@@ -108,7 +142,7 @@ export async function findLocalProjectPath(
|
|
|
108
142
|
}
|
|
109
143
|
}
|
|
110
144
|
|
|
111
|
-
const cached =
|
|
145
|
+
const cached = cache.projects.get(slug);
|
|
112
146
|
if (cached) return cached;
|
|
113
147
|
|
|
114
148
|
for (const dir of standardProjectDirs) {
|
|
@@ -118,7 +152,7 @@ export async function findLocalProjectPath(
|
|
|
118
152
|
if (!await isValidLocalProjectPath(projectPath, adapter)) continue;
|
|
119
153
|
|
|
120
154
|
const absolutePath = projectPath.startsWith("/") ? projectPath : `${cwd()}/${projectPath}`;
|
|
121
|
-
|
|
155
|
+
cache.projects.set(slug, absolutePath);
|
|
122
156
|
logger.debug("Discovered local project", { slug, path: absolutePath });
|
|
123
157
|
return absolutePath;
|
|
124
158
|
} catch (error) {
|
|
@@ -27,6 +27,32 @@ import type { CacheRepository } from "../../../repositories/types.js";
|
|
|
27
27
|
|
|
28
28
|
const logger = serverLogger.component("ssr-service");
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Provides a renderer for a given handler context.
|
|
32
|
+
* Extracted to allow dependency injection in tests.
|
|
33
|
+
*/
|
|
34
|
+
export interface RendererProvider {
|
|
35
|
+
getRenderer(ctx: HandlerContext): Promise<RendererAdapter>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Minimal interface for SSRService consumers (e.g., SSRHandler).
|
|
40
|
+
* Allows dependency injection and mocking in tests.
|
|
41
|
+
*/
|
|
42
|
+
export interface SSRServiceLike {
|
|
43
|
+
checkMemoryPressure(): MemoryStatus;
|
|
44
|
+
renderPage(ctx: HandlerContext, options: SSRRenderOptions): Promise<SSRRenderResult>;
|
|
45
|
+
createMemoryPressureResult(slug: string): SSRRenderResult;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Default RendererProvider that delegates to the real getRendererForProject.
|
|
50
|
+
*/
|
|
51
|
+
const defaultRendererProvider: RendererProvider = {
|
|
52
|
+
getRenderer: (ctx: HandlerContext) =>
|
|
53
|
+
timeAsync("renderer-init", () => getRendererForProject(ctx)),
|
|
54
|
+
};
|
|
55
|
+
|
|
30
56
|
export interface SSRRenderResult {
|
|
31
57
|
status: number;
|
|
32
58
|
html?: string;
|
|
@@ -59,11 +85,16 @@ export interface MemoryStatus {
|
|
|
59
85
|
heapUsedPercent: number;
|
|
60
86
|
}
|
|
61
87
|
|
|
62
|
-
export class SSRService {
|
|
88
|
+
export class SSRService implements SSRServiceLike {
|
|
63
89
|
private readonly cacheRepo?: CacheRepository<string>;
|
|
90
|
+
private readonly rendererProvider: RendererProvider;
|
|
64
91
|
|
|
65
|
-
constructor(options?: {
|
|
92
|
+
constructor(options?: {
|
|
93
|
+
cacheRepo?: CacheRepository<string>;
|
|
94
|
+
rendererProvider?: RendererProvider;
|
|
95
|
+
}) {
|
|
66
96
|
this.cacheRepo = options?.cacheRepo;
|
|
97
|
+
this.rendererProvider = options?.rendererProvider ?? defaultRendererProvider;
|
|
67
98
|
}
|
|
68
99
|
|
|
69
100
|
checkMemoryPressure(): MemoryStatus {
|
|
@@ -78,7 +109,7 @@ export class SSRService {
|
|
|
78
109
|
}
|
|
79
110
|
|
|
80
111
|
async getRenderer(ctx: HandlerContext): Promise<RendererAdapter> {
|
|
81
|
-
return
|
|
112
|
+
return this.rendererProvider.getRenderer(ctx);
|
|
82
113
|
}
|
|
83
114
|
|
|
84
115
|
async renderPage(ctx: HandlerContext, options: SSRRenderOptions): Promise<SSRRenderResult> {
|
|
@@ -64,11 +64,95 @@ export interface RendererAdapter {
|
|
|
64
64
|
destroy(): Promise<void>;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Abstraction over renderer initialization, allowing tests to inject
|
|
69
|
+
* a mock renderer without pulling in the full rendering subsystem.
|
|
70
|
+
*/
|
|
71
|
+
export interface RendererInitializer {
|
|
72
|
+
initialize(options: RendererOptions): Promise<Renderer>;
|
|
73
|
+
isInitialized(): boolean;
|
|
74
|
+
get(): Renderer;
|
|
75
|
+
destroy(): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Default initializer that delegates to the real shared renderer
|
|
80
|
+
* singleton from `#veryfront/rendering/renderer.ts`.
|
|
81
|
+
*/
|
|
82
|
+
const defaultInitializer: RendererInitializer = {
|
|
83
|
+
initialize: initializeRenderer,
|
|
84
|
+
isInitialized: isRendererInitialized,
|
|
85
|
+
get: getRenderer,
|
|
86
|
+
destroy: destroySharedRenderer,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
let activeInitializer: RendererInitializer = defaultInitializer;
|
|
90
|
+
let rendererInitState: { initializer: RendererInitializer; promise: Promise<Renderer> } | null =
|
|
91
|
+
null;
|
|
92
|
+
|
|
93
|
+
function scheduleInitializerDestroy(
|
|
94
|
+
initializer: RendererInitializer,
|
|
95
|
+
pendingPromise?: Promise<unknown>,
|
|
96
|
+
): void {
|
|
97
|
+
const destroy = async () => {
|
|
98
|
+
try {
|
|
99
|
+
await initializer.destroy();
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logger.warn("Failed to destroy renderer initializer", {
|
|
102
|
+
error: error instanceof Error ? error.message : String(error),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (pendingPromise) {
|
|
108
|
+
void pendingPromise
|
|
109
|
+
.catch(() => undefined)
|
|
110
|
+
.then(destroy);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!initializer.isInitialized()) return;
|
|
115
|
+
void destroy();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Replace the renderer initializer used by the adapter layer.
|
|
120
|
+
* Pass `undefined` to restore the default (real) initializer.
|
|
121
|
+
*
|
|
122
|
+
* Returns a disposer that restores the previous initializer — use in
|
|
123
|
+
* `afterEach` or with `using` to prevent test pollution:
|
|
124
|
+
*
|
|
125
|
+
* ```ts
|
|
126
|
+
* afterEach(() => setRendererInitializer(undefined));
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @internal Test-only — not part of the public API.
|
|
130
|
+
*/
|
|
131
|
+
export function setRendererInitializer(
|
|
132
|
+
initializer?: RendererInitializer,
|
|
133
|
+
): void {
|
|
134
|
+
const nextInitializer = initializer ?? defaultInitializer;
|
|
135
|
+
const previous = activeInitializer;
|
|
136
|
+
const previousPendingPromise = rendererInitState?.initializer === previous
|
|
137
|
+
? rendererInitState.promise
|
|
138
|
+
: undefined;
|
|
139
|
+
|
|
140
|
+
activeInitializer = nextInitializer;
|
|
141
|
+
|
|
142
|
+
if (rendererInitState?.initializer !== activeInitializer) {
|
|
143
|
+
rendererInitState = null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (previous !== activeInitializer) {
|
|
147
|
+
scheduleInitializerDestroy(previous, previousPendingPromise);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
68
150
|
|
|
69
151
|
async function getOrInitRenderer(): Promise<Renderer> {
|
|
70
|
-
if (
|
|
71
|
-
if (
|
|
152
|
+
if (activeInitializer.isInitialized()) return activeInitializer.get();
|
|
153
|
+
if (rendererInitState?.initializer === activeInitializer) {
|
|
154
|
+
return rendererInitState.promise;
|
|
155
|
+
}
|
|
72
156
|
|
|
73
157
|
const isProxyMode = getEnvBoolean("PROXY_MODE", false, {
|
|
74
158
|
trueValues: ["1"],
|
|
@@ -99,12 +183,19 @@ async function getOrInitRenderer(): Promise<Renderer> {
|
|
|
99
183
|
cacheType: useApiCache ? "api-distributed" : "memory",
|
|
100
184
|
});
|
|
101
185
|
|
|
102
|
-
|
|
186
|
+
const initializer = activeInitializer;
|
|
187
|
+
const initPromise = initializer.initialize(options);
|
|
188
|
+
rendererInitState = {
|
|
189
|
+
initializer,
|
|
190
|
+
promise: initPromise,
|
|
191
|
+
};
|
|
103
192
|
|
|
104
193
|
try {
|
|
105
|
-
return await
|
|
194
|
+
return await initPromise;
|
|
106
195
|
} finally {
|
|
107
|
-
|
|
196
|
+
if (rendererInitState?.promise === initPromise) {
|
|
197
|
+
rendererInitState = null;
|
|
198
|
+
}
|
|
108
199
|
}
|
|
109
200
|
}
|
|
110
201
|
|
|
@@ -308,6 +399,14 @@ export async function getRendererForProject(ctx: HandlerContext): Promise<Render
|
|
|
308
399
|
}
|
|
309
400
|
|
|
310
401
|
export async function destroyRendererAdapter(): Promise<void> {
|
|
311
|
-
|
|
312
|
-
|
|
402
|
+
const pendingPromise = rendererInitState?.initializer === activeInitializer
|
|
403
|
+
? rendererInitState.promise
|
|
404
|
+
: undefined;
|
|
405
|
+
rendererInitState = null;
|
|
406
|
+
|
|
407
|
+
if (pendingPromise) {
|
|
408
|
+
await pendingPromise.catch(() => undefined);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
await activeInitializer.destroy();
|
|
313
412
|
}
|
|
@@ -4,5 +4,11 @@
|
|
|
4
4
|
* @module server/shared/renderer
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
destroyRendererAdapter,
|
|
9
|
+
getRendererForProject,
|
|
10
|
+
type RendererAdapter,
|
|
11
|
+
type RendererInitializer,
|
|
12
|
+
setRendererInitializer,
|
|
13
|
+
} from "./adapter.js";
|
|
8
14
|
export { shouldRejectDueToMemory } from "./memory/pressure.js";
|