UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

157 lines • 6.61 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { TimeRange, parseTime } from "../time_range.js"; import { eventSpan } from "./day.js"; import { calendarBase, googleDelete, googleGet } from "../google/client.js"; const MAX_PAGES = 50; export const PAGE_LIMIT = 'gcal: listing exceeded 50 pages; narrow the date range'; function rows(data) { if (typeof data !== 'object' || data === null) return []; const items = data.items; if (!Array.isArray(items)) return []; return items.filter((r) => typeof r === 'object' && r !== null && !Array.isArray(r)); } function nextToken(data) { if (typeof data !== 'object' || data === null) return null; const next = data.nextPageToken; return typeof next === 'string' && next !== '' ? next : null; } /** * List the account's calendars. * * showHidden is left at its default (false) so the mount shows what the * Calendar UI shows; a subscribed holiday calendar the user hid stays out. */ export async function listCalendars(tokenManager, minAccessRole) { const url = `${calendarBase(tokenManager)}/users/me/calendarList`; const base = {}; if (minAccessRole !== undefined && minAccessRole !== '') base.minAccessRole = minAccessRole; const items = []; let token = null; for (let i = 0; i < MAX_PAGES; i += 1) { const page = token !== null ? { ...base, pageToken: token } : base; const data = await googleGet(tokenManager, url, page); for (const item of rows(data)) items.push(item); token = nextToken(data); if (token === null) break; } if (token !== null) throw new Error(PAGE_LIMIT); return items; } /** * List a calendar's events overlapping a time window. * * timeMin bounds an event's END and timeMax its START, both exclusive, so * the pair is an overlap query: a multi-day or midnight-crossing event is * returned by every day window it touches, with no extra request. * * singleEvents expands a recurring series into its instances, which is what * makes a day directory well defined; without it the series' own record * would stand in for every occurrence. */ export async function listEvents(tokenManager, calendarId, timeMin, timeMax, timeZone, scope = new TimeRange(), query) { // The id is one path segment and several real ones are not URL-safe: a // holiday calendar is "en.usa#holiday@group.v.calendar.google.com", and an // unencoded "#" opens a fragment, so the request would reach // /calendars/en.usa instead. const url = `${calendarBase(tokenManager)}/calendars/${encodeURIComponent(calendarId)}/events`; const [lo, hi] = scope.clip(timeMin === null ? -Infinity : parseTime(timeMin), parseTime(timeMax)); if (lo >= hi) return []; const base = { timeMax: new Date(Math.ceil(hi) * 1000).toISOString(), singleEvents: 'true', orderBy: 'startTime', maxResults: '2500', }; if (Number.isFinite(lo)) base.timeMin = new Date(Math.floor(lo) * 1000).toISOString(); if (timeZone !== undefined && timeZone !== '') base.timeZone = timeZone; if (query !== undefined) base.q = query; const items = []; let token = null; for (let i = 0; i < MAX_PAGES; i += 1) { const page = token !== null ? { ...base, pageToken: token } : base; const data = await googleGet(tokenManager, url, page); for (const item of rows(data)) items.push(item); token = nextToken(data); if (token === null) break; } if (token !== null) throw new Error(PAGE_LIMIT); return scope.bounded ? items.filter((event) => { const span = eventSpan(event, timeZone ?? 'UTC'); return span !== null && listed(span, lo, hi); }) : items; } /** * Whether a listing of the window `lo`..`hi` (epoch seconds) keeps an event * spanning `span` (epoch ms): one that starts before the window ends and ends * after it starts, or an instant one at or after its start. Mirrors Python's * `listed`. */ export function listed(span, lo, hi) { const [start, end] = [span[0] / 1000, span[1] / 1000]; return start < hi && (end > lo || (start === end && start >= lo)); } /** * A calendar's events changed since `syncToken`, every event when null, and * the token the next sync starts from. Instances of a recurring series come one * by one (singleEvents), as a listing names them, and one deleted since the * token comes back with status `cancelled`. Calendar answers 410 for a token it * no longer honors, which rejects as any failed call does. Mirrors Python's * `sync_events`. */ export async function syncEvents(tokenManager, calendarId, syncToken, timeZone) { const url = `${calendarBase(tokenManager)}/calendars/${encodeURIComponent(calendarId)}/events`; const base = { singleEvents: 'true', maxResults: '2500' }; if (timeZone !== undefined && timeZone !== '') base.timeZone = timeZone; if (syncToken !== null) base.syncToken = syncToken; const items = []; let token = null; for (let i = 0; i < MAX_PAGES; i += 1) { const page = token !== null ? { ...base, pageToken: token } : base; const data = await googleGet(tokenManager, url, page); for (const item of rows(data)) items.push(item); token = nextToken(data); if (token === null) { const done = data.nextSyncToken; return [items, typeof done === 'string' ? done : null]; } } throw new Error(PAGE_LIMIT); } /** Delete one event. */ export async function deleteEvent(tokenManager, calendarId, eventId) { const url = `${calendarBase(tokenManager)}/calendars/${encodeURIComponent(calendarId)}` + `/events/${encodeURIComponent(eventId)}`; await googleDelete(tokenManager, url); } //# sourceMappingURL=client.js.map