Skip to content

Commit

Permalink
Consistent SSR for @convex-dev/react-query (#28392)
Browse files Browse the repository at this point in the history
Experimental consistent query API for ConvexHttpClient, used by @convex-dev/react-query. This exposes experimental new backend functionality intended for server-side rendering.

Note that there is already a batch query API which could also be exposed in the Convex HTTP Client. The API in this PR is intended for the specific case where queries that may depend on the result of other queries and must be consistent with these previous queries, while the batch query API must have all query results requested at once.

GitOrigin-RevId: 7f5301c780ea0f5db01c0bbaa527414c58c25236
  • Loading branch information
thomasballinger authored and Convex, Inc. committed Jul 30, 2024
1 parent e0ae4f6 commit 8394e21
Show file tree
Hide file tree
Showing 15 changed files with 177 additions and 39 deletions.
75 changes: 58 additions & 17 deletions npm-packages/common/config/rush/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion npm-packages/convex/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,6 @@ module.exports = {
"*.js",
"tmpDist*",
"tmpPackage*",
"custom-vitest-enviroment.ts",
"custom-vitest-environment.ts",
],
};
109 changes: 98 additions & 11 deletions npm-packages/convex/src/browser/http_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
jsonToConvex,
} from "../values/index.js";
import { logToConsole } from "./logging.js";
import { UserIdentityAttributes } from "../server/index.js";
import { FunctionArgs, UserIdentityAttributes } from "../server/index.js";

