Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 20 additions & 17 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ hypr-cactus = { path = "crates/cactus", package = "cactus" }
hypr-cactus-model = { path = "crates/cactus-model", package = "cactus-model" }
hypr-calendar = { path = "crates/calendar", package = "calendar" }
hypr-calendar-interface = { path = "crates/calendar-interface", package = "calendar-interface" }
hypr-calendar-worker = { path = "crates/calendar-worker", package = "calendar-worker" }
hypr-chatwoot = { path = "crates/chatwoot", package = "chatwoot" }
hypr-claude = { path = "crates/claude", package = "claude" }
hypr-cloudsync = { path = "crates/cloudsync", package = "cloudsync" }
Expand Down
143 changes: 143 additions & 0 deletions apps/desktop/src/services/calendar-auto-start.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { describe, expect, test, vi } from "vitest";

import {
consumePendingCalendarAutoStarts,
resolveCalendarAutoStartEvent,
} from "./calendar-auto-start";

type MockStoreData = {
events: Record<string, Record<string, unknown>>;
values: Record<string, unknown>;
};

function createStore(data: MockStoreData) {
return {
forEachRow: (
tableId: "events",
callback: (rowId: string, forEachCell: unknown) => void,
) => {
if (tableId !== "events") {
return;
}

for (const rowId of Object.keys(data.events)) {
callback(rowId, () => {});
}
},
getRow: (tableId: "events", rowId: string) => {
if (tableId !== "events") {
return null;
}

return data.events[rowId] ?? null;
},
getValue: (valueId: string) => data.values[valueId],
};
}

describe("resolveCalendarAutoStartEvent", () => {
test("returns pending when the synced event row is not available yet", () => {
const result = resolveCalendarAutoStartEvent(
createStore({
events: {},
values: {},
}),
"tracking-1",
);

expect(result).toEqual({ status: "pending" });
});

test("returns ignored when the event tracking id is ignored", () => {
const result = resolveCalendarAutoStartEvent(
createStore({
events: {
"event-row-1": { tracking_id_event: "tracking-1" },
},
values: {
ignored_events: JSON.stringify([{ tracking_id: "tracking-1" }]),
},
}),
"tracking-1",
);

expect(result).toEqual({ status: "ignored" });
});

test("returns ignored when the recurring series is ignored", () => {
const result = resolveCalendarAutoStartEvent(
createStore({
events: {
"event-row-1": {
tracking_id_event: "tracking-1",
recurrence_series_id: "series-1",
},
},
values: {
ignored_recurring_series: JSON.stringify([{ id: "series-1" }]),
},
}),
"tracking-1",
);

expect(result).toEqual({ status: "ignored" });
});

test("returns the local event row id when the event is ready", () => {
const result = resolveCalendarAutoStartEvent(
createStore({
events: {
"event-row-1": { tracking_id_event: "tracking-1" },
},
values: {},
}),
"tracking-1",
);

expect(result).toEqual({
status: "ready",
eventRowId: "event-row-1",
});
});
});

describe("consumePendingCalendarAutoStarts", () => {
test("keeps unresolved tracking ids queued until sync catches up", () => {
const pending = new Set(["tracking-1"]);
const onReady = vi.fn();

consumePendingCalendarAutoStarts(
createStore({
events: {},
values: {},
}),
pending,
onReady,
);

expect(onReady).not.toHaveBeenCalled();
expect([...pending]).toEqual(["tracking-1"]);
});

test("consumes ready and ignored tracking ids without dropping future retries", () => {
const pending = new Set(["tracking-ready", "tracking-ignored"]);
const onReady = vi.fn();

consumePendingCalendarAutoStarts(
createStore({
events: {
"event-row-1": { tracking_id_event: "tracking-ready" },
"event-row-2": { tracking_id_event: "tracking-ignored" },
},
values: {
ignored_events: JSON.stringify([{ tracking_id: "tracking-ignored" }]),
},
}),
pending,
onReady,
);

expect(onReady).toHaveBeenCalledWith("event-row-1");
expect([...pending]).toEqual([]);
});
});
93 changes: 93 additions & 0 deletions apps/desktop/src/services/calendar-auto-start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
type StoreLike = {
forEachRow: (
tableId: "events",
callback: (rowId: string, forEachCell: unknown) => void,
) => void;
getRow: (tableId: "events", rowId: string) => Record<string, unknown> | null;
getValue: (valueId: "ignored_events" | "ignored_recurring_series") => unknown;
};

export type CalendarAutoStartResolution =
| { status: "pending" }
| { status: "ignored" }
| { status: "ready"; eventRowId: string };

export function resolveCalendarAutoStartEvent(
store: StoreLike,
trackingId: string,
): CalendarAutoStartResolution {
let eventRowId: string | null = null;

store.forEachRow("events", (rowId, _forEachCell) => {
if (eventRowId) return;
const row = store.getRow("events", rowId);
if (row?.tracking_id_event === trackingId) {
eventRowId = rowId;
}
});

if (!eventRowId) {
return { status: "pending" };
}

const eventRow = store.getRow("events", eventRowId);
const seriesId = eventRow?.recurrence_series_id as string | undefined;

if (isTrackingIdIgnored(store, trackingId)) {
return { status: "ignored" };
}

if (seriesId && isRecurringSeriesIgnored(store, seriesId)) {
return { status: "ignored" };
}

return { status: "ready", eventRowId };
}

export function consumePendingCalendarAutoStarts(
store: StoreLike,
pendingTrackingIds: Set<string>,
onReady: (eventRowId: string) => void,
): void {
for (const trackingId of [...pendingTrackingIds]) {
const resolution = resolveCalendarAutoStartEvent(store, trackingId);

if (resolution.status === "pending") {
continue;
}

pendingTrackingIds.delete(trackingId);

if (resolution.status === "ready") {
onReady(resolution.eventRowId);
}
}
}

function isTrackingIdIgnored(store: StoreLike, trackingId: string): boolean {
try {
const raw = store.getValue("ignored_events");
if (!raw || typeof raw !== "string") {
return false;
}

const ignored = JSON.parse(raw) as Array<{ tracking_id: string }>;
return ignored.some((event) => event.tracking_id === trackingId);
} catch {
return false;
}
}

function isRecurringSeriesIgnored(store: StoreLike, seriesId: string): boolean {
try {
const raw = store.getValue("ignored_recurring_series");
if (!raw || typeof raw !== "string") {
return false;
}

const ignored = JSON.parse(raw) as Array<{ id: string }>;
return ignored.some((series) => series.id === seriesId);
} catch {
return false;
}
}
Loading
Loading