diff --git a/packages/db/src/collection/index.ts b/packages/db/src/collection/index.ts index 5a4ab3c07..335d72473 100644 --- a/packages/db/src/collection/index.ts +++ b/packages/db/src/collection/index.ts @@ -2,6 +2,7 @@ import { CollectionRequiresConfigError, CollectionRequiresSyncConfigError, } from "../errors" +import { lazyInitForCFWorkers } from "../utils/lazy-init-for-cf-workers" import { currentStateAsChanges } from "./change-events" import { CollectionStateManager } from "./state" @@ -185,18 +186,37 @@ export function createCollection( utils?: UtilsRecord } ): Collection { - const collection = new CollectionImpl( - options - ) + function _createCollection() { + const collection = new CollectionImpl( + options + ) + + // Copy utils to both top level and .utils namespace + if (options.utils) { + collection.utils = { ...options.utils } + } else { + collection.utils = {} + } + + return collection + } + + // Check if we're running in Cloudflare Workers runtime + // https://developers.cloudflare.com/workers/runtime-apis/web-standards/#navigatoruseragent + const isCloudflareWorkers = + typeof navigator !== `undefined` && + navigator.userAgent === `Cloudflare-Workers` - // Copy utils to both top level and .utils namespace - if (options.utils) { - collection.utils = { ...options.utils } - } else { - collection.utils = {} + // Workers runtime limitation + // Without this, initializing a collection causes this error: + // Disallowed operation called within global scope. + if (isCloudflareWorkers) { + return lazyInitForCFWorkers(() => { + return _createCollection() + }) } - return collection + return _createCollection() } export class CollectionImpl< diff --git a/packages/db/src/utils/lazy-init-for-cf-workers.ts b/packages/db/src/utils/lazy-init-for-cf-workers.ts new file mode 100644 index 000000000..fe8336c3d --- /dev/null +++ b/packages/db/src/utils/lazy-init-for-cf-workers.ts @@ -0,0 +1,87 @@ +/** + * Wraps a factory function in a Proxy to defer initialization until first access. + * This prevents async operations (Like creating Tanstack DB Collections) from running in Cloudflare Workers' global scope. + * + * @param factory - A function that creates and returns the resource. + * Must be a callback to defer execution; passing the value directly + * would evaluate it at module load time, triggering the Cloudflare error. + * @returns A Proxy that lazily initializes the resource on first property access + * + * @example + * ```ts + * export const myCollection = lazyInitForCFWorkers(() => + * createCollection(queryCollectionOptions({ + * queryKey: ["myData"], + * queryFn: async () => fetchData(), + * // ... other options + * })) + * ); + * ``` + */ +export function lazyInitForCFWorkers(factory: () => T): T { + // Closure: This variable is captured by getInstance() and the Proxy traps below. + // It remains in memory as long as the returned Proxy is referenced, enabling singleton behavior. + let instance: T | null = null + + function getInstance() { + if (!instance) { + instance = factory() + } + return instance + } + + return new Proxy({} as T, { + get(_target, prop, receiver) { + const inst = getInstance() + return Reflect.get(inst, prop, receiver) + }, + set(_target, prop, value, receiver) { + const inst = getInstance() + return Reflect.set(inst, prop, value, receiver) + }, + deleteProperty(_target, prop) { + const inst = getInstance() + return Reflect.deleteProperty(inst, prop) + }, + has(_target, prop) { + const inst = getInstance() + return Reflect.has(inst, prop) + }, + ownKeys(_target) { + const inst = getInstance() + return Reflect.ownKeys(inst) + }, + getOwnPropertyDescriptor(_target, prop) { + const inst = getInstance() + return Reflect.getOwnPropertyDescriptor(inst, prop) + }, + defineProperty(_target, prop, descriptor) { + const inst = getInstance() + return Reflect.defineProperty(inst, prop, descriptor) + }, + getPrototypeOf(_target) { + const inst = getInstance() + return Reflect.getPrototypeOf(inst) + }, + setPrototypeOf(_target, proto) { + const inst = getInstance() + return Reflect.setPrototypeOf(inst, proto) + }, + isExtensible(_target) { + const inst = getInstance() + return Reflect.isExtensible(inst) + }, + preventExtensions(_target) { + const inst = getInstance() + return Reflect.preventExtensions(inst) + }, + apply(_target, thisArg, args) { + const inst = getInstance() + return Reflect.apply(inst as any, thisArg, args) + }, + construct(_target, args, newTarget) { + const inst = getInstance() + return Reflect.construct(inst as any, args, newTarget) + }, + }) +}