import { t as after } from "./after-B1IIdH3Y.mjs";
import { getRequestContext } from "./request-context.mjs";

//#region src/object-cache/codec.ts
/**
* Object-cache serialization codec.
*
* Cached values are JSON, with one extension: `Date` instances are preserved
* across the round-trip. EmDash content entries carry `Date` objects for the
* system timestamp columns (`createdAt`, `updatedAt`, `publishedAt`,
* `scheduledAt`) and on `cacheHint.lastModified`; plain `JSON.stringify` would
* silently flatten those to ISO strings, so a value read from cache would no
* longer be `=== instanceof Date` and downstream `value instanceof Date`
* branches (cursor encoding, scheduled-visibility checks) would diverge from a
* fresh database read.
*
* Functions and symbol-keyed properties are NOT preserved — callers that cache
* values carrying either (e.g. content entries with their `.edit` proxy and
* the non-enumerable `CURSOR_RAW_VALUES` symbol) must reduce to a serializable
* snapshot before caching and rebuild the non-serializable parts on read. See
* `query.ts` content snapshot helpers.
*/
/** Tag used to mark a serialized `Date`. Deliberately unlikely to collide. */
const DATE_TAG = "$$emdashDate";
function isTaggedDate(value) {
	if (typeof value !== "object" || value === null) return false;
	return Object.keys(value).length === 1 && typeof value[DATE_TAG] === "string";
}
/**
* Serialize a value to a cache string, preserving `Date` instances.
*
* Uses the JSON replacer's `this` binding to inspect the *original* property
* value: `JSON.stringify` invokes `Date.prototype.toJSON` before the replacer
* sees it, so by the time `value` arrives it is already an ISO string. Reading
* `this[key]` recovers the live `Date` so we can tag it.
*/
function encode(value) {
	return JSON.stringify(value, function(key, val) {
		const original = this[key];
		if (original instanceof Date) return { [DATE_TAG]: original.toISOString() };
		return val;
	});
}
/**
* Parse a cache string produced by {@link encode}, rehydrating tagged `Date`s.
*
* Returns `undefined` if the input is not valid JSON (treated as a cache miss
* by the read-through layer rather than throwing).
*/
function decode(raw) {
	try {
		return JSON.parse(raw, (_key, value) => {
			if (isTaggedDate(value)) return new Date(value[DATE_TAG]);
			return value;
		});
	} catch {
		return;
	}
}

//#endregion
//#region src/object-cache/index.ts
/**
* Object cache — distributed read-through query cache.
*
* Layering (per query):
*
*   requestCached   → in-request dedupe (per render, WeakMap on ALS context)
*   cachedQuery     → THIS layer: distributed L2 (KV / memory), epoch-keyed
*   database        → source of truth
*
* Optional and off by default: when no `objectCache` descriptor is configured,
* `virtual:emdash/object-cache` exports `createObjectCache = undefined`,
* {@link getBackend} resolves to `null`, and {@link cachedQuery} is a
* transparent passthrough to its `load` function. Configure with
* `memoryCache()` (Node) or `kvCache()` from `@emdash-cms/cloudflare`.
*
* Invalidation is epoch-based: each cache key embeds a per-namespace epoch
* ("last changed" marker) read from the backend. A write calls
* {@link invalidateObjectCache}, which stamps the namespace epoch to
* `Date.now()`; every previously-stored key for that namespace is instantly
* orphaned and reclaimed by its TTL. This is O(1) and needs no key
* enumeration (KV has no prefix delete).
*
* The singleton backend/config and the per-isolate epoch cache live on
* `globalThis` behind `Symbol.for` keys so Vite SSR chunk duplication can't
* fork them (same pattern as `request-context.ts`).
*/
const DEFAULT_KEY_PREFIX = "em";
const DEFAULT_TTL_SECONDS = 3600;
const DEFAULT_REVALIDATE_MS = 1e3;
const DEFAULT_TIMEOUT_MS = 2e3;
/**
* Race a backend operation against a timeout so a stalled call (e.g. a KV read
* that never resolves *and* never rejects — a cold cross-region read, or one
* queued behind the Workers simultaneous-connection limit) degrades to a
* rejection instead of hanging the isolate. A rejection is benign: callers
* already treat a failed read as a cache miss / last-known epoch.
*/
function withTimeout(promise, ms, label) {
	if (!(ms > 0)) return promise;
	let timer;
	const timeout = new Promise((_resolve, reject) => {
		timer = setTimeout(() => {
			reject(/* @__PURE__ */ new Error(`object-cache ${label} timed out after ${ms}ms`));
		}, ms);
	});
	return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}
