chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
import {
|
||||
isCacheInvalidationOpaqueIdentifier,
|
||||
type CacheInvalidationTopicDefinition,
|
||||
} from "../../contracts/cache-invalidation.ts";
|
||||
import {
|
||||
createBrowserCrossContextInvalidation,
|
||||
type BroadcastChannelFacade,
|
||||
type BroadcastMessageListener,
|
||||
type BrowserCrossContextInvalidation,
|
||||
type CrossContextInvalidationObservation,
|
||||
type StorageEventTargetFacade,
|
||||
type StoragePulseFacade,
|
||||
type StoragePulseListener,
|
||||
} from "./browser-cross-context-invalidation.ts";
|
||||
|
||||
export type BrowserCrossContextHostDependencies = Readonly<{
|
||||
host?: Record<string, unknown>;
|
||||
cacheEpoch: string;
|
||||
topics: readonly CacheInvalidationTopicDefinition[];
|
||||
observe?: (observation: CrossContextInvalidationObservation) => void;
|
||||
}>;
|
||||
|
||||
type NativeBroadcastChannel = Readonly<{
|
||||
postMessage(value: unknown): void;
|
||||
addEventListener(type: string, listener: (event: unknown) => void): void;
|
||||
removeEventListener(
|
||||
type: string,
|
||||
listener: (event: unknown) => void,
|
||||
): void;
|
||||
close(): void;
|
||||
}>;
|
||||
|
||||
const CHANNEL_NAME = "ca-client-cache-invalidation-v1";
|
||||
const STORAGE_PULSE_KEY =
|
||||
"ca-frontend:cache-invalidation:v1:pulse";
|
||||
|
||||
/**
|
||||
* Captures native capabilities without allowing a SecurityError getter or a
|
||||
* missing random source to fail application boot.
|
||||
*/
|
||||
export function createBrowserCrossContextInvalidationFromHost(
|
||||
dependencies: BrowserCrossContextHostDependencies,
|
||||
): BrowserCrossContextInvalidation | undefined {
|
||||
// A zero-feature build owns no cross-context invalidation runtime. Preserve
|
||||
// that property strictly: do not even probe browser capability getters.
|
||||
if (dependencies.topics.length === 0) return undefined;
|
||||
const host =
|
||||
dependencies.host ??
|
||||
(globalThis as unknown as Record<string, unknown>);
|
||||
if (
|
||||
!isCacheInvalidationOpaqueIdentifier(dependencies.cacheEpoch)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const createOpaqueId = randomIdFactory(host);
|
||||
if (!createOpaqueId) return undefined;
|
||||
|
||||
const sourceId = createOpaqueId("tab");
|
||||
const sourceEpoch = createOpaqueId("page");
|
||||
if (!sourceId || !sourceEpoch) return undefined;
|
||||
|
||||
return createBrowserCrossContextInvalidation({
|
||||
channelName: CHANNEL_NAME,
|
||||
storagePulseKey: STORAGE_PULSE_KEY,
|
||||
sourceId,
|
||||
sourceEpoch,
|
||||
cacheEpoch: dependencies.cacheEpoch,
|
||||
topics: dependencies.topics,
|
||||
createEventId: () => {
|
||||
const eventId = createOpaqueId("event");
|
||||
if (!eventId) throw new TypeError("Secure random is unavailable.");
|
||||
return eventId;
|
||||
},
|
||||
createBroadcastChannel: broadcastFactory(host),
|
||||
storage: storageFacade(host),
|
||||
storageEvents: storageEventTarget(host),
|
||||
observe: dependencies.observe,
|
||||
});
|
||||
}
|
||||
|
||||
function safeGet(
|
||||
target: Record<string, unknown>,
|
||||
property: string,
|
||||
): unknown {
|
||||
try {
|
||||
return Reflect.get(target, property);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function randomIdFactory(
|
||||
host: Record<string, unknown>,
|
||||
): ((prefix: string) => string | null) | undefined {
|
||||
const cryptoCandidate = safeGet(host, "crypto");
|
||||
if (!cryptoCandidate || typeof cryptoCandidate !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const randomUuid = safeGet(
|
||||
cryptoCandidate as Record<string, unknown>,
|
||||
"randomUUID",
|
||||
);
|
||||
if (typeof randomUuid !== "function") return undefined;
|
||||
|
||||
return (prefix) => {
|
||||
try {
|
||||
const value = Reflect.apply(randomUuid, cryptoCandidate, []);
|
||||
if (typeof value !== "string") return null;
|
||||
const candidate = `${prefix}.${value}`;
|
||||
return isCacheInvalidationOpaqueIdentifier(candidate)
|
||||
? candidate
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function broadcastFactory(
|
||||
host: Record<string, unknown>,
|
||||
):
|
||||
| ((name: string) => BroadcastChannelFacade)
|
||||
| undefined {
|
||||
const Constructor = safeGet(host, "BroadcastChannel");
|
||||
if (typeof Constructor !== "function") return undefined;
|
||||
|
||||
return (name) => {
|
||||
const candidate = Reflect.construct(Constructor, [name]) as unknown;
|
||||
if (!isNativeBroadcastChannel(candidate)) {
|
||||
throw new TypeError("BroadcastChannel is incompatible.");
|
||||
}
|
||||
const listenerBindings = new Map<
|
||||
BroadcastMessageListener,
|
||||
(event: unknown) => void
|
||||
>();
|
||||
return Object.freeze({
|
||||
postMessage(value: unknown) {
|
||||
candidate.postMessage(value);
|
||||
},
|
||||
addEventListener(
|
||||
_type: "message",
|
||||
listener: BroadcastMessageListener,
|
||||
) {
|
||||
const bound = (event: unknown) => {
|
||||
listener({
|
||||
data:
|
||||
event && typeof event === "object"
|
||||
? safeGet(
|
||||
event as Record<string, unknown>,
|
||||
"data",
|
||||
)
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
listenerBindings.set(listener, bound);
|
||||
candidate.addEventListener("message", bound);
|
||||
},
|
||||
removeEventListener(
|
||||
_type: "message",
|
||||
listener: BroadcastMessageListener,
|
||||
) {
|
||||
const bound = listenerBindings.get(listener);
|
||||
if (!bound) return;
|
||||
listenerBindings.delete(listener);
|
||||
candidate.removeEventListener("message", bound);
|
||||
},
|
||||
close() {
|
||||
listenerBindings.clear();
|
||||
candidate.close();
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function isNativeBroadcastChannel(
|
||||
value: unknown,
|
||||
): value is NativeBroadcastChannel {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const candidate = value as Record<string, unknown>;
|
||||
return ["postMessage", "addEventListener", "removeEventListener", "close"].every(
|
||||
(method) => typeof safeGet(candidate, method) === "function",
|
||||
);
|
||||
}
|
||||
|
||||
function storageFacade(
|
||||
host: Record<string, unknown>,
|
||||
): StoragePulseFacade | undefined {
|
||||
const candidate = safeGet(host, "localStorage");
|
||||
if (!candidate || typeof candidate !== "object") return undefined;
|
||||
const record = candidate as Record<string, unknown>;
|
||||
const setItem = safeGet(record, "setItem");
|
||||
const removeItem = safeGet(record, "removeItem");
|
||||
if (typeof setItem !== "function" || typeof removeItem !== "function") {
|
||||
return undefined;
|
||||
}
|
||||
return Object.freeze({
|
||||
setItem(key, value) {
|
||||
Reflect.apply(setItem, candidate, [key, value]);
|
||||
},
|
||||
removeItem(key) {
|
||||
Reflect.apply(removeItem, candidate, [key]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function storageEventTarget(
|
||||
host: Record<string, unknown>,
|
||||
): StorageEventTargetFacade | undefined {
|
||||
const addEventListener = safeGet(host, "addEventListener");
|
||||
const removeEventListener = safeGet(host, "removeEventListener");
|
||||
if (
|
||||
typeof addEventListener !== "function" ||
|
||||
typeof removeEventListener !== "function"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const bindings = new Map<
|
||||
StoragePulseListener,
|
||||
(event: unknown) => void
|
||||
>();
|
||||
return Object.freeze({
|
||||
addEventListener(_type: "storage", listener: StoragePulseListener) {
|
||||
const bound = (event: unknown) => {
|
||||
if (!event || typeof event !== "object") {
|
||||
listener({ key: null, newValue: null });
|
||||
return;
|
||||
}
|
||||
const record = event as Record<string, unknown>;
|
||||
const key = safeGet(record, "key");
|
||||
const newValue = safeGet(record, "newValue");
|
||||
listener({
|
||||
key: typeof key === "string" ? key : null,
|
||||
newValue: typeof newValue === "string" ? newValue : null,
|
||||
});
|
||||
};
|
||||
bindings.set(listener, bound);
|
||||
Reflect.apply(addEventListener, host, ["storage", bound]);
|
||||
},
|
||||
removeEventListener(
|
||||
_type: "storage",
|
||||
listener: StoragePulseListener,
|
||||
) {
|
||||
const bound = bindings.get(listener);
|
||||
if (!bound) return;
|
||||
bindings.delete(listener);
|
||||
Reflect.apply(removeEventListener, host, ["storage", bound]);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user