-
Notifications
You must be signed in to change notification settings - Fork 35
feat: implement catalog system with cloud sync (cherry-pick of PR #296) #325
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for fireproof-ai-builder ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Binding defaults to
window.fetchintroduces SSR/test fragility; preferglobalThis.fetchwith asystemFetchfallback to preserve compatibility and avoid Illegal Invocation errors. addCatalogScreenshotStandalonespreadsexistingDoc._fileswithout a nullish guard, which can crash when_filesis absent.- The CID helper uses
atobon large base64 payloads, which is less efficient than hashing anArrayBufferdirectly. - In
publishApp, selectingresult.rows[0]as the “latest” screenshot is fragile unless the query guarantees ordering; explicitly choose the newest doc.
Additional notes (2)
-
Performance |
vibes.diy/pkg/app/utils/cidUtils.ts:17-21
Usingatobwith manual byte loops on the base64 content can be memory-inefficient for large payloads and is browser-only. Since you already rely on Web Crypto, you can avoid base64 expansion by fetching the data URL and hashing the resultingArrayBuffer, which is also friendlier to non-browser environments if you bindfetchoffglobalThis. -
Maintainability |
vibes.diy/pkg/app/utils/publishUtils.ts:197-206
Relying onresult.rows[0]as the "most recent" screenshot is brittle unless the upstream query guarantees ordering. Consider explicitly selecting the latest screenshot document (e.g., by a timestamp field) to avoid persisting an older screenshot to the catalog.
Summary of changes
- Updated auth token utilities to bind
fetchto the window context by default inpollForAuthTokenandextendToken. - Added catalog utilities: database naming helpers, document ID creation, database accessor, and a standalone function to attach screenshot/source files to a catalog doc.
- Introduced CID utilities for hashing data URLs and Files using Web Crypto.
- Enhanced publish flow to persist the latest session screenshot and transformed source code into the catalog after a successful publish.
- Added sync preference helpers for localStorage.
- Updated tests and mocks across the suite to account for new cloud sync interfaces and catalog behavior; added comprehensive
useCatalogtests.
vibes.diy/pkg/app/utils/auth.ts
Outdated
| fetch: typeof fetch; | ||
| toast: { success: (s: string) => void }; | ||
| } = { fetch: systemFetch, toast }, | ||
| } = { fetch: window.fetch.bind(window), toast }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defaulting the mock fetch to window.fetch.bind(window) risks runtime errors in SSR or non-browser environments where window is undefined. This also makes the function less portable to tests/run-time contexts that rely on a polyfilled/global fetch. Prefer a safer default that binds fetch off globalThis with a fallback to the existing systemFetch.
Suggestion
Use a cross-environment safe default:
export async function pollForAuthToken(
resultId: string,
intervalMs = 1500,
timeoutMs = 60000,
mock: {
fetch: typeof fetch;
toast: { success: (s: string) => void };
} = { fetch: (globalThis.fetch ? globalThis.fetch.bind(globalThis) : systemFetch), toast },
): Promise<string | null> {
// ...
}Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.
vibes.diy/pkg/app/utils/auth.ts
Outdated
| export async function extendToken( | ||
| currentToken: string, | ||
| mock = { fetch: systemFetch }, | ||
| mock = { fetch: window.fetch.bind(window) }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same issue here: binding to window.fetch can break outside the browser. Use a globalThis-bound fetch with systemFetch fallback so this utility is safe in broader environments (e.g., SSR, tests) while still avoiding Illegal Invocation errors.
Suggestion
Switch to a global-safe default:
export async function extendToken(
currentToken: string,
mock = { fetch: (globalThis.fetch ? globalThis.fetch.bind(globalThis) : systemFetch) },
): Promise<string | null> {
// ...
}Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.
| return; | ||
| } | ||
|
|
||
| const updatedFiles: any = { ...existingDoc._files }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
existingDoc._files may be undefined, which will cause TypeError: Cannot convert undefined or null to object when spreading. This is a correctness bug that can crash the update path when the catalog doc has no files yet.
Suggestion
Guard against missing _files and avoid any to improve safety:
// Prefer a safer type if available, otherwise keep Record<string, unknown>
const updatedFiles: Record<string, unknown> = { ...(existingDoc._files ?? {}) };Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this fix.
| const response = await fetch(screenshotData); | ||
| const blob = await response.blob(); | ||
| const screenshotFile = new File([blob], "screenshot.png", { | ||
| type: "image/png", | ||
| lastModified: Date.now(), | ||
| }); | ||
| updatedFiles.screenshot = screenshotFile; | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Directly calling the global fetch here can run into the same environment/Illegal Invocation pitfalls discussed elsewhere. For robustness and better testability, bind fetch off globalThis (or accept a fetch in params) before fetching data URLs.
Suggestion
Bind fetch safely before use:
const fetchFn = (globalThis.fetch ? globalThis.fetch.bind(globalThis) : undefined);
if (!fetchFn) throw new Error("fetch is not available in this environment");
const response = await fetchFn(screenshotData);Alternatively, accept a fetch in the function params for easier mocking/testing.
Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- The auth utilities now bind to
window.fetch, which is unsafe in SSR/tests; bind toglobalThis.fetchwith asystemFetchfallback instead. addCatalogScreenshotStandalonespreads a possibly-undefined_filesand usesany, introducing a crash risk and maintainability issues.- The catalog screenshot flow needlessly converts File -> data URL -> Blob; accept
File|Blob(or injectfetch) to avoid the round-trip and improve robustness. cidUtilsusesatobon base64 data and manual byte loops; prefer hashing anArrayBufferfetched from the data URL. Also, selectingresult.rows[0]for the “latest” screenshot is fragile—explicitly pick the newest doc.
Summary of changes
- Switched default fetch binding in auth utilities to
window.fetch.bind(window)forpollForAuthTokenandextendToken. - Added catalog utilities (db naming, database accessor, document ID creation) and a standalone helper to attach screenshot/source files to catalog docs.
- Introduced
cidUtilsfor generating SHA-256-based content IDs from data URLs and Files. - Enhanced publish flow to persist the most recent session screenshot and transformed source code into the user's catalog.
- Added
syncPreferenceutilities for localStorage-backed sync toggling. - Updated tests and mocks across the suite to accommodate new cloud sync behavior and catalog logic; added
useCatalogtests.
| // Add screenshot if provided | ||
| if (screenshotData) { | ||
| const response = await fetch(screenshotData); | ||
| const blob = await response.blob(); | ||
| const screenshotFile = new File([blob], "screenshot.png", { | ||
| type: "image/png", | ||
| lastModified: Date.now(), | ||
| }); | ||
| updatedFiles.screenshot = screenshotFile; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the global fetch directly here risks Illegal Invocation issues and SSR incompatibilities. Additionally, converting File -> data URL in publishApp, then fetching that data URL back to a Blob here is a redundant round-trip. Accepting a File|Blob (or injecting fetch) would be both more robust and more efficient.
Suggestion
Bind fetch safely and support passing a File|Blob to avoid the data URL round-trip:
export async function addCatalogScreenshotStandalone(
userId: string,
vibeId: string,
screenshot: string | File | Blob | null,
sourceCode?: string,
deps: { fetch?: typeof fetch } = {},
): Promise<void> {
// ...
const updatedFiles: Record<string, unknown> = { ...(existingDoc._files ?? {}) };
if (screenshot) {
let screenshotFile: File;
if (typeof screenshot === "string") {
const fetchFn = deps.fetch ?? (globalThis.fetch ? globalThis.fetch.bind(globalThis) : undefined);
if (!fetchFn) throw new Error("fetch is not available in this environment");
const response = await fetchFn(screenshot);
const blob = await response.blob();
screenshotFile = new File([blob], "screenshot.png", { type: "image/png", lastModified: Date.now() });
} else if (screenshot instanceof File) {
screenshotFile = screenshot;
} else {
screenshotFile = new File([screenshot], "screenshot.png", { type: "image/png", lastModified: Date.now() });
}
(updatedFiles as any).screenshot = screenshotFile;
}
// ...
}You can then pass the File directly from publishApp without converting to a data URL first.
Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this refactor across both files.
vibes.diy/pkg/app/utils/cidUtils.ts
Outdated
| const binaryString = atob(base64Content); | ||
| const bytes = new Uint8Array(binaryString.length); | ||
| for (let i = 0; i < binaryString.length; i++) { | ||
| bytes[i] = binaryString.charCodeAt(i); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hashing the base64 payload via atob + manual byte loop is memory-inefficient and browser-only. Since you already rely on Web Crypto, avoid base64 expansion by fetching the data URL and hashing the resulting ArrayBuffer. This improves performance and cross-environment compatibility.
Suggestion
Avoid atob and hash the data URL content as an ArrayBuffer using a safe fetch binding:
export async function generateCid(dataUrl: string): Promise<string> {
const fetchFn = globalThis.fetch ? globalThis.fetch.bind(globalThis) : undefined;
if (!fetchFn) throw new Error("fetch is not available in this environment");
const resp = await fetchFn(dataUrl);
const buff = await resp.arrayBuffer();
const hashBuffer = await crypto.subtle.digest("SHA-256", buff);
const hashHex = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, "0")).join("");
return `sha256-${hashHex}`;
}Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.
| if (result.rows.length > 0) { | ||
| const screenshotDoc = result.rows[0].doc as any; | ||
| if (screenshotDoc._files && screenshotDoc._files.screenshot) { | ||
| try { | ||
| // Get the File and convert to data URL | ||
| const screenshotFile = | ||
| await screenshotDoc._files.screenshot.file(); | ||
| const screenshotDataUrl = await new Promise<string>( | ||
| (resolve, reject) => { | ||
| const reader = new FileReader(); | ||
| reader.onload = () => resolve(reader.result as string); | ||
| reader.onerror = reject; | ||
| reader.readAsDataURL(screenshotFile); | ||
| }, | ||
| ); | ||
|
|
||
| // Use shared catalog utility function | ||
| await addCatalogScreenshotStandalone( | ||
| userId, | ||
| sessionId, | ||
| screenshotDataUrl, | ||
| transformedCode, | ||
| ); | ||
| } catch (err) { | ||
| console.error("Failed to store catalog screenshot:", err); | ||
| } | ||
| } | ||
| } | ||
| } catch (error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Selecting result.rows[0] as the “most recent” screenshot is brittle unless your query guarantees ordering. Persisting an older screenshot to the catalog is a real risk. Explicitly select the newest screenshot document by a reliable timestamp field before exporting to the catalog.
Suggestion
Choose the newest screenshot explicitly:
if (result.rows.length > 0) {
const rowsWithDoc = result.rows.map(r => r.doc as any).filter(Boolean);
const newest = rowsWithDoc
.sort((a, b) => ((b.created_at ?? b.created ?? 0) - (a.created_at ?? a.created ?? 0)))[0];
if (newest?._files?.screenshot) {
const screenshotFile = await newest._files.screenshot.file();
// If you adopt the `File|Blob` change, pass `screenshotFile` directly:
// await addCatalogScreenshotStandalone(userId, sessionId, screenshotFile, transformedCode);
const screenshotDataUrl = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(screenshotFile);
});
await addCatalogScreenshotStandalone(userId, sessionId, screenshotDataUrl, transformedCode);
}
}Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting
| if (sessionResult.rows.length > 0) { | ||
| const screenshot = sessionResult.rows[0].doc as ScreenshotDocument; | ||
| console.log(`🐛 Screenshot doc structure for ${vibeId}:`, { | ||
| docKeys: Object.keys(screenshot), | ||
| hasFiles: !!screenshot._files, | ||
| hasCid: !!screenshot.cid, | ||
| }); | ||
|
|
||
| // Log the full file structure to find where CID is stored | ||
| console.log( | ||
| `🐛 File structure for ${vibeId}:`, | ||
| screenshot._files?.screenshot, | ||
| ); | ||
|
|
||
| // Extract CID from file metadata | ||
| const fileCid = screenshot._files?.screenshot | ||
| ? (screenshot._files.screenshot as any)?.cid | ||
| : null; | ||
| console.log(`🐛 File CID for ${vibeId}:`, fileCid); | ||
|
|
||
| if (screenshot._files?.screenshot && fileCid) { | ||
| // Add the file CID to the screenshot document for deduplication | ||
| screenshot.cid = fileCid.toString(); | ||
| return screenshot; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] Read screenshot CID from wrong place
The new catalog sync assumes the screenshot document’s CID is stored in _files.screenshot, so getLatestScreenshot bails out unless that nested property exists. However addScreenshot writes the CID as a top-level cid field on the screenshot doc (the Fireproof file metadata does not include a cid). In normal usage this means getLatestScreenshot always returns null, preventing useCatalog from ever updating catalog documents with screenshots or deduplication CIDs. Read screenshot.cid (or compute it) instead of probing _files.screenshot.cid so that cataloging runs for the screenshots actually written by useSession.
Useful? React with 👍 / 👎.
Addressed Review FeedbackFixed all issues mentioned in the review: ✅ Fetch binding: Changed from All changes committed in separate commits:
The |
…lling - Fix 'Illegal invocation' error when using fetch in pollForAuthToken - Bind fetch to window context to preserve proper 'this' binding - Affects both pollForAuthToken and extendToken functions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
- Use globalThis.fetch instead of window.fetch.bind(window) to avoid SSR/test issues - Add nullish guard for _files spread in addCatalogScreenshotStandalone - Add clarifying comment about screenshot query ordering 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
Replace atob-based approach with direct ArrayBuffer fetching for: - Better performance with large payloads - Improved browser compatibility - Non-browser environment friendliness 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
…mock The test was failing because updateUserVibespaceDoc was not mocked, causing the publishApp function to fail before reaching the catalog screenshot functionality. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
998d30e to
9476a50
Compare
Summary
This is a cherry-picked version of PR #296 that implements a comprehensive cloud synchronization system for the vibe catalog, updated for the current main branch structure.
Key Features
• Cloud Sync Integration: Full
toCloud()integration for authenticated users• Dual-Mode Operation: Works locally for anonymous users, syncs for authenticated users
• Enhanced Catalog System: Screenshot storage and source code preservation
• User Settings Sync: Synchronized user preferences across devices
• Catalog Screenshot Storage: Persists app screenshots with Uint8Array optimization
• Content ID Generation: CID-based deduplication system for efficient storage
Architecture Updates
• useUserSettings hook: Manages sync preferences and user configuration
• Enhanced useCatalog: Cloud-aware catalog with screenshot/source storage
• Auth Integration: useAuth integration throughout component tree
• Sync Preference: localStorage-based sync toggle with real-time updates
Cherry-Pick Improvements
This version includes improvements over the original PR #296:
• ✅ Updated imports: Uses
@vibes.diy/promptspackage structure• ✅ Modern env structure: Integrates with new
VibesDiyEnvclass system• ✅ Test modernization: Works with current split test file structure
• ✅ Fireproof API updates: Compatible with latest Fireproof version
• ✅ Proper type imports: All types now imported from correct packages
Testing
🤖 Generated with Claude Code