@dayflow/outlook-sync

@dayflow/outlook-sync 将 DayFlow 连接到 Microsoft Graph 日历 API。它是一个独立于 @dayflow/caldav 的包 —— 它直接使用 Microsoft Graph API。

安装

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

快速开始

import { useRef, useEffect, useState } from 'react';
import {
  DayFlowCalendar,
  useCalendarApp,
  createMonthView,
} from '@dayflow/react';
import {
  attachOutlookSyncToDayFlow,
  createOutlookSync,
  createOutlookSyncAdapter,
  type OutlookDayFlowController,
  type OutlookSyncStatus,
} from '@dayflow/outlook-sync';

function MyCalendar() {
  const calendar = useCalendarApp({
    views: [createMonthView()],
    calendars: [],
    events: [],
  });

  const controllerRef = useRef<OutlookDayFlowController | null>(null);
  const [syncStatus, setSyncStatus] = useState<OutlookSyncStatus>({
    state: 'idle',
  });

  useEffect(() => {
    if (controllerRef.current) return;

    const adapter = createOutlookSyncAdapter({
      baseUrl: '/api/outlook-calendar',
    });

    const sync = createOutlookSync(adapter);
    const controller = attachOutlookSyncToDayFlow(calendar.app, sync, {
      writable: true,
      onStatusChange: setSyncStatus,
      onWriteError: (error, ctx) =>
        console.error(`[outlook-sync] ${ctx.action} 失败:`, error.message),
      onSyncComplete: delta => {
        console.log(
          `同步完成: +${delta.events.added} ~${delta.events.updated} -${delta.events.deleted}`
        );
      },
    });

    controllerRef.current = controller;
    controller.start();

    return () => {
      controller.stop();
      controllerRef.current = null;
    };
  }, [calendar.app]);

  return <DayFlowCalendar calendar={calendar} />;
}

令牌注入

使用 getToken(推荐用于客户端令牌)

传递一个 getToken 工厂,以便适配器在每次请求之前获取一个新的令牌。这在使用管理令牌刷新的 MSAL 或其他身份验证库时非常理想:

import { PublicClientApplication } from '@azure/msal-browser';

const msalInstance = new PublicClientApplication(msalConfig);

const adapter = createOutlookSyncAdapter({
  getToken: async () => {
    const result = await msalInstance.acquireTokenSilent({
      scopes: ['Calendars.ReadWrite'],
    });
    return result.accessToken;
  },
});

使用后端代理(推荐用于生产环境)

将 OAuth 令牌保留在服务器上 —— 将所有 Graph API 请求通过代理路由:

const adapter = createOutlookSyncAdapter({
  baseUrl: '/api/outlook-calendar',
  // 不需要 getToken — 代理会注入 Authorization
});

(中略:代理示例与英文文档相同)

增量令牌持久化

默认情况下,Outlook 同步令牌(delta 令牌)存储在内存中,在页面重新加载时丢失。提供一个 OutlookSyncStorage 实现以跨会话持久化它们:

import {
  createOutlookSync,
  type OutlookSyncStorage,
} from '@dayflow/outlook-sync';

const storage: OutlookSyncStorage = {
  getDeltaToken: async calendarId =>
    localStorage.getItem(`outlook-delta:${calendarId}`),
  setDeltaToken: async (calendarId, token) =>
    token
      ? localStorage.setItem(`outlook-delta:${calendarId}`, token)
      : localStorage.removeItem(`outlook-delta:${calendarId}`),
};

const sync = createOutlookSync(adapter, { storage });

通过 storage,每个会话开始时都会进行增量增量同步,而不是从头开始获取所有事件。

本地缓存加载

在第一次远程同步之前从本地存储中为 DayFlow 设定种子,以便日历立即呈现:

const controller = attachOutlookSyncToDayFlow(calendar.app, sync, {
  getInitialSnapshot: async () => {
    const { calendars, events } = await loadFromLocalDB();
    return { calendars, events };
  },
  onSyncComplete: delta => {
    saveChanges(delta);
  },
  onWriteComplete: (operation, event) => {
    persistEvent(operation, event);
  },
});

手动应用远程快照

对于自定义同步编排的应用,applyRemoteSnapshot 在不触发写回的情况下将一批远程事件应用到 DayFlow:

import { applyRemoteSnapshot, getOutlookMeta } from '@dayflow/outlook-sync';

const delta = await applyRemoteSnapshot(
  calendar.app,
  { calendars, events },
  {
    isOwnedEvent: event => Boolean(getOutlookMeta(event)),
    isOwnedCalendar: calendar => calendar.source === 'Outlook',
    snapshotMode: 'authoritative',
    resolveConflict: (remote, local) =>
      mergeLocalEditsOntoRemote(remote, local),
  }
);

(中略:详细信息与英文文档相同)

选项参考

attachOutlookSyncToDayFlow 选项

选项类型默认值描述
writablebooleantrue允许将本地更改写回 Outlook 日历。
onStatusChange(status: OutlookSyncStatus) => void每当同步状态更改时调用。
onWriteError(error: Error, ctx) => voidconsole.error当写回失败时调用。ctx 包括 actioneventId
getInitialSnapshot() => Promise<{ events, calendars }>在第一次远程同步之前从本地缓存中为 DayFlow 设定种子。
onSyncComplete(delta: OutlookSyncDelta) => void在每次成功同步后调用,包含更改计数。
onWriteComplete(operation, event) => void在本地变更成功写回 Outlook 日历后调用。

createOutlookSyncAdapter 选项

(中略:表项与英文相同)

同步原理

日历发现

controller.start() 时,包获取用户的日历列表 (GET /me/calendars) 并将每个日历注册在 DayFlow 中。canEditfalse 的日历被标记为 readOnly: true

事件加载

事件通过 Microsoft Graph 的 calendarView/delta 端点加载,带有 startDateTimeendDateTime 参数,这在时间窗口内展开重复事件。当用户导航时,新范围的事件会自动加载。

带 Delta 令牌的增量同步

初始加载后,Graph API 返回 @odata.deltaLink。在后续同步中,包跟踪此链接仅获取更改的事件 —— 而不是整个范围。提供 OutlookSyncStorage 时,Delta 令牌可以在页面重新加载时存活。

如果 Delta 令牌过期(Graph 返回 410 Gone),包会自动回退到完全范围查询。

写回

writable: true 时,本地事件更改会写回 Outlook 日历:

  • 创建: POST /me/calendars/{calendarId}/events
  • 更新: PATCH /me/calendars/{calendarId}/events/{eventId} (If-Match: <etag> 付き)
  • 删除: DELETE /me/calendars/{calendarId}/events/{eventId}

如果更新返回 412 Precondition Failed (ETag 冲突),包会重新获取最新的 ETag 并重试一次。

重复事件永不写回 —— 它们是只读的。

日历颜色

Outlook 使用命名颜色(例如 lightBluedarkGreen),而不是十六进制代码。该包将它们映射为近似的十六进制值,并通过 getCalendarColorsForHex 传递以实现一致的 DayFlow 主题。

Microsoft Graph API 范围

您的 OAuth 令牌必须包含以下至少一个范围:

范围访问权限
Calendars.ReadWrite完全读/写访问权限
Calendars.Read只读访问权限 (writable: false 一起使用)

对于应用(服务器到服务器)流程,请使用带有服务主体的 .default 范围:

https://graph.microsoft.com/.default

On this page