or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

assets.mdclient.mdentries.mdindex.mdquerying.mdspace-content-types.mdsync.mdtags-taxonomy.md
tile.json

sync.mddocs/

Content Synchronization

Real-time content synchronization with delta updates for keeping local content in sync with Contentful, supporting both initial and incremental synchronization patterns.

Capabilities

Sync Content

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
});

Sync Collection Structure

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' };
};

Sync Patterns

Complete Synchronization Workflow

// 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

Manual Pagination Handling

// 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}`);
}

Content Type Specific Sync

// 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`);
}

Asset-Only Sync

// 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;
}

Deletion Tracking

// 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}`);
    });
  }
}

Sync Best Practices

Error Handling and Retry Logic

// 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`);
      }
    }
  }
}

Sync Token Persistence

// 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 Limitations

Link Resolution Limitations

// 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: [] }
});