mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-05 18:52:03 +00:00
165 lines
5.1 KiB
TypeScript
165 lines
5.1 KiB
TypeScript
|
|
/**
|
||
|
|
* InMemoryCalendarClient — deterministic in-memory CalendarClient for local dev.
|
||
|
|
*
|
||
|
|
* Implements the same CalendarClient interface as GoogleCalendarClient but holds
|
||
|
|
* a small set of FAKE seed events in memory, partitioned by userSub so a user
|
||
|
|
* only ever sees their own calendar. No network, no AWS, no token provider.
|
||
|
|
*
|
||
|
|
* This is for build-plan §4 "in-memory dev clients (tools return real data
|
||
|
|
* locally)" — wire it into a dev server instead of GoogleCalendarClient.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import type {
|
||
|
|
AvailabilityResult,
|
||
|
|
CalendarClient,
|
||
|
|
CalendarEvent,
|
||
|
|
CheckAvailabilityOptions,
|
||
|
|
CreateEventOptions,
|
||
|
|
GetEventsOptions,
|
||
|
|
} from './client.js';
|
||
|
|
|
||
|
|
/** Seed events keyed by userSub. Each user sees only their own list. */
|
||
|
|
function seedEvents(): Map<string, CalendarEvent[]> {
|
||
|
|
const map = new Map<string, CalendarEvent[]>();
|
||
|
|
|
||
|
|
map.set('lauren@seahavenind.com', [
|
||
|
|
{
|
||
|
|
id: 'evt-lauren-001',
|
||
|
|
summary: 'Vendor onboarding sync',
|
||
|
|
description: 'Walk through the new vendor intake checklist.',
|
||
|
|
start: '2026-06-25T15:00:00Z',
|
||
|
|
end: '2026-06-25T15:30:00Z',
|
||
|
|
attendees: [
|
||
|
|
{
|
||
|
|
email: 'lauren@seahavenind.com',
|
||
|
|
responseStatus: 'accepted',
|
||
|
|
self: true,
|
||
|
|
organizer: true,
|
||
|
|
},
|
||
|
|
{ email: 'adam@seahavenind.com', responseStatus: 'accepted' },
|
||
|
|
],
|
||
|
|
hasExternalAttendees: false,
|
||
|
|
status: 'confirmed',
|
||
|
|
htmlLink: 'https://calendar.google.com/event?eid=evt-lauren-001',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'evt-lauren-002',
|
||
|
|
summary: 'Site walkthrough — Marina pier',
|
||
|
|
start: '2026-06-26T13:00:00Z',
|
||
|
|
end: '2026-06-26T14:00:00Z',
|
||
|
|
attendees: [
|
||
|
|
{
|
||
|
|
email: 'lauren@seahavenind.com',
|
||
|
|
responseStatus: 'accepted',
|
||
|
|
self: true,
|
||
|
|
organizer: true,
|
||
|
|
},
|
||
|
|
{ email: 'contractor@example.com', responseStatus: 'needsAction' },
|
||
|
|
],
|
||
|
|
hasExternalAttendees: true,
|
||
|
|
status: 'confirmed',
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
|
||
|
|
map.set('adam@seahavenind.com', [
|
||
|
|
{
|
||
|
|
id: 'evt-adam-001',
|
||
|
|
summary: 'Quarterly finance review',
|
||
|
|
description: 'Review payment dashboard and QBO vendor balances.',
|
||
|
|
start: '2026-06-24T17:00:00Z',
|
||
|
|
end: '2026-06-24T18:00:00Z',
|
||
|
|
attendees: [
|
||
|
|
{ email: 'adam@seahavenind.com', responseStatus: 'accepted', self: true, organizer: true },
|
||
|
|
{ email: 'lauren@seahavenind.com', responseStatus: 'tentative' },
|
||
|
|
],
|
||
|
|
hasExternalAttendees: false,
|
||
|
|
status: 'confirmed',
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
|
||
|
|
return map;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* In-memory CalendarClient. Events are partitioned by userSub; createEvent
|
||
|
|
* appends to the caller's own list and returns the created event.
|
||
|
|
*/
|
||
|
|
export class InMemoryCalendarClient implements CalendarClient {
|
||
|
|
private readonly eventsBySub: Map<string, CalendarEvent[]>;
|
||
|
|
private counter = 1000;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this.eventsBySub = seedEvents();
|
||
|
|
}
|
||
|
|
|
||
|
|
async getEvents(userSub: string, opts: GetEventsOptions): Promise<CalendarEvent[]> {
|
||
|
|
const all = this.eventsBySub.get(userSub) ?? [];
|
||
|
|
const min = Date.parse(opts.timeMin);
|
||
|
|
const max = Date.parse(opts.timeMax);
|
||
|
|
const inWindow = all.filter((e) => {
|
||
|
|
const start = Date.parse(e.start);
|
||
|
|
return start >= min && start <= max;
|
||
|
|
});
|
||
|
|
const limit = opts.maxResults ?? 50;
|
||
|
|
return inWindow.slice(0, limit);
|
||
|
|
}
|
||
|
|
|
||
|
|
async checkAvailability(
|
||
|
|
userSub: string,
|
||
|
|
opts: CheckAvailabilityOptions,
|
||
|
|
): Promise<AvailabilityResult> {
|
||
|
|
const min = Date.parse(opts.timeMin);
|
||
|
|
const max = Date.parse(opts.timeMax);
|
||
|
|
const all = this.eventsBySub.get(userSub) ?? [];
|
||
|
|
|
||
|
|
const busy = all
|
||
|
|
.filter((e) => Date.parse(e.start) < max && Date.parse(e.end) > min)
|
||
|
|
.map((e) => ({ start: e.start, end: e.end }))
|
||
|
|
.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
|
||
|
|
|
||
|
|
const free: Array<{ start: string; end: string }> = [];
|
||
|
|
let cursor = min;
|
||
|
|
for (const slot of busy) {
|
||
|
|
const slotStart = Date.parse(slot.start);
|
||
|
|
if (slotStart > cursor) {
|
||
|
|
free.push({
|
||
|
|
start: new Date(cursor).toISOString(),
|
||
|
|
end: new Date(slotStart).toISOString(),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
cursor = Math.max(cursor, Date.parse(slot.end));
|
||
|
|
}
|
||
|
|
if (cursor < max) {
|
||
|
|
free.push({ start: new Date(cursor).toISOString(), end: new Date(max).toISOString() });
|
||
|
|
}
|
||
|
|
|
||
|
|
return { busy, free };
|
||
|
|
}
|
||
|
|
|
||
|
|
async createEvent(userSub: string, opts: CreateEventOptions): Promise<CalendarEvent> {
|
||
|
|
const event: CalendarEvent = {
|
||
|
|
id: `evt-dev-${this.counter++}`,
|
||
|
|
summary: opts.summary,
|
||
|
|
start: opts.start,
|
||
|
|
end: opts.end,
|
||
|
|
status: 'confirmed',
|
||
|
|
htmlLink: `https://calendar.google.com/event?eid=evt-dev-${this.counter}`,
|
||
|
|
};
|
||
|
|
if (opts.description !== undefined) {
|
||
|
|
event.description = opts.description;
|
||
|
|
}
|
||
|
|
if (opts.attendees !== undefined) {
|
||
|
|
event.attendees = opts.attendees.map((a) =>
|
||
|
|
a.displayName !== undefined
|
||
|
|
? { email: a.email, displayName: a.displayName, responseStatus: 'needsAction' }
|
||
|
|
: { email: a.email, responseStatus: 'needsAction' },
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const list = this.eventsBySub.get(userSub) ?? [];
|
||
|
|
list.push(event);
|
||
|
|
this.eventsBySub.set(userSub, list);
|
||
|
|
return event;
|
||
|
|
}
|
||
|
|
}
|