/**
* Extra time past the configured read timeout before an in-flight epoch read
* is presumed dead. A live owner's read settles within `timeout` (enforced by
* {@link withTimeout}), so the grace only needs to absorb scheduling jitter.
*/
const EPOCH_READ_GRACE_MS = 1e3;
/**
* How long an in-flight epoch read may be trusted. A read older than this can
* only exist because its owning request was cancelled mid-await — on workerd
* a cancelled request's continuations never run, *including the timeout's own
* `setTimeout` callback*, so the shared promise never settles and is never
* replaced. With the timeout disabled (`timeout: 0`) the default is used as
* the deadline; the worst case of guessing too short is one duplicate
* backend read.
*/
function epochReadDeadline() {
	return (holder.config.timeout > 0 ? holder.config.timeout : DEFAULT_TIMEOUT_MS) + EPOCH_READ_GRACE_MS;
}
/**
* Await another request's in-flight epoch read, bounded by the waiter's own
* timer. The bare promise must never be awaited across requests: if the
* owning request is cancelled, the promise never settles (see
* {@link epochReadDeadline}) and an unguarded waiter hangs until the isolate
* is evicted. The race timer below belongs to the *waiter's* request context
* and therefore always fires; on timeout the waiter degrades to `fallback`
* (the last known epoch), the same contract as a failed backend read.
*/
function raceInFlightEpochRead(promise, ms, fallback) {
	let timer;
	const timeout = new Promise((resolve) => {
		timer = setTimeout(resolve, Math.max(ms, 1), fallback);
	});
	return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}
