@dayflow/sync-core

@dayflow/sync-core์—๋Š” DayFlow ๋™๊ธฐํ™” ์—ฐ๋™์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ œ๊ณต์ž ์ค‘๋ฆฝ์ ์ธ ์กฐ์ •(reconciliation) ํ—ฌํผ๊ฐ€ ๋“ค์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ์ด ํŒจํ‚ค์ง€๋ฅผ ์ง์ ‘ ๊ฐ€์ ธ์˜ฌ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋จผ์ € ์ œ๊ณต์ž ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”:

์ง์ ‘ ์ œ๊ณต์ž๋ฅผ ๋งŒ๋“ค๊ฑฐ๋‚˜, ๋ฐฑ์—”๋“œ ์ž‘์—…์œผ๋กœ ๋™๊ธฐํ™”ํ•˜๊ฑฐ๋‚˜, ์ œํ’ˆ ์ „์šฉ ์บ˜๋ฆฐ๋”ยท์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์›๊ฒฉ ์ œ๊ณต์ž ๋ ˆ์ฝ”๋“œ์™€ ๋งž์ถฐ์•ผ ํ•  ๋•Œ @dayflow/sync-core๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

์„ค์น˜

npm install @dayflow/sync-core
pnpm add @dayflow/sync-core
yarn add @dayflow/sync-core
bun add @dayflow/sync-core

๋‹ด๋‹น ๋ฒ”์œ„

@dayflow/sync-core๋Š” ์ œ๊ณต์ž์— ์ข…์†๋˜์ง€ ์•Š๋Š” diffยท์ ์šฉ ๊ณ„์ธต์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค:

ํ—ฌํผ์‚ฌ์šฉํ•˜๋Š” ์ƒํ™ฉ
applyProviderEventsToDayFlow์ œ๊ณต์ž ๋ฐ”์ธ๋”ฉ์ด ์ด๋ฏธ DayFlow ์ด๋ฒคํŠธ๋กœ ๋งคํ•‘ํ•˜๊ณ  ์‚ญ์ œ๋œ ์›๊ฒฉ ์ฐธ์กฐ๋ฅผ ํ™•๋ณดํ•œ ๊ฒฝ์šฐ
applyRemoteSnapshot์ „์ฒด ๋˜๋Š” ์ผ๋ถ€ ์ œ๊ณต์ž ์Šค๋ƒ…์ˆ์ด ์žˆ๊ณ  ์ด๋ฅผ CalendarApp์— ์ ์šฉํ•˜๋ ค๋Š” ๊ฒฝ์šฐ
reconcileProviderCalendars์›๊ฒฉ ์บ˜๋ฆฐ๋”๋ฅผ ์ž์ฒด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ ˆ์ฝ”๋“œ๋กœ ๋™๊ธฐํ™”ํ•˜๋Š” ๊ฒฝ์šฐ
reconcileProviderEvents์›๊ฒฉ ์ด๋ฒคํŠธ๋ฅผ ์ž์ฒด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ๋™๊ธฐํ™”ํ•˜๋ฉด์„œ ์ •๊ทœํ™”๋œ ๋ณ€๊ฒฝ ๋ ˆ์ฝ”๋“œ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ

์ œ๊ณต์ž ์–ด๋Œ‘ํ„ฐ, ์ธ์ฆ ํ๋ฆ„, ํ† ํฐ ๊ฐฑ์‹ , ์ž๊ฒฉ ์ฆ๋ช… ์ €์žฅ, ํžˆ์Šคํ† ๋ฆฌ ์˜์†ํ™”, ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…, ์žฌ์‹œ๋„ ํ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ๋Š” ์ œ๊ณตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ œ๊ณต์ž ์ด๋ฒคํŠธ๋ฅผ DayFlow์— ์ ์šฉํ•˜๊ธฐ

์ œ๊ณต์ž ํŒจํ‚ค์ง€๋Š” ์›๊ฒฉ ์ด๋ฒคํŠธ๋ฅผ DayFlow Event ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•œ ๋’ค applyProviderEventsToDayFlow๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด ์ด๋ฒคํŠธ๋ฅผ ๋จผ์ € ์ œ๊ณต์ž ์‹๋ณ„์ž๋กœ, ๊ทธ๋‹ค์Œ DayFlow ID๋กœ ๋Œ€์กฐํ•˜๊ณ , ๋™๊ธฐํ™” ๋ฃจํ”„๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•ด source: 'remote'๋กœ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

import { applyProviderEventsToDayFlow } from '@dayflow/sync-core';

const delta = applyProviderEventsToDayFlow({
  app: calendar.app,
  events: mappedRemoteEvents,
  deleted: deletedRemoteRefs,
  getProviderEventId: event => event.meta?.myProvider?.href,
  getDeletedProviderEventId: deleted => deleted.href,
  resolveUpdate: (remote, existing) => ({
    ...remote,
    meta: {
      ...existing.meta,
      ...remote.meta,
    },
  }),
});

