Files
WRNexusJS/packages/cache/src/distributed.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

95 lines
3.0 KiB
TypeScript

import type { TagCache } from "./memory.ts";
export interface CacheInvalidationBus {
publish(topic: string, message: unknown): void | Promise<void>;
subscribe(pattern: string, handler: (message: unknown) => void | Promise<void>): () => void;
}
export interface DistributedInvalidationOptions {
namespace?: string;
instanceId?: string;
onError?: (error: unknown) => void;
}
type InvalidationMessage =
| { source: string; operation: "tags"; tags: string[] }
| { source: string; operation: "key"; key: string }
| { source: string; operation: "clear" };
export interface DistributedInvalidation {
invalidateTag(tag: string): Promise<number>;
invalidateTags(tags: Iterable<string>): Promise<number>;
delete(key: string): Promise<boolean>;
clear(): Promise<void>;
close(): void;
}
/**
* Propagate cache invalidations over any structurally compatible pub/sub bus.
* The bus is intentionally not closed because applications commonly share it.
*/
export function connectCacheInvalidation<V>(
cache: TagCache<V>,
bus: CacheInvalidationBus,
options: DistributedInvalidationOptions = {},
): DistributedInvalidation {
const namespace = options.namespace?.trim() || "default";
if (!/^[a-zA-Z0-9._-]+$/.test(namespace)) {
throw new TypeError("cache invalidation namespace contains unsupported characters");
}
const source = options.instanceId?.trim() || crypto.randomUUID();
const topic = `wrnexus:cache:${namespace}:invalidate`;
let closed = false;
const publish = async (message: InvalidationMessage): Promise<void> => {
if (closed) throw new Error("WRN-CACHE-INVALIDATION-CLOSED: invalidation channel is closed");
try {
await bus.publish(topic, message);
} catch (error) {
options.onError?.(error);
throw error;
}
};
const unsubscribe = bus.subscribe(topic, (value) => {
if (!value || typeof value !== "object") return;
const message = value as Partial<InvalidationMessage>;
if (message.source === source) return;
if (message.operation === "tags" && Array.isArray(message.tags)) {
cache.invalidateTags(message.tags.filter((tag): tag is string => typeof tag === "string"));
} else if (message.operation === "key" && typeof message.key === "string") {
cache.delete(message.key);
} else if (message.operation === "clear") {
cache.clear();
}
});
const invalidateTags = async (input: Iterable<string>): Promise<number> => {
const tags = [...new Set(input)].filter(Boolean);
const removed = cache.invalidateTags(tags);
await publish({ source, operation: "tags", tags });
return removed;
};
return {
async invalidateTag(tag) {
return invalidateTags([tag]);
},
invalidateTags,
async delete(key) {
const removed = cache.delete(key);
await publish({ source, operation: "key", key });
return removed;
},
async clear() {
cache.clear();
await publish({ source, operation: "clear" });
},
close() {
if (closed) return;
closed = true;
unsubscribe();
},
};
}