Files
WRNexusJS/packages/native/src/registry.ts
2026-07-12 15:55:18 +05:30

107 lines
3.1 KiB
TypeScript

import type {
NativeAdapter,
NativeCapability,
NativePlatform,
NativeRunOptions,
NativeTarget,
NativeBrowserRuntime,
} from "./types.ts";
export class NativeUnavailableError extends Error {
constructor(message: string) {
super(message);
this.name = "NativeUnavailableError";
}
}
function capacitor():
| {
isNativePlatform?: () => boolean;
getPlatform?: () => string;
}
| undefined {
if (typeof globalThis === "undefined") return undefined;
return (globalThis as typeof globalThis & { Capacitor?: object }).Capacitor;
}
export function isMobile(): boolean {
const bridge = capacitor();
return (
bridge?.isNativePlatform?.() === true ||
["ios", "android"].includes(bridge?.getPlatform?.() ?? "")
);
}
export function platform(): NativePlatform {
const bridge = capacitor();
if (isMobile()) return bridge?.getPlatform?.() ?? "webview";
return typeof window === "undefined" ? "server" : "browser";
}
const capabilities = new Map<string, NativeCapability>();
function browserRuntime(): NativeBrowserRuntime | undefined {
if (typeof globalThis === "undefined") return undefined;
return (globalThis as typeof globalThis & { WrNexusNative?: NativeBrowserRuntime }).WrNexusNative;
}
function pendingRegistrations(): Map<string, NativeCapability> | undefined {
if (typeof window === "undefined") return undefined;
const root = globalThis as typeof globalThis & {
__WrNexusNativePending?: Map<string, NativeCapability>;
};
return (root.__WrNexusNativePending ??= new Map());
}
export function register<TOptions = unknown, TResult = unknown>(
name: string,
capability: NativeCapability<TOptions, TResult>,
): () => void {
if (!/^[a-z][a-z0-9.-]*$/i.test(name))
throw new TypeError(`Invalid native capability name: ${name}`);
const value = capability as NativeCapability;
capabilities.set(name, value);
const runtime = browserRuntime();
if (runtime) runtime.register(name, value);
else pendingRegistrations()?.set(name, value);
return () => {
capabilities.delete(name);
const activeRuntime = browserRuntime();
if (activeRuntime) activeRuntime.unregister(name);
else pendingRegistrations()?.delete(name);
};
}
export function registered(): string[] {
return [...capabilities.keys()].sort();
}
function adapter(name: string, target: NativeTarget): NativeAdapter | undefined {
return capabilities.get(name)?.[target];
}
export function supports(
name: string,
target: NativeTarget = isMobile() ? "mobile" : "browser",
): boolean {
const value = adapter(name, target);
return !!value && (value.supported?.() ?? true);
}
export async function run<TResult = unknown>(
name: string,
options?: unknown,
runOptions: NativeRunOptions = {},
): Promise<TResult> {
const target = runOptions.target ?? (isMobile() ? "mobile" : "browser");
const value = adapter(name, target);
if (!value || !(value.supported?.() ?? true)) {
throw new NativeUnavailableError(`Native capability "${name}" is unavailable on ${target}`);
}
return (await value.run(options)) as TResult;
}
export function clearRegistry(): void {
capabilities.clear();
}