export const STATUS_CODE_OK = 200;
export const STATUS_CODE_BAD_REQUEST = 400;
Expand Down Expand Up @@ -43,6 +43,7 @@ export class ConvexHttpClient {
private readonly address: string;
private auth?: string;
private adminAuth?: string;
private encodedTsPromise?: Promise<string>;
private debug: boolean;
private fetchOptions?: FetchOptions;

Expand All @@ -59,16 +60,27 @@ export class ConvexHttpClient {
if (skipConvexDeploymentUrlCheck !== true) {
validateDeploymentUrl(address);
}
this.address = `${address}/api`;
this.address = address;
this.debug = true;
}

/**
* Obtain the {@link ConvexHttpClient}'s URL to its backend.
* @deprecated Use url, which returns the url without /api at the end.
*
* @returns The URL to the Convex backend, including the client's API version.
*/
backendUrl(): string {
return `${this.address}/api`;
}

/**
* Return the address for this client, useful for creating a new client.
*
* Not guaranteed to match the address with which this client was constructed:
* it may be canonicalized.
*/
get url() {
return this.address;
}

Expand Down Expand Up @@ -125,6 +137,62 @@ export class ConvexHttpClient {
this.fetchOptions = fetchOptions;
}

/**
* This API is experimental: it may change or disappear.
*
* Execute a Convex query function at the same timestamp as every other
* consistent query execution run by this HTTP client.
*
* This doesn't make sense for long-lived ConvexHttpClients as Convex
* backends can read a limited amount into the past: beyond 30 seconds
* in the past may not be available.
*
* Create a new client to use a consistent time.
*
* @param name - The name of the query.
* @param args - The arguments object for the query. If this is omitted,
* the arguments will be `{}`.
* @returns A promise of the query's result.
*
* @deprecated This API is experimental: it may change or disappear.
*/
async consistentQuery<Query extends FunctionReference<"query">>(
query: Query,
...args: OptionalRestArgs<Query>
): Promise<FunctionReturnType<Query>> {
const queryArgs = parseArgs(args[0]);

const timestampPromise = this.getTimestamp();
return await this.queryInner(query, queryArgs, { timestampPromise });
}

private async getTimestamp() {
if (this.encodedTsPromise) {
return this.encodedTsPromise;
}
return (this.encodedTsPromise = this.getTimestampInner());
}

private async getTimestampInner() {
const localFetch = specifiedFetch || fetch;

const headers: Record<string, string> = {
"Content-Type": "application/json",
"Convex-Client": `npm-${version}`,
};
const response = await localFetch(`${this.address}/api/query_ts`, {
...this.fetchOptions,
method: "POST",
headers: headers,
credentials: "include",
});
if (!response.ok) {
throw new Error(await response.text());
}
const { ts } = (await response.json()) as { ts: string };
return ts;
}

/**
* Execute a Convex query function.
*
Expand All @@ -138,12 +206,16 @@ export class ConvexHttpClient {
...args: OptionalRestArgs<Query>
): Promise<FunctionReturnType<Query>> {
const queryArgs = parseArgs(args[0]);
return await this.queryInner(query, queryArgs, {});
}

private async queryInner<Query extends FunctionReference<"query">>(
query: Query,
queryArgs: FunctionArgs<Query>,
options: { timestampPromise?: Promise<string> },
): Promise<FunctionReturnType<Query>> {
const name = getFunctionName(query);
const body = JSON.stringify({
path: name,
format: "convex_encoded_json",
args: [convexToJson(queryArgs)],
});
const args = [convexToJson(queryArgs)];
const headers: Record<string, string> = {
"Content-Type": "application/json",
"Convex-Client": `npm-${version}`,
Expand All @@ -154,7 +226,22 @@ export class ConvexHttpClient {
headers["Authorization"] = `Bearer ${this.auth}`;
}
const localFetch = specifiedFetch || fetch;
const response = await localFetch(`${this.address}/query`, {

const timestamp = options.timestampPromise
? await options.timestampPromise
: undefined;

const body = JSON.stringify({
path: name,
format: "convex_encoded_json",
args,
...(timestamp ? { ts: timestamp } : {}),
});
const endpoint = timestamp
? `${this.address}/api/query_at_ts`
: `${this.address}/api/query`;

const response = await localFetch(endpoint, {
...this.fetchOptions,
body,
method: "POST",
Expand Down Expand Up @@ -216,7 +303,7 @@ export class ConvexHttpClient {
headers["Authorization"] = `Bearer ${this.auth}`;
}
const localFetch = specifiedFetch || fetch;
const response = await localFetch(`${this.address}/mutation`, {
const response = await localFetch(`${this.address}/api/mutation`, {
...this.fetchOptions,
body,
method: "POST",
Expand Down Expand Up @@ -277,7 +364,7 @@ export class ConvexHttpClient {
headers["Authorization"] = `Bearer ${this.auth}`;
}
const localFetch = specifiedFetch || fetch;
const response = await localFetch(`${this.address}/action`, {
const response = await localFetch(`${this.address}/api/action`, {
...this.fetchOptions,
body,
method: "POST",
Expand Down Expand Up @@ -347,7 +434,7 @@ export class ConvexHttpClient {
headers["Authorization"] = `Bearer ${this.auth}`;
}
const localFetch = specifiedFetch || fetch;
const response = await localFetch(`${this.address}/function`, {
const response = await localFetch(`${this.address}/api/function`, {
...this.fetchOptions,
body,
method: "POST",
Expand Down
2 changes: 1 addition & 1 deletion npm-packages/convex/src/browser/sync/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @vitest-environment custom-vitest-enviroment.ts
* @vitest-environment custom-vitest-environment.ts
*/

import { test, expect } from "vitest";
Expand Down
10 changes: 10 additions & 0 deletions npm-packages/convex/src/browser/sync/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,16 @@ export class BaseConvexClient {
return this.webSocketManager.terminate();
}

/**
* Return the address for this client, useful for creating a new client.
*
* Not guaranteed to match the address with which this client was constructed:
* it may be canonicalized.
*/
get url() {
return this.address;
}

/**
* @internal
*/
Expand Down
2 changes: 1 addition & 1 deletion npm-packages/convex/src/browser/sync/protocol.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @vitest-environment custom-vitest-enviroment.ts
* @vitest-environment custom-vitest-environment.ts
*/

import { test, expect } from "vitest";
Expand Down
2 changes: 1 addition & 1 deletion npm-packages/convex/src/nextjs/nextjs.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @vitest-environment custom-vitest-enviroment.ts
* @vitest-environment custom-vitest-environment.ts
*/
import { vi, expect, test, describe, beforeEach, afterEach } from "vitest";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @vitest-environment custom-vitest-enviroment.ts
* @vitest-environment custom-vitest-environment.ts
*/
import { test } from "vitest";
import React from "react";
Expand Down
Loading

0 comments on commit 8394e21

Please sign in to comment.