UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

274 lines (273 loc) 8.39 kB
import { createSingleRowRefProxy, toExpression } from "../query/builder/ref-proxy.js"; import { CollectionConfigurationError } from "../errors.js"; const INDEX_SIGNATURE_VERSION = 1; function compareStringsCodePoint(left, right) { if (left === right) { return 0; } return left < right ? -1 : 1; } function resolveResolverMetadata(resolver) { return { kind: `constructor`, ...resolver.name ? { name: resolver.name } : {} }; } function toSerializableIndexValue(value) { if (value == null) { return value; } switch (typeof value) { case `string`: case `boolean`: return value; case `number`: return Number.isFinite(value) ? value : null; case `bigint`: return { __type: `bigint`, value: value.toString() }; case `function`: case `symbol`: return void 0; case `undefined`: return void 0; } if (Array.isArray(value)) { return value.map((entry) => toSerializableIndexValue(entry) ?? null); } if (value instanceof Date) { return { __type: `date`, value: value.toISOString() }; } if (value instanceof Set) { const serializedValues = Array.from(value).map((entry) => toSerializableIndexValue(entry) ?? null).sort( (a, b) => compareStringsCodePoint( stableStringifyCollectionIndexValue(a), stableStringifyCollectionIndexValue(b) ) ); return { __type: `set`, values: serializedValues }; } if (value instanceof Map) { const serializedEntries = Array.from(value.entries()).map(([mapKey, mapValue]) => ({ key: toSerializableIndexValue(mapKey) ?? null, value: toSerializableIndexValue(mapValue) ?? null })).sort( (a, b) => compareStringsCodePoint( stableStringifyCollectionIndexValue(a.key), stableStringifyCollectionIndexValue(b.key) ) ); return { __type: `map`, entries: serializedEntries }; } if (value instanceof RegExp) { return { __type: `regexp`, value: value.toString() }; } const serializedObject = {}; const entries = Object.entries(value).sort( ([leftKey], [rightKey]) => compareStringsCodePoint(leftKey, rightKey) ); for (const [key, entryValue] of entries) { const serializedEntry = toSerializableIndexValue(entryValue); if (serializedEntry !== void 0) { serializedObject[key] = serializedEntry; } } return serializedObject; } function stableStringifyCollectionIndexValue(value) { if (value === null) { return `null`; } if (Array.isArray(value)) { return `[${value.map(stableStringifyCollectionIndexValue).join(`,`)}]`; } if (typeof value !== `object`) { return JSON.stringify(value); } const sortedKeys = Object.keys(value).sort( (left, right) => compareStringsCodePoint(left, right) ); const serializedEntries = sortedKeys.map( (key) => `${JSON.stringify(key)}:${stableStringifyCollectionIndexValue(value[key])}` ); return `{${serializedEntries.join(`,`)}}`; } function createCollectionIndexMetadata(indexId, expression, name, resolver, options) { const resolverMetadata = resolveResolverMetadata(resolver); const serializedExpression = toSerializableIndexValue(expression) ?? null; const serializedOptions = toSerializableIndexValue(options); const signatureInput = toSerializableIndexValue({ signatureVersion: INDEX_SIGNATURE_VERSION, expression: serializedExpression, options: serializedOptions ?? null }); const normalizedSignatureInput = signatureInput ?? null; const signature = stableStringifyCollectionIndexValue( normalizedSignatureInput ); return { signatureVersion: INDEX_SIGNATURE_VERSION, signature, indexId, name, expression, resolver: resolverMetadata, ...serializedOptions === void 0 ? {} : { options: serializedOptions } }; } function cloneSerializableIndexValue(value) { if (value === null || typeof value !== `object`) { return value; } if (Array.isArray(value)) { return value.map((entry) => cloneSerializableIndexValue(entry)); } const cloned = {}; for (const [key, entryValue] of Object.entries(value)) { cloned[key] = cloneSerializableIndexValue(entryValue); } return cloned; } function cloneExpression(expression) { return JSON.parse(JSON.stringify(expression)); } class CollectionIndexesManager { constructor() { this.indexes = /* @__PURE__ */ new Map(); this.indexMetadata = /* @__PURE__ */ new Map(); this.indexCounter = 0; } setDeps(deps) { this.state = deps.state; this.lifecycle = deps.lifecycle; this.defaultIndexType = deps.defaultIndexType; this.events = deps.events; } /** * Creates an index on a collection for faster queries. * * @example * ```ts * // With explicit index type (recommended for tree-shaking) * import { BasicIndex } from '@tanstack/db' * collection.createIndex((row) => row.userId, { indexType: BasicIndex }) * * // With collection's default index type * collection.createIndex((row) => row.userId) * ``` */ createIndex(indexCallback, config = {}) { this.lifecycle.validateCollectionUsable(`createIndex`); const indexId = ++this.indexCounter; const singleRowRefProxy = createSingleRowRefProxy(); const indexExpression = indexCallback(singleRowRefProxy); const expression = toExpression(indexExpression); const IndexType = config.indexType ?? this.defaultIndexType; if (!IndexType) { throw new CollectionConfigurationError( `No index type specified and no defaultIndexType set on collection. Either pass indexType in config, or set defaultIndexType on the collection: import { BasicIndex } from '@tanstack/db' createCollection({ defaultIndexType: BasicIndex, ... })` ); } const index = new IndexType( indexId, expression, config.name, config.options ); index.build(this.state.entries()); this.indexes.set(indexId, index); const metadata = createCollectionIndexMetadata( indexId, expression, config.name, IndexType, config.options ); this.indexMetadata.set(indexId, metadata); this.events.emitIndexAdded(metadata); return index; } /** * Removes an index from this collection. * Returns true when an index existed and was removed, false otherwise. */ removeIndex(indexOrId) { this.lifecycle.validateCollectionUsable(`removeIndex`); const indexId = typeof indexOrId === `number` ? indexOrId : indexOrId.id; const index = this.indexes.get(indexId); if (!index) { return false; } if (typeof indexOrId !== `number` && index !== indexOrId) { return false; } this.indexes.delete(indexId); const metadata = this.indexMetadata.get(indexId); this.indexMetadata.delete(indexId); if (metadata) { this.events.emitIndexRemoved(metadata); } return true; } /** * Returns a sorted snapshot of index metadata. * This allows persisted wrappers to bootstrap from indexes that were created * before they attached lifecycle listeners. */ getIndexMetadataSnapshot() { return Array.from(this.indexMetadata.values()).sort((left, right) => left.indexId - right.indexId).map((metadata) => ({ ...metadata, expression: cloneExpression(metadata.expression), resolver: { ...metadata.resolver }, ...metadata.options === void 0 ? {} : { options: cloneSerializableIndexValue(metadata.options) } })); } /** * Updates all indexes when the collection changes */ updateIndexes(changes) { for (const index of this.indexes.values()) { for (const change of changes) { switch (change.type) { case `insert`: index.add(change.key, change.value); break; case `update`: if (change.previousValue) { index.update(change.key, change.previousValue, change.value); } else { index.add(change.key, change.value); } break; case `delete`: index.remove(change.key, change.value); break; } } } } /** * Clean up indexes */ cleanup() { this.indexes.clear(); this.indexMetadata.clear(); } } export { CollectionIndexesManager }; //# sourceMappingURL=indexes.js.map