Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions packages/db/src/collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -185,18 +186,37 @@ export function createCollection(
utils?: UtilsRecord
}
): Collection<any, string | number, UtilsRecord, any, any> {
const collection = new CollectionImpl<any, string | number, any, any, any>(
options
)
function _createCollection() {
const collection = new CollectionImpl<any, string | number, any, any, any>(
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<
Expand Down
87 changes: 87 additions & 0 deletions packages/db/src/utils/lazy-init-for-cf-workers.ts
Original file line number Diff line number Diff line change
@@ -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<T extends object>(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)
},
})
}