console.log(delta.added, delta.updated, delta.deleted);

CalDAV์˜ href, Google ์ด๋ฒคํŠธ id, Graph ์ด๋ฒคํŠธ id์ฒ˜๋Ÿผ ์•ˆ์ •์ ์ธ ์ œ๊ณต์ž ์‹๋ณ„์ž๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”. ๊ทธ๋ž˜์•ผ ๋‚˜์ค‘์— DayFlow id ์ „๋žต์„ ๋ฐ”๊พธ๋”๋ผ๋„ ๋กœ์ปฌ ์ƒํƒœ์— ์ด๋ฒคํŠธ๊ฐ€ ์ค‘๋ณต๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์›๊ฒฉ ์Šค๋ƒ…์ˆ ์ ์šฉํ•˜๊ธฐ

applyRemoteSnapshot์€ DayFlow ์บ˜๋ฆฐ๋”์™€ ์ด๋ฒคํŠธ์˜ ์Šค๋ƒ…์ˆ์„ ์•ฑ์— ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค.

import { applyRemoteSnapshot } from '@dayflow/sync-core';

const delta = await applyRemoteSnapshot(
  calendar.app,
  {
    calendars: remoteCalendars,
    events: remoteEvents,
  },
  {
    isOwnedCalendar: calendar => calendar.source === 'Custom Provider',
    isOwnedEvent: event => event.meta?.provider === 'custom',
    resolveConflict: (remote, local) => ({
      ...remote,
      meta: {
        ...local.meta,
        ...remote.meta,
      },
    }),
    snapshotMode: 'authoritative',
  }
);

๊นŒ๋‹ค๋กœ์šด ๋ถ€๋ถ„์€ ์‚ญ์ œ์ž…๋‹ˆ๋‹ค. snapshotMode์˜ ๊ธฐ๋ณธ๊ฐ’์€ 'partial'์ด๋ฏ€๋กœ, ์Šค๋ƒ…์ˆ์— ์—†๋Š” ๋กœ์ปฌ ์†Œ์œ  ๋ ˆ์ฝ”๋“œ๋Š” ๊ทธ๋Œ€๋กœ ๋ณด์กด๋ฉ๋‹ˆ๋‹ค. ํ‘œ์‹œ ๋ฒ”์œ„ ๋™๊ธฐํ™”, ํ•„ํ„ฐ๋ง๋œ ์กฐํšŒ, ํŽ˜์ด์ง€ ๋‹จ์œ„ ์ œ๊ณต์ž ์‘๋‹ต์—๋Š” ์ด ๋ฐฉ์‹์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค:

await applyRemoteSnapshot(calendar.app, partialSnapshot, {
  isOwnedCalendar,
  isOwnedEvent,
  snapshotMode: 'partial',
});

snapshotMode: 'authoritative'๋Š” ์Šค๋ƒ…์ˆ์ด ์ œ๊ณต์ž๊ฐ€ ์†Œ์œ ํ•œ ๋ชจ๋“  ์บ˜๋ฆฐ๋”์™€ ์ด๋ฒคํŠธ๋ฅผ ์˜จ์ „ํžˆ ๋‹ด๊ณ  ์žˆ์„ ๋•Œ๋งŒ ์‚ฌ์šฉํ•˜์„ธ์š”. ์ œ๊ณต์ž๋ณ„ ์ •๋ฆฌ ์ •์ฑ…์„ ์œ„ํ•œ ํ•˜์œ„ ์ˆ˜์ค€ ํ”Œ๋ž˜๊ทธ deleteMissingCalendars์™€ deleteMissingEvents๋„ ๊ทธ๋Œ€๋กœ ๋‚จ์•„ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ ˆ์ฝ”๋“œ ์กฐ์ •ํ•˜๊ธฐ

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ œ๊ณต์ž ๋ ˆ์ฝ”๋“œ๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅํ•œ ๋’ค DayFlow์— ์ ์šฉํ•œ๋‹ค๋ฉด reconcileProviderCalendars์™€ reconcileProviderEvents๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

import {
  reconcileProviderCalendars,
  reconcileProviderEvents,
} from '@dayflow/sync-core';

const calendarResult = await reconcileProviderCalendars({
  provider: 'custom',
  remoteCalendars,
  existingCalendars: await db.calendar.findMany({ where: { accountId } }),
  getRemoteCalendarId: remote => remote.id,
  getRecordId: record => record.id,
  getRecordExternalCalendarId: record => record.externalCalendarId,
  mapRemoteCalendar: (remote, existing) => ({
    id: existing?.id ?? crypto.randomUUID(),
    accountId,
    provider: 'custom',
    externalCalendarId: remote.id,
    name: remote.name,
    color: remote.color ?? null,
    readOnly: remote.readOnly,
    syncEnabled: existing?.syncEnabled ?? true,
    syncToken: existing?.syncToken ?? null,
    syncStatus: 'idle',
  }),
  save: records => db.calendar.upsertMany(records),
  deactivate: records =>
    db.calendar.updateMany(
      records.map(record => ({ ...record, syncEnabled: false }))
    ),
});

