Real-time content synchronization with delta updates for keeping local content in sync with Contentful, supporting both initial and incremental synchronization patterns.
Synchronizes content from Contentful, supporting both initial sync and delta updates.
/**
* Synchronizes either all content or only new content since last sync
* @param query - Sync query parameters
* @param syncOptions - Optional sync configuration
* @returns Promise for sync collection with entries, assets, and tokens
*/
sync<
EntrySkeleton extends EntrySkeletonType = EntrySkeletonType,
Modifiers extends ChainModifiers = ChainModifiers,
Locales extends LocaleCode = LocaleCode
>(
query: SyncQuery,
syncOptions?: SyncOptions
): Promise<SyncCollection<EntrySkeleton, Modifiers, Locales>>;
type SyncQuery = {
/** Initial sync - required for first sync call (mutually exclusive with tokens) */
initial?: true;
/** Token from previous sync for delta updates */
nextSyncToken?: string;
/** Token for continuing paginated sync */
nextPageToken?: string;
/** Limit results per page (default: 100, max: 1000) */
limit?: number;
} & (
| { type: 'Entry'; content_type: string } // When filtering entries, content_type is required
| { type?: 'Asset' | 'Entry' | 'Deletion' | 'DeletedAsset' | 'DeletedEntry' } // Other types
);
interface SyncOptions {
/** Automatically paginate through all sync results */
paginate?: boolean;
}Usage Examples:
// Initial sync - get all content
const initialSync = await client.sync({
initial: true
});
console.log('Entries:', initialSync.entries.length);
console.log('Assets:', initialSync.assets.length);
console.log('Next sync token:', initialSync.nextSyncToken);
// Delta sync - get changes since last sync
const deltaSync = await client.sync({
nextSyncToken: initialSync.nextSyncToken
});
console.log('New/updated entries:', deltaSync.entries.length);
console.log('New/updated assets:', deltaSync.assets.length);
console.log('Deletions:', deltaSync.deletedEntries.length + deltaSync.deletedAssets.length);
// Sync specific content type only
const blogPostSync = await client.sync({
initial: true,
type: 'Entry',
content_type: 'blogPost'
});
// Paginated sync (automatically fetch all pages)
const fullSync = await client.sync({
initial: true
}, {
paginate: true // Collects all pages automatically
});interface SyncCollection<
EntrySkeleton extends EntrySkeletonType,
Modifiers extends ChainModifiers,
Locales extends LocaleCode
> {
/** New or updated entries */
entries: Entry<EntrySkeleton, Modifiers, Locales>[];
/** New or updated assets */
assets: Asset<Modifiers, Locales>[];
/** IDs of deleted entries */
deletedEntries: DeletedEntry[];
/** IDs of deleted assets */
deletedAssets: DeletedAsset[];
/** Token for next delta sync */
nextSyncToken?: string;
/** Token for next page (if pagination incomplete) */
nextPageToken?: string;
}
type DeletedEntry = {
sys: EntitySys & { type: 'DeletedEntry' };
};
type DeletedAsset = {
sys: EntitySys & { type: 'DeletedAsset' };
};// Comprehensive sync implementation
class ContentfulSync {
private client: ContentfulClientApi<undefined>;
private syncToken: string | null = null;
constructor(client: ContentfulClientApi<undefined>) {
this.client = client;
}
async initialSync(): Promise<void> {
console.log('Starting initial sync...');
const syncResult = await this.client.sync({
initial: true
}, {
paginate: true // Get all pages
});
await this.processSyncResult(syncResult);
this.syncToken = syncResult.nextSyncToken;
console.log(`Initial sync complete. Token: ${this.syncToken}`);
}
async deltaSync(): Promise<void> {
if (!this.syncToken) {
throw new Error('No sync token available. Run initial sync first.');
}
console.log('Starting delta sync...');
const syncResult = await this.client.sync({
nextSyncToken: this.syncToken
});
await this.processSyncResult(syncResult);
this.syncToken = syncResult.nextSyncToken;
console.log(`Delta sync complete. Token: ${this.syncToken}`);
}
private async processSyncResult(syncResult: SyncCollection<any, undefined, any>): Promise<void> {
// Process new/updated entries
for (const entry of syncResult.entries) {
await this.saveEntry(entry);
}
// Process new/updated assets
for (const asset of syncResult.assets) {
await this.saveAsset(asset);
}
// Process deletions
for (const deletedEntry of syncResult.deletedEntries) {
await this.removeEntry(deletedEntry.sys.id);
}
for (const deletedAsset of syncResult.deletedAssets) {
await this.removeAsset(deletedAsset.sys.id);
}
}
private async saveEntry(entry: any): Promise<void> {
console.log(`Saving entry: ${entry.sys.id} (${entry.sys.contentType.sys.id})`);
// Implement your storage logic here
}
private async saveAsset(asset: any): Promise<void> {
console.log(`Saving asset: ${asset.sys.id} (${asset.fields.file?.contentType})`);
// Implement your storage logic here
}
private async removeEntry(entryId: string): Promise<void> {
console.log(`Removing entry: ${entryId}`);
// Implement your removal logic here
}
private async removeAsset(assetId: string): Promise<void> {
console.log(`Removing asset: ${assetId}`);
// Implement your removal logic here
}
}
// Usage
const sync = new ContentfulSync(client);
await sync.initialSync();
// Set up periodic delta sync
setInterval(async () => {
try {
await sync.deltaSync();
} catch (error) {
console.error('Delta sync failed:', error);
}
}, 60000); // Every minute// Handle sync pagination manually for more control
async function manualSyncPagination(initialToken?: string): Promise<void> {
let nextPageToken: string | undefined;
let nextSyncToken: string | undefined;
const query = initialToken
? { nextSyncToken: initialToken }
: { initial: true };
do {
const syncResult = await client.sync({
...query,
...(nextPageToken && { nextPageToken })
});
// Process current page
console.log(`Page: ${syncResult.entries.length} entries, ${syncResult.assets.length} assets`);
// Update tokens for next iteration
nextPageToken = syncResult.nextPageToken;
nextSyncToken = syncResult.nextSyncToken;
// Clear initial/nextSyncToken for subsequent pages
delete query.initial;
delete query.nextSyncToken;
} while (nextPageToken);
console.log(`Sync complete. Next sync token: ${nextSyncToken}`);
}// Sync only specific content types
async function syncContentType(contentType: string): Promise<void> {
// Initial sync for content type
const initialSync = await client.sync({
initial: true,
type: 'Entry',
content_type: contentType
});
console.log(`Synced ${initialSync.entries.length} ${contentType} entries`);
// Store sync token for this content type
const syncTokens = new Map<string, string>();
syncTokens.set(contentType, initialSync.nextSyncToken!);
// Delta sync for content type
const deltaSync = await client.sync({
nextSyncToken: syncTokens.get(contentType)!
});
console.log(`Delta: ${deltaSync.entries.length} updated entries`);
}// Sync only assets
async function syncAssets(): Promise<void> {
const assetSync = await client.sync({
initial: true,
type: 'Asset'
});
console.log(`Synced ${assetSync.assets.length} assets`);
// Process assets
for (const asset of assetSync.assets) {
if (asset.fields.file) {
console.log(`Asset: ${asset.fields.title} - ${asset.fields.file.url}`);
}
}
return assetSync.nextSyncToken;
}// Track and handle deletions
async function handleDeletions(syncToken: string): Promise<void> {
const syncResult = await client.sync({
nextSyncToken: syncToken
});
// Process deletions
if (syncResult.deletedEntries.length > 0) {
console.log('Deleted entries:');
syncResult.deletedEntries.forEach(deleted => {
console.log(`- Entry ${deleted.sys.id} deleted at ${deleted.sys.deletedAt}`);
});
}
if (syncResult.deletedAssets.length > 0) {
console.log('Deleted assets:');
syncResult.deletedAssets.forEach(deleted => {
console.log(`- Asset ${deleted.sys.id} deleted at ${deleted.sys.deletedAt}`);
});
}
}// Robust sync with error handling
async function robustSync(maxRetries: number = 3): Promise<void> {
let retries = 0;
while (retries < maxRetries) {
try {
const syncResult = await client.sync({
initial: true
});
// Process successful sync
console.log('Sync successful');
return;
} catch (error) {
retries++;
console.error(`Sync attempt ${retries} failed:`, error.message);
if (retries < maxRetries) {
// Exponential backoff
const delay = Math.pow(2, retries) * 1000;
console.log(`Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw new Error(`Sync failed after ${maxRetries} attempts`);
}
}
}
}// Persist sync tokens for reliability
class SyncTokenManager {
private tokenKey = 'contentful_sync_token';
saveToken(token: string): void {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(this.tokenKey, token);
} else {
// Node.js environment - save to file or database
require('fs').writeFileSync('./sync-token.txt', token);
}
}
loadToken(): string | null {
if (typeof localStorage !== 'undefined') {
return localStorage.getItem(this.tokenKey);
} else {
// Node.js environment - load from file or database
try {
return require('fs').readFileSync('./sync-token.txt', 'utf8');
} catch {
return null;
}
}
}
clearToken(): void {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(this.tokenKey);
} else {
try {
require('fs').unlinkSync('./sync-token.txt');
} catch {
// Token file doesn't exist
}
}
}
}
// Usage with token manager
const tokenManager = new SyncTokenManager();
async function smartSync(): Promise<void> {
const existingToken = tokenManager.loadToken();
const syncResult = existingToken
? await client.sync({ nextSyncToken: existingToken })
: await client.sync({ initial: true });
// Save new token
if (syncResult.nextSyncToken) {
tokenManager.saveToken(syncResult.nextSyncToken);
}
}// Important: Sync API doesn't support include parameter
// Link resolution only works for initial sync, not delta sync
// ✅ This works for initial sync
const initialSync = await client.sync({
initial: true
});
// Links are resolved in initial sync
// ❌ This doesn't resolve links
const deltaSync = await client.sync({
nextSyncToken: token
});
// Delta sync cannot resolve links due to incomplete data
// Manual link resolution for delta sync
const rawClient = client.withoutLinkResolution;
const deltaSyncRaw = await rawClient.sync({
nextSyncToken: token
});
// Resolve links manually if needed
const resolvedDelta = client.parseEntries({
items: deltaSyncRaw.entries,
includes: { Entry: [], Asset: [] }
});