const BACKEND_KEY = Symbol.for("emdash:object-cache:backend");
const EPOCH_KEY = Symbol.for("emdash:object-cache:epochs");
const PENDING_KEY = Symbol.for("emdash:object-cache:pending-bumps");
const g = globalThis;
const holder = g[BACKEND_KEY] ?? (() => {
	const h = {
		initialized: false,
		backend: null,
		initPromise: null,
		config: {
			keyPrefix: DEFAULT_KEY_PREFIX,
			defaultTtl: DEFAULT_TTL_SECONDS,
			revalidate: DEFAULT_REVALIDATE_MS,
			timeout: DEFAULT_TIMEOUT_MS
		}
	};
	g[BACKEND_KEY] = h;
	return h;
})();
const epochCache = g[EPOCH_KEY] ?? (() => {
	const m = /* @__PURE__ */ new Map();
	g[EPOCH_KEY] = m;
	return m;
})();
/** Namespaces with a backend epoch write already scheduled this tick. */
const pendingBumps = g[PENDING_KEY] ?? (() => {
	const s = /* @__PURE__ */ new Set();
	g[PENDING_KEY] = s;
	return s;
})();
/**
* Resolve (once per isolate) the configured object-cache backend.
*
* Loads `virtual:emdash/object-cache`, which exports `createObjectCache`
* (`undefined` when no cache is configured) and the serialized
* `objectCacheConfig`. Returns `null` when the cache is disabled.
*/
async function getBackend() {
	if (holder.initialized) return holder.backend;
	if (holder.initPromise) return holder.initPromise;
	holder.initPromise = (async () => {
		try {
			const mod = await import("virtual:emdash/object-cache");
			const config = mod.objectCacheConfig ?? {};
			holder.config = {
				keyPrefix: typeof config.keyPrefix === "string" && config.keyPrefix.length > 0 ? config.keyPrefix : DEFAULT_KEY_PREFIX,
				defaultTtl: typeof config.defaultTtl === "number" && config.defaultTtl > 0 ? config.defaultTtl : DEFAULT_TTL_SECONDS,
				revalidate: typeof config.revalidate === "number" && config.revalidate >= 0 ? config.revalidate : DEFAULT_REVALIDATE_MS,
				timeout: typeof config.timeout === "number" && config.timeout >= 0 ? config.timeout : DEFAULT_TIMEOUT_MS
			};
			holder.backend = typeof mod.createObjectCache === "function" ? mod.createObjectCache(config) : null;
		} catch (error) {
			if (import.meta.env?.DEV) console.warn("[object-cache] backend unavailable:", error);
			holder.backend = null;
		}
		holder.initialized = true;
		holder.initPromise = null;
		return holder.backend;
	})();
	return holder.initPromise;
}
/** Build the backend key for a namespace's epoch anchor. */
function epochKey(namespace) {
	return `${holder.config.keyPrefix}:epoch:${namespace}`;
}
/**
* Build the (epoch-independent) backend key for a cached value.
*
* The key is stable across invalidations — the namespace epochs are stored
* *inside* the value envelope and validated on read, not baked into the key.
* This lets the value and the epochs be fetched in one parallel round-trip
* (instead of "read epoch, then read value"), and means an invalidated value
* is overwritten in place rather than orphaned under a dead epoch-keyed name.
*/
function valueKey(namespaces, key) {
	return `${holder.config.keyPrefix}:${namespaces.join(",")}:${key}`;
}
function epochsMatch(stored, current) {
	if (stored.length !== current.length) return false;
	for (let i = 0; i < stored.length; i++) if (stored[i] !== current[i]) return false;
	return true;
}
/**
* Requests that must always read live data and never populate the cache:
* visual edit mode, preview tokens, and isolated databases (playground / DO
* preview, whose schema and content diverge from the configured site).
*/
function shouldBypass() {
	const ctx = getRequestContext();
	if (!ctx) return false;
	return ctx.editMode === true || ctx.preview !== void 0 || ctx.dbIsIsolated === true;
}
/**
* Read the current epoch for `namespace`, reusing an isolate-cached value for
* up to `revalidate` ms. A missing epoch (never bumped) is treated as `0`.
*
* Backend errors and stalls are non-fatal: the read is bounded by a timeout,
* and on failure we fall back to the last known epoch (or `0`), so a flaky or
* hung cache degrades to "serve whatever's keyed" rather than throwing or
* hanging.
*/
async function getEpoch(namespace, backend) {
	const now = Date.now();
	const cached = epochCache.get(namespace);
	if (cached && now - cached.at < holder.config.revalidate) return cached.value;
	if (cached?.promise) {
		const age = now - (cached.promiseAt ?? 0);
		const deadline = epochReadDeadline();
		if (age < deadline) return raceInFlightEpochRead(cached.promise, deadline - age, cached.value);
	}
	const promise = (async () => {
		let value;
		try {
			const raw = await withTimeout(backend.get(epochKey(namespace)), holder.config.timeout, "epoch read");
			const parsed = raw === null ? 0 : Number(raw);
			value = Number.isFinite(parsed) ? parsed : 0;
		} catch {
			value = cached?.value ?? 0;
		}
		const merged = Math.max(value, epochCache.get(namespace)?.value ?? 0);
		epochCache.set(namespace, {
			value: merged,
			at: Date.now()
		});
		return merged;
	})();
	after(() => promise.then(() => void 0, () => void 0));
	epochCache.set(namespace, {
		value: cached?.value ?? 0,
		at: cached?.at ?? 0,
		promise,
		promiseAt: now
	});
	return promise;
}
/**
* Distributed read-through cache around `load`.
*
* `T` must be the value as it should be *stored* — i.e. JSON-serializable with
* the codec's `Date` support, carrying no functions or symbol-keyed props.
* Callers caching richer objects (content entries) reduce to a serializable
* snapshot here and rebuild on the way out; see `query.ts`.
*
* On a miss or when the cache is disabled/bypassed, this is equivalent to
* `await load()`. Backend errors never propagate: a failing `get` is a miss, a
* failing `set` is dropped.
*/
async function cachedQuery(options) {
	const backend = await getBackend();
	if (!backend || shouldBypass()) return options.load();
	const namespaces = typeof options.namespace === "string" ? [options.namespace] : options.namespace;
	const fullKey = valueKey(namespaces, options.key);
	const epochsPromise = Promise.all(namespaces.map((ns) => getEpoch(ns, backend)));
	const rawPromise = withTimeout(backend.get(fullKey), holder.config.timeout, "read").catch(() => null);
	const currentEpochs = await epochsPromise;
	const raw = await rawPromise;
	if (raw !== null) {
		const decoded = decode(raw);
		if (decoded !== void 0) {
			const envelope = decoded;
			if (epochsMatch(envelope.e, currentEpochs)) return envelope.v;
		}
	}
	const value = await options.load();
	if (options.cacheable ? options.cacheable(value) : true) {
		const ttl = options.ttl ?? holder.config.defaultTtl;
		after(async () => {
			try {
				const encoded = encode({
					e: currentEpochs,
					v: value
				});
				await backend.set(fullKey, encoded, ttl);
			} catch (error) {
				if (import.meta.env?.DEV) console.warn("[object-cache] set failed:", error);
			}
		});
	}
	return value;
}
/** Whether object-cache reads are active for the current request. */
async function isObjectCacheActive() {
	return await getBackend() !== null && !shouldBypass();
}
/**
* Invalidate every cached value in `namespace` by bumping its epoch.
*
* Sync and non-blocking: the local epoch is stamped immediately (so the
* writing isolate is instantly consistent) and the backend write is deferred
* via `after`. Other isolates pick up the new epoch within their `revalidate`
* window. No-ops when the cache is disabled.
*/
function invalidateObjectCache(namespace) {
	const prev = epochCache.get(namespace)?.value ?? 0;
	const stamp = Math.max(prev + 1, Date.now());
	epochCache.set(namespace, {
		value: stamp,
		at: stamp
	});
	if (pendingBumps.has(namespace)) return;
	pendingBumps.add(namespace);
	after(async () => {
		pendingBumps.delete(namespace);
		try {
			const backend = await getBackend();
			if (!backend) return;
			const latest = epochCache.get(namespace)?.value ?? stamp;
			await backend.set(epochKey(namespace), String(latest));
		} catch (error) {
			console.error("[object-cache] epoch bump failed for", namespace, error);
		}
	});
}
/**
* Fixed namespaces for data shared across collections. Content reads fold the
* `BYLINES` and `TAXONOMIES` epochs into their keys (via {@link cachedQuery})
* because entries hydrate byline and taxonomy-term data — so renaming an
* author or a category correctly invalidates every cached entry that displays
* it, without tracking which collections reference it.
*/
const CacheNamespace = {
	SETTINGS: "settings",
	MENUS: "menus",
	TAXONOMIES: "taxonomies",
	BYLINES: "bylines",
	SCHEMA: "schema",
	COMMENTS: "comments"
};
/** Namespace for a content collection's cached queries. */
function contentNamespace(collection) {
	return `content:${collection}`;
}
/**
* Namespaces a content read depends on: the collection itself plus the shared
* byline/taxonomy data folded into each entry.
*/
function contentNamespaces(collection) {
	return [
		contentNamespace(collection),
		CacheNamespace.BYLINES,
		CacheNamespace.TAXONOMIES
	];
}
/**
* Invalidate all cached reads (list + entry) for a content collection.
* Call from every write path that mutates rows in `ec_<collection>`.
*/
function invalidateCollectionCache(collection) {
	invalidateObjectCache(contentNamespace(collection));
}
/** Invalidate cached taxonomy definitions/terms and all content that hydrates them. */
function invalidateTaxonomyObjectCache() {
	invalidateObjectCache(CacheNamespace.TAXONOMIES);
}
/** Invalidate cached bylines and all content that hydrates them. */
function invalidateBylineObjectCache() {
	invalidateObjectCache(CacheNamespace.BYLINES);
}
/** Invalidate cached navigation menus. */
function invalidateMenuObjectCache() {
	invalidateObjectCache(CacheNamespace.MENUS);
}
/** Invalidate cached collection schema/metadata reads (e.g. getCollectionInfo). */
function invalidateSchemaObjectCache() {
	invalidateObjectCache(CacheNamespace.SCHEMA);
}
/** Invalidate cached public comment reads. */
function invalidateCommentObjectCache() {
	invalidateObjectCache(CacheNamespace.COMMENTS);
}

//#endregion
export { invalidateBylineObjectCache as a, invalidateMenuObjectCache as c, invalidateTaxonomyObjectCache as d, isObjectCacheActive as f, contentNamespaces as i, invalidateObjectCache as l, cachedQuery as n, invalidateCollectionCache as o, contentNamespace as r, invalidateCommentObjectCache as s, CacheNamespace as t, invalidateSchemaObjectCache as u };
//# sourceMappingURL=object-cache-Bok5j2ae.mjs.map