Skip to content

Commit

Permalink
remove superjson dependency, allow config of stringify/revive (#274)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Jerel Miller <jerelmiller@gmail.com>
  • Loading branch information
phryneas and jerelmiller authored Apr 11, 2024
1 parent 1f8cda5 commit b619e76
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 145 deletions.
1 change: 0 additions & 1 deletion .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
ignoreDeps: [
"react",
"react-dom",
"superjson",
"@apollo/experimental-nextjs-app-support",
"@apollo/client-react-streaming",
"@apollo/client",
Expand Down
36 changes: 5 additions & 31 deletions integration-test/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,12 @@ __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.10.0
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
Expand Down Expand Up @@ -82,12 +81,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.10.0
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.10.0"
peerDependencies:
"@apollo/client": ^3.9.0
"@apollo/client": ^3.9.6
next: ^13.4.1 || ^14.0.0
react: ^18
checksum: 10/505b723bac0f3a7f15287ea32fab9f2e8c0cd567149abf11d750855f8a9bfc0aa26e44179ad10c32f7d162ad86318717032413ef8e1a25385185178e022588fa
Expand Down Expand Up @@ -3973,15 +3972,6 @@ __metadata:
languageName: node
linkType: hard

"copy-anything@npm:^3.0.2":
version: 3.0.5
resolution: "copy-anything@npm:3.0.5"
dependencies:
is-what: "npm:^4.1.8"
checksum: 10/4c41385a94a1cff6352a954f9b1c05b6bb1b70713a2d31f4c7b188ae7187ce00ddcc9c09bd58d24cd35b67fc6dd84df5954c0be86ea10700ff74e677db3cb09c
languageName: node
linkType: hard

"core-js-compat@npm:^3.31.0, core-js-compat@npm:^3.34.0":
version: 3.36.0
resolution: "core-js-compat@npm:3.36.0"
Expand Down Expand Up @@ -5405,13 +5395,6 @@ __metadata:
languageName: node
linkType: hard

"is-what@npm:^4.1.8":
version: 4.1.16
resolution: "is-what@npm:4.1.16"
checksum: 10/f6400634bae77be6903365dc53817292e1c4d8db1b467515d0c842505b8388ee8e558326d5e6952cb2a9d74116eca2af0c6adb8aa7e9d5c845a130ce9328bf13
languageName: node
linkType: hard

"isarray@npm:^2.0.5":
version: 2.0.5
resolution: "isarray@npm:2.0.5"
Expand Down Expand Up @@ -7959,15 +7942,6 @@ __metadata:
languageName: node
linkType: hard

"superjson@npm:^1.12.2 || ^2.0.0":
version: 2.2.1
resolution: "superjson@npm:2.2.1"
dependencies:
copy-anything: "npm:^3.0.2"
checksum: 10/bb8743a87c97f7845e0c27af1af0731d3185b32099ebce2aee0e67ac9a6ae9a7c4b9edfca7e1fe48693a78b56d5922d1cd13ef80c2fa12b788d3fc0ca25afe47
languageName: node
linkType: hard

"supports-color@npm:^5.3.0":
version: 5.5.0
resolution: "supports-color@npm:5.5.0"
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
"resolutions": {
"react@18.2.0": "18.3.0-canary-60a927d04-20240113",
"react-dom@18.2.0": "18.3.0-canary-60a927d04-20240113",
"superjson": "1.13.3",
"@microsoft/api-documenter": "7.24.1"
},
"devDependencies": {
Expand Down
2 changes: 0 additions & 2 deletions packages/client-react-streaming/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@
"react-error-boundary": "4.0.13",
"react-server-dom-webpack": "18.3.0-canary-60a927d04-20240113",
"rimraf": "5.0.5",
"superjson": "1.13.3",
"ts-node": "10.9.2",
"tsup": "8.0.2",
"tsx": "4.7.1",
Expand All @@ -148,7 +147,6 @@
"react": "^18"
},
"dependencies": {
"superjson": "^1.12.2 || ^2.0.0",
"ts-invariant": "^0.10.3"
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore depending on the superjson version, this might not be right
import type { SuperJSONResult } from "superjson";
import type { DataTransport } from "./dataTransport.js";

declare global {
interface Window {
[ApolloSSRDataTransport]?: DataTransport<SuperJSONResult>;
[ApolloSSRDataTransport]?: DataTransport<unknown>;
}
}
export const ApolloSSRDataTransport = /*#__PURE__*/ Symbol.for(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,31 @@ import type { RehydrationCache, RehydrationContextValue } from "./types.js";
import type { HydrationContextOptions } from "./RehydrationContext.js";
import { buildApolloRehydrationContext } from "./RehydrationContext.js";
import { registerDataTransport } from "./dataTransport.js";
import { revive, stringify } from "./serialization.js";

interface BuildArgs {
export interface ManualDataTransportOptions {
/**
* A hook that allows for insertion into the stream.
* Will only be called during SSR, doesn't need to actiually return something otherwise.
*/
useInsertHtml(): (callbacks: () => React.ReactNode) => void;
/**
* Prepare data for injecting into the stream by converting it into a string that can be parsed as JavaScript by the browser.
* Could e.g. be `SuperJSON.stringify` or `serialize-javascript`.
* The default implementation act like a JSON.stringify that preserves `undefined`, but not do much on top of that.
*/
stringifyForStream?: (value: any) => string;
/**
* If necessary, additional deserialization steps that need to be applied on top of executing the result of `stringifyForStream` in the browser.
* Could e.g. be `SuperJSON.deserialize`. (Not needed in the case of using `serialize-javascript`)
*/
reviveFromStream?: (value: any) => any;
}

const buildManualDataTransportSSRImpl = ({
useInsertHtml,
}: BuildArgs): DataTransportProviderImplementation<HydrationContextOptions> =>
stringifyForStream = stringify,
}: ManualDataTransportOptions): DataTransportProviderImplementation<HydrationContextOptions> =>
function ManualDataTransportSSRImpl({
extraScriptProps,
children,
Expand All @@ -29,6 +42,7 @@ const buildManualDataTransportSSRImpl = ({
rehydrationContext.current = buildApolloRehydrationContext({
insertHtml,
extraScriptProps,
stringify: stringifyForStream,
});
}

Expand Down Expand Up @@ -59,65 +73,64 @@ const buildManualDataTransportSSRImpl = ({
);
};

const buildManualDataTransportBrowserImpl =
(): DataTransportProviderImplementation<HydrationContextOptions> =>
function ManualDataTransportBrowserImpl({
children,
onQueryEvent,
rerunSimulatedQueries,
}) {
const hookRehydrationCache = useRef<RehydrationCache>({});
registerDataTransport({
onQueryEvent: onQueryEvent!,
onRehydrate(rehydrate) {
Object.assign(hookRehydrationCache.current, rehydrate);
},
});
const buildManualDataTransportBrowserImpl = ({
reviveFromStream = revive,
}: ManualDataTransportOptions): DataTransportProviderImplementation<HydrationContextOptions> =>
function ManualDataTransportBrowserImpl({
children,
onQueryEvent,
rerunSimulatedQueries,
}) {
const hookRehydrationCache = useRef<RehydrationCache>({});
registerDataTransport({
onQueryEvent: onQueryEvent!,
onRehydrate(rehydrate) {
Object.assign(hookRehydrationCache.current, rehydrate);
},
revive: reviveFromStream,
});

useEffect(() => {
if (document.readyState !== "complete") {
// happens simulatenously to `readyState` changing to `"complete"`, see
// https://html.spec.whatwg.org/multipage/parsing.html#the-end (step 9.1 and 9.5)
window.addEventListener("load", rerunSimulatedQueries!, {
once: true,
});
return () =>
window.removeEventListener("load", rerunSimulatedQueries!);
} else {
rerunSimulatedQueries!();
}
}, [rerunSimulatedQueries]);
useEffect(() => {
if (document.readyState !== "complete") {
// happens simulatenously to `readyState` changing to `"complete"`, see
// https://html.spec.whatwg.org/multipage/parsing.html#the-end (step 9.1 and 9.5)
window.addEventListener("load", rerunSimulatedQueries!, {
once: true,
});
return () => window.removeEventListener("load", rerunSimulatedQueries!);
} else {
rerunSimulatedQueries!();
}
}, [rerunSimulatedQueries]);

const useStaticValueRef = useCallback(function useStaticValueRef<T>(
v: T
) {
const id = useId();
const store = hookRehydrationCache.current;
const dataRef = useRef(UNINITIALIZED as T);
if (dataRef.current === UNINITIALIZED) {
if (store && id in store) {
dataRef.current = store[id] as T;
delete store[id];
} else {
dataRef.current = v;
}
const useStaticValueRef = useCallback(function useStaticValueRef<T>(v: T) {
const id = useId();
const store = hookRehydrationCache.current;
const dataRef = useRef(UNINITIALIZED as T);
if (dataRef.current === UNINITIALIZED) {
if (store && id in store) {
dataRef.current = store[id] as T;
delete store[id];
} else {
dataRef.current = v;
}
return dataRef;
}, []);
}
return dataRef;
}, []);

return (
<DataTransportContext.Provider
value={useMemo(
() => ({
useStaticValueRef,
}),
[useStaticValueRef]
)}
>
{children}
</DataTransportContext.Provider>
);
};
return (
<DataTransportContext.Provider
value={useMemo(
() => ({
useStaticValueRef,
}),
[useStaticValueRef]
)}
>
{children}
</DataTransportContext.Provider>
);
};

const UNINITIALIZED = {};

Expand Down Expand Up @@ -170,7 +183,7 @@ const UNINITIALIZED = {};
* @public
*/
export const buildManualDataTransport: (
args: BuildArgs
args: ManualDataTransportOptions
) => DataTransportProviderImplementation<HydrationContextOptions> =
process.env.REACT_ENV === "ssr"
? buildManualDataTransportSSRImpl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import type { RehydrationContextValue } from "./types.js";
import { transportDataToJS } from "./dataTransport.js";
import { invariant } from "ts-invariant";
import type { Stringify } from "./serialization.js";

/**
* @public
Expand Down Expand Up @@ -29,10 +30,12 @@ type ScriptProps = SerializableProps<
>;

export function buildApolloRehydrationContext({
extraScriptProps,
insertHtml,
stringify,
extraScriptProps,
}: HydrationContextOptions & {
insertHtml: (callbacks: () => React.ReactNode) => void;
stringify: Stringify;
}): RehydrationContextValue {
function ensureInserted() {
if (!rehydrationContext.currentlyInjected) {
Expand All @@ -59,15 +62,18 @@ export function buildApolloRehydrationContext({
);
invariant.debug("transporting events", rehydrationContext.incomingEvents);

const __html = transportDataToJS({
rehydrate: Object.fromEntries(
Object.entries(rehydrationContext.transportValueData).filter(
([key, value]) =>
rehydrationContext.transportedValues[key] !== value
)
),
events: rehydrationContext.incomingEvents,
});
const __html = transportDataToJS(
{
rehydrate: Object.fromEntries(
Object.entries(rehydrationContext.transportValueData).filter(
([key, value]) =>
rehydrationContext.transportedValues[key] !== value
)
),
events: rehydrationContext.incomingEvents,
},
stringify
);
Object.assign(
rehydrationContext.transportedValues,
rehydrationContext.transportValueData
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import SuperJSON from "superjson";
import { ApolloSSRDataTransport } from "./ApolloRehydrateSymbols.js";
import type { RehydrationCache } from "./types.js";
import { registerLateInitializingQueue } from "./lateInitializingQueue.js";
import { invariant } from "ts-invariant";
import { htmlEscapeJsonString } from "./htmlescape.js";
import type { QueryEvent } from "@apollo/client-react-streaming";
import type { Revive, Stringify } from "./serialization.js";

export type DataTransport<T> = Array<T> | { push(...args: T[]): void };

Expand All @@ -16,10 +16,10 @@ type DataToTransport = {
/**
* Returns a string of JavaScript that can be used to transport data to the client.
*/
export function transportDataToJS(data: DataToTransport) {
export function transportDataToJS(data: DataToTransport, stringify: Stringify) {
const key = Symbol.keyFor(ApolloSSRDataTransport);
return `(window[Symbol.for("${key}")] ??= []).push(${htmlEscapeJsonString(
SuperJSON.stringify(data)
stringify(data)
)})`;
}

Expand All @@ -30,12 +30,14 @@ export function transportDataToJS(data: DataToTransport) {
export function registerDataTransport({
onQueryEvent,
onRehydrate,
revive,
}: {
onQueryEvent(event: QueryEvent): void;
onRehydrate(rehydrate: RehydrationCache): void;
revive: Revive;
}) {
registerLateInitializingQueue(ApolloSSRDataTransport, (data) => {
const parsed = SuperJSON.deserialize<DataToTransport>(data);
const parsed = revive(data) as DataToTransport;
invariant.debug(`received data from the server:`, parsed);
onRehydrate(parsed.rehydrate);
for (const result of parsed.events) {
Expand Down
Loading

0 comments on commit b619e76

Please sign in to comment.