Skip to content

Commit

Permalink
Merge pull request #66 from basehub-ai/jb/simpler-next-pump
Browse files Browse the repository at this point in the history
next pump
  • Loading branch information
julianbenegas authored Feb 23, 2024
2 parents 776ac89 + b3ff244 commit d68e22e
Show file tree
Hide file tree
Showing 22 changed files with 932 additions and 215 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-islands-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"basehub": major
---

Add next-pump: fast refresh for your content. And also go full ESM.
64 changes: 64 additions & 0 deletions packages/basehub/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,69 @@
# basehub

## 1.4.0

### Minor Changes

- bc3492b: Add next pump

### Patch Changes

- bc3492b: Expose next pump in files
- b05f553: Infer app origin
- bc3492b: tweak react import
- 9d50ff5: fixes
- a39bb9f: append ts-nocheck for generated ts files
- bc3492b: react types bug
- 3529057: support multiple paralel queries

## 1.4.0-next.7

### Patch Changes

- append ts-nocheck for generated ts files

## 1.4.0-next.6

### Patch Changes

- fixes

## 1.4.0-next.5

### Patch Changes

- support multiple paralel queries

## 1.4.0-next.4

### Patch Changes

- Infer app origin

## 1.4.0-next.3

### Patch Changes

- react types bug

## 1.4.0-next.2

### Patch Changes

- tweak react import

## 1.4.0-next.1

### Patch Changes

- Expose next pump in files

## 1.4.0-next.0

### Minor Changes

- Add next pump

## 1.3.17

### Patch Changes
Expand Down
1 change: 1 addition & 0 deletions packages/basehub/index.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/* eslint-disable import/no-unresolved */
export * from "./dist/generated-client/index";
2 changes: 2 additions & 0 deletions packages/basehub/next-pump.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/* eslint-disable import/no-unresolved */
export * from "./dist/generated-client/next-pump";
12 changes: 12 additions & 0 deletions packages/basehub/next-pump.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
try {
module.exports = require("./dist/generated-client/next-pump");
} catch (error) {
throw new Error(
`\`basehub\` SDK not found. Make sure to run \`npx basehub\` in order to generate it.
If you're using a custom \`--output\`, you'll need to import the SDK from that same output directory.
If the error persist, please raise an issue at https://github.com/basehub-ai/basehub
`
);
}
14 changes: 9 additions & 5 deletions packages/basehub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "basehub",
"description": "The first AI-native content hub.",
"author": "JB <[email protected]>",
"version": "1.3.17",
"version": "1.4.0",
"license": "MIT",
"repository": "basehub-ai/basehub",
"bugs": "https://github.com/basehub-ai/basehub/issues",
Expand All @@ -15,8 +15,11 @@
"scripts",
"index.js",
"index.d.ts",
"next-pump.js",
"next-pump.d.ts",
"react.js",
"react.d.ts"
"react.d.ts",
"src-next-pump"
],
"sideEffects": false,
"scripts": {
Expand All @@ -34,17 +37,18 @@
"arg": "5.0.1",
"dotenv-mono": "1.3.10",
"esbuild": "0.19.2",
"pusher-js": "8.3.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"slugify": "1.6.6",
"tsup": "6.2.3",
"zod": "3.22.1"
},
"devDependencies": {
"@types/node": "18.13.0",
"@types/react": "18.2.20",
"@types/react-dom": "18.2.7",
"react": "18.2.0",
"react-dom": "18.2.0",
"tsconfig": "workspace:*",
"tsup": "6.2.3",
"type-fest": "3.0.0"
}
}
256 changes: 256 additions & 0 deletions packages/basehub/src-next-pump/client-pump.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
"use client";
import * as React from "react";
import { PumpProps, QueryResults } from "./server-pump";
import { DataProvider } from "./data-provider";

import {
// @ts-ignore
type QueryGenqlSelection as PumpQuery,
} from "./index";
import type Pusher from "pusher-js/types/src/core/pusher";

let pusherMounted = false;
const subscribers = new Set<() => void>(); // we'll call these when pusher tells us to poke

type ResponseCache = {
data: QueryResults<any>[number];
spaceID: string;
pusherData: {
channel_key: string;
app_key: string;
cluster: string;
};
newPumpToken: string;
};

const clientCache = new Map<
string, // a query string (with the variables included)
{
start: number; // the time the query was started
response: Promise<ResponseCache>; // the promise that resolves to the data
}
>();

const DEDUPE_TIME_MS = 500;

const pumpTokenLocalStorageManager = {
get: (readToken: string) => {
return localStorage.getItem(`bshb-pump-token-${readToken}`);
},
set: (readToken: string, pumpToken: string) => {
localStorage.setItem(`bshb-pump-token-${readToken}`, pumpToken);
},
};

