From e8057fc22da8d7578b497e7c21bc32ad80ab4075 Mon Sep 17 00:00:00 2001
From: Lenz Weber-Tronic
Date: Wed, 3 Apr 2024 15:10:03 +0200
Subject: [PATCH] progress
---
.../nextjs/src/app/cc/ApolloWrapper.tsx | 21 +----
.../cc/dynamic/useBackgroundQuery/page.tsx | 4 +-
.../app/cc/dynamic/useSuspenseQuery/page.tsx | 4 +-
integration-test/nextjs/src/app/rsc/client.ts | 9 +--
.../rsc/dynamic/PreloadQuery/ClientChild.tsx | 15 ++++
.../dynamic/PreloadQuery/PreloadQuery.test.ts | 41 ++++++++++
.../src/app/rsc/dynamic/PreloadQuery/page.tsx | 29 +++++++
.../app/rsc/dynamic/PreloadQuery/shared.tsx | 15 ++++
.../nextjs/src/shared/delayLink.ts | 6 ++
.../nextjs/src/shared/errorLink.tsx | 30 ++++++++
integration-test/package.json | 2 +-
integration-test/yarn.lock | 10 +--
.../WrappedApolloClient.tsx | 77 +++++++++++++------
.../src/PreloadQuery.tsx | 22 ++++--
.../client-react-streaming/src/index.cc.ts | 16 ++--
15 files changed, 228 insertions(+), 73 deletions(-)
create mode 100644 integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/ClientChild.tsx
create mode 100644 integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/PreloadQuery.test.ts
create mode 100644 integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/page.tsx
create mode 100644 integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/shared.tsx
create mode 100644 integration-test/nextjs/src/shared/errorLink.tsx
diff --git a/integration-test/nextjs/src/app/cc/ApolloWrapper.tsx b/integration-test/nextjs/src/app/cc/ApolloWrapper.tsx
index 28c80b3b..f3022dcd 100644
--- a/integration-test/nextjs/src/app/cc/ApolloWrapper.tsx
+++ b/integration-test/nextjs/src/app/cc/ApolloWrapper.tsx
@@ -1,6 +1,6 @@
"use client";
import React from "react";
-import { ApolloLink, HttpLink, Observable } from "@apollo/client";
+import { HttpLink } from "@apollo/client";
import {
ApolloNextAppProvider,
NextSSRInMemoryCache,
@@ -15,29 +15,12 @@ import { delayLink } from "@/shared/delayLink";
import { schema } from "../graphql/schema";
import { useSSROnlySecret } from "ssr-only-secrets";
-import { GraphQLError } from "graphql";
+import { errorLink } from "../../shared/errorLink";
setVerbosity("debug");
loadDevMessages();
loadErrorMessages();
-const errorLink = new ApolloLink((operation, forward) => {
- const context = operation.getContext();
- if (
- context.error === "always" ||
- (typeof window === "undefined" && context.error === "ssr") ||
- (typeof window !== "undefined" && context.error === "browser")
- ) {
- return new Observable((subscriber) => {
- subscriber.next({
- data: null,
- errors: [new GraphQLError("Simulated error")],
- });
- });
- }
- return forward(operation);
-});
-
export function ApolloWrapper({
children,
nonce,
diff --git a/integration-test/nextjs/src/app/cc/dynamic/useBackgroundQuery/page.tsx b/integration-test/nextjs/src/app/cc/dynamic/useBackgroundQuery/page.tsx
index 58928057..ea1d336e 100644
--- a/integration-test/nextjs/src/app/cc/dynamic/useBackgroundQuery/page.tsx
+++ b/integration-test/nextjs/src/app/cc/dynamic/useBackgroundQuery/page.tsx
@@ -27,7 +27,9 @@ const QUERY: TypedDocumentNode = gql`
export const dynamic = "force-dynamic";
export default function Page() {
- const [queryRef] = useBackgroundQuery(QUERY, { context: { delay: 2000 } });
+ const [queryRef] = useBackgroundQuery(QUERY, {
+ context: { delay: 2000, error: "browser" },
+ });
return (
loading
}>
diff --git a/integration-test/nextjs/src/app/cc/dynamic/useSuspenseQuery/page.tsx b/integration-test/nextjs/src/app/cc/dynamic/useSuspenseQuery/page.tsx
index 56148d54..8d25a5f1 100644
--- a/integration-test/nextjs/src/app/cc/dynamic/useSuspenseQuery/page.tsx
+++ b/integration-test/nextjs/src/app/cc/dynamic/useSuspenseQuery/page.tsx
@@ -21,7 +21,9 @@ const QUERY: TypedDocumentNode<{
export const dynamic = "force-dynamic";
export default function Page() {
- const { data } = useSuspenseQuery(QUERY);
+ const { data } = useSuspenseQuery(QUERY, {
+ context: { delay: 1000, error: "browser" },
+ });
globalThis.hydrationFinished?.();
return (
diff --git a/integration-test/nextjs/src/app/rsc/client.ts b/integration-test/nextjs/src/app/rsc/client.ts
index 8aa56097..fb3cffcf 100644
--- a/integration-test/nextjs/src/app/rsc/client.ts
+++ b/integration-test/nextjs/src/app/rsc/client.ts
@@ -4,6 +4,7 @@ import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rs
import { loadErrorMessages, loadDevMessages } from "@apollo/client/dev";
import { setVerbosity } from "ts-invariant";
import { delayLink } from "@/shared/delayLink";
+import { errorLink } from "@/shared/errorLink";
import { SchemaLink } from "@apollo/client/link/schema";
import { schema } from "../graphql/schema";
@@ -15,12 +16,6 @@ loadErrorMessages();
export const { getClient } = registerApolloClient(() => {
return new ApolloClient({
cache: new InMemoryCache(),
- link: delayLink.concat(
- typeof window === "undefined"
- ? new SchemaLink({ schema })
- : new HttpLink({
- uri: "/graphql",
- })
- ),
+ link: delayLink.concat(errorLink.concat(new SchemaLink({ schema }))),
});
});
diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/ClientChild.tsx b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/ClientChild.tsx
new file mode 100644
index 00000000..abd74f4a
--- /dev/null
+++ b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/ClientChild.tsx
@@ -0,0 +1,15 @@
+"use client";
+
+import { useSuspenseQuery } from "@apollo/client";
+import { QUERY } from "./shared";
+
+export function ClientChild() {
+ const { data } = useSuspenseQuery(QUERY, { context: { error: "always" } });
+ return (
+
+ {data.products.map(({ id, title }) => (
+ - {title}
+ ))}
+
+ );
+}
diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/PreloadQuery.test.ts b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/PreloadQuery.test.ts
new file mode 100644
index 00000000..c48c5f11
--- /dev/null
+++ b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/PreloadQuery.test.ts
@@ -0,0 +1,41 @@
+import { expect } from "@playwright/test";
+import { test } from "../../../../../fixture";
+
+test.describe("PreloadQuery", () => {
+ test("query resolves on the server", async ({ page, blockRequest }) => {
+ await page.goto(
+ "http://localhost:3000/rsc/dynamic/PreloadQuery?errorIn=ssr,browser",
+ {
+ waitUntil: "commit",
+ }
+ );
+
+ await expect(page).toBeInitiallyLoading(true);
+ await expect(page.getByText("loading")).not.toBeVisible();
+ await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
+ });
+
+ test("query errors on the server, restarts in the browser", async ({
+ page,
+ }) => {
+ page.allowErrors?.();
+ await page.goto(
+ "http://localhost:3000/rsc/dynamic/PreloadQuery?errorIn=rsc",
+ {
+ waitUntil: "commit",
+ }
+ );
+
+ await expect(page).toBeInitiallyLoading(true);
+
+ await page.waitForEvent("pageerror", (error) => {
+ return (
+ /* prod */ error.message.includes("Minified React error #419") ||
+ /* dev */ error.message.includes("Query failed upstream.")
+ );
+ });
+
+ await expect(page.getByText("loading")).not.toBeVisible();
+ await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible();
+ });
+});
diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/page.tsx b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/page.tsx
new file mode 100644
index 00000000..cf85c659
--- /dev/null
+++ b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/page.tsx
@@ -0,0 +1,29 @@
+import { ApolloWrapper } from "@/app/cc/ApolloWrapper";
+import { PreloadQuery } from "@apollo/client-react-streaming";
+import { ClientChild } from "./ClientChild";
+import { QUERY } from "./shared";
+
+export const dynamic = "force-dynamic";
+import { getClient } from "../../client";
+import { Suspense } from "react";
+
+export default function Page({ searchParams }: { searchParams?: any }) {
+ return (
+
+
+ loading>}>
+
+
+
+
+ );
+}
diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/shared.tsx b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/shared.tsx
new file mode 100644
index 00000000..0fabe82d
--- /dev/null
+++ b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/shared.tsx
@@ -0,0 +1,15 @@
+import { TypedDocumentNode, gql } from "@apollo/client";
+
+export const QUERY: TypedDocumentNode<{
+ products: {
+ id: string;
+ title: string;
+ }[];
+}> = gql`
+ query dynamicProducts {
+ products {
+ id
+ title
+ }
+ }
+`;
diff --git a/integration-test/nextjs/src/shared/delayLink.ts b/integration-test/nextjs/src/shared/delayLink.ts
index d789b796..e4f81b1d 100644
--- a/integration-test/nextjs/src/shared/delayLink.ts
+++ b/integration-test/nextjs/src/shared/delayLink.ts
@@ -1,5 +1,11 @@
import { ApolloLink, Observable } from "@apollo/client";
+declare module "@apollo/client" {
+ export interface DefaultContext {
+ delay?: number;
+ }
+}
+
export const delayLink = new ApolloLink((operation, forward) => {
if (operation.operationName?.includes("dynamic")) {
operation.setContext({
diff --git a/integration-test/nextjs/src/shared/errorLink.tsx b/integration-test/nextjs/src/shared/errorLink.tsx
new file mode 100644
index 00000000..d5b1c14c
--- /dev/null
+++ b/integration-test/nextjs/src/shared/errorLink.tsx
@@ -0,0 +1,30 @@
+import { ApolloLink, Observable } from "@apollo/client";
+import { GraphQLError } from "graphql";
+import * as entryPoint from "@apollo/client-react-streaming";
+
+declare module "@apollo/client" {
+ type Env = "ssr" | "browser" | "rsc";
+ export interface DefaultContext {
+ error?: "always" | Env | `${Env},${Env}`;
+ }
+}
+
+export const errorLink = new ApolloLink((operation, forward) => {
+ const context = operation.getContext();
+ if (
+ context.error === "always" ||
+ ("built_for_ssr" in entryPoint &&
+ context.error?.split(",").includes("ssr")) ||
+ ("built_for_browser" in entryPoint &&
+ context.error?.split(",").includes("browser")) ||
+ ("built_for_rsc" in entryPoint && context.error?.split(",").includes("rsc"))
+ ) {
+ return new Observable((subscriber) => {
+ subscriber.next({
+ data: null,
+ errors: [new GraphQLError("Simulated error")],
+ });
+ });
+ }
+ return forward(operation);
+});
diff --git a/integration-test/package.json b/integration-test/package.json
index f6e1d0c8..15333a07 100644
--- a/integration-test/package.json
+++ b/integration-test/package.json
@@ -9,7 +9,7 @@
"*"
],
"scripts": {
- "trigger-rebuild": "find . -regextype posix-extended -regex '.*/node_modules/@apollo/(client-react-streaming|experimental-nextjs-app-support)' -printf 'rm -r %p\n' -exec rm -r {} +; glob \"../.yarn/cache/@apollo-*exec*\" \"$HOME/.yarn/berry/cache/@apollo-*exec*\" --cmd='rm -v' ; yarn"
+ "trigger:rebuild": "find . -regextype posix-extended -regex '.*/node_modules/@apollo/(client-react-streaming|experimental-nextjs-app-support)' -printf 'rm -r %p\n' -exec rm -r {} +; glob \"../.yarn/cache/@apollo-*exec*\" \"$HOME/.yarn/berry/cache/@apollo-*exec*\" --cmd='rm -v' ; yarn"
},
"devDependencies": {
"glob": "^10.3.10"
diff --git a/integration-test/yarn.lock b/integration-test/yarn.lock
index 22b18208..25e0664b 100644
--- a/integration-test/yarn.lock
+++ b/integration-test/yarn.lock
@@ -32,13 +32,13 @@ __metadata:
linkType: hard
"@apollo/client-react-streaming@exec:./shared/build-client-react-streaming.cjs::locator=%40integration-test%2Froot%40workspace%3A.":
- version: 0.8.0
+ version: 0.9.2
resolution: "@apollo/client-react-streaming@exec:./shared/build-client-react-streaming.cjs#./shared/build-client-react-streaming.cjs::hash=48b117&locator=%40integration-test%2Froot%40workspace%3A."
dependencies:
superjson: "npm:^1.12.2 || ^2.0.0"
ts-invariant: "npm:^0.10.3"
peerDependencies:
- "@apollo/client": ^3.9.0
+ "@apollo/client": ^3.9.6
react: ^18
checksum: 10/8e12155ebcb9672f5b645c364d356018014df750412c61613341121ebb4d4eabb5f42cd9018cc3a81ad988f1b425548d68254ca49ede19c31d0d9e5a9a4f240a
languageName: node
@@ -82,12 +82,12 @@ __metadata:
linkType: hard
"@apollo/experimental-nextjs-app-support@exec:./shared/build-experimental-nextjs-app-support.cjs::locator=%40integration-test%2Froot%40workspace%3A.":
- version: 0.8.0
+ version: 0.9.2
resolution: "@apollo/experimental-nextjs-app-support@exec:./shared/build-experimental-nextjs-app-support.cjs#./shared/build-experimental-nextjs-app-support.cjs::hash=fd83cc&locator=%40integration-test%2Froot%40workspace%3A."
dependencies:
- "@apollo/client-react-streaming": "npm:^0.9.0"
+ "@apollo/client-react-streaming": "npm:0.9.2"
peerDependencies:
- "@apollo/client": ^3.9.0
+ "@apollo/client": ^3.9.6
next: ^13.4.1 || ^14.0.0
react: ^18
checksum: 10/505b723bac0f3a7f15287ea32fab9f2e8c0cd567149abf11d750855f8a9bfc0aa26e44179ad10c32f7d162ad86318717032413ef8e1a25385185178e022588fa
diff --git a/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.tsx b/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.tsx
index c2a6a258..beb596c5 100644
--- a/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.tsx
+++ b/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.tsx
@@ -33,6 +33,14 @@ function getQueryManager(
return client["queryManager"];
}
+/**
+ * Returns the `Trie` constructor without adding a direct dependency on `@wry/trie`.
+ */
+function getTrieConstructor(client: OrigApolloClient) {
+ return getQueryManager(client)["inFlightLinkObservables"]
+ .constructor as typeof import("@wry/trie").Trie;
+}
+
type SimulatedQueryInfo = {
resolve: (result: FetchResult) => void;
reject: (reason: any) => void;
@@ -64,6 +72,7 @@ export class ApolloClientClientBaseImpl<
> extends ApolloClientBase {
constructor(options: ApolloClientOptions) {
super(options);
+ this.onQueryStarted = this.onQueryStarted.bind(this);
getQueryManager(this)[wrappers] = hookWrappers;
}
@@ -77,7 +86,7 @@ export class ApolloClientClientBaseImpl<
WatchQueryOptions
>();
- private identifyUniqueQuery(options: {
+ protected identifyUniqueQuery(options: {
query: DocumentNode;
variables?: unknown;
}) {
@@ -95,24 +104,23 @@ export class ApolloClientClientBaseImpl<
const canonicalVariables = canonicalStringify(options.variables || {});
- const cacheKey = [print(serverQuery), canonicalVariables].toString();
+ const cacheKeyArr = [print(serverQuery), canonicalVariables];
+ const cacheKey = JSON.stringify(cacheKeyArr);
- return { query: serverQuery, cacheKey, varJson: canonicalVariables };
+ return {
+ cacheKey,
+ cacheKeyArr,
+ };
}
- onQueryStarted = ({
- options,
- id,
- }: Extract) => {
- const { query, varJson, cacheKey } = this.identifyUniqueQuery(options);
+ onQueryStarted({ options, id }: Extract) {
+ const { cacheKey, cacheKeyArr } = this.identifyUniqueQuery(options);
this.transportedQueryOptions.set(id, options);
- if (!query) return;
- const printedServerQuery = print(query);
const queryManager = getQueryManager(this);
if (
- !queryManager["inFlightLinkObservables"].peek(printedServerQuery, varJson)
+ !queryManager["inFlightLinkObservables"].peekArray(cacheKeyArr)
?.observable
) {
let simulatedStreamingQuery: SimulatedQueryInfo,
@@ -122,10 +130,7 @@ export class ApolloClientClientBaseImpl<
if (queryManager["fetchCancelFns"].get(cacheKey) === fetchCancelFn)
queryManager["fetchCancelFns"].delete(cacheKey);
- queryManager["inFlightLinkObservables"].remove(
- printedServerQuery,
- varJson
- );
+ queryManager["inFlightLinkObservables"].removeArray(cacheKeyArr);
if (this.simulatedStreamingQueries.get(id) === simulatedStreamingQuery)
this.simulatedStreamingQueries.delete(id);
@@ -151,9 +156,8 @@ export class ApolloClientClientBaseImpl<
});
});
- queryManager["inFlightLinkObservables"].lookup(
- printedServerQuery,
- varJson
+ queryManager["inFlightLinkObservables"].lookupArray(
+ cacheKeyArr
).observable = observable;
queryManager["fetchCancelFns"].set(
@@ -167,7 +171,7 @@ export class ApolloClientClientBaseImpl<
})
);
}
- };
+ }
onQueryProgress = (event: Exclude) => {
const queryInfo = this.simulatedStreamingQueries.get(event.id);
@@ -199,11 +203,19 @@ export class ApolloClientClientBaseImpl<
*/
if (queryInfo) {
this.simulatedStreamingQueries.delete(event.id);
- invariant.debug(
- "query failed on server, rerunning in browser:",
- queryInfo.options
- );
- this.rerunSimulatedQuery(queryInfo);
+ if (process.env.REACT_ENV === "browser") {
+ invariant.debug(
+ "Query failed on server, rerunning in browser:",
+ queryInfo.options
+ );
+ this.rerunSimulatedQuery(queryInfo);
+ } else if (process.env.REACT_ENV === "ssr") {
+ invariant.debug(
+ "Query failed upstream, we will fail it during SSR and rerun it in the browser:",
+ queryInfo.options
+ );
+ queryInfo?.reject?.(new Error("Query failed upstream."));
+ }
}
this.transportedQueryOptions.delete(event.id);
} else if (event.type === "complete") {
@@ -246,6 +258,8 @@ export class ApolloClientClientBaseImpl<
class ApolloClientSSRImpl<
TCacheShape,
> extends ApolloClientClientBaseImpl {
+ private forwardedQueries = new (getTrieConstructor(this))();
+
watchQueryQueue = createBackpressuredCallback<{
event: Extract;
observable: Observable>;
@@ -255,10 +269,15 @@ class ApolloClientSSRImpl<
T = any,
TVariables extends OperationVariables = OperationVariables,
>(options: WatchQueryOptions) {
+ const { cacheKeyArr } = this.identifyUniqueQuery(options);
+
if (
options.fetchPolicy !== "cache-only" &&
- options.fetchPolicy !== "standby"
+ options.fetchPolicy !== "standby" &&
+ !this.forwardedQueries.peekArray(cacheKeyArr)
) {
+ // don't transport the same query over twice
+ this.forwardedQueries.lookupArray(cacheKeyArr);
const observableQuery = super.watchQuery(options);
const queryInfo = observableQuery["queryInfo"] as QueryInfo;
const id = queryInfo.queryId as TransportIdentifier;
@@ -305,6 +324,14 @@ class ApolloClientSSRImpl<
}
return super.watchQuery(options);
}
+
+ onQueryStarted(event: Extract) {
+ const { cacheKeyArr } = this.identifyUniqueQuery(event.options);
+ // this is a replay from another source and doesn't need to be transported
+ // to the browser, since it will be replayed there, too.
+ this.forwardedQueries.lookupArray(cacheKeyArr);
+ super.onQueryStarted(event);
+ }
}
export class ApolloClientBrowserImpl<
diff --git a/packages/client-react-streaming/src/PreloadQuery.tsx b/packages/client-react-streaming/src/PreloadQuery.tsx
index de4ddee7..6bda3e33 100644
--- a/packages/client-react-streaming/src/PreloadQuery.tsx
+++ b/packages/client-react-streaming/src/PreloadQuery.tsx
@@ -12,15 +12,21 @@ export function PreloadQuery({
getClient: () => ApolloClient;
children: ReactNode;
}) {
- const resultPromise = getClient().query({
- ...options,
- // TODO: create a second Client instance only for `PreloadQuery` calls
- // We want to prevent "client" data from leaking into our "RSC" cache,
- // as that data should always be strictly separated.
- fetchPolicy: "no-cache",
- });
+ const resultPromise = getClient()
+ .query({
+ ...options,
+ // TODO: create a second Client instance only for `PreloadQuery` calls
+ // We want to prevent "client" data from leaking into our "RSC" cache,
+ // as that data should always be strictly separated.
+ fetchPolicy: "no-cache",
+ })
+ .then((result) => JSON.parse(JSON.stringify(result)));
+ // while they would serialize nicely over the boundary, React will
+ // confuse the GraphQL `Location` class with the browser `Location` and
+ // complain about `Location` objects not being serializable
+ const cleanedOptions = JSON.parse(JSON.stringify(options));
return (
-
+
{children}
);
diff --git a/packages/client-react-streaming/src/index.cc.ts b/packages/client-react-streaming/src/index.cc.ts
index d58c288f..dd4bfc49 100644
--- a/packages/client-react-streaming/src/index.cc.ts
+++ b/packages/client-react-streaming/src/index.cc.ts
@@ -6,6 +6,7 @@ import type { ApolloClient as WrappedApolloClient } from "./DataTransportAbstrac
import type { TransportIdentifier } from "./DataTransportAbstraction/DataTransportAbstraction.js";
import type { QueryManager } from "@apollo/client/core/QueryManager.js";
import type { ReactNode } from "react";
+import invariant from "ts-invariant";
const handledRequests = new WeakMap();
@@ -23,13 +24,22 @@ export function SimulatePreloadedQuery({
const id =
`preloadedQuery:${(client["queryManager"] as QueryManager).generateQueryId()}` as TransportIdentifier;
handledRequests.set(options, id);
+ invariant.debug(
+ "Preloaded query %s started on the server, simulating ongoing request",
+ id
+ );
client.onQueryStarted!({
type: "started",
id,
options,
});
+
result.then(
(result) => {
+ invariant.debug(
+ "Preloaded query %s finished on the server, simulating result",
+ id
+ );
client.onQueryProgress!({
type: "data",
id,
@@ -41,12 +51,6 @@ export function SimulatePreloadedQuery({
});
},
() => {
- // TODO:
- // This will restart the query in SSR **and** in the browser.
- // Currently there is no way of transporting the result received in SSR to the browser.
- // Layers over layers...
- // Maybe instead we should just "fail" the simulated request on the SSR level
- // and only have it re-attempt in the browser?
client.onQueryProgress!({
type: "error",
id,