UNPKG

convex

Version:

Client for the Convex Cloud

182 lines (160 loc) 5.48 kB
import { Value } from "../../values/index.js"; import { createError } from "../logging.js"; import { OptimisticLocalStore } from "./optimistic_updates.js"; import { MutationId } from "./protocol.js"; import { QueryResult } from "./remote_query_set.js"; import { canonicalizeUdfPath, QueryToken, serializePathAndArgs, } from "./udf_path_utils.js"; /** * An optimistic update function that has been curried over its arguments. */ type WrappedOptimisticUpdate = (locaQueryStore: OptimisticLocalStore) => void; /** * The implementation of `OptimisticLocalStore`. * * This class provides the interface for optimistic updates to modify query results. */ class OptimisticLocalStoreImpl implements OptimisticLocalStore { // A references of the query results in OptimisticQueryResults private readonly queryResults: QueryResultsMap; // All of the queries modified by this class readonly modifiedQueries: QueryToken[]; constructor(queryResults: QueryResultsMap) { this.queryResults = queryResults; this.modifiedQueries = []; } getQuery(name: string, args: Value[]): Value | undefined { const query = this.queryResults.get(serializePathAndArgs(name, args)); if (query === undefined) { return undefined; } return OptimisticLocalStoreImpl.queryValue(query.result); } getAllQueries(name: string): { args: Value[]; value: Value | undefined }[] { const queriesWithName = []; for (const query of this.queryResults.values()) { if (query.udfPath === canonicalizeUdfPath(name)) { queriesWithName.push({ args: query.args, value: OptimisticLocalStoreImpl.queryValue(query.result), }); } } return queriesWithName; } setQuery(name: string, args: Value[], value: Value | undefined): void { const queryToken = serializePathAndArgs(name, args); let result: QueryResult | undefined; if (value === undefined) { result = undefined; } else { result = { success: true, value, }; } const query: Query = { udfPath: name, args, result, }; this.queryResults.set(queryToken, query); this.modifiedQueries.push(queryToken); } private static queryValue( result: QueryResult | undefined ): Value | undefined { if (result === undefined) { return undefined; } else if (result.success) { return result.value; } else { // If the query is an error state, just return `undefined` as though // it's loading. Optimistic updates should already handle `undefined` well // and there isn't a need to break the whole update because it tried // to load a single query that errored. return undefined; } } } type OptimisticUpdateAndId = { update: WrappedOptimisticUpdate; mutationId: MutationId; }; type Query = { // undefined means the query was set to be loading (undefined) in an optimistic update. // Note that we can also have queries not present in the QueryResultMap // at all because they are still loading from the server. result: QueryResult | undefined; udfPath: string; args: Value[]; }; export type QueryResultsMap = Map<QueryToken, Query>; type ChangedQueries = QueryToken[]; /** * A view of all of our query results with optimistic updates applied on top. */ export class OptimisticQueryResults { private queryResults: QueryResultsMap; private optimisticUpdates: OptimisticUpdateAndId[]; constructor() { this.queryResults = new Map(); this.optimisticUpdates = []; } ingestQueryResultsFromServer( serverQueryResults: QueryResultsMap, optimisticUpdatesToDrop: Set<MutationId> ): ChangedQueries { this.optimisticUpdates = this.optimisticUpdates.filter(updateAndId => { return !optimisticUpdatesToDrop.has(updateAndId.mutationId); }); const oldQueryResults = this.queryResults; this.queryResults = new Map(serverQueryResults); const localStore = new OptimisticLocalStoreImpl(this.queryResults); for (const updateAndId of this.optimisticUpdates) { updateAndId.update(localStore); } // To find the changed queries, just do a shallow comparison // TODO(CX-733): Change this so we avoid unnecessary rerenders const changedQueries: ChangedQueries = []; for (const [queryToken, query] of this.queryResults) { const oldQuery = oldQueryResults.get(queryToken); if (oldQuery === undefined || oldQuery.result !== query.result) { changedQueries.push(queryToken); } } return changedQueries; } applyOptimisticUpdate( update: WrappedOptimisticUpdate, mutationId: MutationId ): ChangedQueries { // Apply the update to our store this.optimisticUpdates.push({ update, mutationId, }); const localStore = new OptimisticLocalStoreImpl(this.queryResults); update(localStore); // Notify about any query results that changed // TODO(CX-733): Change this so we avoid unnecessary rerenders return localStore.modifiedQueries; } queryResult(queryToken: QueryToken): Value | undefined { const query = this.queryResults.get(queryToken); if (query === undefined) { return undefined; } const result = query.result; if (result === undefined) { return undefined; } else if (result.success) { return result.value; } else { throw createError("query", query.udfPath, result.errorMessage); } } }