import { randomUUID } from 'node:crypto'; import { Injectable, Logger } from '@nestjs/common'; import { Transactional } from '@nestjs-cls/transactional'; import type { CalendarAccount, Prisma } from '@prisma/client'; import { CalendarProviderRequestError, Config, exponentialBackoffDelay, GraphqlBadRequest, JobQueue, URLHelper, } from '../../base'; import { Models } from '../../models'; import type { CalendarCalDAVProviderPreset } from './config'; import { CalendarProvider, CalendarProviderEvent, CalendarProviderEventTime, CalendarProviderFactory, CalendarProviderName, CalendarSyncTokenInvalid, } from './providers'; import type { LinkCalDAVAccountInput } from './types'; const TOKEN_REFRESH_SKEW_MS = 60 * 1000; const DEFAULT_PAST_DAYS = 90; const DEFAULT_FUTURE_DAYS = 180; const SYNC_FAILURE_BACKOFF_BASE_MS = 5 * 60 * 1000; const SYNC_FAILURE_BACKOFF_MAX_MS = 6 * 60 * 60 * 1000; const DEFAULT_REFRESH_INTERVAL_MINUTES = 30; const CHANNEL_RENEW_RETRY_MS = 15 * 60 * 1000; const UNSUPPORTED_PUSH_CHANNEL_EXPIRATION = new Date( '9999-12-31T23:59:59.999Z' ); @Injectable() export class CalendarService { private readonly logger = new Logger(CalendarService.name); private generatedWebhookToken?: string; constructor( private readonly models: Models, private readonly providerFactory: CalendarProviderFactory, private readonly queue: JobQueue, private readonly config: Config, private readonly url: URLHelper ) {} async listAccounts(userId: string) { const accounts = await this.models.calendarAccount.listByUser(userId); const accountIds = accounts.map(account => account.id); const subscriptions = await this.models.calendarSubscription.listByAccountIds(accountIds); const counts = new Map(); for (const subscription of subscriptions) { counts.set( subscription.accountId, (counts.get(subscription.accountId) ?? 0) + 1 ); } return accounts.map(account => ({ ...account, calendarsCount: counts.get(account.id) ?? 0, })); } async listAccountCalendars(userId: string, accountId: string) { const account = await this.models.calendarAccount.get(accountId); if (!account || account.userId !== userId) { return []; } return await this.models.calendarSubscription.listByAccount(accountId); } async updateAccountRefreshInterval( userId: string, accountId: string, refreshIntervalMinutes: number ) { const account = await this.models.calendarAccount.get(accountId); if (!account || account.userId !== userId) { return null; } const updatedAccount = await this.models.calendarAccount.updateRefreshInterval( accountId, refreshIntervalMinutes ); const subscriptions = await this.models.calendarSubscription.listByAccountForSync(accountId); await Promise.all( subscriptions.map(subscription => this.models.calendarSubscription.updateSync(subscription.id, { nextSyncAt: this.calculateNextSyncAt( subscription.lastSyncAt ?? this.now(), refreshIntervalMinutes ), }) ) ); return updatedAccount; } async unlinkAccount(userId: string, accountId: string) { const account = await this.models.calendarAccount.get(accountId); if (!account || account.userId !== userId) { return false; } const provider = this.providerFactory.get( account.provider as CalendarProviderName ); const subscriptions = await this.models.calendarSubscription.listByAccount(accountId); const needToStopChannel = subscriptions.filter( s => s.customChannelId && s.customResourceId ); if (provider?.stopChannel && needToStopChannel.length > 0) { const accountTokens = this.models.calendarAccount.decryptTokens(account); const accessToken = accountTokens.accessToken; if (accessToken) { await Promise.allSettled( needToStopChannel.map(async s => { if (!s.customChannelId || !s.customResourceId) { return; } return await provider.stopChannel?.({ accessToken, channelId: s.customChannelId, resourceId: s.customResourceId, }); }) ); } } await this.models.calendarAccount.delete(accountId); return true; } async handleOAuthCallback(params: { provider: CalendarProviderName; code: string; redirectUri: string; userId: string; }) { const provider = this.requireProvider(params.provider); const tokens = await provider.exchangeCode(params.code, params.redirectUri); const profile = await provider.getAccountProfile(tokens.accessToken); await this.assertCanPersistProviderAccount( params.userId, params.provider, profile.providerAccountId ); const account = await this.models.calendarAccount.upsert({ userId: params.userId, provider: params.provider, providerAccountId: profile.providerAccountId, displayName: profile.displayName ?? null, email: profile.email ?? null, accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, expiresAt: tokens.expiresAt ?? null, scope: tokens.scope ?? null, status: 'active', lastError: null, }); try { await this.syncAccountCalendars(account.id); } catch (error) { if (error instanceof CalendarProviderRequestError) { await this.models.calendarAccount.updateStatus( account.id, 'invalid', error.message ); } throw error; } return account; } async linkCalDAVAccount(params: { userId: string; input: LinkCalDAVAccountInput; }) { const caldavConfig = this.config.calendar.caldav; if (!caldavConfig?.enabled) { throw new GraphqlBadRequest({ code: 'caldav_disabled', message: 'CalDAV integration is not enabled.', }); } const preset = caldavConfig.providers.find( provider => provider.id === params.input.providerPresetId ); if (!preset) { throw new GraphqlBadRequest({ code: 'caldav_provider_not_found', message: 'CalDAV provider is not available.', }); } const provider = this.requireProvider(CalendarProviderName.CalDAV); if (!('discoverAccount' in provider)) { throw new GraphqlBadRequest({ code: 'caldav_provider_unavailable', message: 'CalDAV provider is not configured.', }); } const discovery = await ( provider as CalendarProvider & { discoverAccount: (input: { preset: CalendarCalDAVProviderPreset; username: string; password: string; }) => Promise<{ providerAccountId: string; serverUrl: string; principalUrl: string; calendarHomeUrl: string; authType?: string | null; }>; } ).discoverAccount({ preset, username: params.input.username, password: params.input.password, }); const account = await this.models.calendarAccount.upsert({ userId: params.userId, provider: CalendarProviderName.CalDAV, providerAccountId: discovery.providerAccountId, displayName: params.input.displayName ?? null, email: params.input.username, accessToken: params.input.password, refreshToken: null, expiresAt: null, scope: null, status: 'active', lastError: null, providerPresetId: params.input.providerPresetId, serverUrl: discovery.serverUrl, principalUrl: discovery.principalUrl, calendarHomeUrl: discovery.calendarHomeUrl, username: params.input.username, authType: discovery.authType ?? null, }); try { await this.syncAccountCalendars(account.id); } catch (error) { if (error instanceof CalendarProviderRequestError) { await this.models.calendarAccount.updateStatus( account.id, 'invalid', error.message ); } throw error; } const calendars = await this.models.calendarSubscription.listByAccount( account.id ); return { ...account, calendarsCount: calendars.length }; } async syncAccountCalendars(accountId: string) { const account = await this.models.calendarAccount.get(accountId); if (!account) { return; } const provider = this.providerFactory.get( account.provider as CalendarProviderName ); if (!provider) { return; } const { accessToken } = await this.ensureAccessToken(account); if (!accessToken) { return; } const calendars = await provider.listCalendars({ accessToken, account, }); const upserted = []; for (const calendar of calendars) { upserted.push( await this.models.calendarSubscription.upsert({ accountId: account.id, provider: account.provider, externalCalendarId: calendar.id, displayName: calendar.summary ?? null, timezone: calendar.timeZone ?? null, color: calendar.colorId ?? null, enabled: true, }) ); } await Promise.allSettled( upserted.map(subscription => this.syncSubscription(subscription.id, { reason: 'initial' }) ) ); } async syncSubscription( subscriptionId: string, options?: { reason?: string; forceFull?: boolean } ) { const subscription = await this.models.calendarSubscription.listWithAccount(subscriptionId); if (!subscription || !subscription.enabled) { return; } const account = subscription.account; if (account.status !== 'active') { return; } const provider = this.providerFactory.get( account.provider as CalendarProviderName ); if (!provider) { return; } let accessToken: string | null = null; try { const tokens = await this.ensureAccessToken(account); if (!tokens.accessToken) return; accessToken = tokens.accessToken; } catch (error) { await this.handleSubscriptionSyncFailure({ error, subscription, account, provider, }); return; } const { timeMin, timeMax } = this.getSyncWindow(); const shouldUseSyncToken = !!subscription.syncToken && options?.forceFull !== true; let synced = false; try { await this.syncWithProvider({ provider, subscriptionId: subscription.id, calendarId: subscription.externalCalendarId, accessToken, account, syncToken: shouldUseSyncToken ? (subscription.syncToken ?? undefined) : undefined, timeMin: shouldUseSyncToken ? undefined : timeMin, timeMax: shouldUseSyncToken ? undefined : timeMax, subscriptionTimezone: subscription.timezone ?? undefined, }); synced = true; } catch (error) { if (error instanceof CalendarSyncTokenInvalid) { await this.models.calendarSubscription.updateSync(subscription.id, { syncToken: null, }); try { await this.syncWithProvider({ provider, subscriptionId: subscription.id, calendarId: subscription.externalCalendarId, accessToken, account, timeMin, timeMax, subscriptionTimezone: subscription.timezone ?? undefined, }); synced = true; } catch (syncTokenRetryError) { await this.handleSubscriptionSyncFailure({ error: syncTokenRetryError, subscription, account, provider, accessToken, disableOnNotFound: true, }); return; } } else { await this.handleSubscriptionSyncFailure({ error, subscription, account, provider, accessToken, disableOnNotFound: true, }); return; } } if (synced) { const syncedAt = this.now(); let nextSyncAt = this.calculateNextSyncAt( syncedAt, account.refreshIntervalMinutes ); try { await this.ensureWebhookChannel(subscription, provider, accessToken); } catch (error) { nextSyncAt = this.calculateChannelRetryAt(nextSyncAt); this.logger.warn( `Failed to ensure webhook channel for subscription ${subscription.id}`, this.toError(error) ); } await this.models.calendarSubscription.updateSync(subscription.id, { lastSyncAt: syncedAt, nextSyncAt, syncRetryCount: 0, }); } } async listWorkspaceEvents(params: { workspaceCalendarId: string; from: Date; to: Date; }) { const items = await this.models.workspaceCalendar.listItems( params.workspaceCalendarId ); const subscriptionIds = items.map(item => item.subscriptionId); const events = await this.models.calendarEvent.listBySubscriptionsInRange( subscriptionIds, params.from, params.to ); const subscriptions = await this.models.calendarSubscription.listWithAccounts(subscriptionIds); const staleSubscriptions = subscriptions.filter( subscription => subscription.enabled && subscription.account.status === 'active' && subscription.nextSyncAt.getTime() <= this.nowMs() ); Promise.allSettled( staleSubscriptions.map(subscription => this.enqueueSyncSubscription(subscription.id, 'on-demand') ) ).catch(error => { this.logger.warn('Calendar on-demand sync failed', error as Error); }); return events; } @Transactional() async updateWorkspaceCalendars(params: { workspaceId: string; userId: string; items: Array<{ subscriptionId: string; sortOrder?: number | null; colorOverride?: string | null; }>; }) { const calendar = await this.models.workspaceCalendar.getOrCreateDefault( params.workspaceId, params.userId ); await this.models.workspaceCalendar.updateItems(calendar.id, params.items); return calendar; } async getWorkspaceCalendars(workspaceId: string) { const calendars = await this.models.workspaceCalendar.getByWorkspace(workspaceId); if (calendars.length === 0) { return []; } const items = await Promise.all( calendars.map(calendar => this.models.workspaceCalendar.listItems(calendar.id) ) ); return calendars.map((calendar, index) => ({ ...calendar, items: items[index], })); } async handleWebhook(providerName: CalendarProviderName, channelId: string) { if (providerName !== CalendarProviderName.Google) { return; } const subscription = await this.models.calendarSubscription.getByChannelId(channelId); if (!subscription) { return; } await this.enqueueSyncSubscription(subscription.id, 'webhook'); } getWebhookToken() { const configured = this.config.calendar.google.webhookVerificationToken; if (configured) { return configured; } if (!this.generatedWebhookToken) { this.generatedWebhookToken = randomUUID(); } return this.generatedWebhookToken; } getWebhookAddress(provider: string) { const externalWebhookUrl = this.config.calendar.google.externalWebhookUrl; if (!externalWebhookUrl) { return null; } return new URL( `/api/calendar/webhook/${provider}`, externalWebhookUrl ).toString(); } getCallbackUrl() { return this.url.link('/api/calendar/oauth/callback'); } isProviderAvailable(provider: CalendarProviderName) { return this.isProviderAvailableFor(provider); } isProviderAvailableFor( provider: CalendarProviderName, options?: { oauth?: boolean } ) { const instance = this.providerFactory.get(provider); if (!instance) { return false; } if (options?.oauth) { return instance.supportsOAuth; } return true; } async assertCanLinkProvider(userId: string, provider: CalendarProviderName) { if (await this.canLinkProvider(userId, provider)) { return; } throw this.providerLinkDisabledError(provider); } async canLinkProvider( userId: string | null | undefined, provider: CalendarProviderName ) { if (this.canCreateNewAccounts(provider)) { return true; } if (!userId) { return false; } const accounts = await this.models.calendarAccount.listByUser(userId); return accounts.some(account => account.provider === provider); } getAuthUrl( provider: CalendarProviderName, state: string, redirectUri: string ) { const instance = this.requireProvider(provider); if (!instance.supportsOAuth) { throw new GraphqlBadRequest({ code: 'calendar_provider_oauth_unsupported', message: 'Selected calendar provider does not support OAuth.', }); } return instance.getAuthUrl(state, redirectUri); } private async assertCanPersistProviderAccount( userId: string, provider: CalendarProviderName, providerAccountId: string ) { if (this.canCreateNewAccounts(provider)) { return; } const account = await this.models.calendarAccount.getByProviderAccount( userId, provider, providerAccountId ); if (account) { return; } throw this.providerLinkDisabledError(provider); } private canCreateNewAccounts(provider: CalendarProviderName) { return ( provider !== CalendarProviderName.Google || this.config.calendar.google.allowNewAccounts !== false ); } private providerLinkDisabledError(provider: CalendarProviderName) { return new GraphqlBadRequest({ code: 'calendar_provider_link_disabled', message: `${provider} calendar account linking is disabled.`, }); } private async syncWithProvider(params: { provider: CalendarProvider; subscriptionId: string; calendarId: string; accessToken: string; account: CalendarAccount; syncToken?: string; timeMin?: string; timeMax?: string; subscriptionTimezone?: string; }) { const response = await params.provider.listEvents({ accessToken: params.accessToken, calendarId: params.calendarId, account: params.account, syncToken: params.syncToken, timeMin: params.timeMin, timeMax: params.timeMax, }); const cancelledEventIds: string[] = []; const failedEventIds: string[] = []; for (const event of response.events) { if (event.status === 'cancelled') { cancelledEventIds.push(event.id); continue; } try { await this.models.calendarEvent.upsert( this.mapProviderEvent( params.subscriptionId, event, params.subscriptionTimezone ) ); } catch { failedEventIds.push(event.id); } } if (cancelledEventIds.length > 0) { await this.models.calendarEvent.deleteByExternalIds( params.subscriptionId, cancelledEventIds ); } if (failedEventIds.length > 0) { this.logger.warn( `Failed to upsert ${failedEventIds.length} events for subscription ${params.subscriptionId}`, { failedEventIds } ); } if (response.nextSyncToken) { await this.models.calendarSubscription.updateSync(params.subscriptionId, { syncToken: response.nextSyncToken, }); } } private mapProviderEvent( subscriptionId: string, event: CalendarProviderEvent, fallbackTimezone?: string ) { const { timeZone, start, end, allDay } = this.resolveEventTimes( event, fallbackTimezone ); return { subscriptionId, externalEventId: event.id, recurrenceId: this.resolveRecurrenceId(event), etag: event.etag ?? null, status: event.status ?? null, title: event.summary ?? null, description: event.description ?? null, location: event.location ?? null, startAtUtc: start, endAtUtc: end, originalTimezone: timeZone ?? null, allDay, providerUpdatedAt: event.updated ? new Date(event.updated) : null, raw: event.raw as Prisma.InputJsonValue, }; } private resolveEventTimes( event: CalendarProviderEvent, fallbackTimezone?: string ) { const startTime = this.resolveEventTime(event.start, fallbackTimezone); const endTime = this.resolveEventTime(event.end, fallbackTimezone); const timeZone = event.start.timeZone ?? event.end.timeZone ?? fallbackTimezone ?? null; return { start: startTime.date, end: endTime.date, allDay: startTime.allDay || endTime.allDay, timeZone, }; } private resolveEventTime( time: CalendarProviderEventTime, fallbackTimezone?: string ) { if (time.dateTime) { return { date: new Date(time.dateTime), allDay: false, }; } const zone = time.timeZone ?? fallbackTimezone ?? 'UTC'; if (!time.date) { throw new Error('Calendar provider returned all-day event without date'); } return { date: this.convertDateToUtc(time.date, zone), allDay: true, }; } private resolveRecurrenceId(event: CalendarProviderEvent) { if (event.originalStartTime?.dateTime) { return event.originalStartTime.dateTime; } if (event.originalStartTime?.date) { return event.originalStartTime.date; } return null; } private convertDateToUtc(dateString: string, timeZone: string) { const [year, month, day] = dateString.split('-').map(Number); const utcDate = new Date(Date.UTC(year, month - 1, day, 0, 0, 0)); const offsetMinutes = this.getTimeZoneOffset(utcDate, timeZone); return new Date(utcDate.getTime() - offsetMinutes * 60 * 1000); } private getTimeZoneOffset(date: Date, timeZone: string) { const formatter = new Intl.DateTimeFormat('en-US', { timeZone, hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', }); const parts = formatter.formatToParts(date); const lookup = (type: string) => { const part = parts.find(item => item.type === type); return part ? Number(part.value) : 0; }; const asUtc = Date.UTC( lookup('year'), lookup('month') - 1, lookup('day'), lookup('hour'), lookup('minute'), lookup('second') ); return (asUtc - date.getTime()) / 60000; } private getSyncWindow() { const now = this.now(); const timeMin = new Date(now); const timeMax = new Date(now); timeMin.setDate(timeMin.getDate() - DEFAULT_PAST_DAYS); timeMax.setDate(timeMax.getDate() + DEFAULT_FUTURE_DAYS); return { timeMin: timeMin.toISOString(), timeMax: timeMax.toISOString(), }; } private async ensureAccessToken(account: CalendarAccount) { const provider = this.providerFactory.get( account.provider as CalendarProviderName ); if (!provider) { return { accessToken: null }; } const decrypted = this.models.calendarAccount.decryptTokens(account); const accessToken = decrypted.accessToken; if ( accessToken && account.expiresAt && account.expiresAt.getTime() > this.nowMs() + TOKEN_REFRESH_SKEW_MS ) { return { accessToken }; } if (!decrypted.refreshToken) { return { accessToken }; } const refreshed = await provider.refreshTokens(decrypted.refreshToken); await this.models.calendarAccount.updateTokens(account.id, { accessToken: refreshed.accessToken, refreshToken: refreshed.refreshToken ?? decrypted.refreshToken, expiresAt: refreshed.expiresAt ?? null, scope: refreshed.scope ?? null, status: 'active', lastError: null, }); return { accessToken: refreshed.accessToken }; } private isTokenInvalidError(error: unknown) { if (error instanceof CalendarProviderRequestError) { const status = error.data?.status ?? error.status; if (status === 401) { return true; } return error.message.includes('invalid_grant'); } if (error instanceof Error) { return error.message.includes('invalid_grant'); } return false; } private isPushUnsupportedError(error: unknown) { if (!(error instanceof CalendarProviderRequestError)) { return false; } const status = error.data?.status ?? error.status; if (status !== 400) return false; return error.data?.message?.includes( 'pushNotSupportedForRequestedResource' ); } private requireProvider(name: CalendarProviderName) { const provider = this.providerFactory.get(name); if (!provider) { throw new Error(`Calendar provider ${name} not configured`); } return provider; } private async ensureWebhookChannel( subscription: { id: string; externalCalendarId: string; displayName: string | null; customChannelId: string | null; customResourceId: string | null; channelExpiration: Date | null; }, provider: CalendarProvider, accessToken: string ) { if (!provider.watchCalendar) { return; } const address = this.getWebhookAddress(provider.provider); if (!address) { return; } const renewThreshold = this.nowMs() + 24 * 60 * 60 * 1000; if ( subscription.channelExpiration && subscription.channelExpiration.getTime() > renewThreshold ) { return; } if ( provider.stopChannel && subscription.customChannelId && subscription.customResourceId ) { try { await provider.stopChannel({ accessToken, channelId: subscription.customChannelId, resourceId: subscription.customResourceId, }); } catch (error) { if (!this.isNotFoundError(error)) { throw error; } } await this.models.calendarSubscription.updateChannel(subscription.id, { customChannelId: null, customResourceId: null, channelExpiration: null, }); } const channelId = randomUUID(); const token = this.getWebhookToken(); let result: Awaited>>; try { result = await provider.watchCalendar({ accessToken, calendarId: subscription.externalCalendarId, address, token, channelId, }); } catch (error) { if (!this.isPushUnsupportedError(error)) { throw error; } await this.models.calendarSubscription.updateChannel(subscription.id, { customChannelId: null, customResourceId: null, channelExpiration: UNSUPPORTED_PUSH_CHANNEL_EXPIRATION, }); this.logger.log( `Calendar subscription ${subscription.id} (${subscription.displayName ?? subscription.externalCalendarId}) does not support push notifications; falling back to polling` ); return; } await this.models.calendarSubscription.updateChannel(subscription.id, { customChannelId: result.channelId, customResourceId: result.resourceId, channelExpiration: result.expiration ?? null, }); } private async handleSubscriptionSyncFailure(params: { error: unknown; subscription: { id: string; externalCalendarId: string; syncRetryCount: number; customChannelId: string | null; customResourceId: string | null; }; account: CalendarAccount; provider: CalendarProvider; accessToken?: string; disableOnNotFound?: boolean; }) { if (params.disableOnNotFound && this.isNotFoundError(params.error)) { await this.disableSubscription({ subscriptionId: params.subscription.id, provider: params.provider, accessToken: params.accessToken, customChannelId: params.subscription.customChannelId, customResourceId: params.subscription.customResourceId, }); this.logger.warn( `Calendar subscription ${params.subscription.id} was disabled because provider returned 404 for calendar ${params.subscription.externalCalendarId}` ); return; } if (this.isTokenInvalidError(params.error)) { await this.models.calendarAccount.invalidateAndPurge( params.account.id, this.formatSyncError(params.error) ); return; } const attempt = params.subscription.syncRetryCount + 1; const nextRetryAt = this.calculateFailureRetryAt(attempt); await this.models.calendarSubscription.updateSync(params.subscription.id, { nextSyncAt: nextRetryAt, syncRetryCount: attempt, }); this.logger.warn( `Calendar sync failed for subscription ${params.subscription.id}, attempt ${attempt}, next retry at ${nextRetryAt.toISOString()}`, this.toError(params.error) ); } private isNotFoundError(error: unknown) { if (!(error instanceof CalendarProviderRequestError)) { return false; } const status = error.data?.status ?? error.status; return status === 404; } private async disableSubscription(params: { subscriptionId: string; provider: CalendarProvider; accessToken?: string; customChannelId: string | null; customResourceId: string | null; }) { if ( params.provider.stopChannel && params.accessToken && params.customChannelId && params.customResourceId ) { try { await params.provider.stopChannel({ accessToken: params.accessToken, channelId: params.customChannelId, resourceId: params.customResourceId, }); } catch (error) { this.logger.warn( `Failed to stop webhook channel for disabled calendar subscription ${params.subscriptionId}`, this.toError(error) ); } } await this.models.calendarSubscription.disableAndPurge( params.subscriptionId ); } async enqueueSyncSubscription( subscriptionId: string, reason: 'polling' | 'webhook' | 'on-demand' ) { await this.queue.add( 'calendar.syncSubscription', { subscriptionId, reason, }, { jobId: subscriptionId, } ); } private calculateNextSyncAt(base: Date, refreshIntervalMinutes?: number) { const intervalMinutes = refreshIntervalMinutes ?? DEFAULT_REFRESH_INTERVAL_MINUTES; return new Date(base.getTime() + intervalMinutes * 60 * 1000); } private calculateChannelRetryAt(nextSyncAt: Date) { return new Date( Math.min(nextSyncAt.getTime(), this.nowMs() + CHANNEL_RENEW_RETRY_MS) ); } private calculateFailureRetryAt(attempt: number) { return new Date( this.nowMs() + exponentialBackoffDelay(attempt - 1, { baseDelayMs: SYNC_FAILURE_BACKOFF_BASE_MS, maxDelayMs: SYNC_FAILURE_BACKOFF_MAX_MS, }) ); } private now() { return new Date(this.nowMs()); } private nowMs() { return Date.now(); } private formatSyncError(error: unknown) { if (error instanceof Error && error.message) { return error.message; } return String(error); } private toError(error: unknown) { if (error instanceof Error) { return error; } return new Error(this.formatSyncError(error)); } }