diff --git a/.nycrc.json b/.nycrc.json
index 758da34..6767fa5 100644
--- a/.nycrc.json
+++ b/.nycrc.json
@@ -6,5 +6,8 @@
"check-coverage": true,
"lines": 20,
"branches": 80,
- "statements": 20
+ "statements": 20,
+ "exclude": [
+ "test/fixtures/**"
+ ]
}
diff --git a/docs/API.md b/docs/API.md
index 5c802a5..ff4db8e 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -1,12 +1,158 @@
-
+# API Documentation
-## main(name) ⇒ string
-This is the main function
+## CacheOverride
-**Kind**: global function
-**Returns**: string - a greeting
+The `CacheOverride` class provides a unified API for controlling cache behavior across both Fastly Compute and Cloudflare Workers platforms.
-| Param | Type | Description |
-| --- | --- | --- |
-| name | string | name of the person to greet |
+### Import
+```javascript
+import { CacheOverride, fetch } from '@adobe/fetch';
+```
+
+### Constructor
+
+#### `new CacheOverride(mode, init)`
+
+Creates a new CacheOverride instance with the specified mode and options.
+
+**Parameters:**
+
+- `mode` (string): Cache override mode. One of:
+ - `"none"`: Respect origin cache control headers (default behavior)
+ - `"pass"`: Prevent caching regardless of origin headers
+ - `"override"`: Apply custom cache settings
+- `init` (object, optional): Cache configuration options
+
+**Alternative Signature:**
+
+#### `new CacheOverride(init)`
+
+Creates a new CacheOverride instance with `"override"` mode and the specified options.
+
+**Parameters:**
+
+- `init` (object): Cache configuration options
+
+### Cross-Platform Options
+
+The CacheOverride API only includes options that work on **both** Fastly and Cloudflare platforms to ensure true cross-platform compatibility:
+
+| Option | Type | Description | Platform Mapping |
+|--------|------|-------------|------------------|
+| `ttl` | number | Time-to-live in seconds | Fastly: native `ttl`
Cloudflare: `cf.cacheTtl` |
+| `cacheKey` | string | Custom cache key | Fastly: native `cacheKey`
Cloudflare: `cf.cacheKey` |
+| `surrogateKey` | string | Space-separated surrogate keys for cache purging | Fastly: native `surrogateKey`
Cloudflare: `cf.cacheTags` (array) |
+
+**Note:** Platform-specific options (like Fastly's `swr`, `pci`, `beforeSend`, `afterSend`) are intentionally excluded to maintain cross-platform compatibility. If you pass unsupported options, they will be ignored with a console warning.
+
+### Usage Examples
+
+#### Basic TTL Override
+
+```javascript
+import { fetch, CacheOverride } from '@adobe/fetch';
+
+const cacheOverride = new CacheOverride('override', {
+ ttl: 3600 // Cache for 1 hour
+});
+
+const response = await fetch('https://example.com/api', {
+ cacheOverride
+});
+```
+
+#### Prevent Caching
+
+```javascript
+const cacheOverride = new CacheOverride('pass');
+
+const response = await fetch('https://example.com/api', {
+ cacheOverride
+});
+```
+
+#### Advanced Configuration
+
+```javascript
+const cacheOverride = new CacheOverride({
+ ttl: 3600, // Cache for 1 hour
+ cacheKey: 'my-key', // Custom cache key
+ surrogateKey: 'api v1' // Surrogate keys for purging
+});
+
+const response = await fetch('https://example.com/api', {
+ cacheOverride
+});
+```
+
+#### Conditional Caching by Path
+
+```javascript
+import { fetch, CacheOverride } from '@adobe/fetch';
+
+export async function main(request, context) {
+ const url = new URL(request.url);
+ let cacheOverride;
+
+ if (url.pathname.startsWith('/static/')) {
+ // Long cache for static resources
+ cacheOverride = new CacheOverride({ ttl: 86400 });
+ } else if (url.pathname === '/') {
+ // Short cache for homepage
+ cacheOverride = new CacheOverride({ ttl: 60 });
+ } else {
+ // Respect origin cache headers
+ cacheOverride = new CacheOverride('none');
+ }
+
+ return fetch(url, { cacheOverride });
+}
+```
+
+### Platform-Specific Behavior
+
+#### Fastly Compute
+
+On Fastly, `CacheOverride` uses the native `fastly:cache-override` module. Only cross-platform compatible options are passed through to ensure consistent behavior.
+
+```javascript
+// On Fastly, this uses native CacheOverride with cross-platform options
+const override = new CacheOverride('override', {
+ ttl: 3600,
+ cacheKey: 'my-key',
+ surrogateKey: 'homepage main'
+});
+```
+
+#### Cloudflare Workers
+
+On Cloudflare, `CacheOverride` options are automatically mapped to the `cf` object in fetch options:
+
+| CacheOverride | Cloudflare cf object |
+|---------------|---------------------|
+| `mode: "pass"` | `cf: { cacheTtl: 0 }` |
+| `mode: "none"` | No cf options added |
+| `ttl: 3600` | `cf: { cacheTtl: 3600 }` |
+| `cacheKey: "key"` | `cf: { cacheKey: "key" }` |
+| `surrogateKey: "a b"` | `cf: { cacheTags: ["a", "b"] }` |
+
+```javascript
+// On Cloudflare, this is converted to:
+// fetch(url, { cf: { cacheTtl: 3600, cacheKey: 'my-key', cacheTags: ['api', 'v1'] } })
+const override = new CacheOverride({
+ ttl: 3600,
+ cacheKey: 'my-key',
+ surrogateKey: 'api v1'
+});
+
+await fetch(url, { cacheOverride: override });
+```
+
+### Notes
+
+- **Cross-Platform Compatibility**: Only options supported on both platforms are included in this API
+- **Unsupported Options**: If you pass platform-specific options (like `swr`, `pci`, `beforeSend`, `afterSend`), they will be ignored with a console warning
+- **Cloudflare Enterprise**: The `cacheKey` feature requires a Cloudflare Enterprise plan
+- **Surrogate Keys**: On Cloudflare, the space-separated `surrogateKey` string is automatically split into an array for `cf.cacheTags`
+- **For Platform-Specific Features**: If you need platform-specific cache features, use platform detection and native APIs directly instead of the cross-platform `CacheOverride` API
diff --git a/package.json b/package.json
index 91d6ee8..2ada3fd 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,8 @@
"main": "src/index.js",
"type": "module",
"scripts": {
- "test": "c8 mocha -i -g Integration",
- "integration-ci": "c8 mocha -g Integration",
+ "test": "c8 --exclude 'test/fixtures/**' mocha -i -g Integration",
+ "integration-ci": "c8 --exclude 'test/fixtures/**' mocha -g Integration",
"lint": "eslint .",
"semantic-release": "semantic-release",
"semantic-release-dry": "semantic-release --dry-run --branches $CI_BRANCH",
@@ -26,7 +26,10 @@
"require": "test/setup-env.js",
"recursive": "true",
"reporter": "mocha-multi-reporters",
- "reporter-options": "configFile=.mocha-multi.json"
+ "reporter-options": "configFile=.mocha-multi.json",
+ "exclude": [
+ "test/fixtures/**"
+ ]
},
"devDependencies": {
"@adobe/eslint-config-helix": "3.0.14",
diff --git a/src/template/polyfills/fetch.js b/src/template/polyfills/fetch.js
index cad34f8..9ed30e8 100644
--- a/src/template/polyfills/fetch.js
+++ b/src/template/polyfills/fetch.js
@@ -11,10 +11,233 @@
*/
/* eslint-env serviceworker */
-module.exports = {
- // replacing @adobe/fetch with the built-in APIs
- fetch,
- Request,
- Response,
- Headers,
+// Platform detection and native CacheOverride loading
+let nativeCacheOverride = null;
+let isFastly = false;
+let isCloudflare = false;
+let fastlyModulePromise = null;
+
+// Try to import Fastly's CacheOverride module
+// Use a function to prevent webpack from trying to resolve this at build time
+async function loadFastlyModule() {
+ try {
+ // Dynamic import - webpack will leave this as-is because it's external
+ const moduleName = 'fastly:cache-override';
+ // eslint-disable-next-line import/no-unresolved
+ const module = await import(/* webpackIgnore: true */ moduleName);
+ nativeCacheOverride = module.CacheOverride;
+ isFastly = true;
+ return module;
+ } catch {
+ // Not Fastly environment - this is expected on other platforms
+ return null;
+ }
+}
+
+// Start loading the module if available
+try {
+ fastlyModulePromise = loadFastlyModule();
+} catch {
+ fastlyModulePromise = null;
+}
+
+// Detect Cloudflare environment
+try {
+ if (typeof caches !== 'undefined' && caches.default) {
+ isCloudflare = true;
+ }
+} catch {
+ // Not Cloudflare
+}
+
+/**
+ * Unified CacheOverride class that works across Fastly and Cloudflare platforms
+ */
+class UnifiedCacheOverride {
+ /**
+ * Creates a new CacheOverride instance
+ * @param {string|object} modeOrInit - Either a mode string or init object
+ * @param {object} [init] - Optional init object when mode is first param
+ * @param {number} [init.ttl] - Time-to-live in seconds
+ * @param {string} [init.cacheKey] - Custom cache key
+ * @param {string} [init.surrogateKey] - Surrogate keys for cache purging
+ */
+ constructor(modeOrInit, init) {
+ let mode;
+ let options;
+
+ // Parse constructor arguments (supports both signatures)
+ if (typeof modeOrInit === 'string') {
+ mode = modeOrInit;
+ options = init || {};
+ } else {
+ mode = 'override';
+ options = modeOrInit || {};
+ }
+
+ // Validate that only supported cross-platform options are used
+ const supportedOptions = ['ttl', 'cacheKey', 'surrogateKey'];
+ const unsupported = Object.keys(options)
+ .filter((key) => !supportedOptions.includes(key));
+ if (unsupported.length > 0) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `CacheOverride: Unsupported options ignored: ${unsupported.join(', ')}`,
+ );
+ }
+
+ this.mode = mode;
+ this.options = {
+ ...(typeof options.ttl === 'number' && { ttl: options.ttl }),
+ ...(options.cacheKey && { cacheKey: options.cacheKey }),
+ ...(options.surrogateKey && { surrogateKey: options.surrogateKey }),
+ };
+
+ this.modeOrInit = modeOrInit;
+ this.native = null;
+ this.nativeInitialized = false;
+ }
+
+ /**
+ * Lazy initialization of native Fastly CacheOverride
+ * @private
+ */
+ async initNative() {
+ if (this.nativeInitialized) {
+ return;
+ }
+
+ this.nativeInitialized = true;
+
+ // Wait for Fastly module to load if needed
+ if (fastlyModulePromise) {
+ await fastlyModulePromise;
+ }
+
+ // Create native instance if on Fastly
+ if (isFastly && nativeCacheOverride) {
+ // eslint-disable-next-line new-cap
+ const NativeCacheOverride = nativeCacheOverride;
+ if (typeof this.modeOrInit === 'string') {
+ this.native = new NativeCacheOverride(this.modeOrInit, this.options);
+ } else {
+ this.native = new NativeCacheOverride(this.options);
+ }
+ }
+ }
+
+ /**
+ * Converts this CacheOverride to Cloudflare cf options
+ * @returns {object|undefined} Cloudflare cf object or undefined
+ */
+ toCloudflareOptions() {
+ const cf = {};
+
+ if (this.mode === 'pass') {
+ // Pass mode = don't cache
+ cf.cacheTtl = 0;
+ return cf;
+ }
+
+ if (this.mode === 'none') {
+ // None mode = respect origin headers (no cf options needed)
+ return undefined;
+ }
+
+ // Override mode - map cross-platform options
+ if (typeof this.options.ttl === 'number') {
+ cf.cacheTtl = this.options.ttl;
+ }
+
+ if (this.options.cacheKey) {
+ cf.cacheKey = this.options.cacheKey;
+ }
+
+ if (this.options.surrogateKey) {
+ // Map surrogateKey to cacheTags (Cloudflare uses array format)
+ cf.cacheTags = this.options.surrogateKey.split(/\s+/);
+ }
+
+ return Object.keys(cf).length > 0 ? cf : undefined;
+ }
+
+ /**
+ * Gets the native Fastly CacheOverride instance if available
+ * @returns {Promise