Skip to content

src/plain.ts

63 lines · 1.8 KB · TypeScript
Raw
/**
 * Turn Workers RPC Proxies / stubs into plain JSON data.
 * Subscription and execute results cannot carry Proxies across the isolate
 * boundary ("Proxy could not be serialized...").
 *
 * packageStorage object values hit the same wall on get/list — store JSON
 * strings instead, and always plainClone on read.
 */
export function plainClone<T>(value: T): T {
	if (value === null || value === undefined) return value
	const t = typeof value
	if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint') {
		return value
	}
	try {
		return JSON.parse(JSON.stringify(value)) as T
	} catch {
		if (Array.isArray(value)) return [] as unknown as T
		if (value && t === 'object') return {} as T
		return value
	}
}

function isProxySerializeError(error: unknown): boolean {
	const message = error instanceof Error ? error.message : String(error)
	return /Proxy could not be serialized/i.test(message)
}

/** Read packageStorage.get and always return structured-cloneable data. */
export async function storageGet(
	get: (key: string) => Promise<unknown>,
	key: string,
): Promise<unknown> {
	try {
		const raw = await get(key)
		if (raw === null || raw === undefined) return null
		return plainClone(raw)
	} catch (error) {
		if (isProxySerializeError(error)) return null
		throw error
	}
}

/**
 * Write packageStorage values as JSON strings when they are objects/arrays.
 * String/number/boolean stay as-is. Avoids unreadable Proxy object values.
 */
export async function storageSet(
	set: (key: string, value: unknown) => Promise<unknown>,
	key: string,
	value: unknown,
): Promise<void> {
	if (value === null || value === undefined) {
		await set(key, value as never)
		return
	}
	const t = typeof value
	if (t === 'string' || t === 'number' || t === 'boolean') {
		await set(key, value)
		return
	}
	await set(key, JSON.stringify(value))
}