export const ClientPump = <Queries extends PumpQuery[]>({
children,
rawQueries,
token,
appOrigin,
initialData,
initialResolvedChildren,
}: {
children: PumpProps<Queries>["children"];
rawQueries: Array<{ query: string; variables?: any }>;
token: string;
appOrigin: string;
initialData?: QueryResults<Queries>;
initialResolvedChildren?: React.ReactNode;
}) => {
const [pumpToken, setPumpToken] = React.useState<string | null>();

/**
* Get cached pump token from localStorage.
*/
React.useEffect(() => {
if (!token) return;
// First check if we already have this in localStorage. If we do and it hasn't expired, we can skip the login step.
const cached = pumpTokenLocalStorageManager.get(token);
setPumpToken(cached);
}, [token]);

const [result, setResult] = React.useState<{
data: QueryResults<Queries>;
spaceID: string;
pusherData: {
channel_key: string;
app_key: string;
cluster: string;
};
} | null>();

type Result = NonNullable<typeof result>;

/**
* Query the Draft API.
*/
const refetch = React.useCallback(async () => {
let newPumpToken: string | undefined;
let pusherData: Result["pusherData"] | undefined = undefined;
let spaceID: Result["spaceID"] | undefined = undefined;
const responses = await Promise.all(
rawQueries.map(async (rawQueryOp) => {
const cacheKey = JSON.stringify(rawQueryOp);

if (clientCache.has(cacheKey)) {
const cached = clientCache.get(cacheKey)!;
if (Date.now() - cached.start < DEDUPE_TIME_MS) {
const response = await cached.response;
if (response.newPumpToken) {
newPumpToken = response.newPumpToken;
}
pusherData = response.pusherData;
spaceID = response.spaceID;
return response;
}
}

const responsePromise = fetch(`${appOrigin}/api/pump`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-basehub-token": token,
...(pumpToken ? { "x-basehub-pump-token": pumpToken } : {}),
},
body: JSON.stringify(rawQueryOp),
}).then(async (response) => {
const { data, newPumpToken, spaceID, pusherData } =
await response.json();

return {
data,
spaceID,
pusherData,
newPumpToken,
} as ResponseCache;
});

// we quickly set the cache (without awaiting)
clientCache.set(cacheKey, {
start: Date.now(),
response: responsePromise,
});

// then await and set local state
const response = await responsePromise;
if (response.newPumpToken) {
newPumpToken = response.newPumpToken;
}
pusherData = response.pusherData;
spaceID = response.spaceID;
return response;
})
);

if (!pusherData || !spaceID) return;

setResult({
data: responses.map((r) => r.data) as any,
pusherData,
spaceID,
});

if (newPumpToken) {
pumpTokenLocalStorageManager.set(token, newPumpToken);
setPumpToken(newPumpToken);
}
}, [appOrigin, pumpToken, rawQueries, token]);

/**
* First query plus subscribe to pusher pokes.
*/
React.useEffect(() => {
if (!token || pumpToken === undefined) return;

function boundRefetch() {
refetch();
}

boundRefetch(); // initial fetch
subscribers.add(boundRefetch);
return () => {
subscribers.delete(boundRefetch);
};
}, [pumpToken, refetch, token]);

const pusherChannelKey = result?.pusherData?.channel_key;

const [pusher, setPusher] = React.useState<Pusher | null>(null);

/**
* Dynamic pusher import!
*/
React.useEffect(() => {
if (pusherMounted) return; // dedupe across multiple pumps
if (!result?.pusherData) return;

pusherMounted = true;

import("pusher-js")
.then((mod) => {
setPusher(
new mod.default(result.pusherData.app_key, {
cluster: result.pusherData.cluster,
})
);
})
.catch((err) => {
console.log("error importing pusher");
console.error(err);
});

return () => {
pusherMounted = false;
};
}, [result?.pusherData]);

/**
* Subscribe to Pusher channel and query.
*/
React.useEffect(() => {
if (!pusherChannelKey) return;
if (!pusher) return;

const channel = pusher.subscribe(pusherChannelKey);
channel.bind("poke", () => {
subscribers.forEach((sub) => sub());
});

return () => {
channel.unsubscribe();
};
}, [pusher, refetch, pusherChannelKey]);

const resolvedData = result?.data ?? initialData ?? null;

const [resolvedChildren, setResolvedChildren] =
React.useState<React.ReactNode>(
typeof children === "function"
? // if function, we'll resolve in React.useEffect below
initialResolvedChildren
: children
);

/**
* Resolve dynamic children
*/
React.useEffect(() => {
if (!resolvedData) return;
if (typeof children === "function") {
const res = children(resolvedData);
if (res instanceof Promise) {
res.then(setResolvedChildren);
} else {
setResolvedChildren(res);
}
} else {
setResolvedChildren(children);
}
}, [children, resolvedData]);

return (
<DataProvider data={resolvedData}>
{resolvedChildren ?? initialResolvedChildren}
</DataProvider>
);
};
Loading

0 comments on commit d68e22e

Please sign in to comment.