@daveyplate/better-auth-persistent
Version:
Persistent session management for Better Auth
200 lines (195 loc) • 5.78 kB
JavaScript
import { atom, computed, task } from 'nanostores';
import { persistentAtom } from '@nanostores/persistent';
import SuperJSON from 'superjson';
import { multiSessionClient } from 'better-auth/client/plugins';
import { createAuthClient } from 'better-auth/react';
import { useEffect, useSyncExternalStore } from 'react';
import { useStore } from '@nanostores/react';
// src/auth-client-store.ts
var $authClient = atom(null);
var $persistentSession = persistentAtom(
"session",
{
data: null,
isPending: true,
isRefetching: true,
error: null,
refetch: void 0
},
{
encode: SuperJSON.stringify,
decode: SuperJSON.parse
}
);
var emptyResponse = {
data: null,
isPending: true,
isRefetching: true,
error: null,
refetch: void 0
};
var $persistentSessions = persistentAtom(
"device-sessions",
emptyResponse,
{
encode: SuperJSON.stringify,
decode: SuperJSON.parse
}
);
var $freshSessions = computed(
[$authClient, $persistentSession],
(authClient2, persistentSession) => task(async () => {
if (!authClient2) return emptyResponse;
if (!persistentSession.data) return emptyResponse;
try {
const deviceSessions = await authClient2.multiSession.listDeviceSessions({
fetchOptions: { throw: true }
});
return {
data: deviceSessions,
isPending: false,
isRefetching: false,
error: null,
refetch: void 0
};
} catch (error) {
return {
data: null,
isPending: false,
isRefetching: false,
error,
refetch: void 0
};
}
})
);
var $deviceSessions = computed(
[$persistentSession, $persistentSessions, $freshSessions],
(persistentSession, persistentSessions, freshSessions) => {
if (persistentSession.isPending) return emptyResponse;
if (!freshSessions) return persistentSessions;
if (!persistentSession.data) {
$persistentSessions.set(emptyResponse);
} else if (freshSessions.data || !persistentSessions.data && freshSessions.error) {
$persistentSessions.set(freshSessions);
}
return $persistentSessions.get();
}
);
var authClient = createAuthClient({
plugins: [multiSessionClient()]
});
// src/set-active-session.ts
async function setActiveSession({
sessionToken
}) {
const deviceSessions = $deviceSessions.get();
if (!deviceSessions.data) return;
const session = deviceSessions.data.find(
(session2) => session2.session.token === sessionToken
);
if (!session)
throw {
error: {
message: "Invalid session token",
code: "INVALID_SESSION_TOKEN"
}
};
$persistentSession.set({
data: session,
isPending: false,
isRefetching: false,
error: null,
refetch: void 0
});
authClient.multiSession.setActive({
sessionToken: session.session.token
}).then(({ error }) => {
if (error) {
$persistentSession.set({
data: session,
isPending: false,
isRefetching: false,
optimistic: true,
error: null,
refetch: void 0
});
}
});
}
function subscribePersistSession(authClient2) {
if (!$authClient.get()) {
$authClient.set(authClient2);
}
const persistSession = () => {
var _a;
const value = authClient2.$store.atoms.session.get();
const sessionData = value == null ? void 0 : value.data;
const persistentSessionData = (_a = $persistentSession.get()) == null ? void 0 : _a.data;
if (!persistentSessionData || !sessionData && !(value == null ? void 0 : value.error) || sessionData && SuperJSON.stringify(sessionData) !== SuperJSON.stringify(persistentSessionData)) {
$persistentSession.set(value);
}
};
const restoreSession = () => {
var _a;
const value = authClient2.$store.atoms.session.get();
const sessionData = value == null ? void 0 : value.data;
const persistentValue = $persistentSession.get();
const persistentSessionData = (_a = $persistentSession.get()) == null ? void 0 : _a.data;
if (!persistentSessionData) return;
if (!sessionData || persistentSessionData.user.id !== sessionData.user.id) {
if (sessionData) {
console.log("set active session", {
sessionToken: persistentSessionData.session.token
});
}
authClient2.$store.atoms.session.set({
...persistentValue,
refetch: value == null ? void 0 : value.refetch
});
}
};
const unbindPersistentSessionListener = $persistentSession.subscribe(() => {
restoreSession();
});
const unbindSessionListener = authClient2.$store.atoms.session.subscribe(
() => {
persistSession();
restoreSession();
}
);
const checkActiveSession = () => {
const persistentSession = $persistentSession.get();
if (persistentSession.optimistic && persistentSession.data) {
authClient2.multiSession.setActive({
sessionToken: persistentSession.data.session.token
});
}
};
window.addEventListener("online", checkActiveSession);
return () => {
unbindSessionListener();
unbindPersistentSessionListener();
};
}
function usePersistSession(authClient2) {
useEffect(() => subscribePersistSession(authClient2), [authClient2]);
}
function subscribe() {
return () => {
};
}
function useIsHydrated() {
return useSyncExternalStore(
subscribe,
() => true,
() => false
);
}
// src/use-list-device-sessions.ts
function useListDeviceSessions() {
const isHydrated = useIsHydrated();
const result = useStore($deviceSessions);
return isHydrated ? result : emptyResponse;
}
export { $authClient, $deviceSessions, $freshSessions, $persistentSession, emptyResponse, setActiveSession, subscribePersistSession, useListDeviceSessions, usePersistSession };