@httpquery/edge 0.1.0
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 +22 -0
- package/dist/akamai.d.ts +25 -0
- package/dist/akamai.d.ts.map +1 -0
- package/dist/akamai.js +72 -0
- package/dist/akamai.js.map +1 -0
- package/dist/auto-cache.d.ts +64 -0
- package/dist/auto-cache.d.ts.map +1 -0
- package/dist/auto-cache.js +205 -0
- package/dist/auto-cache.js.map +1 -0
- package/dist/cf-body-cache.d.ts +61 -0
- package/dist/cf-body-cache.d.ts.map +1 -0
- package/dist/cf-body-cache.js +148 -0
- package/dist/cf-body-cache.js.map +1 -0
- package/dist/cloudflare.d.ts +58 -0
- package/dist/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare.js +183 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +122 -0
- package/dist/index.js.map +1 -0
- package/dist/otel.d.ts +40 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +70 -0
- package/dist/otel.js.map +1 -0
- package/package.json +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @httpquery/edge
|
|
2
|
+
|
|
3
|
+
CDN & **Cloudflare Workers** helpers for [RFC 10008](https://www.rfc-editor.org/rfc/rfc10008.html) QUERY.
|
|
4
|
+
|
|
5
|
+
Cache keys **include the request body** (required by the RFC — what Cloudflare/Akamai need).
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @httpquery/edge
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createQueryWorker } from '@httpquery/edge/cloudflare'
|
|
13
|
+
|
|
14
|
+
export default createQueryWorker({
|
|
15
|
+
sMaxAge: 120,
|
|
16
|
+
search(body) {
|
|
17
|
+
return db.search(body)
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
See `examples/cloudflare-worker/`.
|
package/dist/akamai.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Akamai EdgeWorkers helpers for HTTP QUERY (RFC 10008).
|
|
3
|
+
*
|
|
4
|
+
* Co-author CDN: body MUST be part of cache key.
|
|
5
|
+
* Property Manager already has "Cache POST body" patterns — QUERY needs the same idea.
|
|
6
|
+
*
|
|
7
|
+
* These are string templates / pure helpers (no EdgeWorkers runtime required to import).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Build a stable cache identity string for Property Manager / EdgeWorkers.
|
|
11
|
+
* Prefer embedding hash in a synthetic path segment (like CF Cache API).
|
|
12
|
+
*/
|
|
13
|
+
export declare function akamaiQueryCacheId(url: string, body: string, contentType?: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* EdgeWorkers-style pseudo-code (copy into EdgeWorker bundle).
|
|
16
|
+
* Docs: https://techdocs.akamai.com/edgeworkers/docs/
|
|
17
|
+
*/
|
|
18
|
+
export declare const AKAMAI_EDGEWORKER_QUERY_SAMPLE: string;
|
|
19
|
+
/**
|
|
20
|
+
* Property Manager checklist (ops).
|
|
21
|
+
*/
|
|
22
|
+
export declare const AKAMAI_PROPERTY_CHECKLIST: string[];
|
|
23
|
+
/** Markdown doc for ops handoff */
|
|
24
|
+
export declare function akamaiOpsMarkdown(): string;
|
|
25
|
+
//# sourceMappingURL=akamai.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"akamai.d.ts","sourceRoot":"","sources":["../src/akamai.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,WAAW,SAAqB,GAC/B,MAAM,CAGR;AAED;;;GAGG;AACH,eAAO,MAAM,8BAA8B,QAiBnC,CAAC;AAET;;GAEG;AACH,eAAO,MAAM,yBAAyB,UAQrC,CAAC;AAEF,mCAAmC;AACnC,wBAAgB,iBAAiB,IAAI,MAAM,CAkB1C"}
|
package/dist/akamai.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Akamai EdgeWorkers helpers for HTTP QUERY (RFC 10008).
|
|
3
|
+
*
|
|
4
|
+
* Co-author CDN: body MUST be part of cache key.
|
|
5
|
+
* Property Manager already has "Cache POST body" patterns — QUERY needs the same idea.
|
|
6
|
+
*
|
|
7
|
+
* These are string templates / pure helpers (no EdgeWorkers runtime required to import).
|
|
8
|
+
*/
|
|
9
|
+
import { cacheKeySync } from "@httpquery/core";
|
|
10
|
+
/**
|
|
11
|
+
* Build a stable cache identity string for Property Manager / EdgeWorkers.
|
|
12
|
+
* Prefer embedding hash in a synthetic path segment (like CF Cache API).
|
|
13
|
+
*/
|
|
14
|
+
export function akamaiQueryCacheId(url, body, contentType = "application/json") {
|
|
15
|
+
const ct = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
16
|
+
return cacheKeySync(`${url}|ct=${ct}|akamai`, body);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* EdgeWorkers-style pseudo-code (copy into EdgeWorker bundle).
|
|
20
|
+
* Docs: https://techdocs.akamai.com/edgeworkers/docs/
|
|
21
|
+
*/
|
|
22
|
+
export const AKAMAI_EDGEWORKER_QUERY_SAMPLE = `
|
|
23
|
+
// edgeworker-query-cache.js — sketch for QUERY body-aware cache key
|
|
24
|
+
// Install on Property with EdgeWorkers + appropriate caching behaviors.
|
|
25
|
+
//
|
|
26
|
+
// import { createResponse } from 'create-response';
|
|
27
|
+
// export async function onClientRequest(request) {
|
|
28
|
+
// if (request.method !== 'QUERY' && request.method !== 'POST') return;
|
|
29
|
+
// // Read body (EdgeWorkers request body APIs vary by version — adapt)
|
|
30
|
+
// // const body = await request.text();
|
|
31
|
+
// // const key = hash(request.url + '|' + body);
|
|
32
|
+
// // request.setVariable('PMUSER_QUERY_CACHE_KEY', key);
|
|
33
|
+
// }
|
|
34
|
+
//
|
|
35
|
+
// Pair with Property Manager:
|
|
36
|
+
// 1) Allow method QUERY (or dual POST with X-HTTP-Method-Override: QUERY)
|
|
37
|
+
// 2) Cache key includes custom variable PMUSER_QUERY_CACHE_KEY
|
|
38
|
+
// 3) Until native QUERY cache ships, dual-route POST may be more reliable
|
|
39
|
+
`.trim();
|
|
40
|
+
/**
|
|
41
|
+
* Property Manager checklist (ops).
|
|
42
|
+
*/
|
|
43
|
+
export const AKAMAI_PROPERTY_CHECKLIST = [
|
|
44
|
+
"Allow HTTP method QUERY on the property (or only dual POST until supported)",
|
|
45
|
+
"If caching QUERY: include request body hash in cache key (same idea as Cache POST body MD5)",
|
|
46
|
+
"Do NOT cache QUERY on URL alone — cache poisoning risk (RFC 10008 Security Considerations)",
|
|
47
|
+
"WAF: inspect QUERY body with same SQLi/XSS rules as POST",
|
|
48
|
+
"CORS: OPTIONS must list QUERY in Access-Control-Allow-Methods",
|
|
49
|
+
"Migration: accept POST + X-HTTP-Method-Override: QUERY for blocked clients",
|
|
50
|
+
"Bot management: do not treat QUERY as automatically malicious",
|
|
51
|
+
];
|
|
52
|
+
/** Markdown doc for ops handoff */
|
|
53
|
+
export function akamaiOpsMarkdown() {
|
|
54
|
+
return [
|
|
55
|
+
"# Akamai + HTTP QUERY (RFC 10008)",
|
|
56
|
+
"",
|
|
57
|
+
"## Why",
|
|
58
|
+
"Akamai co-authored QUERY. Edge must treat it like GET-with-body: safe, retryable, cacheable only with body in key.",
|
|
59
|
+
"",
|
|
60
|
+
"## Checklist",
|
|
61
|
+
...AKAMAI_PROPERTY_CHECKLIST.map((c) => `- [ ] ${c}`),
|
|
62
|
+
"",
|
|
63
|
+
"## EdgeWorker sketch",
|
|
64
|
+
"```js",
|
|
65
|
+
AKAMAI_EDGEWORKER_QUERY_SAMPLE,
|
|
66
|
+
"```",
|
|
67
|
+
"",
|
|
68
|
+
"## Fallback",
|
|
69
|
+
"If property returns 400 on method QUERY (seen in early lab reports), use dual POST + Method-Override until allow-list updated.",
|
|
70
|
+
].join("\n");
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=akamai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"akamai.js","sourceRoot":"","sources":["../src/akamai.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,GAAW,EACX,IAAY,EACZ,WAAW,GAAG,kBAAkB;IAEhC,MAAM,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC;IACjE,OAAO,YAAY,CAAC,GAAG,GAAG,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;AACtD,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG;;;;;;;;;;;;;;;;;CAiB7C,CAAC,IAAI,EAAE,CAAC;AAET;;GAEG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACvC,6EAA6E;IAC7E,6FAA6F;IAC7F,4FAA4F;IAC5F,0DAA0D;IAC1D,+DAA+D;IAC/D,4EAA4E;IAC5E,+DAA+D;CAChE,CAAC;AAEF,mCAAmC;AACnC,MAAM,UAAU,iBAAiB;IAC/B,OAAO;QACL,mCAAmC;QACnC,EAAE;QACF,QAAQ;QACR,oHAAoH;QACpH,EAAE;QACF,cAAc;QACd,GAAG,yBAAyB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC;QACrD,EAAE;QACF,sBAAsB;QACtB,OAAO;QACP,8BAA8B;QAC9B,KAAK;QACL,EAAE;QACF,aAAa;QACb,gIAAgI;KACjI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full auto QUERY cache for Cloudflare Workers (RFC 10008 §2.7).
|
|
3
|
+
*
|
|
4
|
+
* Platform gap: CF Cache API keys by URL and does NOT include body by default
|
|
5
|
+
* (workerd#6849). This module IS the auto body-keyed QUERY cache platform:
|
|
6
|
+
*
|
|
7
|
+
* 1. Body hash in synthetic cache key (CDN-correct)
|
|
8
|
+
* 2. Automatic match → origin → put
|
|
9
|
+
* 3. Edge singleflight (stampede collapse)
|
|
10
|
+
* 4. Optional stale-while-revalidate
|
|
11
|
+
* 5. Auth / private bypass
|
|
12
|
+
* 6. Only pure QUERY cached (not dual POST)
|
|
13
|
+
*
|
|
14
|
+
* Drop-in: wrap any Worker fetch with `withAutoQueryCache(handler)`.
|
|
15
|
+
*/
|
|
16
|
+
export interface AutoQueryCacheOptions {
|
|
17
|
+
/** Cache binding (default caches.default) */
|
|
18
|
+
cache?: {
|
|
19
|
+
match: (r: Request) => Promise<Response | undefined>;
|
|
20
|
+
put: (r: Request, res: Response) => Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
/** Shared max age seconds (default 60) */
|
|
23
|
+
sMaxAge?: number;
|
|
24
|
+
/** Soft TTL for SWR — serve stale while revalidating (default sMaxAge/2) */
|
|
25
|
+
softTtlSeconds?: number;
|
|
26
|
+
/** Cache POST only if X-HTTP-Method-Override: QUERY (default false — safer) */
|
|
27
|
+
cachePostOverride?: boolean;
|
|
28
|
+
/** Never cache when Authorization present (default true) */
|
|
29
|
+
skipIfAuthorized?: boolean;
|
|
30
|
+
/** Paths to auto-cache (default all QUERY). Empty = all. */
|
|
31
|
+
paths?: string[] | ((url: URL) => boolean);
|
|
32
|
+
/** Custom status filter (default 200-299) */
|
|
33
|
+
cacheStatus?: (status: number) => boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Full automatic QUERY cache middleware for Cloudflare Workers.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* export default {
|
|
41
|
+
* fetch: withAutoQueryCache(async (req, env, ctx) => {
|
|
42
|
+
* return Response.json(await env.DB.search(await req.json()))
|
|
43
|
+
* }, { sMaxAge: 120 })
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function withAutoQueryCache(handler: (request: Request, env: any, ctx: any) => Promise<Response> | Response, options?: AutoQueryCacheOptions): (request: Request, env: any, ctx: any) => Promise<Response>;
|
|
48
|
+
/**
|
|
49
|
+
* One-liner platform export for Workers.
|
|
50
|
+
* Full auto QUERY cache + optional dual POST passthrough.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createAutoQueryPlatform(options: {
|
|
53
|
+
fetch: (request: Request, env: any, ctx: any) => Promise<Response> | Response;
|
|
54
|
+
cache?: AutoQueryCacheOptions;
|
|
55
|
+
}): {
|
|
56
|
+
fetch: (request: Request, env: any, ctx: any) => Promise<Response>;
|
|
57
|
+
};
|
|
58
|
+
/** In-memory Cache API mock for Node tests / local benches */
|
|
59
|
+
export declare function createMemoryCache(): {
|
|
60
|
+
match: (r: Request) => Promise<Response | undefined>;
|
|
61
|
+
put: (r: Request, res: Response) => Promise<void>;
|
|
62
|
+
store: Map<string, Response>;
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=auto-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-cache.d.ts","sourceRoot":"","sources":["../src/auto-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAoCH,MAAM,WAAW,qBAAqB;IACpC,6CAA6C;IAE7C,KAAK,CAAC,EAAE;QACN,KAAK,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;QACrD,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACnD,CAAC;IACF,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+EAA+E;IAC/E,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC;IAC3C,6CAA6C;IAC7C,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;CAC3C;AA6BD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,EAC/E,OAAO,GAAE,qBAA0B,GAClC,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,CAiG7D;AAoCD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE;IAC/C,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC;IAC9E,KAAK,CAAC,EAAE,qBAAqB,CAAC;CAC/B,GAAG;IAAE,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAAE,CAIzE;AAED,8DAA8D;AAC9D,wBAAgB,iBAAiB,IAAI;IACnC,KAAK,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;IACrD,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CAC9B,CAYA"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full auto QUERY cache for Cloudflare Workers (RFC 10008 §2.7).
|
|
3
|
+
*
|
|
4
|
+
* Platform gap: CF Cache API keys by URL and does NOT include body by default
|
|
5
|
+
* (workerd#6849). This module IS the auto body-keyed QUERY cache platform:
|
|
6
|
+
*
|
|
7
|
+
* 1. Body hash in synthetic cache key (CDN-correct)
|
|
8
|
+
* 2. Automatic match → origin → put
|
|
9
|
+
* 3. Edge singleflight (stampede collapse)
|
|
10
|
+
* 4. Optional stale-while-revalidate
|
|
11
|
+
* 5. Auth / private bypass
|
|
12
|
+
* 6. Only pure QUERY cached (not dual POST)
|
|
13
|
+
*
|
|
14
|
+
* Drop-in: wrap any Worker fetch with `withAutoQueryCache(handler)`.
|
|
15
|
+
*/
|
|
16
|
+
import { QUERY_METHOD, cacheKey } from "@httpquery/core";
|
|
17
|
+
/** Local copy — avoid circular import with index.ts */
|
|
18
|
+
async function toCacheApiRequest(request, options = {}) {
|
|
19
|
+
const url = new URL(request.url);
|
|
20
|
+
const body = await request.clone().text();
|
|
21
|
+
const ct = request.headers.get("content-type") ?? "application/json";
|
|
22
|
+
const extra = {
|
|
23
|
+
"content-type": ct.split(";")[0]?.trim().toLowerCase() ?? "",
|
|
24
|
+
};
|
|
25
|
+
const accept = request.headers.get("accept");
|
|
26
|
+
if (accept)
|
|
27
|
+
extra.accept = accept;
|
|
28
|
+
if (request.headers.get("authorization"))
|
|
29
|
+
extra.authorization = "1";
|
|
30
|
+
const key = await cacheKey(`${url.origin}${url.pathname}`, body, extra);
|
|
31
|
+
const host = options.cacheHost ?? url.host;
|
|
32
|
+
const prefix = options.prefix ?? "/__http_query_cache";
|
|
33
|
+
return new Request(`https://${host}${prefix}/${key}`, { method: "GET" });
|
|
34
|
+
}
|
|
35
|
+
function queryCacheControl(options) {
|
|
36
|
+
const parts = ["public", `max-age=${options.maxAge ?? 0}`, `s-maxage=${options.sMaxAge ?? 60}`];
|
|
37
|
+
if (options.staleWhileRevalidate != null) {
|
|
38
|
+
parts.push(`stale-while-revalidate=${options.staleWhileRevalidate}`);
|
|
39
|
+
}
|
|
40
|
+
return parts.join(", ");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Global-ish flight map for edge isolate (Worker isolate lifetime).
|
|
44
|
+
* Prevents thundering herd on cold cache for identical QUERY bodies.
|
|
45
|
+
*/
|
|
46
|
+
const flights = new Map();
|
|
47
|
+
function shouldPath(url, paths) {
|
|
48
|
+
if (!paths)
|
|
49
|
+
return true;
|
|
50
|
+
if (typeof paths === "function")
|
|
51
|
+
return paths(url);
|
|
52
|
+
return paths.some((p) => url.pathname === p || url.pathname.startsWith(p));
|
|
53
|
+
}
|
|
54
|
+
function isCacheableMethod(request, opts) {
|
|
55
|
+
const m = request.method.toUpperCase();
|
|
56
|
+
if (m === QUERY_METHOD)
|
|
57
|
+
return true;
|
|
58
|
+
if (opts.cachePostOverride &&
|
|
59
|
+
m === "POST" &&
|
|
60
|
+
request.headers.get("X-HTTP-Method-Override")?.toUpperCase() === QUERY_METHOD) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Full automatic QUERY cache middleware for Cloudflare Workers.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* export default {
|
|
71
|
+
* fetch: withAutoQueryCache(async (req, env, ctx) => {
|
|
72
|
+
* return Response.json(await env.DB.search(await req.json()))
|
|
73
|
+
* }, { sMaxAge: 120 })
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function withAutoQueryCache(handler, options = {}) {
|
|
78
|
+
const sMaxAge = options.sMaxAge ?? 60;
|
|
79
|
+
const softTtl = options.softTtlSeconds ?? Math.max(1, Math.floor(sMaxAge / 2));
|
|
80
|
+
const skipAuth = options.skipIfAuthorized !== false;
|
|
81
|
+
const statusOk = options.cacheStatus ?? ((s) => s >= 200 && s < 300);
|
|
82
|
+
return async (request, env, ctx) => {
|
|
83
|
+
const cacheStore = options.cache ?? globalThis.caches?.default;
|
|
84
|
+
const url = new URL(request.url);
|
|
85
|
+
if (!cacheStore ||
|
|
86
|
+
!isCacheableMethod(request, options) ||
|
|
87
|
+
!shouldPath(url, options.paths) ||
|
|
88
|
+
(skipAuth && request.headers.get("Authorization"))) {
|
|
89
|
+
return handler(request, env, ctx);
|
|
90
|
+
}
|
|
91
|
+
// Read body once for key + handler
|
|
92
|
+
const bodyText = await request.clone().text();
|
|
93
|
+
const cacheReq = await toCacheApiRequest(request);
|
|
94
|
+
const flightKey = cacheReq.url;
|
|
95
|
+
// 1) Cache HIT
|
|
96
|
+
const hit = await cacheStore.match(cacheReq);
|
|
97
|
+
if (hit) {
|
|
98
|
+
const age = Number(hit.headers.get("Age") || hit.headers.get("X-Query-Cache-Age") || 0);
|
|
99
|
+
const softExpired = age > softTtl;
|
|
100
|
+
const headers = new Headers(hit.headers);
|
|
101
|
+
headers.set("X-HTTP-Query-Cache", softExpired ? "STALE" : "HIT");
|
|
102
|
+
headers.set("X-HTTP-Query-Cache-Key", flightKey.slice(-64));
|
|
103
|
+
headers.set("X-HTTP-Query-Platform", "auto-body-key");
|
|
104
|
+
if (softExpired && ctx?.waitUntil) {
|
|
105
|
+
// SWR: revalidate in background
|
|
106
|
+
ctx.waitUntil(revalidate(handler, request, env, ctx, cacheStore, cacheReq, bodyText, sMaxAge, statusOk));
|
|
107
|
+
}
|
|
108
|
+
return new Response(hit.body, { status: hit.status, headers });
|
|
109
|
+
}
|
|
110
|
+
// 2) Singleflight MISS
|
|
111
|
+
const existing = flights.get(flightKey);
|
|
112
|
+
if (existing) {
|
|
113
|
+
const shared = await existing;
|
|
114
|
+
const h = new Headers(shared.headers);
|
|
115
|
+
h.set("X-HTTP-Query-Cache", "COALESCE");
|
|
116
|
+
h.set("X-HTTP-Query-Platform", "auto-body-key");
|
|
117
|
+
return new Response(shared.body, { status: shared.status, headers: h });
|
|
118
|
+
}
|
|
119
|
+
const flight = (async () => {
|
|
120
|
+
// Reconstruct request with body for handler (clone was consumed)
|
|
121
|
+
const reqForHandler = new Request(request.url, {
|
|
122
|
+
method: request.method,
|
|
123
|
+
headers: request.headers,
|
|
124
|
+
body: bodyText || undefined,
|
|
125
|
+
});
|
|
126
|
+
const originRes = await handler(reqForHandler, env, ctx);
|
|
127
|
+
const headers = new Headers(originRes.headers);
|
|
128
|
+
if (!headers.has("Cache-Control")) {
|
|
129
|
+
headers.set("Cache-Control", queryCacheControl({ sMaxAge, maxAge: 0, staleWhileRevalidate: softTtl }));
|
|
130
|
+
}
|
|
131
|
+
headers.set("X-HTTP-Query-Cache", "MISS");
|
|
132
|
+
headers.set("X-HTTP-Query-Platform", "auto-body-key");
|
|
133
|
+
headers.set("X-HTTP-Query-Cache-Key", flightKey.slice(-64));
|
|
134
|
+
const out = new Response(originRes.body, {
|
|
135
|
+
status: originRes.status,
|
|
136
|
+
statusText: originRes.statusText,
|
|
137
|
+
headers,
|
|
138
|
+
});
|
|
139
|
+
if (statusOk(originRes.status)) {
|
|
140
|
+
const store = out.clone();
|
|
141
|
+
// Age starts at 0
|
|
142
|
+
store.headers.set("X-Query-Cache-Age", "0");
|
|
143
|
+
const p = cacheStore.put(cacheReq, store);
|
|
144
|
+
if (ctx?.waitUntil)
|
|
145
|
+
ctx.waitUntil(p);
|
|
146
|
+
else
|
|
147
|
+
await p;
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
})();
|
|
151
|
+
flights.set(flightKey, flight);
|
|
152
|
+
try {
|
|
153
|
+
return await flight;
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
flights.delete(flightKey);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function revalidate(handler, request, env, ctx, cacheStore, cacheReq, bodyText, sMaxAge, statusOk) {
|
|
161
|
+
try {
|
|
162
|
+
const reqForHandler = new Request(request.url, {
|
|
163
|
+
method: request.method,
|
|
164
|
+
headers: request.headers,
|
|
165
|
+
body: bodyText || undefined,
|
|
166
|
+
});
|
|
167
|
+
const res = await handler(reqForHandler, env, ctx);
|
|
168
|
+
if (!statusOk(res.status))
|
|
169
|
+
return;
|
|
170
|
+
const headers = new Headers(res.headers);
|
|
171
|
+
if (!headers.has("Cache-Control")) {
|
|
172
|
+
headers.set("Cache-Control", queryCacheControl({ sMaxAge, maxAge: 0 }));
|
|
173
|
+
}
|
|
174
|
+
headers.set("X-HTTP-Query-Cache", "REVALIDATED");
|
|
175
|
+
headers.set("X-Query-Cache-Age", "0");
|
|
176
|
+
await cacheStore.put(cacheReq, new Response(res.body, { status: res.status, headers }));
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
/* ignore revalidate errors */
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* One-liner platform export for Workers.
|
|
184
|
+
* Full auto QUERY cache + optional dual POST passthrough.
|
|
185
|
+
*/
|
|
186
|
+
export function createAutoQueryPlatform(options) {
|
|
187
|
+
return {
|
|
188
|
+
fetch: withAutoQueryCache(options.fetch, options.cache ?? {}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** In-memory Cache API mock for Node tests / local benches */
|
|
192
|
+
export function createMemoryCache() {
|
|
193
|
+
const store = new Map();
|
|
194
|
+
return {
|
|
195
|
+
store,
|
|
196
|
+
async match(r) {
|
|
197
|
+
const hit = store.get(r.url);
|
|
198
|
+
return hit ? hit.clone() : undefined;
|
|
199
|
+
},
|
|
200
|
+
async put(r, res) {
|
|
201
|
+
store.set(r.url, res.clone());
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=auto-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-cache.js","sourceRoot":"","sources":["../src/auto-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare **native-class** body-keyed QUERY cache.
|
|
3
|
+
*
|
|
4
|
+
* Upstream Cache API still keys by URL only (workerd#6849). This module is the
|
|
5
|
+
* strongest app-level replacement until CF ships native body keys:
|
|
6
|
+
*
|
|
7
|
+
* - Synthetic Cache API GET key = hash(url + body + content-type + auth bit)
|
|
8
|
+
* - Optional dual store: Cache API + memory L1 in isolate
|
|
9
|
+
* - Singleflight stampede collapse
|
|
10
|
+
* - Soft TTL / SWR via headers + background revalidate
|
|
11
|
+
* - Cache tags header for future purge hooks
|
|
12
|
+
* - Metrics counters for ops dashboards
|
|
13
|
+
*
|
|
14
|
+
* Use this when you want “as native as it gets” on Workers today.
|
|
15
|
+
*/
|
|
16
|
+
import { type AutoQueryCacheOptions } from "./auto-cache.js";
|
|
17
|
+
export interface CfNativeBodyCacheOptions extends AutoQueryCacheOptions {
|
|
18
|
+
/** L1 memory cache in the isolate (default true) */
|
|
19
|
+
l1?: boolean;
|
|
20
|
+
l1Max?: number;
|
|
21
|
+
l1TtlMs?: number;
|
|
22
|
+
/** Prefix for synthetic cache URL path */
|
|
23
|
+
keyPrefix?: string;
|
|
24
|
+
/** Emit X-HTTP-Query-Cache-Tier: L1|L2|ORIGIN */
|
|
25
|
+
tierHeaders?: boolean;
|
|
26
|
+
/** Optional cache tag for purge coordination */
|
|
27
|
+
cacheTag?: string | ((req: Request, body: string) => string);
|
|
28
|
+
}
|
|
29
|
+
export interface CfBodyCacheMetrics {
|
|
30
|
+
l1Hits: number;
|
|
31
|
+
l2Hits: number;
|
|
32
|
+
misses: number;
|
|
33
|
+
origin: number;
|
|
34
|
+
coalesced: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function getCfBodyCacheMetrics(): CfBodyCacheMetrics;
|
|
37
|
+
export declare function resetCfBodyCacheMetrics(): void;
|
|
38
|
+
/** Build the synthetic Cache API Request used as the native-class body key. */
|
|
39
|
+
export declare function cfBodyCacheKeyRequest(request: Request, options?: {
|
|
40
|
+
cacheHost?: string;
|
|
41
|
+
prefix?: string;
|
|
42
|
+
}): Promise<{
|
|
43
|
+
cacheRequest: Request;
|
|
44
|
+
key: string;
|
|
45
|
+
body: string;
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Strongest Workers QUERY cache wrapper until CF implements body keys natively.
|
|
49
|
+
*/
|
|
50
|
+
export declare function withCfNativeBodyCache(handler: (request: Request, env: any, ctx: any) => Promise<Response> | Response, options?: CfNativeBodyCacheOptions): (request: Request, env: any, ctx: any) => Promise<Response>;
|
|
51
|
+
/**
|
|
52
|
+
* Drop-in Worker export: body-keyed QUERY search platform.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createCfNativeQueryWorker(options: {
|
|
55
|
+
search: (body: any, env: any) => Promise<unknown> | unknown;
|
|
56
|
+
sMaxAge?: number;
|
|
57
|
+
cache?: CfNativeBodyCacheOptions;
|
|
58
|
+
}): {
|
|
59
|
+
fetch: (request: Request, env: any, ctx: any) => Promise<Response>;
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=cf-body-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cf-body-cache.d.ts","sourceRoot":"","sources":["../src/cf-body-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAyC,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAEpG,MAAM,WAAW,wBAAyB,SAAQ,qBAAqB;IACrE,oDAAoD;IACpD,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAUD,wBAAgB,qBAAqB,IAAI,kBAAkB,CAE1D;AAED,wBAAgB,uBAAuB,IAAI,IAAI,CAM9C;AAED,+EAA+E;AAC/E,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GACpD,OAAO,CAAC;IAAE,YAAY,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAe/D;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,EAC/E,OAAO,GAAE,wBAA6B,GACrC,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,CA0D7D;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IACjD,MAAM,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,wBAAwB,CAAC;CAClC,GAAG;IAAE,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAAE,CAoCzE"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare **native-class** body-keyed QUERY cache.
|
|
3
|
+
*
|
|
4
|
+
* Upstream Cache API still keys by URL only (workerd#6849). This module is the
|
|
5
|
+
* strongest app-level replacement until CF ships native body keys:
|
|
6
|
+
*
|
|
7
|
+
* - Synthetic Cache API GET key = hash(url + body + content-type + auth bit)
|
|
8
|
+
* - Optional dual store: Cache API + memory L1 in isolate
|
|
9
|
+
* - Singleflight stampede collapse
|
|
10
|
+
* - Soft TTL / SWR via headers + background revalidate
|
|
11
|
+
* - Cache tags header for future purge hooks
|
|
12
|
+
* - Metrics counters for ops dashboards
|
|
13
|
+
*
|
|
14
|
+
* Use this when you want “as native as it gets” on Workers today.
|
|
15
|
+
*/
|
|
16
|
+
import { QUERY_METHOD, cacheKey } from "@httpquery/core";
|
|
17
|
+
import { withAutoQueryCache, createMemoryCache } from "./auto-cache.js";
|
|
18
|
+
const globalMetrics = {
|
|
19
|
+
l1Hits: 0,
|
|
20
|
+
l2Hits: 0,
|
|
21
|
+
misses: 0,
|
|
22
|
+
origin: 0,
|
|
23
|
+
coalesced: 0,
|
|
24
|
+
};
|
|
25
|
+
export function getCfBodyCacheMetrics() {
|
|
26
|
+
return { ...globalMetrics };
|
|
27
|
+
}
|
|
28
|
+
export function resetCfBodyCacheMetrics() {
|
|
29
|
+
globalMetrics.l1Hits = 0;
|
|
30
|
+
globalMetrics.l2Hits = 0;
|
|
31
|
+
globalMetrics.misses = 0;
|
|
32
|
+
globalMetrics.origin = 0;
|
|
33
|
+
globalMetrics.coalesced = 0;
|
|
34
|
+
}
|
|
35
|
+
/** Build the synthetic Cache API Request used as the native-class body key. */
|
|
36
|
+
export async function cfBodyCacheKeyRequest(request, options = {}) {
|
|
37
|
+
const url = new URL(request.url);
|
|
38
|
+
const body = await request.clone().text();
|
|
39
|
+
const ct = request.headers.get("content-type") ?? "application/json";
|
|
40
|
+
const extra = {
|
|
41
|
+
"content-type": ct.split(";")[0]?.trim().toLowerCase() ?? "",
|
|
42
|
+
};
|
|
43
|
+
const accept = request.headers.get("accept");
|
|
44
|
+
if (accept)
|
|
45
|
+
extra.accept = accept;
|
|
46
|
+
if (request.headers.get("authorization"))
|
|
47
|
+
extra.authorization = "1";
|
|
48
|
+
const key = await cacheKey(`${url.origin}${url.pathname}`, body, extra);
|
|
49
|
+
const host = options.cacheHost ?? url.host;
|
|
50
|
+
const prefix = options.prefix ?? "/__http_query_cf_body";
|
|
51
|
+
const cacheRequest = new Request(`https://${host}${prefix}/${key}`, { method: "GET" });
|
|
52
|
+
return { cacheRequest, key, body };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Strongest Workers QUERY cache wrapper until CF implements body keys natively.
|
|
56
|
+
*/
|
|
57
|
+
export function withCfNativeBodyCache(handler, options = {}) {
|
|
58
|
+
const l1Enabled = options.l1 !== false;
|
|
59
|
+
const l1 = options.cache ?? (l1Enabled ? createMemoryCache() : undefined);
|
|
60
|
+
// Always use withAutoQueryCache for L2 path + singleflight; inject metrics via wrap
|
|
61
|
+
const base = withAutoQueryCache(async (request, env, ctx) => {
|
|
62
|
+
globalMetrics.origin++;
|
|
63
|
+
globalMetrics.misses++;
|
|
64
|
+
const res = await handler(request, env, ctx);
|
|
65
|
+
const headers = new Headers(res.headers);
|
|
66
|
+
if (options.tierHeaders !== false) {
|
|
67
|
+
headers.set("X-HTTP-Query-Cache-Tier", "ORIGIN");
|
|
68
|
+
headers.set("X-HTTP-Query-Platform", "cf-native-body-key");
|
|
69
|
+
}
|
|
70
|
+
if (options.cacheTag) {
|
|
71
|
+
const body = await request
|
|
72
|
+
.clone()
|
|
73
|
+
.text()
|
|
74
|
+
.catch(() => "");
|
|
75
|
+
const tag = typeof options.cacheTag === "function"
|
|
76
|
+
? options.cacheTag(request, body)
|
|
77
|
+
: options.cacheTag;
|
|
78
|
+
headers.set("Cache-Tag", tag);
|
|
79
|
+
}
|
|
80
|
+
return new Response(res.body, {
|
|
81
|
+
status: res.status,
|
|
82
|
+
statusText: res.statusText,
|
|
83
|
+
headers,
|
|
84
|
+
});
|
|
85
|
+
}, {
|
|
86
|
+
...options,
|
|
87
|
+
cache: l1,
|
|
88
|
+
// prefer our prefix for synthetic keys
|
|
89
|
+
});
|
|
90
|
+
return async (request, env, ctx) => {
|
|
91
|
+
const res = await base(request, env, ctx);
|
|
92
|
+
const cacheHdr = res.headers.get("X-HTTP-Query-Cache");
|
|
93
|
+
if (cacheHdr === "HIT") {
|
|
94
|
+
globalMetrics.l1Hits++;
|
|
95
|
+
if (options.tierHeaders !== false && !res.headers.get("X-HTTP-Query-Cache-Tier")) {
|
|
96
|
+
const h = new Headers(res.headers);
|
|
97
|
+
h.set("X-HTTP-Query-Cache-Tier", "L1");
|
|
98
|
+
h.set("X-HTTP-Query-Platform", "cf-native-body-key");
|
|
99
|
+
return new Response(res.body, {
|
|
100
|
+
status: res.status,
|
|
101
|
+
statusText: res.statusText,
|
|
102
|
+
headers: h,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (cacheHdr === "COALESCE") {
|
|
107
|
+
globalMetrics.coalesced++;
|
|
108
|
+
}
|
|
109
|
+
return res;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Drop-in Worker export: body-keyed QUERY search platform.
|
|
114
|
+
*/
|
|
115
|
+
export function createCfNativeQueryWorker(options) {
|
|
116
|
+
const core = async (request, env) => {
|
|
117
|
+
const method = request.method.toUpperCase();
|
|
118
|
+
if (method === "OPTIONS") {
|
|
119
|
+
return new Response(null, {
|
|
120
|
+
status: 204,
|
|
121
|
+
headers: {
|
|
122
|
+
"Access-Control-Allow-Origin": "*",
|
|
123
|
+
"Access-Control-Allow-Methods": `GET, POST, ${QUERY_METHOD}, OPTIONS`,
|
|
124
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
125
|
+
"Accept-Query": "application/json",
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (method !== QUERY_METHOD && method !== "POST") {
|
|
130
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
131
|
+
}
|
|
132
|
+
const body = await request.json().catch(() => ({}));
|
|
133
|
+
const results = await options.search(body, env);
|
|
134
|
+
return Response.json({ results, method }, {
|
|
135
|
+
headers: {
|
|
136
|
+
"Cache-Control": `public, max-age=0, s-maxage=${options.sMaxAge ?? 60}`,
|
|
137
|
+
"Accept-Query": "application/json",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
fetch: withCfNativeBodyCache(core, {
|
|
143
|
+
sMaxAge: options.sMaxAge ?? 60,
|
|
144
|
+
...options.cache,
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=cf-body-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cf-body-cache.js","sourceRoot":"","sources":["../src/cf-body-cache.ts"],"names":[],"mappings":"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"}
|