Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

remove superjson dependency #274

Merged
merged 8 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";
import type { DataTransport, JSONResult } from "./dataTransport.js";

declare global {
interface Window {
[ApolloSSRDataTransport]?: DataTransport<SuperJSONResult>;
[ApolloSSRDataTransport]?: DataTransport<JSONResult>;
}
}
export const ApolloSSRDataTransport = /*#__PURE__*/ Symbol.for(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,30 @@ 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`.
*/
phryneas marked this conversation as resolved.
Show resolved Hide resolved
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
reviveFromStream?: (value: any) => any;
parseFromStream?: (value: any) => any;

"revive" sorta makes it sound like something died and you're trying to bring it back to life. How about parse as the verb here since that's a typical term used for going from string -> structured type?


Suggested change
reviveFromStream?: (value: any) => any;
reviveFromStream?: (value: string) => any;

I'm assuming this is the function called for deserialization from the result of stringifyForStream correct? Since stringifyForStream always returns a string, should the argument here always take a string?

Copy link
Member Author

@phryneas phryneas Apr 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the argument is already an object. The string from stringifyForStream gets directly injected into the stream, which makes it end up in JavaScript code. (The browser essentially evals it)

So if the output of stringifyForStream is "{ \"foo\": 1 }", then reviveFromStream will be called with an object { foo: 1 }.

That's also why our own revive implementation is a no-op: nothing needs to be done. Our stringifyForStream implementation stringifies the object { foo: 1, bar: undefined } as "{ \"foo\":1, \"bar\": undefined }" (which is not valid JSON, but valid JavaScript) - and revive will then be called with the { foo: 1, bar: undefined } object again.

revive is just "adding additional life" to an already parsed/eval'ed object.
So you could have a stringifyForStream implementation that converts { foo: 1, bar: undefined } to "{ \"foo\":1, \"bar\": \"$u\" }" and revive would then be called with { foo: 1, bar: "$u" } and would be expected to swap out the "$u" with undefined.

Something like that can be useful with e.g. superjson which allows you to transport over custom types, but it's not required for our implementation or something like serialize-javascript, which just outputs valid JavaScript:
{ foo: new Date() } is just serialized as "{\"foo\":new Date(\"2024-04-09T08:49:42.044Z\")}".

Copy link
Member Author

@phryneas phryneas Apr 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"revive" sorta makes it sound like something died and you're trying to bring it back to life.

That's kinda the point. We have a half-alive zombie object that maybe needs some last finishing touches. I thought about rehydrate, but that's very confusing as React is already taken the name.

SuperJSON uses deserialize here, which is a tad better than parse, but I'm still not super happy with it.

Realistically: I don't expect that a few people will ever use this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just shared this yak shave with my RKT co-maintainers and @EskiMojo14 pointed out that JSON.parse has a reviver function argument. I probably took the name from there? 🤔

}

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

Expand Down Expand Up @@ -59,65 +72,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 +182,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
Loading