Skip to content

Commit

Permalink
progress
Browse files Browse the repository at this point in the history
  • Loading branch information
phryneas committed Apr 3, 2024
1 parent a47cac9 commit c048ef5
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ function getQueryManager<TCacheShape>(
return client["queryManager"];
}

/**
* Returns the `Trie` constructor without adding a direct dependency on `@wry/trie`.
*/
function getTrieConstructor(client: OrigApolloClient<unknown>) {
return getQueryManager(client)["inFlightLinkObservables"]
.constructor as typeof import("@wry/trie").Trie;
}

type SimulatedQueryInfo = {
resolve: (result: FetchResult) => void;
reject: (reason: any) => void;
Expand Down Expand Up @@ -64,6 +72,7 @@ export class ApolloClientClientBaseImpl<
> extends ApolloClientBase<TCacheShape> {
constructor(options: ApolloClientOptions<TCacheShape>) {
super(options);
this.onQueryStarted = this.onQueryStarted.bind(this);

getQueryManager(this)[wrappers] = hookWrappers;
}
Expand All @@ -77,7 +86,7 @@ export class ApolloClientClientBaseImpl<
WatchQueryOptions
>();

private identifyUniqueQuery(options: {
protected identifyUniqueQuery(options: {
query: DocumentNode;
variables?: unknown;
}) {
Expand All @@ -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<QueryEvent, { type: "started" }>) => {
const { query, varJson, cacheKey } = this.identifyUniqueQuery(options);
onQueryStarted({ options, id }: Extract<QueryEvent, { type: "started" }>) {
const { cacheKey, cacheKeyArr } = this.identifyUniqueQuery(options);
this.transportedQueryOptions.set(id, options);

if (!query) return;
const printedServerQuery = print(query);
const queryManager = getQueryManager<TCacheShape>(this);

if (
!queryManager["inFlightLinkObservables"].peek(printedServerQuery, varJson)
!queryManager["inFlightLinkObservables"].peekArray(cacheKeyArr)
?.observable
) {
let simulatedStreamingQuery: SimulatedQueryInfo,
Expand All @@ -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);
Expand All @@ -151,9 +156,8 @@ export class ApolloClientClientBaseImpl<
});
});

queryManager["inFlightLinkObservables"].lookup(
printedServerQuery,
varJson
queryManager["inFlightLinkObservables"].lookupArray(
cacheKeyArr
).observable = observable;

queryManager["fetchCancelFns"].set(
Expand All @@ -167,7 +171,7 @@ export class ApolloClientClientBaseImpl<
})
);
}
};
}

onQueryProgress = (event: Exclude<QueryEvent, { type: "started" }>) => {
const queryInfo = this.simulatedStreamingQueries.get(event.id);
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -246,6 +258,8 @@ export class ApolloClientClientBaseImpl<
class ApolloClientSSRImpl<
TCacheShape,
> extends ApolloClientClientBaseImpl<TCacheShape> {
private forwardedQueries = new (getTrieConstructor(this))();

watchQueryQueue = createBackpressuredCallback<{
event: Extract<QueryEvent, { type: "started" }>;
observable: Observable<Exclude<QueryEvent, { type: "started" }>>;
Expand All @@ -255,10 +269,15 @@ class ApolloClientSSRImpl<
T = any,
TVariables extends OperationVariables = OperationVariables,
>(options: WatchQueryOptions<TVariables, T>) {
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;
Expand Down Expand Up @@ -305,6 +324,14 @@ class ApolloClientSSRImpl<
}
return super.watchQuery(options);
}

onQueryStarted(event: Extract<QueryEvent, { type: "started" }>) {
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<
Expand Down
22 changes: 14 additions & 8 deletions packages/client-react-streaming/src/PreloadQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,21 @@ export function PreloadQuery({
getClient: () => ApolloClient<any>;
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 (
<SimulatePreloadedQuery options={options} result={resultPromise}>
<SimulatePreloadedQuery options={cleanedOptions} result={resultPromise}>
{children}
</SimulatePreloadedQuery>
);
Expand Down
16 changes: 10 additions & 6 deletions packages/client-react-streaming/src/index.cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<WatchQueryOptions, TransportIdentifier>();

Expand All @@ -23,13 +24,22 @@ export function SimulatePreloadedQuery({
const id =
`preloadedQuery:${(client["queryManager"] as QueryManager<any>).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,
Expand All @@ -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,
Expand Down

0 comments on commit c048ef5

Please sign in to comment.