Skip to content

Commit

Permalink
fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity committed Oct 9, 2024
1 parent f18f0d3 commit e1d95cc
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 103 deletions.
4 changes: 2 additions & 2 deletions packages/fdr-sdk/src/api-definition/__test__/join.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { APIV1Read } from "../../client";
import { join } from "../join";
import { joiner } from "../join";
import * as Latest from "../latest";

const PRIMITIVE_SHAPE: Latest.TypeReference.Primitive = {
Expand Down Expand Up @@ -276,7 +276,7 @@ const api3: Latest.ApiDefinition = {

describe("join", () => {
it("should prune endpoint1 and its types", () => {
const pruned = join(api1, api2, api3);
const pruned = joiner()(api1, api2, api3);

expect(Object.keys(pruned.endpoints)).toStrictEqual([endpoint1.id, endpoint2.id]);
expect(Object.keys(pruned.websockets)).toStrictEqual([websocket1.id]);
Expand Down
120 changes: 62 additions & 58 deletions packages/fdr-sdk/src/api-definition/join.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,78 +8,82 @@ import * as Latest from "./latest";
* @param apis list of API definitions to join (must have the same ID)
* @returns a new API definition that is the result of joining the input API definitions
*/
export function join(first: Latest.ApiDefinition, ...apis: Latest.ApiDefinition[]): Latest.ApiDefinition {
const joined: Latest.ApiDefinition = {
id: first.id,
endpoints: { ...first.endpoints },
websockets: { ...first.websockets },
webhooks: { ...first.webhooks },
types: { ...first.types },
subpackages: { ...first.subpackages },
auths: { ...first.auths },
globalHeaders: first.globalHeaders ? [...first.globalHeaders] : undefined,
};

let isJoined = false;
for (const api of apis) {
if (api.id !== joined.id) {
throw new Error("Cannot join API definitions with different IDs");
}
export function joiner(
force = false,
): (first: Latest.ApiDefinition, ...apis: Latest.ApiDefinition[]) => Latest.ApiDefinition {
return (first, ...apis) => {
const joined: Latest.ApiDefinition = {
id: first.id,
endpoints: { ...first.endpoints },
websockets: { ...first.websockets },
webhooks: { ...first.webhooks },
types: { ...first.types },
subpackages: { ...first.subpackages },
auths: { ...first.auths },
globalHeaders: first.globalHeaders ? [...first.globalHeaders] : undefined,
};

for (const [endpointId, endpoint] of Object.entries(api.endpoints)) {
if (!isJoined && !first.endpoints[Latest.EndpointId(endpointId)]) {
isJoined = true;
let isJoined = false;
for (const api of apis) {
if (api.id !== joined.id) {
throw new Error("Cannot join API definitions with different IDs");
}
joined.endpoints[Latest.EndpointId(endpointId)] = endpoint;
}

for (const [webSocketId, webSocket] of Object.entries(api.websockets)) {
if (!isJoined && !first.websockets[Latest.WebSocketId(webSocketId)]) {
isJoined = true;
for (const [endpointId, endpoint] of Object.entries(api.endpoints)) {
if (!isJoined && !first.endpoints[Latest.EndpointId(endpointId)]) {
isJoined = true;
}
joined.endpoints[Latest.EndpointId(endpointId)] = endpoint;
}
joined.websockets[Latest.WebSocketId(webSocketId)] = webSocket;
}

for (const [webhookId, webhook] of Object.entries(api.webhooks)) {
if (!isJoined && !first.webhooks[Latest.WebhookId(webhookId)]) {
isJoined = true;
for (const [webSocketId, webSocket] of Object.entries(api.websockets)) {
if (!isJoined && !first.websockets[Latest.WebSocketId(webSocketId)]) {
isJoined = true;
}
joined.websockets[Latest.WebSocketId(webSocketId)] = webSocket;
}
joined.webhooks[Latest.WebhookId(webhookId)] = webhook;
}

for (const [typeId, type] of Object.entries(api.types)) {
if (!isJoined && !first.types[Latest.TypeId(typeId)]) {
isJoined = true;
for (const [webhookId, webhook] of Object.entries(api.webhooks)) {
if (!isJoined && !first.webhooks[Latest.WebhookId(webhookId)]) {
isJoined = true;
}
joined.webhooks[Latest.WebhookId(webhookId)] = webhook;
}
joined.types[Latest.TypeId(typeId)] = type;
}

for (const [subpackageId, subpackage] of Object.entries(api.subpackages)) {
if (!isJoined && !first.subpackages[Latest.SubpackageId(subpackageId)]) {
isJoined = true;
for (const [typeId, type] of Object.entries(api.types)) {
if (!isJoined && !first.types[Latest.TypeId(typeId)]) {
isJoined = true;
}
joined.types[Latest.TypeId(typeId)] = type;
}
joined.subpackages[Latest.SubpackageId(subpackageId)] = subpackage;
}

for (const [authId, auth] of Object.entries(api.auths)) {
if (!isJoined && !first.auths[Latest.AuthSchemeId(authId)]) {
isJoined = true;
for (const [subpackageId, subpackage] of Object.entries(api.subpackages)) {
if (!isJoined && !first.subpackages[Latest.SubpackageId(subpackageId)]) {
isJoined = true;
}
joined.subpackages[Latest.SubpackageId(subpackageId)] = subpackage;
}
joined.auths[Latest.AuthSchemeId(authId)] = auth;
}

const globalHeaders = (joined.globalHeaders ??= []);
api.globalHeaders?.forEach((header) => {
if (!globalHeaders.find((h) => h.key === header.key)) {
isJoined = true;
globalHeaders.push(header);
for (const [authId, auth] of Object.entries(api.auths)) {
if (!isJoined && !first.auths[Latest.AuthSchemeId(authId)]) {
isJoined = true;
}
joined.auths[Latest.AuthSchemeId(authId)] = auth;
}
});
}

if (!isJoined) {
return first;
}
const globalHeaders = (joined.globalHeaders ??= []);
api.globalHeaders?.forEach((header) => {
if (!globalHeaders.find((h) => h.key === header.key)) {
isJoined = true;
globalHeaders.push(header);
}
});
}

if (!isJoined && !force) {
return first;
}

return joined;
return joined;
};
}
5 changes: 2 additions & 3 deletions packages/ui/app/src/api-reference/ApiEndpointPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { EMPTY_OBJECT } from "@fern-api/ui-core-utils";
import { useSetAtom } from "jotai";
import { useEffect } from "react";
import { WRITE_API_DEFINITION_ATOM, useNavigationNodes } from "../atoms";
import { useNavigationNodes, useWriteApiDefinitionAtom } from "../atoms";
import { ALL_ENVIRONMENTS_ATOM } from "../atoms/environment";
import { BottomNavigationNeighbors } from "../components/BottomNavigationNeighbors";
import { FernErrorBoundary } from "../components/FernErrorBoundary";
Expand All @@ -18,8 +18,7 @@ export declare namespace ApiEndpointPage {
}

export const ApiEndpointPage: React.FC<ApiEndpointPage.Props> = ({ content }) => {
const set = useSetAtom(WRITE_API_DEFINITION_ATOM);
useEffect(() => set(content.apiDefinition), [content.apiDefinition, set]);
useWriteApiDefinitionAtom(content.apiDefinition);

// TODO: Why are we doing this here?
const setEnvironmentIds = useSetAtom(ALL_ENVIRONMENTS_ATOM);
Expand Down
12 changes: 10 additions & 2 deletions packages/ui/app/src/api-reference/ApiReferenceContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import type { ApiDefinition } from "@fern-api/fdr-sdk/api-definition";
import type * as FernDocs from "@fern-api/fdr-sdk/docs";
import * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { dfs } from "@fern-api/fdr-sdk/traversers";
import { memo, useEffect, useMemo } from "react";
import { ReactElement, memo, useEffect, useMemo } from "react";
import { useIsReady } from "../atoms";
import { FernErrorBoundary } from "../components/FernErrorBoundary";
import { useIsLocalPreview } from "../contexts/local-preview";
import { scrollToRoute } from "../util/anchor";
import { ApiPackageContent, ApiPackageContentNode } from "./ApiPackageContent";

Expand Down Expand Up @@ -104,4 +105,11 @@ const UnmemoizedApiReferenceContent: React.FC<ApiReferenceContentProps> = ({
);
};

export const ApiReferenceContent = memo(UnmemoizedApiReferenceContent, (prev, next) => prev.node.id === next.node.id);
const MemoizedApiReferenceContent = memo(UnmemoizedApiReferenceContent, (prev, next) => prev.node.id === next.node.id);

export function ApiReferenceContent(props: ApiReferenceContentProps): ReactElement {
const isLocalPreview = useIsLocalPreview();
// do not memoize when in local preview mode to ensure that the page is re-rendered on every change
const Component = isLocalPreview ? UnmemoizedApiReferenceContent : MemoizedApiReferenceContent;
return <Component {...props} />;
}
7 changes: 2 additions & 5 deletions packages/ui/app/src/api-reference/ApiReferencePage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { useSetAtom } from "jotai";
import { useEffect } from "react";
import { WRITE_API_DEFINITION_ATOM, useIsReady, useNavigationNodes } from "../atoms";
import { useIsReady, useNavigationNodes, useWriteApiDefinitionAtom } from "../atoms";
import { ApiPageContext } from "../contexts/api-page";
import { DocsContent } from "../resolver/DocsContent";
import { BuiltWithFern } from "../sidebar/BuiltWithFern";
Expand All @@ -15,8 +13,7 @@ export declare namespace ApiReferencePage {
export const ApiReferencePage: React.FC<ApiReferencePage.Props> = ({ content }) => {
const hydrated = useIsReady();

const set = useSetAtom(WRITE_API_DEFINITION_ATOM);
useEffect(() => set(content.apiDefinition), [content.apiDefinition, set]);
useWriteApiDefinitionAtom(content.apiDefinition);

const node = useNavigationNodes().get(content.apiReferenceNodeId);

Expand Down
55 changes: 41 additions & 14 deletions packages/ui/app/src/atoms/apis.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,55 @@
import type * as ApiDefinition from "@fern-api/fdr-sdk/api-definition";
import { join } from "@fern-api/fdr-sdk/api-definition";
import { joiner } from "@fern-api/fdr-sdk/api-definition";
import type * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { atom, useAtomValue } from "jotai";
import { atom, useAtomValue, useSetAtom } from "jotai";
import { atomFamily } from "jotai/utils";
import { useEffect } from "react";
import { useMemoOne } from "use-memo-one";
import { useIsLocalPreview } from "../contexts/local-preview";
import { FEATURE_FLAGS_ATOM } from "./flags";
import { RESOLVED_API_DEFINITION_ATOM, RESOLVED_PATH_ATOM } from "./navigation";

const SETTABLE_APIS_ATOM = atom<Record<ApiDefinition.ApiDefinitionId, ApiDefinition.ApiDefinition>>({});
SETTABLE_APIS_ATOM.debugLabel = "SETTABLE_APIS_ATOM";

export const WRITE_API_DEFINITION_ATOM = atom(null, (_get, set, apiDefinition: ApiDefinition.ApiDefinition) => {
set(SETTABLE_APIS_ATOM, (prev) => {
const prevDefinition = prev[apiDefinition.id];
if (prevDefinition == null) {
return { ...prev, [apiDefinition.id]: apiDefinition };
}
const merged = join(prevDefinition, apiDefinition);
if (merged === prevDefinition) {
return prev;
export const WRITE_API_DEFINITION_ATOM = atom(
null,
(_get, set, apiDefinition: ApiDefinition.ApiDefinition, force?: boolean) => {
set(SETTABLE_APIS_ATOM, (prev) => {
const prevDefinition = prev[apiDefinition.id];
if (prevDefinition == null) {
return { ...prev, [apiDefinition.id]: apiDefinition };
}
const merged = joiner(force)(prevDefinition, apiDefinition);
if (merged === prevDefinition) {
return prev;
}
return { ...prev, [apiDefinition.id]: merged };
});
},
);

export function useWriteApiDefinitionAtom(api: ApiDefinition.ApiDefinition | undefined): void {
const isLocalPreview = useIsLocalPreview();
const set = useSetAtom(WRITE_API_DEFINITION_ATOM);
useEffect(() => {
if (api != null) {
set(api, isLocalPreview);
}
return { ...prev, [apiDefinition.id]: merged };
});
});
}, [api, set, isLocalPreview]);
}

export function useWriteApiDefinitionsAtom(
apis: Record<ApiDefinition.ApiDefinitionId, ApiDefinition.ApiDefinition>,
): void {
const isLocalPreview = useIsLocalPreview();
const set = useSetAtom(WRITE_API_DEFINITION_ATOM);
useEffect(() => {
Object.values(apis).forEach((api) => {
set(api, isLocalPreview);
});
}, [apis, set]);

Check warning on line 51 in packages/ui/app/src/atoms/apis.ts

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'isLocalPreview'. Either include it or remove the dependency array

Check warning on line 51 in packages/ui/app/src/atoms/apis.ts

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'isLocalPreview'. Either include it or remove the dependency array
}

export const READ_APIS_ATOM = atom((get) => get(SETTABLE_APIS_ATOM));

Expand Down
13 changes: 3 additions & 10 deletions packages/ui/app/src/docs/CustomMarkdownPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useSetAtom } from "jotai";
import { ReactElement, useEffect } from "react";
import { WRITE_API_DEFINITION_ATOM } from "../atoms";
import { ReactElement } from "react";
import { useWriteApiDefinitionsAtom } from "../atoms";
import { MdxContent } from "../mdx/MdxContent";
import { DocsContent } from "../resolver/DocsContent";

Expand All @@ -9,12 +8,6 @@ interface CustomMarkdownPageProps {
}

export function CustomMarkdownPage({ content }: CustomMarkdownPageProps): ReactElement {
const set = useSetAtom(WRITE_API_DEFINITION_ATOM);
useEffect(() => {
Object.values(content.apis).forEach((api) => {
set(api);
});
}, [content.apis, set]);

useWriteApiDefinitionsAtom(content.apis);
return <MdxContent mdx={content.mdx} />;
}
12 changes: 3 additions & 9 deletions packages/ui/app/src/playground/hooks/useEndpointContext.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { createEndpointContext, type ApiDefinition, type EndpointContext } from "@fern-api/fdr-sdk/api-definition";
import type * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { useSetAtom } from "jotai";
import { useEffect, useMemo } from "react";
import { useMemo } from "react";
import useSWRImmutable from "swr/immutable";
import { WRITE_API_DEFINITION_ATOM } from "../../atoms";
import { useWriteApiDefinitionAtom } from "../../atoms";
import { useApiRoute } from "../../hooks/useApiRoute";

interface LoadableEndpointContext {
Expand All @@ -23,12 +22,7 @@ export function useEndpointContext(node: FernNavigation.EndpointNode | undefined
);
const { data: apiDefinition, isLoading } = useSWRImmutable(node != null ? route : null, fetcher);
const context = useMemo(() => createEndpointContext(node, apiDefinition), [node, apiDefinition]);
const set = useSetAtom(WRITE_API_DEFINITION_ATOM);
useEffect(() => {
if (apiDefinition != null) {
set(apiDefinition);
}
}, [apiDefinition, set]);
useWriteApiDefinitionAtom(apiDefinition);

return { context, isLoading };
}

0 comments on commit e1d95cc

Please sign in to comment.