UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

513 lines (512 loc) 17.7 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const index = require("./collection/index.cjs"); const collectionOptions$1 = require("./collection-options.cjs"); const transactions = require("./transactions.cjs"); const collectionRegistry = require("./query/live/collection-registry.cjs"); const liveQueryCollection = require("./query/live-query-collection.cjs"); const liveQueryObserver = require("./live-query-observer.cjs"); const liveQueryOptions = require("./live-query-options.cjs"); const collectionConfigFactory = /* @__PURE__ */ Symbol.for( `@tanstack/db.collectionConfig.factory` ); function withCollectionConfigFactory(config, factory) { Object.defineProperty(config, collectionConfigFactory, { value: factory, enumerable: false }); return config; } function collectionOptions(optionsOrId, explicitFactory) { const config = typeof optionsOrId === `string` ? void 0 : optionsOrId; const id = typeof optionsOrId === `string` ? optionsOrId : optionsOrId.id; if (!id) { throw new Error( `collectionOptions requires a non-empty explicit id so the descriptor is stable across DbClient instances and SSR boundaries.` ); } if (typeof optionsOrId === `string` && !explicitFactory) { throw new Error( `collectionOptions("${id}") requires a factory as its second argument.` ); } const reusableFactory = config ? config[collectionConfigFactory] : explicitFactory; let owner; const materialize = (client) => { let materialized; if (reusableFactory) { materialized = reusableFactory(client); } else { if (owner && owner !== client) { throw new Error( `Collection descriptor "${id}" was created from a concrete config that cannot be safely reused across DbClient instances. Use collectionOptions("${id}", (client) => adapterCollectionOptions(...)) or an adapter options creator that supports DbClient materialization.` ); } owner = client; materialized = config; } if (materialized.id !== void 0 && materialized.id !== id) { throw new Error( `Collection descriptor "${id}" materialized a config with id "${materialized.id}". Descriptor and collection ids must match.` ); } return materialized.id === id ? materialized : { ...materialized, id }; }; const descriptor = { id, ...config?.singleResult === true ? { singleResult: true } : {} }; Object.defineProperties(descriptor, { [collectionOptions$1.collectionOptionsBrand]: { value: true, enumerable: false }, [collectionOptions$1.collectionOptionsFactory]: { value: materialize, enumerable: false } }); return Object.freeze(descriptor); } function isCollectionOptions(value) { return collectionOptions$1.hasCollectionOptionsBrand(value); } class DbClient { constructor(options = {}) { this.options = options; this.collectionsByOptions = /* @__PURE__ */ new WeakMap(); this.collectionsById = /* @__PURE__ */ new Map(); this.pendingHydration = /* @__PURE__ */ new Map(); this.liveQueries = /* @__PURE__ */ new Map(); this.preloadedLiveQueries = /* @__PURE__ */ new Map(); this.liveQueryResources = /* @__PURE__ */ new Map(); this.listeners = /* @__PURE__ */ new Set(); this.ssrStreamingEnabled = false; this.ssrServerCleanupEnabled = false; this.lastLiveQueryTimestamp = 0; this.transactionScope = new transactions.TransactionScope(); } getDependency(key) { return this.options[key]; } requireDependency(key) { const dependency = this.getDependency(key); if (dependency === void 0) { throw new Error( `DbClient is missing the required "${key}" dependency. Pass it explicitly when constructing the client: new DbClient({ ${key} }).` ); } return dependency; } get activeTransaction() { return this.transactionScope.getActiveTransaction(); } createTransaction(config) { return this.transactionScope.createTransaction(config); } preloadLiveQuery(options) { const deferredCollections = /* @__PURE__ */ new Set(); try { const prepared = liveQueryOptions.prepareLiveQueryValue(options, this, deferredCollections); const queryHash = liveQueryOptions.getLiveQueryHash(prepared, options.queryKey); const existing = this.liveQueries.get(queryHash); if (existing && existing.status !== `error`) return existing.promise; const failedPreload = this.preloadedLiveQueries.get(queryHash); if (failedPreload) { failedPreload.observer.dispose(); void failedPreload.collection.cleanup().catch(() => { }); this.preloadedLiveQueries.delete(queryHash); } const collection = liveQueryCollection.createLiveQueryCollection({ ...prepared, startSync: true }); const observer = liveQueryObserver.createLiveQueryObserver(collection, { client: this, queryHash, mode: `wholesale` }); this.preloadedLiveQueries.set(queryHash, { collection, observer }); return this._registerLiveQuery( queryHash, collection.preload().then(() => observer.dehydrate()) ); } finally { for (const source of deferredCollections) source._resumeSyncStart(); deferredCollections.clear(); } } collection(options, materializeOptions) { return this.materializeCollection(options, materializeOptions, false); } /** @internal */ _materializeCollectionForRender(options) { return this.materializeCollection(options, void 0, true); } materializeCollection(options, materializeOptions, deferSyncStart) { const existing = this.collectionsByOptions.get(options); if (existing) { if (!deferSyncStart) { this.collectionsById.get(existing.id).shouldDehydrate = true; } if (deferSyncStart) { existing._deferSyncStart(); } return existing; } if (this.collectionsById.has(options.id)) { throw new Error( `Cannot materialize collection "${options.id}" because this DbClient already has a different collection with that id. SSR hydration requires collection ids to be unique per DbClient.` ); } const config = options[collectionOptions$1.collectionOptionsFactory](this); const shouldStartSync = config.startSync === true; const collection = index.createCollection({ ...config, startSync: false }); collection._setTransactionScope(this.transactionScope); if (deferSyncStart) { collection._deferSyncStart(); } this.collectionsByOptions.set(options, collection); this.collectionsById.set(collection.id, { collection, shouldDehydrate: !deferSyncStart }); if (materializeOptions?.initialData?.length) { this.applyRows( collection, { collectionId: collection.id, rows: materializeOptions.initialData.map((value) => ({ value })) }, `initialData` ); } const pendingChunks = this.pendingHydration.get(collection.id); if (pendingChunks) { for (const chunk of pendingChunks) { this.applyRows(collection, chunk, `hydration`); } this.pendingHydration.delete(collection.id); } if (shouldStartSync) { collection.startSyncImmediate(); } return collection; } dehydrate(options = {}) { const collections = []; for (const { collection, shouldDehydrate } of this.collectionsById.values()) { const collectionDecision = options.shouldDehydrateCollection?.(collection); if (collectionRegistry.getBuilderFromConfig(collection.config) || collectionDecision === false || !shouldDehydrate && collectionDecision !== true) { continue; } const rows = Array.from(collection._state.syncedData.entries()).map( ([key, value]) => { const metadata = collection._state.syncedMetadata.get(key); return { key, value, ...metadata === void 0 ? {} : { metadata } }; } ); collections.push({ collectionId: collection.id, rows, syncMeta: collection.config.sync.exportSyncMeta?.() }); } const liveQueries = Array.from(this.liveQueries.values()).flatMap( (query) => { const shouldDehydrate = options.shouldDehydrateLiveQuery?.(query) ?? query.status === `success`; if (!shouldDehydrate || query.status === `error`) { return []; } return [ { queryHash: query.queryHash, dehydratedAt: query.dehydratedAt, ...query.snapshot ? { snapshot: query.snapshot } : { promise: query.resultPromise } } ]; } ); return { collections, ...liveQueries.length > 0 ? { liveQueries } : {} }; } hydrate(state) { for (const chunk of state.collections) { const record = this.collectionsById.get(chunk.collectionId); if (record) { this.applyRows(record.collection, chunk, `hydration`); continue; } const pendingChunks = this.pendingHydration.get(chunk.collectionId) ?? []; pendingChunks.push(chunk); this.pendingHydration.set(chunk.collectionId, pendingChunks); } for (const dehydratedQuery of state.liveQueries ?? []) { this.hydrateLiveQuery(dehydratedQuery); } } applyCollectionChunk(chunk) { this.hydrate({ collections: [chunk] }); } subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); } /** @internal */ _setSsrStreamingEnabled(enabled) { this.ssrStreamingEnabled = enabled; } /** @internal */ _isSsrStreamingEnabled() { return this.ssrStreamingEnabled; } /** @internal */ _setSsrServerCleanupEnabled(enabled) { this.ssrServerCleanupEnabled = enabled; } /** @internal */ _isSsrServerCleanupEnabled() { return this.ssrServerCleanupEnabled; } /** @internal */ _getLiveQuery(queryHash) { return this.liveQueries.get(queryHash); } /** @internal */ _consumeLiveQueryResult(queryHash, dehydratedAt) { const record = this.liveQueries.get(queryHash); if (record?.dehydratedAt === dehydratedAt) { this.liveQueries.delete(queryHash); } } /** @internal */ _registerLiveQuery(queryHash, promise) { const existing = this.liveQueries.get(queryHash); if (existing && existing.status !== `error`) { void Promise.resolve(promise).catch(() => { }); return existing.promise; } const record = this.createLiveQueryRecord( queryHash, this.nextLiveQueryTimestamp() ); this.liveQueries.set(queryHash, record); this.emit({ type: `liveQueryAdded`, query: record }); Promise.resolve(promise).then(record.succeed, record.fail); return record.promise; } /** @internal */ _registerLiveQueryResource(owner, cleanup) { this.liveQueryResources.set(owner, cleanup); return () => { if (this.liveQueryResources.get(owner) === cleanup) { this.liveQueryResources.delete(owner); } }; } /** @internal */ _failPendingLiveQueries(error) { for (const record of this.liveQueries.values()) { if (record.status === `pending`) record.fail(error); } this.emit({ type: `liveQueryStreamError`, error }); } async cleanup() { try { const materializedCollections = Array.from( this.collectionsById.values(), ({ collection }) => collection ); const preloadedQueries = Array.from(this.preloadedLiveQueries.values()); const liveQueryCollections = /* @__PURE__ */ new Set([ ...preloadedQueries.map(({ collection }) => collection), ...materializedCollections.filter( (collection) => collectionRegistry.getBuilderFromConfig(collection.config) ) ]); for (const { observer } of preloadedQueries) observer.dispose(); const cleanupResults = [ ...await Promise.allSettled( Array.from(this.liveQueryResources.values(), (cleanup) => cleanup()) ), ...await Promise.allSettled( Array.from( liveQueryCollections, (collection) => collection.cleanup() ) ), ...await Promise.allSettled( materializedCollections.filter((collection) => !liveQueryCollections.has(collection)).map((collection) => collection.cleanup()) ) ]; const failure = cleanupResults.find( (result) => result.status === `rejected` ); if (failure) throw failure.reason; } finally { this.transactionScope.clear(); this.collectionsByOptions = /* @__PURE__ */ new WeakMap(); this.collectionsById.clear(); this.pendingHydration.clear(); this.liveQueries.clear(); this.preloadedLiveQueries.clear(); this.liveQueryResources.clear(); this.listeners.clear(); this.ssrStreamingEnabled = false; this.ssrServerCleanupEnabled = false; this.lastLiveQueryTimestamp = 0; } } hydrateLiveQuery(dehydratedQuery) { const existing = this.liveQueries.get(dehydratedQuery.queryHash); if (existing && existing.dehydratedAt >= dehydratedQuery.dehydratedAt) { return; } const record = this.createLiveQueryRecord( dehydratedQuery.queryHash, dehydratedQuery.dehydratedAt ); this.liveQueries.set(record.queryHash, record); if (existing?.status === `pending`) { void record.resultPromise.then(existing.succeed, existing.fail); } this.emit({ type: `liveQueryAdded`, query: record }); if (dehydratedQuery.snapshot) { record.succeed(dehydratedQuery.snapshot); } else if (dehydratedQuery.promise) { Promise.resolve(dehydratedQuery.promise).then(record.succeed, record.fail); } else { record.fail( new Error( `Dehydrated live query "${dehydratedQuery.queryHash}" has neither a snapshot nor a promise.` ) ); } } nextLiveQueryTimestamp() { this.lastLiveQueryTimestamp = Math.max( Date.now(), this.lastLiveQueryTimestamp + 1 ); return this.lastLiveQueryTimestamp; } createLiveQueryRecord(queryHash, dehydratedAt) { this.lastLiveQueryTimestamp = Math.max( this.lastLiveQueryTimestamp, dehydratedAt ); let resolveResult; let rejectResult; let settled = false; const resultPromise = new Promise( (resolve, reject) => { resolveResult = resolve; rejectResult = reject; } ); const promise = resultPromise.then(() => void 0); resultPromise.catch(() => { }); promise.catch(() => { }); const record = { queryHash, dehydratedAt, status: `pending`, promise, resultPromise, succeed: (snapshot) => { if (settled) return; settled = true; record.status = `success`; record.snapshot = snapshot; resolveResult(snapshot); if (this.liveQueries.get(queryHash) === record) { this.emit({ type: `liveQueryUpdated`, query: record }); } }, fail: (error) => { if (settled) return; settled = true; record.status = `error`; record.error = error; rejectResult(error); if (this.liveQueries.get(queryHash) === record) { this.emit({ type: `liveQueryUpdated`, query: record }); } } }; return record; } emit(event) { for (const listener of this.listeners) { listener(event); } } applyRows(collection, chunk, seedKind) { const rows = chunk.rows.flatMap((row) => { const value = collection.validateData(row.value, `insert`); const key = collection.config.getKey(value); const isAdapterAuthoritative = seedKind === `hydration` && collection._state.syncedData.has(key) && !collection._state.hydrationSeedKeys.has(key); return isAdapterAuthoritative ? [] : [{ ...row, key, value }]; }); const rowMetadataWrites = /* @__PURE__ */ new Map(); for (const row of rows) { if (row.metadata !== void 0) { rowMetadataWrites.set(row.key, { type: `set`, value: row.metadata }); } } if (seedKind) { for (const row of rows) { collection._state.hydrationSeedKeys.add(row.key); if (seedKind === `hydration`) { collection._state.hydratedKeys.add(row.key); } } } if (rows.length > 0) { collection._state.pendingSyncedTransactions.push({ committed: true, layoutChanged: false, operations: rows.map((row) => ({ type: collection._state.syncedData.has(row.key) ? `update` : `insert`, key: row.key, value: row.value })), deletedKeys: /* @__PURE__ */ new Set(), rowMetadataWrites, collectionMetadataWrites: /* @__PURE__ */ new Map(), immediate: true, preserveHydrationSeedKeys: seedKind !== void 0 }); collection._state.commitPendingTransactions(); } if (chunk.syncMeta !== void 0) { const currentMeta = collection.config.sync.exportSyncMeta?.(); const mergedMeta = currentMeta === void 0 ? chunk.syncMeta : collection.config.sync.mergeSyncMeta?.( currentMeta, chunk.syncMeta ) ?? chunk.syncMeta; collection.config.sync.importSyncMeta?.(mergedMeta); } } } exports.DbClient = DbClient; exports.collectionOptions = collectionOptions; exports.isCollectionOptions = isCollectionOptions; exports.withCollectionConfigFactory = withCollectionConfigFactory; //# sourceMappingURL=client.cjs.map