A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.

record caching

Changed files
+297 -97
lib
+80 -31
lib/hooks/useAtProtoRecord.ts
···
-
import { useEffect, useState } from "react";
+
import { useEffect, useState, useRef } from "react";
import { useDidResolution } from "./useDidResolution";
import { usePdsEndpoint } from "./usePdsEndpoint";
import { createAtprotoClient } from "../utils/atproto-client";
import { useBlueskyAppview } from "./useBlueskyAppview";
+
import { useAtProto } from "../providers/AtProtoProvider";
/**
* Identifier trio required to address an AT Protocol record.
···
collection,
rkey,
}: AtProtoRecordKey): AtProtoRecordState<T> {
+
const { recordCache } = useAtProto();
const isBlueskyCollection = collection?.startsWith("app.bsky.");
-
+
// Always call all hooks (React rules) - conditionally use results
const blueskyResult = useBlueskyAppview<T>({
did: isBlueskyCollection ? handleOrDid : undefined,
collection: isBlueskyCollection ? collection : undefined,
rkey: isBlueskyCollection ? rkey : undefined,
});
-
+
const {
did,
error: didError,
···
const [state, setState] = useState<AtProtoRecordState<T>>({
loading: !!(handleOrDid && collection && rkey),
});
+
+
const releaseRef = useRef<(() => void) | undefined>(undefined);
useEffect(() => {
let cancelled = false;
···
});
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
···
assignState({ loading: false, error: didError });
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
···
assignState({ loading: false, error: endpointError });
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
···
assignState({ loading: true, error: undefined });
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
assignState({ loading: true, error: undefined, record: undefined });
-
(async () => {
-
try {
-
const { rpc } = await createAtprotoClient({
-
service: endpoint,
-
});
-
const res = await (
-
rpc as unknown as {
-
get: (
-
nsid: string,
-
opts: {
-
params: {
-
repo: string;
-
collection: string;
-
rkey: string;
-
};
-
},
-
) => Promise<{ ok: boolean; data: { value: T } }>;
-
}
-
).get("com.atproto.repo.getRecord", {
-
params: { repo: did, collection, rkey },
-
});
-
if (!res.ok) throw new Error("Failed to load record");
-
const record = (res.data as { value: T }).value;
-
assignState({ record, loading: false });
-
} catch (e) {
-
const err = e instanceof Error ? e : new Error(String(e));
-
assignState({ error: err, loading: false });
+
// Use recordCache.ensure for deduplication and caching
+
const { promise, release } = recordCache.ensure<T>(
+
did,
+
collection,
+
rkey,
+
() => {
+
const controller = new AbortController();
+
+
const fetchPromise = (async () => {
+
const { rpc } = await createAtprotoClient({
+
service: endpoint,
+
});
+
const res = await (
+
rpc as unknown as {
+
get: (
+
nsid: string,
+
opts: {
+
params: {
+
repo: string;
+
collection: string;
+
rkey: string;
+
};
+
},
+
) => Promise<{ ok: boolean; data: { value: T } }>;
+
}
+
).get("com.atproto.repo.getRecord", {
+
params: { repo: did, collection, rkey },
+
});
+
if (!res.ok) throw new Error("Failed to load record");
+
return (res.data as { value: T }).value;
+
})();
+
+
return {
+
promise: fetchPromise,
+
abort: () => controller.abort(),
+
};
}
-
})();
+
);
+
+
releaseRef.current = release;
+
+
promise
+
.then((record) => {
+
if (!cancelled) {
+
assignState({ record, loading: false });
+
}
+
})
+
.catch((e) => {
+
if (!cancelled) {
+
const err = e instanceof Error ? e : new Error(String(e));
+
assignState({ error: err, loading: false });
+
}
+
});
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}, [
handleOrDid,
···
resolvingEndpoint,
didError,
endpointError,
+
recordCache,
]);
// Return Bluesky result for app.bsky.* collections
+101 -62
lib/hooks/useBlueskyAppview.ts
···
-
import { useEffect, useReducer } from "react";
+
import { useEffect, useReducer, useRef } from "react";
import { useDidResolution } from "./useDidResolution";
import { usePdsEndpoint } from "./usePdsEndpoint";
import { createAtprotoClient, SLINGSHOT_BASE_URL } from "../utils/atproto-client";
+
import { useAtProto } from "../providers/AtProtoProvider";
/**
* Extended blob reference that includes CDN URL from appview responses.
···
appviewService,
skipAppview = false,
}: UseBlueskyAppviewOptions): UseBlueskyAppviewResult<T> {
+
const { recordCache } = useAtProto();
const {
did,
error: didError,
···
source: undefined,
});
+
const releaseRef = useRef<(() => void) | undefined>(undefined);
+
useEffect(() => {
let cancelled = false;
···
if (!cancelled) dispatch({ type: "RESET" });
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
···
if (!cancelled) dispatch({ type: "SET_ERROR", error: didError });
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
···
if (!cancelled) dispatch({ type: "SET_ERROR", error: endpointError });
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
···
if (!cancelled) dispatch({ type: "SET_LOADING", loading: true });
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}
// Start fetching
dispatch({ type: "SET_LOADING", loading: true });
-
(async () => {
-
let lastError: Error | undefined;
+
// Use recordCache.ensure for deduplication and caching
+
const { promise, release } = recordCache.ensure<T>(
+
did,
+
collection,
+
rkey,
+
() => {
+
const controller = new AbortController();
-
// Tier 1: Try Bluesky appview API
-
if (!skipAppview && BLUESKY_COLLECTION_TO_ENDPOINT[collection]) {
-
try {
-
const result = await fetchFromAppview<T>(
-
did,
-
collection,
-
rkey,
-
appviewService ?? DEFAULT_APPVIEW_SERVICE,
-
);
-
if (!cancelled && result) {
-
dispatch({
-
type: "SET_SUCCESS",
-
record: result,
-
source: "appview",
-
});
-
return;
+
const fetchPromise = (async () => {
+
let lastError: Error | undefined;
+
+
// Tier 1: Try Bluesky appview API
+
if (!skipAppview && BLUESKY_COLLECTION_TO_ENDPOINT[collection]) {
+
try {
+
const result = await fetchFromAppview<T>(
+
did,
+
collection,
+
rkey,
+
appviewService ?? DEFAULT_APPVIEW_SERVICE,
+
);
+
if (result) {
+
return result;
+
}
+
} catch (err) {
+
lastError = err as Error;
+
// Continue to next tier
+
}
}
-
} catch (err) {
-
lastError = err as Error;
-
// Continue to next tier
-
}
+
+
// Tier 2: Try Slingshot getRecord
+
try {
+
const result = await fetchFromSlingshot<T>(did, collection, rkey);
+
if (result) {
+
return result;
+
}
+
} catch (err) {
+
lastError = err as Error;
+
// Continue to next tier
+
}
+
+
// Tier 3: Try PDS directly
+
try {
+
const result = await fetchFromPds<T>(
+
did,
+
collection,
+
rkey,
+
pdsEndpoint,
+
);
+
if (result) {
+
return result;
+
}
+
} catch (err) {
+
lastError = err as Error;
+
}
+
+
// All tiers failed
+
throw lastError ?? new Error("Failed to fetch record from all sources");
+
})();
+
+
return {
+
promise: fetchPromise,
+
abort: () => controller.abort(),
+
};
}
+
);
-
// Tier 2: Try Slingshot getRecord
-
try {
-
const result = await fetchFromSlingshot<T>(did, collection, rkey);
-
if (!cancelled && result) {
+
releaseRef.current = release;
+
+
promise
+
.then((record) => {
+
if (!cancelled) {
dispatch({
type: "SET_SUCCESS",
-
record: result,
-
source: "slingshot",
+
record,
+
source: "appview",
});
-
return;
}
-
} catch (err) {
-
lastError = err as Error;
-
// Continue to next tier
-
}
-
-
// Tier 3: Try PDS directly
-
try {
-
const result = await fetchFromPds<T>(
-
did,
-
collection,
-
rkey,
-
pdsEndpoint,
-
);
-
if (!cancelled && result) {
+
})
+
.catch((err) => {
+
if (!cancelled) {
dispatch({
-
type: "SET_SUCCESS",
-
record: result,
-
source: "pds",
+
type: "SET_ERROR",
+
error: err instanceof Error ? err : new Error(String(err)),
});
-
return;
}
-
} catch (err) {
-
lastError = err as Error;
-
}
-
-
// All tiers failed
-
if (!cancelled) {
-
dispatch({
-
type: "SET_ERROR",
-
error:
-
lastError ??
-
new Error("Failed to fetch record from all sources"),
-
});
-
}
-
})();
+
});
return () => {
cancelled = true;
+
if (releaseRef.current) {
+
releaseRef.current();
+
releaseRef.current = undefined;
+
}
};
}, [
handleOrDid,
···
resolvingEndpoint,
didError,
endpointError,
+
recordCache,
]);
return state;
+9 -4
lib/providers/AtProtoProvider.tsx
···
useRef,
} from "react";
import { ServiceResolver, normalizeBaseUrl } from "../utils/atproto-client";
-
import { BlobCache, DidCache } from "../utils/cache";
+
import { BlobCache, DidCache, RecordCache } from "../utils/cache";
/**
* Props for the AT Protocol context provider.
···
didCache: DidCache;
/** Cache for fetched blob data. */
blobCache: BlobCache;
+
/** Cache for fetched AT Protocol records. */
+
recordCache: RecordCache;
}
const AtProtoContext = createContext<AtProtoContextValue | undefined>(
···
const cachesRef = useRef<{
didCache: DidCache;
blobCache: BlobCache;
+
recordCache: RecordCache;
} | null>(null);
if (!cachesRef.current) {
cachesRef.current = {
didCache: new DidCache(),
blobCache: new BlobCache(),
+
recordCache: new RecordCache(),
};
}
···
plcDirectory: normalizedPlc,
didCache: cachesRef.current!.didCache,
blobCache: cachesRef.current!.blobCache,
+
recordCache: cachesRef.current!.recordCache,
}),
[resolver, normalizedPlc],
);
···
/**
* Hook that accesses the AT Protocol context provided by `AtProtoProvider`.
*
-
* This hook exposes the service resolver, DID cache, and blob cache for building
-
* custom AT Protocol functionality.
+
* This hook exposes the service resolver, DID cache, blob cache, and record cache
+
* for building custom AT Protocol functionality.
*
* @throws {Error} When called outside of an `AtProtoProvider`.
* @returns {AtProtoContextValue} Object containing resolver, caches, and PLC directory URL.
···
* import { useAtProto } from 'atproto-ui';
*
* function MyCustomComponent() {
-
* const { resolver, didCache, blobCache } = useAtProto();
+
* const { resolver, didCache, blobCache, recordCache } = useAtProto();
* // Use the resolver and caches for custom AT Protocol operations
* }
* ```
+107
lib/utils/cache.ts
···
}
}
}
+
+
interface RecordCacheEntry<T = unknown> {
+
record: T;
+
timestamp: number;
+
}
+
+
interface InFlightRecordEntry<T = unknown> {
+
promise: Promise<T>;
+
abort: () => void;
+
refCount: number;
+
}
+
+
interface RecordEnsureResult<T = unknown> {
+
promise: Promise<T>;
+
release: () => void;
+
}
+
+
export class RecordCache {
+
private store = new Map<string, RecordCacheEntry>();
+
private inFlight = new Map<string, InFlightRecordEntry>();
+
+
private key(did: string, collection: string, rkey: string): string {
+
return `${did}::${collection}::${rkey}`;
+
}
+
+
get<T = unknown>(
+
did?: string,
+
collection?: string,
+
rkey?: string,
+
): T | undefined {
+
if (!did || !collection || !rkey) return undefined;
+
return this.store.get(this.key(did, collection, rkey))?.record as
+
| T
+
| undefined;
+
}
+
+
set<T = unknown>(
+
did: string,
+
collection: string,
+
rkey: string,
+
record: T,
+
): void {
+
this.store.set(this.key(did, collection, rkey), {
+
record,
+
timestamp: Date.now(),
+
});
+
}
+
+
ensure<T = unknown>(
+
did: string,
+
collection: string,
+
rkey: string,
+
loader: () => { promise: Promise<T>; abort: () => void },
+
): RecordEnsureResult<T> {
+
const cached = this.get<T>(did, collection, rkey);
+
if (cached !== undefined) {
+
return { promise: Promise.resolve(cached), release: () => {} };
+
}
+
+
const key = this.key(did, collection, rkey);
+
const existing = this.inFlight.get(key) as
+
| InFlightRecordEntry<T>
+
| undefined;
+
if (existing) {
+
existing.refCount += 1;
+
return {
+
promise: existing.promise,
+
release: () => this.release(key),
+
};
+
}
+
+
const { promise, abort } = loader();
+
const wrapped = promise.then((record) => {
+
this.set(did, collection, rkey, record);
+
return record;
+
});
+
+
const entry: InFlightRecordEntry<T> = {
+
promise: wrapped,
+
abort,
+
refCount: 1,
+
};
+
+
this.inFlight.set(key, entry as InFlightRecordEntry);
+
+
wrapped
+
.catch(() => {})
+
.finally(() => {
+
this.inFlight.delete(key);
+
});
+
+
return {
+
promise: wrapped,
+
release: () => this.release(key),
+
};
+
}
+
+
private release(key: string) {
+
const entry = this.inFlight.get(key);
+
if (!entry) return;
+
entry.refCount -= 1;
+
if (entry.refCount <= 0) {
+
this.inFlight.delete(key);
+
entry.abort();
+
}
+
}
+
}