UNPKG

convex

Version:

Client for the Convex Cloud

274 lines (273 loc) 9.52 kB
"use strict"; import { v4 as uuidv4 } from "uuid"; import { version } from "../../index.js"; import { convexToJson } from "../../values/index.js"; import { logFatalError } from "../logging.js"; import { ActionManager } from "./action_manager.js"; import { LocalSyncState } from "./local_state.js"; import { RequestManager } from "./request_manager.js"; import { OptimisticQueryResults } from "./optimistic_updates_impl.js"; import { RemoteQuerySet } from "./remote_query_set.js"; import { serializePathAndArgs } from "./udf_path_utils.js"; import { WebSocketManager } from "./web_socket_manager.js"; const DEFAULT_OPTIONS = { unsavedChangesWarning: true }; export class InternalConvexClient { constructor(address, onTransition, options) { this.fetchToken = null; if (typeof address === "object") { throw new Error( "Passing a ClientConfig object is no longer supported. Pass the URL of the Convex deployment as a string directly." ); } options = { ...DEFAULT_OPTIONS, ...options }; let webSocketConstructor = options.webSocketConstructor; if (!webSocketConstructor && typeof WebSocket === "undefined") { throw new Error( "No WebSocket global variable defined! To use Convex in an environment without WebSocket try the HTTP client: https://docs.convex.dev/api/classes/browser.ConvexHttpClient" ); } webSocketConstructor = webSocketConstructor || WebSocket; const i = address.search("://"); if (i === -1) { throw new Error("Provided address was not an absolute URL."); } const origin = address.substring(i + 3); const protocol = address.substring(0, i); let wsProtocol; if (protocol === "http") { wsProtocol = "ws"; } else if (protocol === "https") { wsProtocol = "wss"; } else { throw new Error(`Unknown parent protocol ${protocol}`); } const wsUri = `${wsProtocol}://${origin}/api/${version}/sync`; this.state = new LocalSyncState(); this.remoteQuerySet = new RemoteQuerySet( (queryId) => this.state.queryPath(queryId) ); this.requestManager = new RequestManager(); this.actionManager = new ActionManager(); this.optimisticQueryResults = new OptimisticQueryResults(); this.onTransition = onTransition; this.nextRequestId = 0; this.sessionId = uuidv4(); const { unsavedChangesWarning } = options; if (typeof window === "undefined" && unsavedChangesWarning) { throw new Error( "unsavedChangesWarning enabled, but no window object found! Navigating away from the page could cause in-flight mutations to be dropped. Pass {unsavedChangesWarning: false} in Convex client options to disable this feature." ); } unsavedChangesWarning && window.addEventListener("beforeunload", (e) => { if (this.requestManager.hasIncompleteRequests() || this.actionManager.hasInflightActions()) { e.preventDefault(); const confirmationMessage = "Are you sure you want to leave? Your changes may not be saved."; (e || window.event).returnValue = confirmationMessage; return confirmationMessage; } }); this.webSocketManager = new WebSocketManager( wsUri, (reconnectMetadata) => { this.webSocketManager.sendMessage({ ...reconnectMetadata, type: "Connect", sessionId: this.sessionId }); this.remoteQuerySet = new RemoteQuerySet( (queryId) => this.state.queryPath(queryId) ); const [querySetModification, authModification] = this.state.restart(); if (authModification) { this.webSocketManager.sendMessage(authModification); } this.webSocketManager.sendMessage(querySetModification); this.actionManager.restart(); for (const message of this.requestManager.restart()) { this.webSocketManager.sendMessage(message); } }, (serverMessage) => { switch (serverMessage.type) { case "Transition": { this.remoteQuerySet.transition(serverMessage); this.state.saveQueryJournals(serverMessage); const completedMutations = this.requestManager.removeCompleted( this.remoteQuerySet.timestamp() ); this.notifyOnQueryResultChanges(completedMutations); break; } case "MutationResponse": { const completedMutationId = this.requestManager.onResponse(serverMessage); if (completedMutationId) { this.notifyOnQueryResultChanges(/* @__PURE__ */ new Set([completedMutationId])); } break; } case "ActionResponse": { this.actionManager.onResponse(serverMessage); break; } case "AuthError": { this.tryToReauthenticate(serverMessage).then().catch((error) => { logFatalError(error); void this.webSocketManager.stop(); }); break; } case "FatalError": { const error = logFatalError(serverMessage.error); void this.webSocketManager.stop(); throw error; } case "Ping": break; default: { const _typeCheck = serverMessage; } } }, webSocketConstructor ); } notifyOnQueryResultChanges(completedMutations) { const remoteQueryResults = this.remoteQuerySet.remoteQueryResults(); const queryTokenToValue = /* @__PURE__ */ new Map(); for (const [queryId, result] of remoteQueryResults) { const queryToken = this.state.queryToken(queryId); if (queryToken !== null) { const query = { result, udfPath: this.state.queryPath(queryId), args: this.state.queryArgs(queryId) }; queryTokenToValue.set(queryToken, query); } } this.onTransition( this.optimisticQueryResults.ingestQueryResultsFromServer( queryTokenToValue, completedMutations ) ); } async setAuth(fetchToken) { this.fetchToken = fetchToken; const token = await fetchToken(); if (token) { this.authenticate(token); } else if (this.state.hasAuth()) { this.clearAuth(); } } async tryToReauthenticate(serverMessage) { if (!this.fetchToken) { throw new Error(serverMessage.error); } console.log("Attempting to reauthenticate"); await this.webSocketManager.pause(); const token = await this.fetchToken(); if (token && this.state.isNewAuth(token)) { this.state.setAuth(token); } else if (this.state.hasAuth()) { console.log("Reauthentication failed, clearing auth"); this.state.clearAuth(); } await this.webSocketManager.resume(); } authenticate(token) { const message = this.state.setAuth(token); this.webSocketManager.sendMessage(message); } setAdminAuth(value) { const message = this.state.setAdminAuth(value); this.webSocketManager.sendMessage(message); } clearAuth() { const message = this.state.clearAuth(); this.webSocketManager.sendMessage(message); } subscribe(name, args, journal) { if (!Array.isArray(args)) { throw new Error( `Query arguments to \`InternalConvexClient.subcribe\` must be an array. Received ${args}.` ); } const { modification, queryToken, unsubscribe } = this.state.subscribe( name, args, journal ); if (modification !== null) { this.webSocketManager.sendMessage(modification); } return { queryToken, unsubscribe: () => { const modification2 = unsubscribe(); if (modification2) { this.webSocketManager.sendMessage(modification2); } } }; } localQueryResult(udfPath, args) { const queryToken = serializePathAndArgs(udfPath, args); return this.optimisticQueryResults.queryResult(queryToken); } queryJournal(name, args) { const queryToken = serializePathAndArgs(name, args); return this.state.queryJournal(queryToken); } connectionState() { return { hasInflightRequests: this.requestManager.hasInflightRequests() || this.actionManager.hasInflightActions(), isWebSocketConnected: this.webSocketManager.socketState() === "ready" }; } async mutate(udfPath, args, optimisticUpdate = null) { const requestId = this.nextRequestId; this.nextRequestId++; if (optimisticUpdate !== null) { const wrappedUpdate = (localQueryStore) => { optimisticUpdate(localQueryStore, ...args); }; const changedQueries = this.optimisticQueryResults.applyOptimisticUpdate( wrappedUpdate, requestId ); this.onTransition(changedQueries); } const message = { type: "Mutation", requestId, udfPath, args: convexToJson(args) }; const result = this.requestManager.request(message); this.webSocketManager.sendMessage(message); return result; } async action(udfPath, args) { const requestId = this.nextRequestId; this.nextRequestId++; const message = { type: "Action", requestId, udfPath, args: convexToJson(args) }; const result = this.actionManager.request(message); this.webSocketManager.sendMessage(message); return result; } async close() { return this.webSocketManager.stop(); } } //# sourceMappingURL=client.js.map