-
Notifications
You must be signed in to change notification settings - Fork 2.7k
/
Copy pathRenderPromises.ts
144 lines (130 loc) · 4.77 KB
/
RenderPromises.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import type * as ReactTypes from "react";
import type { ObservableQuery, OperationVariables } from "../../core/index.js";
import type { QueryDataOptions } from "../types/types.js";
import { Trie } from "@wry/trie";
import { canonicalStringify } from "../../cache/index.js";
// TODO: A vestigial interface from when hooks were implemented with utility
// classes, which should be deleted in the future.
interface QueryData {
getOptions(): any;
fetchData(): Promise<void>;
}
type QueryInfo = {
seen: boolean;
observable: ObservableQuery<any, any> | null;
};
function makeQueryInfoTrie() {
// these Tries are very short-lived, so we don't need to worry about making it
// "weak" - it's easier to test and debug as a strong Trie.
return new Trie<QueryInfo>(false, () => ({
seen: false,
observable: null,
}));
}
export class RenderPromises {
// Map from Query component instances to pending fetchData promises.
private queryPromises = new Map<QueryDataOptions<any, any>, Promise<any>>();
// Two-layered map from (query document, stringified variables) to QueryInfo
// objects. These QueryInfo objects are intended to survive through the whole
// getMarkupFromTree process, whereas specific Query instances do not survive
// beyond a single call to renderToStaticMarkup.
private queryInfoTrie = makeQueryInfoTrie();
private stopped = false;
public stop() {
if (!this.stopped) {
this.queryPromises.clear();
this.queryInfoTrie = makeQueryInfoTrie();
this.stopped = true;
}
}
// Registers the server side rendered observable.
public registerSSRObservable<TData, TVariables extends OperationVariables>(
observable: ObservableQuery<any, TVariables>
) {
if (this.stopped) return;
this.lookupQueryInfo(observable.options).observable = observable;
}
// Get's the cached observable that matches the SSR Query instances query and variables.
public getSSRObservable<TData, TVariables extends OperationVariables>(
props: QueryDataOptions<TData, TVariables>
): ObservableQuery<any, TVariables> | null {
return this.lookupQueryInfo(props).observable;
}
public addQueryPromise(
queryInstance: QueryData,
finish?: () => ReactTypes.ReactNode
): ReactTypes.ReactNode {
if (!this.stopped) {
const info = this.lookupQueryInfo(queryInstance.getOptions());
if (!info.seen) {
this.queryPromises.set(
queryInstance.getOptions(),
new Promise((resolve) => {
resolve(queryInstance.fetchData());
})
);
// Render null to abandon this subtree for this rendering, so that we
// can wait for the data to arrive.
return null;
}
}
return finish ? finish() : null;
}
public addObservableQueryPromise<
TData,
TVariables extends OperationVariables,
>(obsQuery: ObservableQuery<TData, TVariables>) {
return this.addQueryPromise({
// The only options which seem to actually be used by the
// RenderPromises class are query and variables.
getOptions: () => obsQuery.options,
fetchData: () =>
new Promise<void>((resolve) => {
const sub = obsQuery.subscribe({
next(result) {
if (!result.loading) {
resolve();
sub.unsubscribe();
}
},
error() {
resolve();
sub.unsubscribe();
},
complete() {
resolve();
},
});
}),
});
}
public hasPromises() {
return this.queryPromises.size > 0;
}
public consumeAndAwaitPromises() {
const promises: Promise<any>[] = [];
this.queryPromises.forEach((promise, queryInstance) => {
// Make sure we never try to call fetchData for this query document and
// these variables again. Since the queryInstance objects change with
// every rendering, deduplicating them by query and variables is the
// best we can do. If a different Query component happens to have the
// same query document and variables, it will be immediately rendered
// by calling finish() in addQueryPromise, which could result in the
// rendering of an unwanted loading state, but that's not nearly as bad
// as getting stuck in an infinite rendering loop because we kept calling
// queryInstance.fetchData for the same Query component indefinitely.
this.lookupQueryInfo(queryInstance).seen = true;
promises.push(promise);
});
this.queryPromises.clear();
return Promise.all(promises);
}
private lookupQueryInfo<TData, TVariables extends OperationVariables>(
props: QueryDataOptions<TData, TVariables>
): QueryInfo {
return this.queryInfoTrie.lookup(
props.query,
canonicalStringify(props.variables)
);
}
}