macsync/docs/modules/ical.md

3.6 KiB

iCal (ICalSync)

Purpose

Sync the user's calendars and events with the server, and apply web-initiated event edits back into Calendar.app.

Direction

Bidirectional. Inbound: EventKit -> server. Outbound: icloud.calendar_send_queue -> EventKit save/remove.

OS surface

EventKit (EKEventStore). Package.swift:73-77 adds linkedFramework("EventKit"). Requires Calendars (full access) — module surfaces .calendarAccessRequired if denied (@packages/ical/Sources/ICalSync/SyncManager.swift:103-105).

Files

  • Reader.swiftCalendarReader.shared; owns the shared EKEventStore (consumed by the Sender at SyncManager.swift:57).
  • APIClient.swift:
    • syncCalendars(_:) -> POST /client/ical/calendars (line 108)
    • syncEvents(_:) -> POST /client/ical/events (line 122)
    • getPendingSends() -> GET /client/ical/send-queue/pending (line 137)
    • reportSendResult(...) -> POST /client/ical/send-queue/:id/result (line 165)
    • getStats() -> GET /client/ical/stats (line 173)
  • Sender.swiftCalendarSender(eventStore:); the apply(item) closure is SyncManager.swift:62-64.
  • SyncManager.swift — overrides didStartSync / willStopSync to start / stop the send queue (SyncManager.swift:67-73). Authorization hooks (lines 97-106) gate cycles. Event batch size 200 (line 52).

Timing

  • Read interval: 300s (@packages/ical/Sources/ICalSync/SyncManager.swift:80).
  • Outbound poll interval: 60s (@packages/ical/Sources/ICalSync/SyncManager.swift:61).
  • Event batch size: 200.

Server surface

  • Entity tables: icloud.calendars, icloud.events, icloud.calendar_send_queue (app/server.ts:44-45, 51).
  • Allowed send-queue actions: create_event, update_event, delete_event (src/server/src/entities/calendarSendItem/types.ts:24).
  • Client routes (src/server/src/surfaces/client/ical.ts):
    • GET /client/ical/stats (line 55)
    • POST /client/ical/calendars (line 59)
    • POST /client/ical/events (line 65)
    • GET /client/ical/send-queue/pending (line 71)
    • POST /client/ical/send-queue/:id/result (line 89)
  • Web routes (src/server/src/surfaces/my/calendar.ts):
    • GET /my/calendar/stats (line 46)
    • GET /my/calendar/calendars (line 49)
    • GET /my/calendar/events (line 57)
    • POST /my/calendar/events (line 80)
    • PUT /my/calendar/events/:id (line 90)
    • DELETE /my/calendar/events/:id (line 102)
  • Admin enqueue: POST /admin/calendar-send-queue/enqueue (src/server/src/surfaces/admin/calendar-send-queue.ts:16).

Web surface

  • Tab: /calendar (web/src/App.tsx:60).
  • API helpers: web/src/api/calendar.ts.
  • UI is intentionally minimal (list + form) per ~/.claude/plans/magical-tumbling-peach.md:180.

EventKit snippets

Reader fetches via EKEventStore.events(matching:) over a date predicate; writer saves with EKEventStore.save(_:span:) / EKEventStore.remove(_:span:). The Sender layer maps the JSON payload's recurrence/alarm fields onto EKRecurrenceRule / EKAlarm.

Known limitations

  • Last-write-wins on simultaneous edits (see known-limitations).
  • Calendar source (iCloud / on-device / Exchange) is captured but the web UI does not let the user pick which source to create new events on; it uses the default.

Tests (@packages/ical/Tests/ICalSyncTests/)

  • ICalSyncTests.swift — Codable round-trips, payload shape (hermetic).
  • SenderTests.swiftCalendarSender.apply dispatch logic against a fake EventKit interface (hermetic).

Not covered: real EventKit save/remove (needs the OS), authorization prompts.