@textjs/app 0.0.0-stage → 1.0.3
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 +34 -2
- package/bin/text.js +29 -0
- package/bin/text.ts +13 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/cli/build.d.ts +39 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +157 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/config.d.ts +22 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +73 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/dev.d.ts +12 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +49 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/index.d.ts +26 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +192 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/run.d.ts +31 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +111 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/start.d.ts +14 -0
- package/dist/cli/start.d.ts.map +1 -0
- package/dist/cli/start.js +183 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/cli/ui.d.ts +35 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +73 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +93 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +143 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +44 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +26 -0
- package/dist/context.js.map +1 -0
- package/dist/dev-errors.d.ts +68 -0
- package/dist/dev-errors.d.ts.map +1 -0
- package/dist/dev-errors.js +121 -0
- package/dist/dev-errors.js.map +1 -0
- package/dist/devtools.d.ts +64 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +186 -0
- package/dist/devtools.js.map +1 -0
- package/dist/frame-mirror.d.ts +19 -0
- package/dist/frame-mirror.d.ts.map +1 -0
- package/dist/frame-mirror.js +141 -0
- package/dist/frame-mirror.js.map +1 -0
- package/dist/frame-protocol.d.ts +175 -0
- package/dist/frame-protocol.d.ts.map +1 -0
- package/dist/frame-protocol.js +369 -0
- package/dist/frame-protocol.js.map +1 -0
- package/dist/history.d.ts +54 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +152 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/link.d.ts +32 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +54 -0
- package/dist/link.js.map +1 -0
- package/dist/live-player.d.ts +83 -0
- package/dist/live-player.d.ts.map +1 -0
- package/dist/live-player.js +413 -0
- package/dist/live-player.js.map +1 -0
- package/dist/live-protocol.d.ts +132 -0
- package/dist/live-protocol.d.ts.map +1 -0
- package/dist/live-protocol.js +278 -0
- package/dist/live-protocol.js.map +1 -0
- package/dist/live-stub.d.ts +10 -0
- package/dist/live-stub.d.ts.map +1 -0
- package/dist/live-stub.js +32 -0
- package/dist/live-stub.js.map +1 -0
- package/dist/mcp/cli.d.ts +22 -0
- package/dist/mcp/cli.d.ts.map +1 -0
- package/dist/mcp/cli.js +177 -0
- package/dist/mcp/cli.js.map +1 -0
- package/dist/mcp/frame.d.ts +58 -0
- package/dist/mcp/frame.d.ts.map +1 -0
- package/dist/mcp/frame.js +169 -0
- package/dist/mcp/frame.js.map +1 -0
- package/dist/mcp/index.d.ts +15 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +15 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/inspect.d.ts +95 -0
- package/dist/mcp/inspect.d.ts.map +1 -0
- package/dist/mcp/inspect.js +386 -0
- package/dist/mcp/inspect.js.map +1 -0
- package/dist/mcp/keys.d.ts +20 -0
- package/dist/mcp/keys.d.ts.map +1 -0
- package/dist/mcp/keys.js +87 -0
- package/dist/mcp/keys.js.map +1 -0
- package/dist/mcp/logs.d.ts +63 -0
- package/dist/mcp/logs.d.ts.map +1 -0
- package/dist/mcp/logs.js +217 -0
- package/dist/mcp/logs.js.map +1 -0
- package/dist/mcp/server.d.ts +64 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +485 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session.d.ts +230 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +742 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/navigation-errors.d.ts +28 -0
- package/dist/navigation-errors.d.ts.map +1 -0
- package/dist/navigation-errors.js +43 -0
- package/dist/navigation-errors.js.map +1 -0
- package/dist/navigation.d.ts +21 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +33 -0
- package/dist/navigation.js.map +1 -0
- package/dist/router.d.ts +76 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +296 -0
- package/dist/router.js.map +1 -0
- package/dist/routing.d.ts +101 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +286 -0
- package/dist/routing.js.map +1 -0
- package/dist/server/adapters/build-step.d.ts +34 -0
- package/dist/server/adapters/build-step.d.ts.map +1 -0
- package/dist/server/adapters/build-step.js +112 -0
- package/dist/server/adapters/build-step.js.map +1 -0
- package/dist/server/adapters/bun.d.ts +8 -0
- package/dist/server/adapters/bun.d.ts.map +1 -0
- package/dist/server/adapters/bun.js +89 -0
- package/dist/server/adapters/bun.js.map +1 -0
- package/dist/server/adapters/bundle.d.ts +21 -0
- package/dist/server/adapters/bundle.d.ts.map +1 -0
- package/dist/server/adapters/bundle.js +86 -0
- package/dist/server/adapters/bundle.js.map +1 -0
- package/dist/server/adapters/cloudflare.d.ts +29 -0
- package/dist/server/adapters/cloudflare.d.ts.map +1 -0
- package/dist/server/adapters/cloudflare.js +105 -0
- package/dist/server/adapters/cloudflare.js.map +1 -0
- package/dist/server/adapters/index.d.ts +10 -0
- package/dist/server/adapters/index.d.ts.map +1 -0
- package/dist/server/adapters/index.js +24 -0
- package/dist/server/adapters/index.js.map +1 -0
- package/dist/server/adapters/runtime-node.d.ts +16 -0
- package/dist/server/adapters/runtime-node.d.ts.map +1 -0
- package/dist/server/adapters/runtime-node.js +29 -0
- package/dist/server/adapters/runtime-node.js.map +1 -0
- package/dist/server/adapters/runtime.d.ts +42 -0
- package/dist/server/adapters/runtime.d.ts.map +1 -0
- package/dist/server/adapters/runtime.js +115 -0
- package/dist/server/adapters/runtime.js.map +1 -0
- package/dist/server/adapters/shared.d.ts +66 -0
- package/dist/server/adapters/shared.d.ts.map +1 -0
- package/dist/server/adapters/shared.js +124 -0
- package/dist/server/adapters/shared.js.map +1 -0
- package/dist/server/adapters/static.d.ts +9 -0
- package/dist/server/adapters/static.d.ts.map +1 -0
- package/dist/server/adapters/static.js +70 -0
- package/dist/server/adapters/static.js.map +1 -0
- package/dist/server/adapters/vercel.d.ts +43 -0
- package/dist/server/adapters/vercel.d.ts.map +1 -0
- package/dist/server/adapters/vercel.js +119 -0
- package/dist/server/adapters/vercel.js.map +1 -0
- package/dist/server/bun-jsx.d.ts +18 -0
- package/dist/server/bun-jsx.d.ts.map +1 -0
- package/dist/server/bun-jsx.js +56 -0
- package/dist/server/bun-jsx.js.map +1 -0
- package/dist/server/cache-route.d.ts +38 -0
- package/dist/server/cache-route.d.ts.map +1 -0
- package/dist/server/cache-route.js +109 -0
- package/dist/server/cache-route.js.map +1 -0
- package/dist/server/cache.d.ts +131 -0
- package/dist/server/cache.d.ts.map +1 -0
- package/dist/server/cache.js +409 -0
- package/dist/server/cache.js.map +1 -0
- package/dist/server/frames.d.ts +45 -0
- package/dist/server/frames.d.ts.map +1 -0
- package/dist/server/frames.js +294 -0
- package/dist/server/frames.js.map +1 -0
- package/dist/server/http.d.ts +80 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +453 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/jsx-dev-runtime.d.ts +5 -0
- package/dist/server/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/server/jsx-dev-runtime.js +6 -0
- package/dist/server/jsx-dev-runtime.js.map +1 -0
- package/dist/server/jsx-runtime.d.ts +10 -0
- package/dist/server/jsx-runtime.d.ts.map +1 -0
- package/dist/server/jsx-runtime.js +11 -0
- package/dist/server/jsx-runtime.js.map +1 -0
- package/dist/server/live-app.d.ts +87 -0
- package/dist/server/live-app.d.ts.map +1 -0
- package/dist/server/live-app.js +307 -0
- package/dist/server/live-app.js.map +1 -0
- package/dist/server/live-build.d.ts +70 -0
- package/dist/server/live-build.d.ts.map +1 -0
- package/dist/server/live-build.js +222 -0
- package/dist/server/live-build.js.map +1 -0
- package/dist/server/live-dev.d.ts +62 -0
- package/dist/server/live-dev.d.ts.map +1 -0
- package/dist/server/live-dev.js +313 -0
- package/dist/server/live-dev.js.map +1 -0
- package/dist/server/live-manifest.d.ts +23 -0
- package/dist/server/live-manifest.d.ts.map +1 -0
- package/dist/server/live-manifest.js +7 -0
- package/dist/server/live-manifest.js.map +1 -0
- package/dist/server/live-prod.d.ts +30 -0
- package/dist/server/live-prod.d.ts.map +1 -0
- package/dist/server/live-prod.js +97 -0
- package/dist/server/live-prod.js.map +1 -0
- package/dist/server/live-routes.d.ts +72 -0
- package/dist/server/live-routes.d.ts.map +1 -0
- package/dist/server/live-routes.js +274 -0
- package/dist/server/live-routes.js.map +1 -0
- package/dist/server/live-web.d.ts +16 -0
- package/dist/server/live-web.d.ts.map +1 -0
- package/dist/server/live-web.js +159 -0
- package/dist/server/live-web.js.map +1 -0
- package/dist/server/live.d.ts +138 -0
- package/dist/server/live.d.ts.map +1 -0
- package/dist/server/live.js +542 -0
- package/dist/server/live.js.map +1 -0
- package/dist/server/middleware-node.d.ts +48 -0
- package/dist/server/middleware-node.d.ts.map +1 -0
- package/dist/server/middleware-node.js +167 -0
- package/dist/server/middleware-node.js.map +1 -0
- package/dist/server/middleware-runtime.d.ts +97 -0
- package/dist/server/middleware-runtime.d.ts.map +1 -0
- package/dist/server/middleware-runtime.js +447 -0
- package/dist/server/middleware-runtime.js.map +1 -0
- package/dist/server/middleware.d.ts +145 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +365 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/ppr.d.ts +22 -0
- package/dist/server/ppr.d.ts.map +1 -0
- package/dist/server/ppr.js +58 -0
- package/dist/server/ppr.js.map +1 -0
- package/dist/server/scan.d.ts +48 -0
- package/dist/server/scan.d.ts.map +1 -0
- package/dist/server/scan.js +161 -0
- package/dist/server/scan.js.map +1 -0
- package/dist/server/server-components.d.ts +65 -0
- package/dist/server/server-components.d.ts.map +1 -0
- package/dist/server/server-components.js +215 -0
- package/dist/server/server-components.js.map +1 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +5 -0
- package/dist/terminal.js.map +1 -0
- package/dist/testing.d.ts +5 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +5 -0
- package/dist/testing.js.map +1 -0
- package/dist/vite-plugin.d.ts +51 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +403 -0
- package/dist/vite-plugin.js.map +1 -0
- package/package.json +130 -4
- package/src/cli/build.ts +182 -0
- package/src/cli/config.ts +72 -0
- package/src/cli/dev.ts +54 -0
- package/src/cli/index.ts +185 -0
- package/src/cli/run.ts +124 -0
- package/src/cli/start.ts +194 -0
- package/src/cli/ui.ts +93 -0
- package/src/client.tsx +122 -0
- package/src/config.ts +155 -0
- package/src/context.ts +69 -0
- package/src/dev-errors.ts +167 -0
- package/src/devtools.tsx +419 -0
- package/src/frame-mirror.ts +145 -0
- package/src/frame-protocol.ts +435 -0
- package/src/history.ts +201 -0
- package/src/index.ts +9 -0
- package/src/link.tsx +108 -0
- package/src/live-player.ts +449 -0
- package/src/live-protocol.ts +317 -0
- package/src/live-stub.tsx +32 -0
- package/src/mcp/cli.ts +188 -0
- package/src/mcp/frame.ts +224 -0
- package/src/mcp/index.ts +15 -0
- package/src/mcp/inspect.ts +445 -0
- package/src/mcp/keys.ts +86 -0
- package/src/mcp/logs.ts +225 -0
- package/src/mcp/server.ts +595 -0
- package/src/mcp/session.ts +821 -0
- package/src/navigation-errors.ts +60 -0
- package/src/navigation.ts +44 -0
- package/src/router.tsx +497 -0
- package/src/routing.ts +344 -0
- package/src/server/adapters/build-step.ts +135 -0
- package/src/server/adapters/bun.ts +92 -0
- package/src/server/adapters/bundle.ts +97 -0
- package/src/server/adapters/cloudflare.ts +111 -0
- package/src/server/adapters/index.ts +29 -0
- package/src/server/adapters/runtime-node.ts +34 -0
- package/src/server/adapters/runtime.ts +145 -0
- package/src/server/adapters/shared.ts +159 -0
- package/src/server/adapters/static.ts +68 -0
- package/src/server/adapters/vercel.ts +149 -0
- package/src/server/bun-jsx.ts +65 -0
- package/src/server/cache-route.ts +139 -0
- package/src/server/cache.ts +498 -0
- package/src/server/frames.ts +326 -0
- package/src/server/http.ts +467 -0
- package/src/server/jsx-dev-runtime.ts +9 -0
- package/src/server/jsx-runtime.ts +13 -0
- package/src/server/live-app.tsx +374 -0
- package/src/server/live-build.ts +258 -0
- package/src/server/live-dev.ts +357 -0
- package/src/server/live-manifest.ts +25 -0
- package/src/server/live-prod.ts +120 -0
- package/src/server/live-routes.ts +298 -0
- package/src/server/live-web.ts +170 -0
- package/src/server/live.ts +551 -0
- package/src/server/middleware-node.ts +168 -0
- package/src/server/middleware-runtime.ts +478 -0
- package/src/server/middleware.ts +451 -0
- package/src/server/ppr.ts +62 -0
- package/src/server/scan.ts +190 -0
- package/src/server/server-components.ts +224 -0
- package/src/terminal.ts +5 -0
- package/src/testing.ts +5 -0
- package/src/vite-plugin.ts +413 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@textjs/app/cache` — the data cache: `unstable_cache()` / `cache()` with
|
|
3
|
+
* tags and time-based revalidation, `revalidateTag()` / `revalidatePath()`,
|
|
4
|
+
* stale-while-revalidate semantics and a pluggable store (in-memory by default).
|
|
5
|
+
*
|
|
6
|
+
* State lives on `globalThis` so the CLI, the route-handler bundle and user
|
|
7
|
+
* code share one cache even when they load separate copies of this module.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
11
|
+
|
|
12
|
+
type Awaitable<T> = T | Promise<T>;
|
|
13
|
+
|
|
14
|
+
export interface CacheEntry {
|
|
15
|
+
/** Serialized value (JSON for the data cache, `{ status, headers, body }` for routes). */
|
|
16
|
+
value: unknown;
|
|
17
|
+
/** Epoch ms when the computation that produced `value` started. */
|
|
18
|
+
lastModified: number;
|
|
19
|
+
/** Seconds until the entry turns stale; `false` = only on-demand revalidation. */
|
|
20
|
+
revalidate: number | false;
|
|
21
|
+
tags: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TagState {
|
|
25
|
+
/** Latest `revalidateTag()` / `revalidatePath()` that expired one of the tags (epoch ms, `0` = never). */
|
|
26
|
+
expiredAt: number;
|
|
27
|
+
/** Latest `revalidateTag(tag, "max")` that marked one of the tags stale. */
|
|
28
|
+
staleAt: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A cache backend. Implement this to share the cache across instances
|
|
33
|
+
* (Redis, Upstash, Workers KV, …) and point `cacheHandler` in
|
|
34
|
+
* `text.config.ts` at a module that default-exports it.
|
|
35
|
+
*/
|
|
36
|
+
export interface CacheStore {
|
|
37
|
+
get(key: string): Awaitable<CacheEntry | undefined>;
|
|
38
|
+
set(key: string, entry: CacheEntry): Awaitable<void>;
|
|
39
|
+
delete(key: string): Awaitable<void>;
|
|
40
|
+
/** Record that `tags` were revalidated at `at`. */
|
|
41
|
+
markTags(tags: readonly string[], mode: "expire" | "stale", at: number): Awaitable<void>;
|
|
42
|
+
getTagState(tags: readonly string[]): Awaitable<TagState>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface MemoryCacheStoreOptions {
|
|
46
|
+
/** Maximum entries before the least recently used are evicted (default `1000`). */
|
|
47
|
+
maxEntries?: number;
|
|
48
|
+
/** Approximate maximum size in bytes (default 50 MB). */
|
|
49
|
+
maxSize?: number;
|
|
50
|
+
/** Maximum remembered tag revalidations before the oldest are dropped (default `10000`). */
|
|
51
|
+
maxTags?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sizeOf(value: unknown): number {
|
|
55
|
+
if (typeof value === "string") return value.length * 2;
|
|
56
|
+
try {
|
|
57
|
+
return (JSON.stringify(value)?.length ?? 0) * 2;
|
|
58
|
+
} catch {
|
|
59
|
+
return 1024;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The default store: a per-process LRU map. */
|
|
64
|
+
export class MemoryCacheStore implements CacheStore {
|
|
65
|
+
readonly #entries = new Map<string, { entry: CacheEntry; size: number }>();
|
|
66
|
+
readonly #tags = new Map<string, TagState>();
|
|
67
|
+
readonly #maxEntries: number;
|
|
68
|
+
readonly #maxSize: number;
|
|
69
|
+
readonly #maxTags: number;
|
|
70
|
+
#size = 0;
|
|
71
|
+
|
|
72
|
+
constructor(options: MemoryCacheStoreOptions = {}) {
|
|
73
|
+
this.#maxEntries = options.maxEntries ?? 1000;
|
|
74
|
+
this.#maxSize = options.maxSize ?? 50 * 1024 * 1024;
|
|
75
|
+
this.#maxTags = options.maxTags ?? 10_000;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get size(): number {
|
|
79
|
+
return this.#entries.size;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get(key: string): CacheEntry | undefined {
|
|
83
|
+
const hit = this.#entries.get(key);
|
|
84
|
+
if (!hit) return undefined;
|
|
85
|
+
this.#entries.delete(key);
|
|
86
|
+
this.#entries.set(key, hit);
|
|
87
|
+
return hit.entry;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
set(key: string, entry: CacheEntry): void {
|
|
91
|
+
this.delete(key);
|
|
92
|
+
const size = sizeOf(entry.value);
|
|
93
|
+
if (size > this.#maxSize) return;
|
|
94
|
+
this.#entries.set(key, { entry, size });
|
|
95
|
+
this.#size += size;
|
|
96
|
+
for (const [k, v] of this.#entries) {
|
|
97
|
+
if (this.#entries.size <= this.#maxEntries && this.#size <= this.#maxSize) break;
|
|
98
|
+
this.#entries.delete(k);
|
|
99
|
+
this.#size -= v.size;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
delete(key: string): void {
|
|
104
|
+
const hit = this.#entries.get(key);
|
|
105
|
+
if (!hit) return;
|
|
106
|
+
this.#entries.delete(key);
|
|
107
|
+
this.#size -= hit.size;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
markTags(tags: readonly string[], mode: "expire" | "stale", at: number): void {
|
|
111
|
+
for (const tag of tags) {
|
|
112
|
+
const s = this.#tags.get(tag) ?? { expiredAt: 0, staleAt: 0 };
|
|
113
|
+
if (mode === "expire") s.expiredAt = Math.max(s.expiredAt, at);
|
|
114
|
+
else s.staleAt = Math.max(s.staleAt, at);
|
|
115
|
+
this.#tags.delete(tag);
|
|
116
|
+
this.#tags.set(tag, s);
|
|
117
|
+
}
|
|
118
|
+
// Forgetting the oldest revalidation is safe for entries written after it;
|
|
119
|
+
// drop the entries that may still predate it too.
|
|
120
|
+
for (const [tag, st] of this.#tags) {
|
|
121
|
+
if (this.#tags.size <= this.#maxTags) break;
|
|
122
|
+
this.#tags.delete(tag);
|
|
123
|
+
for (const [key, hit] of this.#entries) if (hit.entry.lastModified <= Math.max(st.expiredAt, st.staleAt) && hit.entry.tags.includes(tag)) this.delete(key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getTagState(tags: readonly string[]): TagState {
|
|
128
|
+
const out: TagState = { expiredAt: 0, staleAt: 0 };
|
|
129
|
+
for (const tag of tags) {
|
|
130
|
+
const s = this.#tags.get(tag);
|
|
131
|
+
if (!s) continue;
|
|
132
|
+
out.expiredAt = Math.max(out.expiredAt, s.expiredAt);
|
|
133
|
+
out.staleAt = Math.max(out.staleAt, s.staleAt);
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
clear(): void {
|
|
139
|
+
this.#entries.clear();
|
|
140
|
+
this.#tags.clear();
|
|
141
|
+
this.#size = 0;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Shared state
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
/** Collects tags and the shortest `revalidate` of everything cached while a route handler runs. */
|
|
150
|
+
export interface CacheScope {
|
|
151
|
+
tags: Set<string>;
|
|
152
|
+
revalidate: number | false;
|
|
153
|
+
/** Path-derived tags (`revalidatePath`) inherited by data cached inside the scope. */
|
|
154
|
+
implicitTags: readonly string[];
|
|
155
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface CacheState {
|
|
159
|
+
store: CacheStore;
|
|
160
|
+
inflight: Map<string, { generation: number; promise: Promise<ResolvedEntry> }>;
|
|
161
|
+
/** Bumped by every `revalidateTag()` / `revalidatePath()`: refreshes started before it can't serve readers after it. */
|
|
162
|
+
generation: number;
|
|
163
|
+
scope: AsyncLocalStorage<CacheScope>;
|
|
164
|
+
clock: () => number;
|
|
165
|
+
last: number;
|
|
166
|
+
onError: (error: unknown, key: string) => void;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const STATE = Symbol.for("textjs.cache");
|
|
170
|
+
|
|
171
|
+
function state(): CacheState {
|
|
172
|
+
const g = globalThis as unknown as Record<symbol, CacheState | undefined>;
|
|
173
|
+
let s = g[STATE];
|
|
174
|
+
if (!s) {
|
|
175
|
+
s = {
|
|
176
|
+
store: new MemoryCacheStore(),
|
|
177
|
+
inflight: new Map(),
|
|
178
|
+
generation: 0,
|
|
179
|
+
scope: new AsyncLocalStorage<CacheScope>(),
|
|
180
|
+
clock: Date.now,
|
|
181
|
+
last: 0,
|
|
182
|
+
onError: (error, key) => console.error(`▼ Text.js: background revalidation failed for ${key}\n`, error),
|
|
183
|
+
};
|
|
184
|
+
g[STATE] = s;
|
|
185
|
+
}
|
|
186
|
+
return s;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Strictly increasing timestamps, so a revalidation in the same millisecond as a write still wins. */
|
|
190
|
+
export function cacheNow(): number {
|
|
191
|
+
const s = state();
|
|
192
|
+
let t = Math.floor(s.clock());
|
|
193
|
+
if (t <= s.last) t = s.last + 1;
|
|
194
|
+
s.last = t;
|
|
195
|
+
return t;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Replace the cache backend (`text.config.ts` → `cacheHandler` does this for you). */
|
|
199
|
+
export function setCacheStore(store: CacheStore): void {
|
|
200
|
+
const s = state();
|
|
201
|
+
s.store = store;
|
|
202
|
+
s.inflight.clear();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function getCacheStore(): CacheStore {
|
|
206
|
+
return state().store;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** @internal Test hooks: a fake clock, error sink, or a fresh in-memory store. */
|
|
210
|
+
export function __configureCache(options: { clock?: () => number; onError?: (error: unknown, key: string) => void; reset?: boolean }): void {
|
|
211
|
+
const s = state();
|
|
212
|
+
if (options.clock) {
|
|
213
|
+
s.clock = options.clock;
|
|
214
|
+
s.last = 0;
|
|
215
|
+
}
|
|
216
|
+
if (options.onError) s.onError = options.onError;
|
|
217
|
+
if (options.reset) {
|
|
218
|
+
s.store = new MemoryCacheStore();
|
|
219
|
+
s.inflight.clear();
|
|
220
|
+
s.last = 0;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function currentCacheScope(): CacheScope | undefined {
|
|
225
|
+
return state().scope.getStore();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function runInCacheScope<T>(scope: CacheScope, fn: () => T): T {
|
|
229
|
+
return state().scope.run(scope, fn);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function track(scope: CacheScope | undefined, tags: readonly string[], revalidate: number | false): void {
|
|
233
|
+
if (!scope) return;
|
|
234
|
+
for (const t of tags) scope.tags.add(t);
|
|
235
|
+
if (revalidate !== false && (scope.revalidate === false || revalidate < scope.revalidate)) scope.revalidate = revalidate;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Core: stale-while-revalidate lookup
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
/** Entries older than a year are regenerated in the foreground (Next.js `expireTime`). */
|
|
243
|
+
export const EXPIRE_SECONDS = 31_536_000;
|
|
244
|
+
|
|
245
|
+
export type CacheStatus = "HIT" | "STALE" | "MISS";
|
|
246
|
+
|
|
247
|
+
interface ResolvedEntry {
|
|
248
|
+
entry: CacheEntry;
|
|
249
|
+
status: CacheStatus;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface Computed {
|
|
253
|
+
value: unknown;
|
|
254
|
+
tags: readonly string[];
|
|
255
|
+
/** `0` = don't store (dynamic). */
|
|
256
|
+
revalidate: number | false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function classifyEntry(entry: CacheEntry, tags: TagState, now: number): "fresh" | "stale" | "expired" {
|
|
260
|
+
if (tags.expiredAt > entry.lastModified) return "expired";
|
|
261
|
+
const age = now - entry.lastModified;
|
|
262
|
+
if (entry.revalidate !== false && age > EXPIRE_SECONDS * 1000) return "expired";
|
|
263
|
+
if (tags.staleAt > entry.lastModified) return "stale";
|
|
264
|
+
if (entry.revalidate !== false && age > entry.revalidate * 1000) return "stale";
|
|
265
|
+
return "fresh";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function compute(key: string, fn: () => Promise<Computed>): Promise<ResolvedEntry> {
|
|
269
|
+
const s = state();
|
|
270
|
+
const startedAt = cacheNow();
|
|
271
|
+
const result = await fn();
|
|
272
|
+
const entry: CacheEntry = { value: result.value, lastModified: startedAt, revalidate: result.revalidate, tags: [...new Set(result.tags)] };
|
|
273
|
+
if (result.revalidate !== 0) await s.store.set(key, entry);
|
|
274
|
+
return { entry, status: "MISS" };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function dedupe(key: string, fn: () => Promise<Computed>): Promise<ResolvedEntry> {
|
|
278
|
+
const s = state();
|
|
279
|
+
const running = s.inflight.get(key);
|
|
280
|
+
if (running && running.generation === s.generation) return running.promise;
|
|
281
|
+
const generation = s.generation;
|
|
282
|
+
const p = compute(key, fn).finally(() => {
|
|
283
|
+
if (s.inflight.get(key)?.promise === p) s.inflight.delete(key);
|
|
284
|
+
});
|
|
285
|
+
s.inflight.set(key, { generation, promise: p });
|
|
286
|
+
return p;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Read `key`, serving stale entries while one deduplicated background
|
|
291
|
+
* refresh runs (registered with `waitUntil` on serverless runtimes).
|
|
292
|
+
*/
|
|
293
|
+
export async function getOrCompute(key: string, fn: () => Promise<Computed>, waitUntil?: (p: Promise<unknown>) => void): Promise<ResolvedEntry> {
|
|
294
|
+
const s = state();
|
|
295
|
+
const entry = await s.store.get(key);
|
|
296
|
+
if (entry) {
|
|
297
|
+
const verdict = classifyEntry(entry, await s.store.getTagState(entry.tags), cacheNow());
|
|
298
|
+
if (verdict === "fresh") return { entry, status: "HIT" };
|
|
299
|
+
if (verdict === "stale") {
|
|
300
|
+
if (s.inflight.get(key)?.generation !== s.generation) {
|
|
301
|
+
const refresh = dedupe(key, fn).catch((error: unknown) => {
|
|
302
|
+
s.onError(error, key);
|
|
303
|
+
});
|
|
304
|
+
if (waitUntil) waitUntil(refresh);
|
|
305
|
+
}
|
|
306
|
+
return { entry, status: "STALE" };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return dedupe(key, fn);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// Public API
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
export interface CacheOptions {
|
|
317
|
+
/** Tags for `revalidateTag()`. */
|
|
318
|
+
tags?: string[];
|
|
319
|
+
/** Seconds before the value turns stale (served while refreshing). `false` (default) = until revalidated. */
|
|
320
|
+
revalidate?: number | false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function validateRevalidate(value: unknown, where: string): number | false {
|
|
324
|
+
if (value === undefined || value === false) return false;
|
|
325
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
|
|
326
|
+
throw new TypeError(`Invalid revalidate value ${JSON.stringify(value)} in ${where}; must be a non-negative number or false.`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function validateTags(tags: unknown, where: string): string[] {
|
|
330
|
+
if (tags === undefined) return [];
|
|
331
|
+
if (!Array.isArray(tags) || tags.some((t) => typeof t !== "string")) throw new TypeError(`Invalid tags in ${where}; must be an array of strings.`);
|
|
332
|
+
return tags as string[];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** A collision-free cache key for arguments (`undefined` ≠ `null`, `Map` / `Set` / `Date` / `bigint` by value). */
|
|
336
|
+
export function serializeCacheArgs(value: unknown, seen = new Set<object>()): string {
|
|
337
|
+
if (value === undefined) return "u";
|
|
338
|
+
if (value === null) return "n";
|
|
339
|
+
switch (typeof value) {
|
|
340
|
+
case "string":
|
|
341
|
+
return JSON.stringify(value);
|
|
342
|
+
case "number":
|
|
343
|
+
return Object.is(value, -0) ? "-0" : Number.isNaN(value) ? "NaN" : String(value);
|
|
344
|
+
case "boolean":
|
|
345
|
+
return value ? "t" : "f";
|
|
346
|
+
case "bigint":
|
|
347
|
+
return `${value}n`;
|
|
348
|
+
case "function":
|
|
349
|
+
case "symbol":
|
|
350
|
+
throw new TypeError(`unstable_cache arguments must be serializable; got a ${typeof value}.`);
|
|
351
|
+
}
|
|
352
|
+
const obj = value as object;
|
|
353
|
+
if (seen.has(obj)) throw new TypeError("unstable_cache arguments must not be circular.");
|
|
354
|
+
seen.add(obj);
|
|
355
|
+
try {
|
|
356
|
+
if (Array.isArray(obj)) return `[${obj.map((v) => serializeCacheArgs(v, seen)).join(",")}]`;
|
|
357
|
+
if (obj instanceof Date) return `D${obj.getTime()}`;
|
|
358
|
+
if (obj instanceof URL) return `L${JSON.stringify(obj.href)}`;
|
|
359
|
+
if (obj instanceof URLSearchParams) return `Q${JSON.stringify(obj.toString())}`;
|
|
360
|
+
if (obj instanceof RegExp) return `R${JSON.stringify(obj.toString())}`;
|
|
361
|
+
if (obj instanceof Map) return `M[${[...obj].map(([k, v]) => `${serializeCacheArgs(k, seen)}:${serializeCacheArgs(v, seen)}`).sort().join(",")}]`;
|
|
362
|
+
if (obj instanceof Set) return `S[${[...obj].map((v) => serializeCacheArgs(v, seen)).sort().join(",")}]`;
|
|
363
|
+
const keys = Object.keys(obj).sort();
|
|
364
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${serializeCacheArgs((obj as Record<string, unknown>)[k], seen)}`).join(",")}}`;
|
|
365
|
+
} finally {
|
|
366
|
+
seen.delete(obj);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** `file:line:col` of the code that called `unstable_cache()` (stable across processes running one build). */
|
|
371
|
+
function callSite(): string {
|
|
372
|
+
const frames = (new Error().stack ?? "").split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at "));
|
|
373
|
+
// [callSite, unstable_cache, (cache), caller, …]
|
|
374
|
+
let i = 2;
|
|
375
|
+
if (frames[i]?.startsWith("at cache ")) i++;
|
|
376
|
+
return (frames[i] ?? "").replace(/^at\s+/, "");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const warnedKeys = new Map<string, WeakRef<object>>();
|
|
380
|
+
|
|
381
|
+
function hashString(s: string): string {
|
|
382
|
+
let h = 0x811c9dc5;
|
|
383
|
+
for (let i = 0; i < s.length; i++) {
|
|
384
|
+
h ^= s.charCodeAt(i);
|
|
385
|
+
h = Math.imul(h, 0x01000193);
|
|
386
|
+
}
|
|
387
|
+
return (h >>> 0).toString(36);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Cache the result of an async function (Next.js `unstable_cache`). Results
|
|
392
|
+
* are JSON-serialized, so every call returns a fresh copy. The cache key is
|
|
393
|
+
* `keyParts` (or the function source) plus the JSON-encoded arguments.
|
|
394
|
+
*/
|
|
395
|
+
export function unstable_cache<Args extends unknown[], R>(fn: (...args: Args) => Promise<R> | R, keyParts?: readonly string[], options: CacheOptions = {}): (...args: Args) => Promise<R> {
|
|
396
|
+
const where = `unstable_cache(${keyParts?.join(", ") ?? fn.name ?? "anonymous"})`;
|
|
397
|
+
const revalidate = validateRevalidate(options.revalidate, where);
|
|
398
|
+
const tags = validateTags(options.tags, where);
|
|
399
|
+
let base: string;
|
|
400
|
+
if (keyParts && keyParts.length > 0) base = keyParts.join("\u0000");
|
|
401
|
+
else {
|
|
402
|
+
base = `fn:${hashString(fn.toString())}:${hashString(callSite())}`;
|
|
403
|
+
// Closures created at one call site (in a loop, per request) share source and site: they'd share entries.
|
|
404
|
+
const prior = warnedKeys.get(base)?.deref();
|
|
405
|
+
if (prior && prior !== fn) {
|
|
406
|
+
console.warn(`▼ Text.js: unstable_cache(${fn.name || "anonymous"}) was created more than once from the same code without keyParts; the instances share cache entries. Pass keyParts that identify what the function closes over.`);
|
|
407
|
+
}
|
|
408
|
+
warnedKeys.set(base, new WeakRef(fn));
|
|
409
|
+
}
|
|
410
|
+
return async (...args: Args): Promise<R> => {
|
|
411
|
+
const parent = currentCacheScope();
|
|
412
|
+
if (revalidate === 0) {
|
|
413
|
+
track(parent, tags, 0);
|
|
414
|
+
return fn(...args);
|
|
415
|
+
}
|
|
416
|
+
const key = `data:${base}:${serializeCacheArgs(args)}`;
|
|
417
|
+
const inherited = parent?.implicitTags ?? [];
|
|
418
|
+
const resolved = await getOrCompute(
|
|
419
|
+
key,
|
|
420
|
+
async () => {
|
|
421
|
+
const child: CacheScope = { tags: new Set([...tags, ...inherited]), revalidate, implicitTags: inherited, waitUntil: parent?.waitUntil };
|
|
422
|
+
const value = await runInCacheScope(child, () => fn(...args));
|
|
423
|
+
let json: string | null;
|
|
424
|
+
try {
|
|
425
|
+
json = value === undefined ? null : (JSON.stringify(value) ?? null);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
throw new TypeError(`${where} returned a value that can't be cached as JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
428
|
+
}
|
|
429
|
+
return { value: json, tags: [...child.tags], revalidate: child.revalidate };
|
|
430
|
+
},
|
|
431
|
+
parent?.waitUntil,
|
|
432
|
+
);
|
|
433
|
+
track(parent, resolved.entry.tags, resolved.entry.revalidate);
|
|
434
|
+
const json = resolved.entry.value as string | null;
|
|
435
|
+
return (json === null ? undefined : JSON.parse(json)) as R;
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** `unstable_cache` with an options object: `cache(fn, { key, tags, revalidate })`. */
|
|
440
|
+
export function cache<Args extends unknown[], R>(fn: (...args: Args) => Promise<R> | R, options: CacheOptions & { key?: string | string[] } = {}): (...args: Args) => Promise<R> {
|
|
441
|
+
const key = options.key === undefined ? undefined : Array.isArray(options.key) ? options.key : [options.key];
|
|
442
|
+
return unstable_cache(fn, key, options);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function after(promise: Promise<void>): Promise<void> {
|
|
446
|
+
const scope = currentCacheScope();
|
|
447
|
+
const p = promise.catch((error: unknown) => state().onError(error, "revalidate"));
|
|
448
|
+
scope?.waitUntil?.(p);
|
|
449
|
+
return p;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Invalidate every entry tagged `tag`. By default the next request recomputes
|
|
454
|
+
* in the foreground; pass `"max"` to serve the stale value while it refreshes.
|
|
455
|
+
*/
|
|
456
|
+
export function revalidateTag(tag: string | readonly string[], profile?: "max"): Promise<void> {
|
|
457
|
+
const tags = typeof tag === "string" ? [tag] : [...tag];
|
|
458
|
+
state().generation++;
|
|
459
|
+
return after(Promise.resolve(state().store.markTags(tags, profile === "max" ? "stale" : "expire", cacheNow())));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function normalizeCachePath(path: string): string {
|
|
463
|
+
let p = (path.startsWith("/") ? path : `/${path}`).split(/[?#]/)[0] as string;
|
|
464
|
+
try {
|
|
465
|
+
p = decodeURIComponent(p);
|
|
466
|
+
} catch {
|
|
467
|
+
// keep the raw path
|
|
468
|
+
}
|
|
469
|
+
return p.length > 1 ? p.replace(/\/+$/, "") || "/" : p;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Tags a route entry carries: `_P:<path>` for itself and `_L:<prefix>` for every enclosing segment. */
|
|
473
|
+
export function implicitTags(pathname: string, pattern?: string): string[] {
|
|
474
|
+
const out = new Set<string>();
|
|
475
|
+
for (const p of pattern && pattern !== pathname ? [pathname, pattern] : [pathname]) {
|
|
476
|
+
const path = normalizeCachePath(p);
|
|
477
|
+
out.add(`_P:${path}`);
|
|
478
|
+
out.add("_L:/");
|
|
479
|
+
let prefix = "";
|
|
480
|
+
for (const seg of path.split("/").filter(Boolean)) {
|
|
481
|
+
prefix += `/${seg}`;
|
|
482
|
+
out.add(`_L:${prefix}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return [...out];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Invalidate a cached path (`"page"`, the default) or everything below it
|
|
490
|
+
* (`"layout"`). Dynamic patterns work too: `revalidatePath("/api/posts/[id]")`.
|
|
491
|
+
*/
|
|
492
|
+
export function revalidatePath(path: string, type: "page" | "layout" = "page"): Promise<void> {
|
|
493
|
+
const p = normalizeCachePath(path);
|
|
494
|
+
state().generation++;
|
|
495
|
+
return after(Promise.resolve(state().store.markTags([type === "layout" ? `_L:${p}` : `_P:${p}`], "expire", cacheNow())));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export { validateRevalidate as __validateRevalidate };
|