const eventResult = await reconcileProviderEvents({
  provider: 'custom',
  calendarId: localCalendar.id,
  remoteEvents,
  deletedRemoteEvents,
  existingRecords: await db.event.findMany({
    where: { calendarId: localCalendar.id },
  }),
  getRemoteEventId: remote => remote.id,
  getDeletedRemoteEventId: deleted => deleted.id,
  getRecordExternalEventId: record => record.externalEventId,
  isRecordDeleted: record => Boolean(record.deletedAt),
  mapRemoteEvent: (remote, existing) => ({
    id: existing?.id ?? crypto.randomUUID(),
    accountId,
    calendarId: localCalendar.id,
    provider: 'custom',
    externalEventId: remote.id,
    title: remote.title,
    description: remote.description ?? null,
    startsAt: remote.startsAt,
    endsAt: remote.endsAt,
    timezone: remote.timezone ?? null,
    allDay: remote.allDay,
    location: remote.location ?? null,
    rawData: remote,
    externalUpdatedAt: remote.updatedAt ?? null,
    localUpdatedAt: existing?.localUpdatedAt ?? null,
    syncStatus: 'synced',
    deletedAt: null,
  }),
  save: records => db.event.upsertMany(records),
  softDelete: record =>
    db.event.update(record.id, { deletedAt: new Date().toISOString() }),
});

await db.syncHistory.insertMany([
  ...calendarResult.changes,
  ...eventResult.changes,
]);

์ด ํ—ฌํผ๋“ค์€ calendar.created, event.updated, event.deleted ๊ฐ™์€ ์ •๊ทœํ™”๋œ ๋ณ€๊ฒฝ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ๋ณ€๊ฒฝ์„ ๊ฐ์‚ฌ ๋กœ๊ทธ๋กœ ๋‚จ๊ธธ์ง€, ์•Œ๋ฆผ์œผ๋กœ ๋ณด๋‚ผ์ง€, ๋ถ„์„์— ์“ธ์ง€, ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€ ์•Š์„์ง€๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.

์“ฐ์ง€ ์•Š์•„๋„ ๋˜๋Š” ๊ฒฝ์šฐ

ํ‘œ์ค€ ์›๊ฒฉ ์บ˜๋ฆฐ๋”๋ฅผ ์—ฐ๊ฒฐํ•˜๋ ค๋Š” ๋ชฉ์ ๋งŒ์œผ๋กœ @dayflow/sync-core๋ฅผ ์ถ”๊ฐ€ํ•˜์ง€ ๋งˆ์„ธ์š”. ์ œ๊ณต์ž ํŒจํ‚ค์ง€๋“ค์ด ์ด๋ฏธ ๋‚ด๋ถ€์ ์œผ๋กœ ์•Œ๋งž์€ sync-core ํ—ฌํผ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์ œ๊ณต์ž๋ณ„ ๋งคํ•‘ยท๋ฉ”ํƒ€๋ฐ์ดํ„ฐยท๊ถŒํ•œยท์›๊ฒฉ ๋ฐ˜์˜ ๋™์ž‘ยทDayFlow ์—ฐ๊ฒฐ ์ปจํŠธ๋กค๋Ÿฌ๊นŒ์ง€ ํ•จ๊ป˜ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ ๊ฒฝ๊ณ„๋ฅผ ๋„˜์„ ๋•Œ๋งŒ ์ง์ ‘ ์‚ฌ์šฉํ•˜์„ธ์š”:

๊ฒฝ๊ณ„sync-core๊ฐ€ ๋„์›€์ด ๋˜๋Š” ์ด์œ 
์ง์ ‘ ๋งŒ๋“œ๋Š” ์ œ๊ณต์ž ํŒจํ‚ค์ง€๊ณต์‹ ์ œ๊ณต์ž์™€ ๋™์ผํ•œ DayFlow ์ ์šฉ ๋™์ž‘์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๋ฐฑ์—”๋“œ ์ฃผ๋„ ๋™๊ธฐํ™”DayFlow๋ฅผ ๋ Œ๋”๋งํ•˜๊ธฐ ์ „์— ์ œ๊ณต์ž ๋ ˆ์ฝ”๋“œ๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๊ฐ์‚ฌยทํžˆ์Šคํ† ๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์ œ๊ณต์ž ์–ด๋Œ‘ํ„ฐ์— ๋ฌถ์ง€ ์•Š๊ณ  ์ •๊ทœํ™”๋œ SyncChange ๊ฐ์ฒด๋ฅผ ์†Œ๋น„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
id ์ „๋žต ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜DayFlow ๋กœ์ปฌ id๋ฅผ ๋ฐ”๊พธ๋ฉด์„œ๋„ ์•ˆ์ •์ ์ธ ์ œ๊ณต์ž ์‹๋ณ„์ž๋กœ ๋Œ€์กฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ํŽ˜์ด์ง€์˜ ๋‚ด์šฉ