-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #66 from basehub-ai/jb/simpler-next-pump
next pump
- Loading branch information
Showing
22 changed files
with
932 additions
and
215 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
/* eslint-disable import/no-unresolved */ | ||
export * from "./dist/generated-client/index"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
` | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
@@ -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": { | ||
|
@@ -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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
Oops, something went wrong.