Skip to content

Commit

Permalink
make it configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
phryneas committed Apr 8, 2024
1 parent 2bde0f7 commit 909bff1
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 89 deletions.
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`.
*/
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 +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
Expand Up @@ -4,6 +4,7 @@ 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 @@ -15,7 +16,7 @@ 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(
stringify(data)
Expand All @@ -29,9 +30,11 @@ 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 = revive(data) as DataToTransport;
Expand All @@ -42,22 +45,3 @@ export function registerDataTransport({
}
});
}

/**
* Stringifies a value to be injected into JavaScript "text" - preverves `undefined` values.
*/
export function stringify(value: any) {
let undefinedPlaceholder = "$apollo.undefined$";

const stringified = JSON.stringify(value);
while (stringified.includes(JSON.stringify(undefinedPlaceholder))) {
undefinedPlaceholder = "$" + undefinedPlaceholder;
}
return JSON.stringify(value, (_, v) =>
v === undefined ? undefinedPlaceholder : v
).replaceAll(JSON.stringify(undefinedPlaceholder), "undefined");
}

export function revive(value: any): any {
return value;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test, { describe } from "node:test";
import { revive, stringify } from "./dataTransport.js";
import { revive, stringify } from "./serialization.js";
import { outsideOf } from "../util/runInConditions.js";
import { htmlEscapeJsonString } from "./htmlescape.js";
import assert from "node:assert";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Stringifies a value to be injected into JavaScript "text" - preverves `undefined` values.
*/
export function stringify(value: any) {
let undefinedPlaceholder = "$apollo.undefined$";

const stringified = JSON.stringify(value);
while (stringified.includes(JSON.stringify(undefinedPlaceholder))) {
undefinedPlaceholder = "$" + undefinedPlaceholder;
}
return JSON.stringify(value, (_, v) =>
v === undefined ? undefinedPlaceholder : v
).replaceAll(JSON.stringify(undefinedPlaceholder), "undefined");
}

export function revive(value: any): any {
return value;
}

export type Stringify = typeof stringify;
export type Revive = typeof revive;

0 comments on commit 909bff1

Please sign in to comment.