Skip to content

Commit 30c3892

Browse files
committed
fix: correctly and fully SSR on deferred fragments
1 parent 13cde64 commit 30c3892

10 files changed

Lines changed: 240 additions & 174 deletions

.changeset/fast-plants-dress.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"solid-relay": minor
3+
---
4+
5+
fix: correctly and fully SSR on deferred fragments

src/RelayEnvironment.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { IEnvironment } from "relay-runtime";
22
import {
33
type Accessor,
44
type JSXElement,
5+
type Resource,
56
createComponent,
67
createContext,
78
createMemo,
@@ -16,14 +17,15 @@ interface Props {
1617

1718
const RelayContext = createContext<{
1819
environment: Accessor<IEnvironment>;
20+
dataStores: WeakMap<Resource<unknown>, unknown>;
1921
}>();
2022

2123
export function RelayEnvironmentProvider(props: Props): JSXElement {
2224
const environment = createMemo(() => props.environment);
2325

2426
return createComponent(RelayContext.Provider, {
2527
get value() {
26-
return { environment };
28+
return { environment, dataStores: new WeakMap() };
2729
},
2830
get children() {
2931
return props.children;
@@ -46,3 +48,10 @@ export function useRelayEnvironment(): () => IEnvironment {
4648

4749
return context.environment;
4850
}
51+
52+
export function useDataStores():
53+
| WeakMap<Resource<unknown>, unknown>
54+
| undefined {
55+
const context = useContext(RelayContext);
56+
return context?.dataStores;
57+
}

src/primitives/createFragment.ts

Lines changed: 98 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,16 @@ import type {
22
GraphQLResponse,
33
GraphQLTaggedNode,
44
Subscribable,
5+
Subscription,
56
} from "relay-runtime";
67
import { observeFragment } from "relay-runtime/experimental.js";
7-
import {
8-
batch,
9-
createComputed,
10-
createMemo,
11-
createResource,
12-
onCleanup,
13-
} from "solid-js";
8+
import { batch, createResource, createSignal, untrack } from "solid-js";
149
import type { Accessor } from "solid-js";
15-
import { createStore, reconcile, unwrap } from "solid-js/store";
10+
import { type SetStoreFunction, reconcile, unwrap } from "solid-js/store";
11+
import { isServer } from "solid-js/web";
1612
import { useRelayEnvironment } from "../RelayEnvironment";
1713
import type { KeyType, KeyTypeData } from "../types/keyType";
18-
import { type DataProxy, makeDataProxy } from "../utils/dataProxy";
14+
import { type DataStore, createDataStore } from "../utils/dataStore";
1915

2016
type FragmentResult<T> =
2117
| {
@@ -37,15 +33,15 @@ type FragmentResult<T> =
3733
export function createFragment<TKey extends KeyType>(
3834
fragment: GraphQLTaggedNode,
3935
key: Accessor<TKey>,
40-
): DataProxy<KeyTypeData<TKey>>;
36+
): DataStore<KeyTypeData<TKey>>;
4137
export function createFragment<TKey extends KeyType>(
4238
fragment: GraphQLTaggedNode,
4339
key: Accessor<TKey | null | undefined>,
44-
): DataProxy<KeyTypeData<TKey> | null | undefined>;
40+
): DataStore<KeyTypeData<TKey> | null | undefined>;
4541
export function createFragment<TKey extends KeyType>(
4642
fragment: GraphQLTaggedNode,
4743
key: Accessor<TKey | null | undefined>,
48-
): DataProxy<KeyTypeData<TKey> | null | undefined> {
44+
): DataStore<KeyTypeData<TKey> | null | undefined> {
4945
return createFragmentInternal(fragment, key);
5046
}
5147

@@ -55,21 +51,67 @@ export function createFragmentInternal<TKey extends KeyType>(
5551
options?: Accessor<{
5652
parentOperation: Subscribable<GraphQLResponse> | null | undefined;
5753
}>,
58-
): DataProxy<KeyTypeData<TKey> | null | undefined> {
54+
): DataStore<KeyTypeData<TKey> | null | undefined> {
5955
const environment = useRelayEnvironment();
6056

61-
const source = createMemo(() => {
62-
const k = unwrap(key());
63-
return k && observeFragment(environment(), fragment, k);
64-
});
57+
const initialResult: FragmentResult<TKey[" $data"]> = {
58+
data: undefined,
59+
error: undefined,
60+
pending: false,
61+
};
62+
63+
type FragmentObserver = Parameters<
64+
ReturnType<typeof observeFragment>["subscribe"]
65+
>[0];
66+
const resultUpdateObserver = {
67+
next(res) {
68+
batch(() => {
69+
switch (res.state) {
70+
case "ok":
71+
setResult("error", undefined);
72+
setResult("pending", false);
73+
setResult(
74+
"data",
75+
reconcile(res.value as Record<string, unknown>, {
76+
key: "__id",
77+
merge: true,
78+
}),
79+
);
80+
break;
81+
case "error":
82+
setResult("data", undefined);
83+
setResult("error", res.error);
84+
setResult("pending", false);
85+
break;
86+
}
87+
});
88+
},
89+
} satisfies FragmentObserver;
90+
const [subscription, setSubscription] = createSignal<Subscription>();
91+
92+
const setResultQueue: unknown[][] = [];
93+
let setResult: SetStoreFunction<FragmentResult<TKey[" $data"]>> = (
94+
...args: unknown[]
95+
) => {
96+
setResultQueue.push(args);
97+
};
6598

6699
const [resource] = createResource(
67100
() => {
68-
const s = source();
69-
if (!s) return;
70-
return { source: s, parentOperation: options?.().parentOperation };
101+
batch(() => {
102+
untrack(subscription)?.unsubscribe();
103+
setSubscription(undefined);
104+
setResult(initialResult);
105+
});
106+
107+
void environment();
108+
const k = unwrap(key());
109+
if (!k) return;
110+
return { key: k, parentOperation: options?.().parentOperation };
71111
},
72-
async ({ source, parentOperation }) => {
112+
async ({ key, parentOperation }) => {
113+
setResult("pending", true);
114+
73115
if (parentOperation) {
74116
await new Promise<void>((resolve, reject) => {
75117
parentOperation.subscribe({
@@ -79,66 +121,45 @@ export function createFragmentInternal<TKey extends KeyType>(
79121
});
80122
}
81123

124+
const source = observeFragment(environment(), fragment, key);
125+
82126
return new Promise<true>((resolve, reject) => {
83-
const subscription = source.subscribe({
84-
next(value) {
85-
if (value.state === "ok") {
86-
resolve(true);
87-
queueMicrotask(() => subscription.unsubscribe());
88-
} else if (value.state === "error") {
89-
reject(value.error);
90-
queueMicrotask(() => subscription.unsubscribe());
91-
}
92-
},
93-
});
127+
setSubscription(
128+
source.subscribe({
129+
next(res) {
130+
resultUpdateObserver.next(res);
131+
if (res.state === "ok") resolve(true);
132+
else if (res.state === "error") reject(res.error);
133+
},
134+
}),
135+
);
136+
}).finally(() => {
137+
if (isServer) {
138+
subscription()?.unsubscribe();
139+
setSubscription(undefined);
140+
}
94141
});
95142
},
96-
);
97-
98-
const initialResult: FragmentResult<TKey[" $data"]> = {
99-
data: undefined,
100-
error: undefined,
101-
pending: false,
102-
};
103-
const [result, setResult] =
104-
createStore<FragmentResult<TKey[" $data"]>>(initialResult);
105-
106-
createComputed(() => {
107-
setResult(initialResult);
108-
const currentSource = source();
109-
if (!currentSource) return;
110-
111-
setResult("pending", true);
112-
113-
const subscription = currentSource.subscribe({
114-
next(res) {
115-
batch(() => {
116-
switch (res.state) {
117-
case "ok":
118-
setResult("error", undefined);
119-
setResult("pending", false);
120-
setResult(
121-
"data",
122-
reconcile(res.value as Record<string, unknown>, {
123-
key: "__id",
124-
merge: true,
125-
}),
126-
);
127-
break;
128-
case "error":
129-
setResult("data", undefined);
130-
setResult("error", res.error);
131-
setResult("pending", false);
132-
break;
133-
}
134-
});
143+
{
144+
onHydrated(source) {
145+
if (!source) return;
146+
setSubscription(
147+
observeFragment(environment(), fragment, source.key).subscribe(
148+
resultUpdateObserver,
149+
),
150+
);
135151
},
136-
});
152+
},
153+
);
137154

138-
onCleanup(() => {
139-
subscription.unsubscribe();
140-
});
141-
});
155+
const store = createDataStore<FragmentResult<TKey[" $data"]>>(
156+
initialResult,
157+
resource,
158+
);
159+
for (const args of setResultQueue) {
160+
store[1].apply(undefined, args as never);
161+
}
162+
setResult = store[1];
142163

143-
return makeDataProxy(result, resource);
164+
return store[0];
144165
}

src/primitives/createLazyLoadQuery.ts

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,14 @@ import {
2020
createComputed,
2121
createMemo,
2222
createResource,
23-
createSignal,
2423
onCleanup,
2524
} from "solid-js";
26-
import { createStore, reconcile } from "solid-js/store";
25+
import { reconcile } from "solid-js/store";
2726
import { useRelayEnvironment } from "../RelayEnvironment";
2827
import { type QueryCacheEntry, getQueryCache } from "../queryCache";
2928
import { type MaybeAccessor, access } from "../utils/access";
3029
import { createMemoOperationDescriptor } from "../utils/createMemoOperationDescriptor";
31-
import { type DataProxy, makeDataProxy } from "../utils/dataProxy";
30+
import { type DataStore, createDataStore } from "../utils/dataStore";
3231
import { getQueryRef } from "../utils/getQueryRef";
3332

3433
type QueryResult<T> =
@@ -55,7 +54,7 @@ export function createLazyLoadQuery<TQuery extends OperationType>(
5554
fetchPolicy?: MaybeAccessor<FetchPolicy | undefined>;
5655
networkCacheConfig?: MaybeAccessor<CacheConfig | undefined>;
5756
},
58-
): DataProxy<TQuery["response"]> {
57+
): DataStore<TQuery["response"]> {
5958
const environment = useRelayEnvironment();
6059
const operation = createMemoOperationDescriptor(
6160
gqlQuery,
@@ -85,10 +84,9 @@ export function createLazyLoadQueryInternal<
8584
fetchObservable: Accessor<Observable<GraphQLResponse> | null | undefined>;
8685
fetchKey?: Accessor<string | number | null | undefined>;
8786
fetchPolicy?: Accessor<FetchPolicy | undefined>;
88-
}): DataProxy<TQuery["response"]> {
87+
}): DataStore<TQuery["response"]> {
8988
const environment = useRelayEnvironment();
9089
const queryCache = createMemo(() => getQueryCache(environment()));
91-
const [serverData, setServerData] = createSignal<TQuery["response"]>();
9290

9391
const isLiveQuery = createMemo(
9492
() => params.query().request.node.params.metadata.live !== undefined,
@@ -182,7 +180,7 @@ export function createLazyLoadQueryInternal<
182180
},
183181
);
184182

185-
let entry: QueryCacheEntry = false;
183+
let entry: QueryCacheEntry = null;
186184
if (shouldFetch) {
187185
const subscription = subscriptionTarget?.subscribe({});
188186
let retainCount = 0;
@@ -224,14 +222,10 @@ export function createLazyLoadQueryInternal<
224222
error: undefined,
225223
pending: true,
226224
};
227-
const [result, setResult] =
228-
createStore<QueryResult<TQuery["response"]>>(initialResult);
229-
230-
const updateData = (data: TQuery["response"]) => {
231-
if (typeof window !== "undefined") {
232-
setResult("data", reconcile(data, { key: "__id", merge: true }));
233-
} else setServerData(() => data);
234-
};
225+
const [result, setResult] = createDataStore<QueryResult<TQuery["response"]>>(
226+
initialResult,
227+
() => cacheEntry()?.resource,
228+
);
235229

236230
createComputed(() => {
237231
setResult(initialResult);
@@ -250,9 +244,12 @@ export function createLazyLoadQueryInternal<
250244
if (state.state === "ok") {
251245
setResult("error", undefined);
252246
setResult("pending", false);
253-
updateData(state.value);
247+
setResult(
248+
"data",
249+
reconcile(state.value, { key: "__id", merge: true }),
250+
);
254251
} else if (state.state === "error") {
255-
updateData(undefined);
252+
setResult("data", undefined);
256253
setResult("error", state.error);
257254
setResult("pending", false);
258255
}
@@ -264,12 +261,5 @@ export function createLazyLoadQueryInternal<
264261
});
265262
});
266263

267-
return makeDataProxy(
268-
result,
269-
() => {
270-
const entry = cacheEntry();
271-
if (entry) entry.resource();
272-
},
273-
typeof window === "undefined" ? serverData : undefined,
274-
);
264+
return result;
275265
}

src/primitives/createPaginationFragment.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import invariant from "tiny-invariant";
2929
import { useRelayEnvironment } from "../RelayEnvironment";
3030
import type { KeyType, KeyTypeData } from "../types/keyType";
3131
import { createFetchTracker } from "../utils/createFetchTracker";
32-
import type { DataProxy } from "../utils/dataProxy";
32+
import type { DataStore } from "../utils/dataStore";
3333
import { getConnectionState } from "../utils/getConnectionState";
3434
import { useIsMounted } from "../utils/useIsMounted";
3535
import { useIsOperationNodeActive } from "../utils/useIsOperationNodeActive";
@@ -43,7 +43,7 @@ type CreatePaginationFragmentReturn<
4343
TQuery extends OperationType,
4444
TKey extends KeyType | null | undefined,
4545
TFragmentData,
46-
> = DataProxy<TFragmentData> & {
46+
> = DataStore<TFragmentData> & {
4747
loadNext: LoadMoreFn<TQuery>;
4848
loadPrevious: LoadMoreFn<TQuery>;
4949
hasNext: boolean;
@@ -169,7 +169,7 @@ function createLoadMore<TQuery extends OperationType, TKey extends KeyType>({
169169
fragmentNode: ReaderFragment;
170170
fragmentRef: Accessor<TKey | null | undefined>;
171171
fragmentIdentifier: Accessor<string>;
172-
fragmentData: DataProxy<KeyTypeData<TKey>>;
172+
fragmentData: DataStore<KeyTypeData<TKey>>;
173173
connectionPathInFragmentData: readonly (string | number)[];
174174
paginationRequest: ConcreteRequest;
175175
paginationMetadata: ReaderPaginationMetadata;

0 commit comments

Comments
 (0)