@tanstack/db
Version:
A reactive client store for building super fast apps on sync
513 lines (512 loc) • 17.7 kB
JavaScript
"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(
`/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