veryfront 0.0.81 → 0.0.83
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/README.md +15 -1
- package/esm/deno.js +1 -1
- package/esm/proxy/cache/index.d.ts +41 -0
- package/esm/proxy/cache/index.d.ts.map +1 -0
- package/esm/proxy/cache/index.js +75 -0
- package/esm/proxy/cache/memory-cache.d.ts +18 -0
- package/esm/proxy/cache/memory-cache.d.ts.map +1 -0
- package/esm/proxy/cache/memory-cache.js +100 -0
- package/esm/proxy/cache/redis-cache.d.ts +27 -0
- package/esm/proxy/cache/redis-cache.d.ts.map +1 -0
- package/esm/proxy/cache/redis-cache.js +183 -0
- package/esm/proxy/cache/resilient-cache.d.ts +44 -0
- package/esm/proxy/cache/resilient-cache.d.ts.map +1 -0
- package/esm/proxy/cache/resilient-cache.js +178 -0
- package/esm/proxy/cache/types.d.ts +65 -0
- package/esm/proxy/cache/types.d.ts.map +1 -0
- package/esm/proxy/cache/types.js +7 -0
- package/esm/proxy/handler.d.ts +81 -0
- package/esm/proxy/handler.d.ts.map +1 -0
- package/esm/proxy/handler.js +417 -0
- package/esm/proxy/logger.d.ts +29 -0
- package/esm/proxy/logger.d.ts.map +1 -0
- package/esm/proxy/logger.js +258 -0
- package/esm/proxy/oauth-client.d.ts +15 -0
- package/esm/proxy/oauth-client.d.ts.map +1 -0
- package/esm/proxy/oauth-client.js +52 -0
- package/esm/proxy/token-manager.d.ts +59 -0
- package/esm/proxy/token-manager.d.ts.map +1 -0
- package/esm/proxy/token-manager.js +125 -0
- package/esm/proxy/tracing.d.ts +39 -0
- package/esm/proxy/tracing.d.ts.map +1 -0
- package/esm/proxy/tracing.js +194 -0
- package/esm/src/cache/backend.d.ts +22 -0
- package/esm/src/cache/backend.d.ts.map +1 -1
- package/esm/src/cache/backend.js +59 -0
- package/esm/src/cache/cache-key-builder.d.ts +0 -4
- package/esm/src/cache/cache-key-builder.d.ts.map +1 -1
- package/esm/src/cache/cache-key-builder.js +0 -6
- package/esm/src/cache/hash.d.ts +107 -0
- package/esm/src/cache/hash.d.ts.map +1 -0
- package/esm/src/cache/hash.js +166 -0
- package/esm/src/cache/index.d.ts +3 -0
- package/esm/src/cache/index.d.ts.map +1 -1
- package/esm/src/cache/index.js +3 -0
- package/esm/src/cache/module-cache.d.ts +82 -0
- package/esm/src/cache/module-cache.d.ts.map +1 -0
- package/esm/src/cache/module-cache.js +214 -0
- package/esm/src/cache/multi-tier.d.ts +148 -0
- package/esm/src/cache/multi-tier.d.ts.map +1 -0
- package/esm/src/cache/multi-tier.js +326 -0
- package/esm/src/cli/app/actions.d.ts +26 -0
- package/esm/src/cli/app/actions.d.ts.map +1 -0
- package/esm/src/cli/app/actions.js +152 -0
- package/esm/src/cli/app/components/inline-input.d.ts +35 -0
- package/esm/src/cli/app/components/inline-input.d.ts.map +1 -0
- package/esm/src/cli/app/components/inline-input.js +220 -0
- package/esm/src/cli/app/components/list-select.d.ts +69 -0
- package/esm/src/cli/app/components/list-select.d.ts.map +1 -0
- package/esm/src/cli/app/components/list-select.js +137 -0
- package/esm/src/cli/app/index.d.ts +45 -0
- package/esm/src/cli/app/index.d.ts.map +1 -0
- package/esm/src/cli/app/index.js +1252 -0
- package/esm/src/cli/app/state.d.ts +122 -0
- package/esm/src/cli/app/state.d.ts.map +1 -0
- package/esm/src/cli/app/state.js +232 -0
- package/esm/src/cli/app/views/dashboard.d.ts +19 -0
- package/esm/src/cli/app/views/dashboard.d.ts.map +1 -0
- package/esm/src/cli/app/views/dashboard.js +178 -0
- package/esm/src/cli/index/command-router.d.ts.map +1 -1
- package/esm/src/cli/index/command-router.js +9 -39
- package/esm/src/cli/index/start-handler.d.ts +3 -0
- package/esm/src/cli/index/start-handler.d.ts.map +1 -0
- package/esm/src/cli/index/start-handler.js +145 -0
- package/esm/src/cli/mcp/index.d.ts +11 -0
- package/esm/src/cli/mcp/index.d.ts.map +1 -0
- package/esm/src/cli/mcp/index.js +10 -0
- package/esm/src/cli/templates/integration-loader.d.ts.map +1 -1
- package/esm/src/cli/templates/integration-loader.js +2 -4
- package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts +2 -0
- package/esm/src/middleware/builtin/security/redis-rate-limit.d.ts.map +1 -1
- package/esm/src/middleware/builtin/security/redis-rate-limit.js +23 -9
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts +10 -0
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/cache/redis.js +30 -42
- package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/loader.js +148 -20
- package/esm/src/observability/tracing/span-names.d.ts +2 -0
- package/esm/src/observability/tracing/span-names.d.ts.map +1 -1
- package/esm/src/observability/tracing/span-names.js +2 -0
- package/esm/src/platform/adapters/fs/cache/file-cache.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/cache/file-cache.js +9 -3
- package/esm/src/rendering/orchestrator/module-loader/cache.d.ts +10 -2
- package/esm/src/rendering/orchestrator/module-loader/cache.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/module-loader/cache.js +11 -6
- package/esm/src/rendering/orchestrator/module-loader/index.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/module-loader/index.js +72 -77
- package/esm/src/server/context/cache-invalidation.d.ts.map +1 -1
- package/esm/src/server/context/cache-invalidation.js +4 -0
- package/esm/src/server/handlers/dev/dashboard/api.js +4 -0
- package/esm/src/server/handlers/dev/projects/ui-handler.d.ts.map +1 -1
- package/esm/src/server/handlers/dev/projects/ui-handler.js +6 -0
- package/esm/src/transforms/esm/http-cache.d.ts.map +1 -1
- package/esm/src/transforms/esm/http-cache.js +145 -93
- package/esm/src/transforms/esm/transform-cache.d.ts +25 -0
- package/esm/src/transforms/esm/transform-cache.d.ts.map +1 -1
- package/esm/src/transforms/esm/transform-cache.js +45 -0
- package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.d.ts.map +1 -1
- package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.js +2 -36
- package/esm/src/utils/constants/cache.d.ts +4 -0
- package/esm/src/utils/constants/cache.d.ts.map +1 -1
- package/esm/src/utils/constants/cache.js +14 -1
- package/esm/src/utils/index.d.ts +1 -1
- package/esm/src/utils/index.d.ts.map +1 -1
- package/esm/src/utils/index.js +1 -1
- package/package.json +2 -1
- package/src/deno.js +1 -1
- package/src/proxy/cache/index.ts +93 -0
- package/src/proxy/cache/memory-cache.ts +120 -0
- package/src/proxy/cache/redis-cache.ts +203 -0
- package/src/proxy/cache/resilient-cache.ts +205 -0
- package/src/proxy/cache/types.ts +72 -0
- package/src/proxy/handler.ts +593 -0
- package/src/proxy/logger.ts +329 -0
- package/src/proxy/oauth-client.ts +91 -0
- package/src/proxy/token-manager.ts +174 -0
- package/src/proxy/tracing.ts +237 -0
- package/src/src/cache/backend.ts +65 -0
- package/src/src/cache/cache-key-builder.ts +0 -9
- package/src/src/cache/hash.ts +205 -0
- package/src/src/cache/index.ts +3 -0
- package/src/src/cache/module-cache.ts +252 -0
- package/src/src/cache/multi-tier.ts +462 -0
- package/src/src/cli/app/actions.ts +190 -0
- package/src/src/cli/app/components/inline-input.ts +255 -0
- package/src/src/cli/app/components/list-select.ts +215 -0
- package/src/src/cli/app/index.ts +1471 -0
- package/src/src/cli/app/state.ts +385 -0
- package/src/src/cli/app/views/dashboard.ts +212 -0
- package/src/src/cli/index/command-router.ts +9 -40
- package/src/src/cli/index/start-handler.ts +195 -0
- package/src/src/cli/mcp/index.ts +11 -0
- package/src/src/cli/templates/integration-loader.ts +2 -8
- package/src/src/middleware/builtin/security/redis-rate-limit.ts +24 -11
- package/src/src/modules/react-loader/ssr-module-loader/cache/redis.ts +36 -50
- package/src/src/modules/react-loader/ssr-module-loader/loader.ts +168 -25
- package/src/src/observability/tracing/span-names.ts +2 -0
- package/src/src/platform/adapters/fs/cache/file-cache.ts +9 -3
- package/src/src/rendering/orchestrator/module-loader/cache.ts +14 -8
- package/src/src/rendering/orchestrator/module-loader/index.ts +94 -89
- package/src/src/server/context/cache-invalidation.ts +4 -0
- package/src/src/server/handlers/dev/dashboard/api.ts +2 -0
- package/src/src/server/handlers/dev/projects/ui-handler.ts +6 -0
- package/src/src/transforms/esm/http-cache.ts +160 -105
- package/src/src/transforms/esm/transform-cache.ts +53 -0
- package/src/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts +2 -40
- package/src/src/utils/constants/cache.ts +21 -1
- package/src/src/utils/index.ts +0 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry OTLP tracing for proxy.
|
|
3
|
+
* Env: OTEL_TRACES_ENABLED, OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS
|
|
4
|
+
*/
|
|
5
|
+
import * as dntShim from "../_dnt.shims.js";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import type { Context, Span, Tracer } from "@opentelemetry/api";
|
|
9
|
+
|
|
10
|
+
// Inline cross-runtime getEnv to avoid dependency on src/platform/compat (not copied in Docker)
|
|
11
|
+
function getEnv(key: string): string | undefined {
|
|
12
|
+
// Deno
|
|
13
|
+
if (typeof dntShim.Deno !== "undefined" && dntShim.Deno.env?.get) {
|
|
14
|
+
return dntShim.Deno.env.get(key);
|
|
15
|
+
}
|
|
16
|
+
// Node.js / Bun
|
|
17
|
+
const nodeProcess = (dntShim.dntGlobalThis as { process?: { env?: Record<string, string> } }).process;
|
|
18
|
+
return nodeProcess?.env?.[key];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let initialized = false;
|
|
22
|
+
let tracerProvider: { shutdown: () => Promise<void> } | null = null;
|
|
23
|
+
let tracer: Tracer | null = null;
|
|
24
|
+
|
|
25
|
+
interface OTLPConfig {
|
|
26
|
+
serviceName: string;
|
|
27
|
+
endpoint: string;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseHeaders(headerString: string | undefined): Record<string, string> {
|
|
33
|
+
if (!headerString) return {};
|
|
34
|
+
const headers: Record<string, string> = {};
|
|
35
|
+
for (const part of headerString.split(",")) {
|
|
36
|
+
const [key, ...valueParts] = part.split("=");
|
|
37
|
+
if (key && valueParts.length > 0) {
|
|
38
|
+
headers[key.trim()] = valueParts.join("=").trim();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return headers;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getConfig(): OTLPConfig {
|
|
45
|
+
return {
|
|
46
|
+
enabled: getEnv("OTEL_TRACES_ENABLED") === "true",
|
|
47
|
+
serviceName: getEnv("OTEL_SERVICE_NAME") || "veryfront-proxy",
|
|
48
|
+
endpoint: getEnv("OTEL_EXPORTER_OTLP_ENDPOINT") || "",
|
|
49
|
+
headers: parseHeaders(getEnv("OTEL_EXPORTER_OTLP_HEADERS")),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let traceApi: typeof import("@opentelemetry/api") | null = null;
|
|
54
|
+
let propagationApi: typeof import("@opentelemetry/core") | null = null;
|
|
55
|
+
|
|
56
|
+
async function loadApis(): Promise<void> {
|
|
57
|
+
if (traceApi) return;
|
|
58
|
+
traceApi = await import("@opentelemetry/api");
|
|
59
|
+
propagationApi = await import("@opentelemetry/core");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function initializeOTLP(): Promise<void> {
|
|
63
|
+
if (initialized) return;
|
|
64
|
+
|
|
65
|
+
const config = getConfig();
|
|
66
|
+
|
|
67
|
+
if (!config.enabled) {
|
|
68
|
+
initialized = true;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!config.endpoint) {
|
|
73
|
+
console.warn("[otel] No endpoint configured");
|
|
74
|
+
initialized = true;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const { trace } = await import("@opentelemetry/api");
|
|
80
|
+
const { BasicTracerProvider, BatchSpanProcessor } = await import(
|
|
81
|
+
"@opentelemetry/sdk-trace-base"
|
|
82
|
+
);
|
|
83
|
+
const { OTLPTraceExporter } = await import("@opentelemetry/exporter-trace-otlp-http");
|
|
84
|
+
const { Resource } = await import("@opentelemetry/resources");
|
|
85
|
+
const { ATTR_SERVICE_NAME } = await import("@opentelemetry/semantic-conventions");
|
|
86
|
+
|
|
87
|
+
const resource = new Resource({ [ATTR_SERVICE_NAME]: config.serviceName });
|
|
88
|
+
const exporter = new OTLPTraceExporter({
|
|
89
|
+
url: `${config.endpoint}/v1/traces`,
|
|
90
|
+
headers: config.headers,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const provider = new BasicTracerProvider({ resource });
|
|
94
|
+
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
|
|
95
|
+
provider.register();
|
|
96
|
+
|
|
97
|
+
tracerProvider = provider;
|
|
98
|
+
tracer = trace.getTracer(config.serviceName);
|
|
99
|
+
initialized = true;
|
|
100
|
+
|
|
101
|
+
await loadApis();
|
|
102
|
+
|
|
103
|
+
console.log("[otel] Initialized", {
|
|
104
|
+
serviceName: config.serviceName,
|
|
105
|
+
endpoint: config.endpoint,
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error("[otel] Init failed", { error });
|
|
109
|
+
initialized = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function shutdownOTLP(): Promise<void> {
|
|
114
|
+
if (tracerProvider) {
|
|
115
|
+
try {
|
|
116
|
+
await tracerProvider.shutdown();
|
|
117
|
+
console.log("[otel] Shutdown complete");
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.warn("[otel] Shutdown error", { error });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isOTLPEnabled(): boolean {
|
|
125
|
+
return initialized && tracerProvider !== null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function extractContext(headers: dntShim.Headers): Context | undefined {
|
|
129
|
+
if (!traceApi || !propagationApi) return undefined;
|
|
130
|
+
const carrier: Record<string, string> = {};
|
|
131
|
+
headers.forEach((v, k) => (carrier[k.toLowerCase()] = v));
|
|
132
|
+
return new propagationApi.W3CTraceContextPropagator().extract(
|
|
133
|
+
traceApi.context.active(),
|
|
134
|
+
carrier,
|
|
135
|
+
traceApi.defaultTextMapGetter,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function injectContext(headers: dntShim.Headers): void {
|
|
140
|
+
if (!traceApi || !propagationApi) return;
|
|
141
|
+
const carrier: Record<string, string> = {};
|
|
142
|
+
new propagationApi.W3CTraceContextPropagator().inject(
|
|
143
|
+
traceApi.context.active(),
|
|
144
|
+
carrier,
|
|
145
|
+
traceApi.defaultTextMapSetter,
|
|
146
|
+
);
|
|
147
|
+
Object.entries(carrier).forEach(([k, v]) => headers.set(k, v));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function startServerSpan(
|
|
151
|
+
method: string,
|
|
152
|
+
path: string,
|
|
153
|
+
parentContext?: Context,
|
|
154
|
+
): { span: Span; context: Context } | null {
|
|
155
|
+
if (!traceApi || !tracer) return null;
|
|
156
|
+
const ctx = parentContext || traceApi.context.active();
|
|
157
|
+
const span = tracer.startSpan(`${method} ${path}`, { kind: traceApi.SpanKind.SERVER }, ctx);
|
|
158
|
+
return { span, context: traceApi.trace.setSpan(ctx, span) };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function endSpan(span: Span | undefined, statusCode: number, error?: Error): void {
|
|
162
|
+
if (!span || !traceApi) return;
|
|
163
|
+
span.setAttribute("http.status_code", statusCode);
|
|
164
|
+
if (error) {
|
|
165
|
+
span.setStatus({ code: traceApi.SpanStatusCode.ERROR, message: error.message });
|
|
166
|
+
span.recordException(error);
|
|
167
|
+
} else if (statusCode >= 400) {
|
|
168
|
+
span.setStatus({ code: traceApi.SpanStatusCode.ERROR });
|
|
169
|
+
}
|
|
170
|
+
span.end();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function withContext<T>(spanContext: Context, fn: () => Promise<T>): Promise<T> {
|
|
174
|
+
if (!traceApi) return fn();
|
|
175
|
+
return traceApi.context.with(spanContext, fn);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function getTraceContext(): { traceId?: string; spanId?: string } {
|
|
179
|
+
if (!traceApi) return {};
|
|
180
|
+
const span = traceApi.trace.getSpan(traceApi.context.active());
|
|
181
|
+
if (!span) return {};
|
|
182
|
+
const ctx = span.spanContext();
|
|
183
|
+
return { traceId: ctx.traceId, spanId: ctx.spanId };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Span names for proxy tracing.
|
|
188
|
+
*/
|
|
189
|
+
export const ProxySpanNames = {
|
|
190
|
+
PROXY_REQUEST: "proxy.request",
|
|
191
|
+
PROXY_PROCESS: "proxy.process",
|
|
192
|
+
PROXY_TOKEN_FETCH: "proxy.token_fetch",
|
|
193
|
+
PROXY_DOMAIN_LOOKUP: "proxy.domain_lookup",
|
|
194
|
+
OAUTH_TOKEN_REQUEST: "oauth.token_request",
|
|
195
|
+
HTTP_CLIENT_FETCH: "http.client.fetch",
|
|
196
|
+
} as const;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Execute an async function within a tracing span.
|
|
200
|
+
* If tracing is disabled, executes the function directly.
|
|
201
|
+
*/
|
|
202
|
+
export async function withSpan<T>(
|
|
203
|
+
name: string,
|
|
204
|
+
fn: () => Promise<T>,
|
|
205
|
+
attributes?: Record<string, string | number | boolean>,
|
|
206
|
+
): Promise<T> {
|
|
207
|
+
if (!traceApi || !tracer) {
|
|
208
|
+
return await fn();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const parentContext = traceApi.context.active();
|
|
212
|
+
const span = tracer.startSpan(name, {
|
|
213
|
+
kind: traceApi.SpanKind.INTERNAL,
|
|
214
|
+
attributes,
|
|
215
|
+
}, parentContext);
|
|
216
|
+
|
|
217
|
+
const spanContext = traceApi.trace.setSpan(parentContext, span);
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const result = await traceApi.context.with(spanContext, fn);
|
|
221
|
+
span.setStatus({ code: traceApi.SpanStatusCode.OK });
|
|
222
|
+
return result;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
span.setStatus({
|
|
225
|
+
code: traceApi.SpanStatusCode.ERROR,
|
|
226
|
+
message: error instanceof Error ? error.message : String(error),
|
|
227
|
+
});
|
|
228
|
+
if (error instanceof Error) {
|
|
229
|
+
span.recordException(error);
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
232
|
+
} finally {
|
|
233
|
+
span.end();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export { initializeOTLP as initializeOTLPWithApis };
|
package/src/src/cache/backend.ts
CHANGED
|
@@ -587,6 +587,68 @@ export function createCacheBackend(config: CacheBackendConfig = {}): Promise<Cac
|
|
|
587
587
|
);
|
|
588
588
|
}
|
|
589
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Check if a cache backend supports distributed (cross-pod) caching.
|
|
592
|
+
*
|
|
593
|
+
* Use this instead of checking `backend.type === "memory"` directly,
|
|
594
|
+
* which is a leaky abstraction that exposes implementation details.
|
|
595
|
+
*/
|
|
596
|
+
export function isDistributedBackend(backend: CacheBackend): boolean {
|
|
597
|
+
return backend.type !== "memory";
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Create a lazy-initialized distributed cache accessor.
|
|
602
|
+
*
|
|
603
|
+
* This encapsulates the common pattern of:
|
|
604
|
+
* 1. Lazy-init a cache backend via Singleflight
|
|
605
|
+
* 2. Skip if memory-only (not useful for cross-pod sharing)
|
|
606
|
+
* 3. Return null if init fails
|
|
607
|
+
*
|
|
608
|
+
* @param factory - Function that creates the cache backend
|
|
609
|
+
* @param name - Log prefix for debug messages
|
|
610
|
+
* @returns A function that returns the distributed cache backend or null
|
|
611
|
+
*/
|
|
612
|
+
export function createDistributedCacheAccessor(
|
|
613
|
+
factory: () => Promise<CacheBackend>,
|
|
614
|
+
name: string,
|
|
615
|
+
): () => Promise<CacheBackend | null> {
|
|
616
|
+
let backend: CacheBackend | null | undefined;
|
|
617
|
+
const singleflight = new (class {
|
|
618
|
+
private promise: Promise<CacheBackend | null> | null = null;
|
|
619
|
+
do(fn: () => Promise<CacheBackend | null>): Promise<CacheBackend | null> {
|
|
620
|
+
if (!this.promise) {
|
|
621
|
+
this.promise = fn().finally(() => {
|
|
622
|
+
this.promise = null;
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return this.promise;
|
|
626
|
+
}
|
|
627
|
+
})();
|
|
628
|
+
|
|
629
|
+
return () => {
|
|
630
|
+
if (backend !== undefined) return Promise.resolve(backend);
|
|
631
|
+
|
|
632
|
+
return singleflight.do(async () => {
|
|
633
|
+
try {
|
|
634
|
+
const b = await factory();
|
|
635
|
+
if (!isDistributedBackend(b)) {
|
|
636
|
+
backend = null;
|
|
637
|
+
logger.debug(`[${name}] No distributed cache available (memory only)`);
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
backend = b;
|
|
641
|
+
logger.debug(`[${name}] Distributed cache initialized`, { type: b.type });
|
|
642
|
+
return b;
|
|
643
|
+
} catch (error) {
|
|
644
|
+
logger.debug(`[${name}] Failed to initialize distributed cache`, { error });
|
|
645
|
+
backend = null;
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
590
652
|
/** Convenience wrappers for common cache patterns. */
|
|
591
653
|
export const CacheBackends = {
|
|
592
654
|
/** Transform cache for compiled code. */
|
|
@@ -609,6 +671,9 @@ export const CacheBackends = {
|
|
|
609
671
|
httpModule: () =>
|
|
610
672
|
createCacheBackend({ keyPrefix: "http-module", circuitBreakerName: "api-cache-http" }),
|
|
611
673
|
|
|
674
|
+
/** SSR module cache for React loader (cross-pod sharing). */
|
|
675
|
+
ssrModule: () => createCacheBackend({ keyPrefix: "ssr-module" }),
|
|
676
|
+
|
|
612
677
|
/** Project CSS cache for Tailwind CSS output (cross-pod sharing). */
|
|
613
678
|
projectCSS: () => createCacheBackend({ keyPrefix: "project-css" }),
|
|
614
679
|
};
|
|
@@ -119,12 +119,3 @@ export function extractCacheKeyContext(handlerCtx: HandlerContext): CacheKeyCont
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
export type { MultiProjectRequestContextType as MultiProjectRequestContext };
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* @deprecated Use tryGetCacheKeyContext() which auto-detects context
|
|
125
|
-
*/
|
|
126
|
-
export function extractCacheKeyContextFromRequestContext(
|
|
127
|
-
reqCtx: MultiProjectRequestContextType,
|
|
128
|
-
): CacheKeyContext {
|
|
129
|
-
return extractCacheKeyContextFromMultiProjectContext(reqCtx);
|
|
130
|
-
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized Cache Hashing Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent hashing for cache keys across the codebase.
|
|
5
|
+
* All cache keys should use these utilities to ensure:
|
|
6
|
+
* - Consistent format with type prefixes
|
|
7
|
+
* - Collision resistance between different cache types
|
|
8
|
+
* - Easy debugging and key parsing
|
|
9
|
+
*
|
|
10
|
+
* Key format: `{type}:{hash}` or `{type}:{version}:{hash}`
|
|
11
|
+
*
|
|
12
|
+
* @module cache/hash
|
|
13
|
+
*/
|
|
14
|
+
import * as dntShim from "../../_dnt.shims.js";
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
import { simpleHash } from "../utils/hash-utils.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Cache key types for different cache domains.
|
|
21
|
+
* Using type prefixes prevents collisions between different caches.
|
|
22
|
+
*/
|
|
23
|
+
export type CacheKeyType =
|
|
24
|
+
| "http" // HTTP module cache (esm.sh bundles)
|
|
25
|
+
| "mod" // Module transform cache
|
|
26
|
+
| "esm" // ESM resolution cache
|
|
27
|
+
| "render" // Render result cache
|
|
28
|
+
| "mdx" // MDX bundle cache
|
|
29
|
+
| "css" // CSS/Tailwind cache
|
|
30
|
+
| "file" // File content cache
|
|
31
|
+
| "config"; // Configuration cache
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fast, synchronous hash function for cache keys.
|
|
35
|
+
*
|
|
36
|
+
* Uses DJB2 algorithm - good distribution, very fast.
|
|
37
|
+
* Returns a positive number suitable for string conversion.
|
|
38
|
+
*
|
|
39
|
+
* @param input - String to hash
|
|
40
|
+
* @returns Positive integer hash
|
|
41
|
+
*/
|
|
42
|
+
export function fastHash(input: string): number {
|
|
43
|
+
let hash = 5381;
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < input.length; i++) {
|
|
46
|
+
hash = ((hash << 5) + hash) ^ input.charCodeAt(i);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return hash >>> 0; // Convert to unsigned 32-bit
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Convert a hash number to a compact string representation.
|
|
54
|
+
*
|
|
55
|
+
* Uses base36 for compact output (0-9, a-z).
|
|
56
|
+
*/
|
|
57
|
+
export function hashToString(hash: number): string {
|
|
58
|
+
return hash.toString(36);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fast synchronous hash that returns a string.
|
|
63
|
+
*
|
|
64
|
+
* Combines fastHash + hashToString for convenience.
|
|
65
|
+
*/
|
|
66
|
+
export function hashString(input: string): string {
|
|
67
|
+
return hashToString(fastHash(input));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate a cache key with type prefix.
|
|
72
|
+
*
|
|
73
|
+
* Format: `{type}:{hash}`
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* getCacheKey("http", "https://esm.sh/react@18.3.1")
|
|
78
|
+
* // Returns: "http:1abc2def"
|
|
79
|
+
*
|
|
80
|
+
* getCacheKey("mod", "pages/index.tsx")
|
|
81
|
+
* // Returns: "mod:xyz789"
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function getCacheKey(type: CacheKeyType, input: string): string {
|
|
85
|
+
const hash = hashString(input);
|
|
86
|
+
return `${type}:${hash}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Generate a versioned cache key with type prefix.
|
|
91
|
+
*
|
|
92
|
+
* Format: `{type}:v{version}:{hash}`
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* getVersionedCacheKey("mod", 12, "pages/index.tsx:abc123")
|
|
97
|
+
* // Returns: "mod:v12:xyz789"
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function getVersionedCacheKey(
|
|
101
|
+
type: CacheKeyType,
|
|
102
|
+
version: number | string,
|
|
103
|
+
input: string,
|
|
104
|
+
): string {
|
|
105
|
+
const hash = hashString(input);
|
|
106
|
+
return `${type}:v${version}:${hash}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Generate a cache key with multiple components.
|
|
111
|
+
*
|
|
112
|
+
* Useful for keys that depend on multiple inputs.
|
|
113
|
+
* Components are joined with colons before hashing.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* getCompoundCacheKey("mod", ["projectId", "filePath", "contentHash"])
|
|
118
|
+
* // Returns: "mod:abc123"
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function getCompoundCacheKey(type: CacheKeyType, components: string[]): string {
|
|
122
|
+
const combined = components.join(":");
|
|
123
|
+
return getCacheKey(type, combined);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parse a cache key into its components.
|
|
128
|
+
*
|
|
129
|
+
* @returns The type prefix and hash, or null if invalid format
|
|
130
|
+
*/
|
|
131
|
+
export function parseCacheKey(
|
|
132
|
+
key: string,
|
|
133
|
+
): { type: string; hash: string; version?: string } | null {
|
|
134
|
+
const parts = key.split(":");
|
|
135
|
+
|
|
136
|
+
if (parts.length < 2) return null;
|
|
137
|
+
|
|
138
|
+
const type = parts[0]!;
|
|
139
|
+
const rest = parts.slice(1);
|
|
140
|
+
|
|
141
|
+
// Check for version: type:vN:hash
|
|
142
|
+
if (rest[0]?.startsWith("v") && /^v\d+$/.test(rest[0])) {
|
|
143
|
+
return {
|
|
144
|
+
type,
|
|
145
|
+
version: rest[0].slice(1),
|
|
146
|
+
hash: rest.slice(1).join(":"),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
type,
|
|
152
|
+
hash: rest.join(":"),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* SHA-256 async hash for content-addressed keys.
|
|
158
|
+
*
|
|
159
|
+
* Use this when you need cryptographic strength (e.g., content hashes).
|
|
160
|
+
* For cache keys where speed matters more, use hashString().
|
|
161
|
+
*/
|
|
162
|
+
export async function sha256Hash(input: string): Promise<string> {
|
|
163
|
+
const data = new TextEncoder().encode(input);
|
|
164
|
+
const hashBuffer = await dntShim.crypto.subtle.digest("SHA-256", data);
|
|
165
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
166
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
167
|
+
.join("");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Short SHA-256 hash (8 characters).
|
|
172
|
+
*
|
|
173
|
+
* Good balance between collision resistance and key length.
|
|
174
|
+
*/
|
|
175
|
+
export async function sha256Short(input: string): Promise<string> {
|
|
176
|
+
const full = await sha256Hash(input);
|
|
177
|
+
return full.slice(0, 8);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Generate HTTP bundle filename from URL.
|
|
182
|
+
*
|
|
183
|
+
* Consistent with existing http-cache.ts format: `http-{hash}.mjs`
|
|
184
|
+
*/
|
|
185
|
+
export function getHttpBundleFilename(normalizedUrl: string): string {
|
|
186
|
+
const hash = simpleHash(normalizedUrl);
|
|
187
|
+
return `http-${hash}.mjs`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Extract hash from HTTP bundle filename.
|
|
192
|
+
*
|
|
193
|
+
* @returns The hash string, or null if not a valid bundle filename
|
|
194
|
+
*/
|
|
195
|
+
export function parseHttpBundleFilename(filename: string): string | null {
|
|
196
|
+
const match = filename.match(/^http-(\d+)\.mjs$/);
|
|
197
|
+
return match?.[1] ?? null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if a value looks like a cache key.
|
|
202
|
+
*/
|
|
203
|
+
export function isCacheKey(value: string): boolean {
|
|
204
|
+
return /^[a-z]+:[a-z0-9]+/.test(value);
|
|
205
|
+
}
|