This document provides detailed API documentation for the JSON-first storage architecture components.
Browser-safe base64 encoding for MV3 compatibility:
import { encodeBase64 } from '~/utils/base64';
const encoded = encodeBase64('Hello, 世界! 🌍');
// Returns: Base64 string compatible with GitHub APIParameters:
content: String content to encode (supports Unicode)
Returns: Base64 encoded string
Features:
- ✅ Works in MV3 service workers (no Node.js Buffer)
- ✅ Handles Unicode characters and emojis correctly
- ✅ Compatible with GitHub API requirements
- ✅ Error handling for invalid input
Algorithm:
encodeURIComponent()- Convert Unicode to percent-encoded UTF-8unescape()- Convert percent encoding to byte stringbtoa()- Encode byte string to base64
Browser-safe base64 decoding for MV3 compatibility:
import { decodeBase64 } from '~/utils/base64';
const decoded = decodeBase64('SGVsbG8sIFdvcmxkIQ==');
// Returns: "Hello, World!"Parameters:
encodedContent: Base64 encoded string to decode
Returns: Original string content (with Unicode support)
Throws: Error if decoding fails (invalid base64 input)
Algorithm:
atob()- Decode base64 to byte stringescape()- Convert byte string to percent encodingdecodeURIComponent()- Convert percent encoding to Unicode
Validates base64 string format:
import { isValidBase64 } from '~/utils/base64';
console.log(isValidBase64('SGVsbG8=')); // true
console.log(isValidBase64('not-base64')); // falseParameters:
input: String to validate
Returns: True if string appears to be valid base64
Legacy Buffer-style interface for easier migration:
import { BrowserBuffer } from '~/utils/base64';
// Drop-in replacement for Buffer.from().toString()
const encoded = BrowserBuffer.from(content, 'utf8').toString('base64');Note: Provided for migration convenience. Use encodeBase64() directly for new code.
Performance testing utilities:
import { Base64Performance } from '~/utils/base64';
const results = Base64Performance.compare(testContent, 1000);
console.log(`Encoding: ${results.browserSafe.encode}ms`);
console.log(`Decoding: ${results.browserSafe.decode}ms`);Advanced encoding with detailed results:
import { encodeBase64Advanced } from '~/utils/base64';
const result = encodeBase64Advanced(content, { validate: true });
if (result.success) {
console.log(`Encoded ${result.size.original} bytes to ${result.size.encoded} bytes`);
console.log(`Compression ratio: ${result.size.ratio}`);
} else {
console.error(`Encoding failed: ${result.error}`);
}Configuration options for advanced base64 operations:
interface Base64Options {
validate?: boolean; // Whether to validate input before encoding
lineBreaks?: boolean; // Whether to add line breaks (not recommended for API usage)
}Detailed result from advanced base64 operations:
interface Base64Result {
content: string; // The encoded/decoded content
success: boolean; // Whether the operation was successful
error?: string; // Error message if operation failed
size: { // Size information
original: number; // Original content size in bytes
encoded: number; // Encoded content size in bytes
ratio: number; // Compression ratio (encoded/original)
};
}Complete bookmark data structure matching JSON schema:
interface HubMarkBookmark {
id: string; // Stable ID (format: hm_[32-char-hash])
title: string; // Bookmark title (minimum 1 character)
url: string; // Valid URL
folder: string; // Folder path (can be empty)
tags: string[]; // Array of tag strings
notes: string; // Optional notes (can be empty)
dateAdded: number; // Unix timestamp (milliseconds)
dateModified: number; // Unix timestamp (milliseconds)
archived: boolean; // Archive status
favorite: boolean; // Favorite status
}Complete data structure for JSON file:
interface HubMarkData {
schemaVersion: 1; // Schema version (currently 1)
generatedAt?: string; // ISO 8601 timestamp
bookmarks: HubMarkBookmark[]; // Array of bookmarks
meta?: { // Optional metadata
generator?: string; // Generator name
generatorVersion?: string; // Generator version
lastSync?: number; // Last sync timestamp
[key: string]: any; // Additional metadata
};
}Schema validation result:
interface ValidationResult {
valid: boolean; // Whether data is valid
errors: string[]; // Array of error messages
}JSON schema validator using Ajv:
Constructor
new SchemaValidator()Methods
Validates data against HubMark schema:
const result = schemaValidator.validate(data);
if (!result.valid) {
console.error('Validation errors:', result.errors);
}Parameters:
data: Data to validate (any type)
Returns: ValidationResult with validation status and errors
Validates data and throws on error:
try {
schemaValidator.validateOrThrow(data);
// data is now typed as HubMarkData
} catch (error) {
console.error('Invalid data:', error.message);
}Parameters:
data: Data to validate
Throws: Error with detailed validation message if invalid
Creates valid empty data structure:
const emptyData = createEmptyData();
// Returns:
// {
// schemaVersion: 1,
// generatedAt: "2023-09-05T12:00:00.000Z",
// bookmarks: [],
// meta: {
// generator: "HubMark",
// generatorVersion: "0.1.0",
// lastSync: 0
// }
// }Singleton validator instance ready for use:
import { schemaValidator } from '~/utils/json-schema';
const result = schemaValidator.validate(myData);Generates deterministic bookmark ID:
const id = await generateStableId(
'https://example.com/page',
'Example Page'
);
// Returns: "hm_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"Parameters:
url: Bookmark URLtitle: Bookmark titlepromoteHttps: Whether to promote HTTP to HTTPS (default:true)
Returns: Promise resolving to stable ID string
Algorithm:
- Canonicalize URL using
canonicalUrl() - Normalize title using
normalizeTitle() - Create composite key:
"${canonicalUrl}\n${normalizedTitle}" - Generate SHA-256 hash
- Return
"hm_" + hash.substring(0, 32)
Canonicalizes URL for consistent matching:
const canonical = canonicalUrl('HTTP://www.Example.com/path/?utm_source=test#section');
// Returns: "https://example.com/path"Parameters:
rawUrl: Raw URL stringpromoteHttps: Whether to promote HTTP to HTTPS (default:true)
Returns: Canonical URL string
Transformations:
- Convert hostname to lowercase
- Remove
www.prefix - Promote HTTP to HTTPS (if enabled)
- Remove tracking parameters:
utm_*,gclid,fbclid,ref,referrer,source,_ga,_gl,mc_cid,mc_eid - Remove hash fragments
- Remove trailing slashes (except root path)
Normalizes title for consistent matching:
const normalized = normalizeTitle(' Multiple Spaces Here ');
// Returns: "Multiple Spaces Here"Parameters:
title: Raw bookmark title
Returns: Normalized title string
Transformations:
- Trim leading/trailing whitespace
- Collapse multiple consecutive spaces to single spaces
Validates stable ID format:
const isValid = isValidStableId('hm_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6');
// Returns: trueParameters:
id: ID string to validate
Returns: true if ID matches format /^hm_[a-z0-9]{32}$/
Compares bookmark content for differences:
const differs = bookmarkContentDiffers(bookmark1, bookmark2);Parameters:
a: First bookmark for comparisonb: Second bookmark for comparison
Returns: true if content differs (ignoring dateAdded and dateModified)
Comparison Logic:
- Compares:
title,url,folder,notes,archived,favorite - Tags compared order-independently
- Normalizes undefined values to defaults
Interface:
interface BookmarkForComparison {
title: string;
url: string;
folder?: string;
tags?: string[];
notes?: string;
archived?: boolean;
favorite?: boolean;
}Generates content hash for quick comparison:
const hash = await generateContentHash(bookmark);
// Returns: "a1b2c3d4e5f6g7h8" (16-character hash)Parameters:
bookmark: Bookmark to hash
Returns: Promise resolving to 16-character hex hash string
Features:
- Order-independent tag hashing
- Excludes date fields
- Consistent output for identical content
Browser-safe base64 encoding for MV3:
const encoded = browserSafeEncode('Hello, World!');
// Equivalent to btoa(unescape(encodeURIComponent(content)))Parameters:
content: String content to encode
Returns: Base64 encoded string safe for GitHub API
Browser-safe base64 decoding for MV3:
const decoded = browserSafeDecode(encoded);
// Returns: "Hello, World!"Parameters:
content: Base64 encoded string from GitHub API
Returns: Decoded string content
Conflict information for manual resolution:
interface BookmarkConflict {
id: string; // Bookmark ID
local: HubMarkBookmark; // Local version
remote: HubMarkBookmark; // Remote version
base?: HubMarkBookmark; // Last known common version
}Result of 3-way merge operation:
interface MergeResult {
merged: HubMarkBookmark[]; // Merged bookmark array
conflicts: BookmarkConflict[]; // Unresolved conflicts
stats: { // Change statistics
added: number;
modified: number;
deleted: number;
};
}Conflict resolution strategies:
type ConflictStrategy = 'latest-wins' | 'local-wins' | 'github-wins' | 'manual';latest-wins: Use bookmark with latestdateModifiedlocal-wins: Always prefer local changesgithub-wins: Always prefer remote changesmanual: Surface conflicts for user resolution
Main client for JSON-first GitHub operations:
Constructor
const client = new JSONGitHubClient(githubConfig);Parameters:
githubConfig: GitHub configuration object
Methods
Authenticates with GitHub API:
await client.authenticate();Throws: Error if authentication fails
Reads bookmark data from repository:
const { data, sha } = await client.readBookmarkData();Returns: Object with:
data: HubMark data structure (empty if file doesn't exist)sha: File SHA for updates (undefined for new files)
Features:
- Handles 404 gracefully (returns empty data)
- Validates JSON schema
- Browser-safe base64 decoding
writeBookmarkData(data: HubMarkData, message: string, sha?: string, retries?: number): Promise<string>
Writes bookmark data with conflict retry:
const newSha = await client.writeBookmarkData(
data,
'Update bookmarks',
currentSha,
3
);Parameters:
data: HubMark data to writemessage: Commit messagesha: Current file SHA (for updates, omit for creation)retries: Number of 409 retry attempts (default: 3)
Returns: Promise resolving to new file SHA
Features:
- Schema validation before writing
- 409 conflict retry with exponential backoff
- Automatic metadata updates (
generatedAt,lastSync) - Browser-safe base64 encoding
mergeBookmarks(base: HubMarkBookmark[], local: HubMarkBookmark[], remote: HubMarkBookmark[], deletions?: string[], strategy?: ConflictStrategy): Promise<MergeResult>
Performs 3-way merge with conflict resolution:
const result = await client.mergeBookmarks(
baseBookmarks,
localChanges,
remoteBookmarks,
['id1', 'id2'], // IDs to delete
'latest-wins'
);Parameters:
base: Base bookmark state (last known sync)local: Local bookmark changesremote: Remote bookmark statedeletions: Array of bookmark IDs to delete (default:[])strategy: Conflict resolution strategy (default:'latest-wins')
Returns: Promise resolving to MergeResult
Merge Algorithm:
- Create maps by bookmark ID for efficient lookup
- Start with remote state as base
- Apply deletions
- Process local changes:
- New bookmarks: add directly
- Modified bookmarks: check for conflicts
- Conflicts: apply resolution strategy
- Return merged state with conflict information
Generates README.md content from bookmark data:
const markdown = client.generateReadme(data);Parameters:
data: HubMark data structure
Returns: Markdown string for README.md
Features:
- Folder-based organization
- Favorite marking (⭐)
- Inline tag display with backticks
- Archived bookmarks in collapsible section
- Generation timestamp and statistics
- Auto-generation notice
Updates README.md only if content changed:
const wasUpdated = await client.updateReadmeIfChanged(data);Parameters:
data: HubMark data structure
Returns: Promise resolving to true if README was updated, false if no changes
Optimization:
- Compares generated content with existing file
- Only makes API call if content differs
- Creates new file if README doesn't exist
import { encodeBase64, decodeBase64, isValidBase64 } from '~/utils/base64';
// Encoding bookmark content for GitHub API
const markdownContent = `# My Bookmarks
- [Example](https://example.com) - Test site with Unicode: 世界 🌍
`;
const encoded = encodeBase64(markdownContent);
console.log('Encoded for GitHub API:', encoded);
// Validate before sending to API
if (isValidBase64(encoded)) {
// Send to GitHub API...
console.log('Ready for GitHub API upload');
}
// Decoding content from GitHub API
const decoded = decodeBase64(encoded);
console.log('Decoded content:', decoded);
// Performance testing for large content
import { Base64Performance } from '~/utils/base64';
const largeContent = 'Large bookmark file content...'.repeat(1000);
const results = Base64Performance.compare(largeContent, 100);
console.log(`Encoding 100 iterations: ${results.browserSafe.encode}ms`);
console.log(`Decoding 100 iterations: ${results.browserSafe.decode}ms`);import { JSONGitHubClient } from '~/utils/json-github';
import { schemaValidator } from '~/utils/json-schema';
import { encodeBase64, decodeBase64 } from '~/utils/base64';
// Initialize client
const client = new JSONGitHubClient(githubConfig);
await client.authenticate();
// Read current state
const { data: remoteData, sha } = await client.readBookmarkData();
// Prepare local changes
const localBookmarks = await getLocalBookmarks();
const baseBookmarks = await getLastSyncState();
const deletions = ['bookmark-id-to-delete'];
// Perform merge
const mergeResult = await client.mergeBookmarks(
baseBookmarks,
localBookmarks,
remoteData.bookmarks,
deletions,
'latest-wins'
);
// Handle conflicts
if (mergeResult.conflicts.length > 0) {
console.log('Conflicts found:', mergeResult.conflicts);
// Handle manually or with different strategy
return;
}
// Update data structure
const updatedData = {
...remoteData,
bookmarks: mergeResult.merged
};
// Validate before writing
schemaValidator.validateOrThrow(updatedData);
// Write back to GitHub
const newSha = await client.writeBookmarkData(
updatedData,
`sync: +${mergeResult.stats.added} ~${mergeResult.stats.modified} -${mergeResult.stats.deleted}`,
sha
);
// Update README
await client.updateReadmeIfChanged(updatedData);
console.log('Sync completed successfully');import { generateStableId, isValidStableId, canonicalUrl } from '~/utils/stable-id';
// Generate ID for new bookmark
const url = 'https://example.com/article?utm_source=social';
const title = ' Amazing Article ';
const bookmarkId = await generateStableId(url, title);
console.log('Generated ID:', bookmarkId); // "hm_..."
// Validate ID format
if (!isValidStableId(bookmarkId)) {
throw new Error('Invalid bookmark ID format');
}
// Check URL canonicalization
const canonical = canonicalUrl(url);
console.log('Canonical URL:', canonical); // "https://example.com/article"import { schemaValidator, createEmptyData } from '~/utils/json-schema';
// Create valid empty structure
const emptyData = createEmptyData();
// Validate existing data
const result = schemaValidator.validate(existingData);
if (!result.valid) {
console.error('Schema validation failed:');
result.errors.forEach(error => console.error(' -', error));
return;
}
// Use type assertion after validation
schemaValidator.validateOrThrow(existingData);
// existingData is now typed as HubMarkDataimport { encodeBase64, decodeBase64, isValidBase64 } from '~/utils/base64';
try {
const { data } = await client.readBookmarkData();
schemaValidator.validateOrThrow(data);
// Process data...
} catch (error) {
if (error.message.includes('not found')) {
// Repository or file doesn't exist
console.log('Creating new bookmark repository');
} else if (error.message.includes('Schema validation failed')) {
// Invalid data structure
console.error('Data corruption detected:', error.message);
} else if (error.message.includes('Failed to decode base64')) {
// Base64 decoding error from GitHub API
console.error('Invalid base64 content from GitHub:', error.message);
} else if (error.status === 409) {
// Write conflict (should be handled by retry logic)
console.warn('Write conflict - retrying...');
} else if (error.status === 429) {
// Rate limit
console.warn('Rate limit exceeded - backing off...');
} else {
// Other GitHub API error
console.error('GitHub API error:', error.message);
}
}// Safe base64 encoding with error handling
function safeEncodeForGitHub(content: string): string | null {
try {
const encoded = encodeBase64(content);
// Validate the result
if (!isValidBase64(encoded)) {
console.error('Generated invalid base64');
return null;
}
return encoded;
} catch (error) {
console.error('Base64 encoding failed:', error.message);
return null;
}
}
// Safe base64 decoding with validation
function safeDecodeFromGitHub(encodedContent: string): string | null {
// Validate input first
if (!isValidBase64(encodedContent)) {
console.error('Invalid base64 format from GitHub API');
return null;
}
try {
return decodeBase64(encodedContent);
} catch (error) {
console.error('Base64 decoding failed:', error.message);
return null;
}
}Schema validation errors provide detailed path information:
const result = schemaValidator.validate(invalidData);
// Example errors:
// [
// "/bookmarks/0/id: must match pattern \"^hm_[a-z0-9]{32}$\"",
// "/bookmarks/0/title: must NOT have fewer than 1 characters",
// "/schemaVersion: must be <= 1"
// ]- Conditional Operations: Always check if changes are needed before API calls
- Content Hashing: Use
generateContentHash()for quick bookmark comparison - Batch Changes: Combine multiple bookmark changes into single sync operation
- Cache SHA Values: Store file SHAs in memory during sync operations
GitHub API has rate limits. The client implements:
- Exponential backoff: 250ms, 750ms, 2250ms delays
- Jitter: Random component to prevent synchronized retries
- Conditional writes: Only update files when content changes
For large bookmark collections:
- Maps are used for O(1) bookmark lookup by ID
- Content hashing provides quick comparison without deep object comparison
- JSON parsing is more efficient than Markdown parsing