mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 15:12:48 +00:00
Reuse verified host-scoped plugin bundles across client launches and reconstruct complete catalogs when unchanged code is omitted. Keep store ownership on the connected client, including adopted bootstrap clients, and persist in the background with bounded writes. Remove catalog catch-up deferral and fix repeated timeline preparation that promoted cached completed history into a live head. This preserves completed plugin-rendered chat when catalog installation precedes timeline catch-up. Carries forward #6732 on an upstream branch, with the original hash/cache contribution and integration attribution preserved. Co-authored-by: luke <luopeihuan@gmail.com> Co-authored-by: RoundRedDot <roundreddot@gmail.com>
57 lines
1.9 KiB
TypeScript
57 lines
1.9 KiB
TypeScript
import type {
|
|
PluginClientBundle,
|
|
PluginClientBundleStore,
|
|
} from "@getpaseo/client/internal/daemon-client";
|
|
|
|
/** Each adapter is scoped to one host. Writes publish a complete file/record atomically. */
|
|
export interface PluginBundleFiles {
|
|
list(): Promise<string[]>;
|
|
read(name: string): Promise<string>;
|
|
writeAtomic(name: string, contents: string): Promise<void>;
|
|
remove(name: string): Promise<void>;
|
|
}
|
|
|
|
export function createPluginClientBundleStore(
|
|
files: PluginBundleFiles,
|
|
sha256: (contents: string) => Promise<string>,
|
|
): PluginClientBundleStore {
|
|
const verified = new Set<string>();
|
|
const filename = (bundle: PluginClientBundle) =>
|
|
`${encodeURIComponent(bundle.id)}.${bundle.clientBundleHash}.js`;
|
|
return {
|
|
async load() {
|
|
const bundles: PluginClientBundle[] = [];
|
|
for (const name of await files.list()) {
|
|
try {
|
|
const match = /^(.*)\.([a-f0-9]{64})\.js$/.exec(name);
|
|
if (!match) continue;
|
|
const id = decodeURIComponent(match[1]!);
|
|
const clientBundleHash = match[2]!;
|
|
const clientBundle = await files.read(name);
|
|
if ((await sha256(clientBundle)) !== clientBundleHash) continue;
|
|
verified.add(name);
|
|
bundles.push({ id, clientBundleHash, clientBundle });
|
|
} catch {
|
|
// Missing, unreadable or corrupt files are cache misses, never executable code.
|
|
}
|
|
}
|
|
return bundles;
|
|
},
|
|
async replace(bundles) {
|
|
const retained = new Set<string>();
|
|
for (const bundle of bundles) {
|
|
const name = filename(bundle);
|
|
retained.add(name);
|
|
if (verified.has(name)) continue;
|
|
await files.writeAtomic(name, bundle.clientBundle);
|
|
verified.add(name);
|
|
}
|
|
for (const name of await files.list()) {
|
|
if (retained.has(name)) continue;
|
|
await files.remove(name);
|
|
verified.delete(name);
|
|
}
|
|
},
|
|
};
|
|
}
|