93 lines
3.6 KiB
Markdown
93 lines
3.6 KiB
Markdown
|
|
# 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.swift` — `CalendarReader.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.swift` — `CalendarSender(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](../known-limitations.md#conflict-resolution-last-write-wins)).
|
||
|
|
- 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.swift` — `CalendarSender.apply` dispatch logic against a fake
|
||
|
|
EventKit interface (hermetic).
|
||
|
|
|
||
|
|
Not covered: real EventKit save/remove (needs the OS), authorization prompts.
|