Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ cronos list
cronos list --date 2025-04-10
cronos next
cronos add --date 2025-04-10 "Lunch 12pm-1pm #green"
cronos delete --id event-12-1713456789
cronos delete --id event-<uuid>
```

## Keyboard Shortcuts
Expand Down
4 changes: 2 additions & 2 deletions src/cli/commands/list.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getAllEvents } from "@features/events/eventsState";
import { findAllEvents } from "@data/repository";
import type { CalendarEvent } from "@shared/types";
import { Effect } from "effect";
import { formatEventLine } from "../format";
Expand Down Expand Up @@ -57,7 +57,7 @@ export function runList(options: ListOptions): void {
return;
}

const allEvents = Effect.runSync(getAllEvents);
const allEvents = Effect.runSync(findAllEvents());
const filtered = date
? allEvents.filter((event) => event.date === date)
: filterByRange(allEvents, from, to);
Expand Down
2 changes: 1 addition & 1 deletion src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ Examples:
cronos list --date 2025-04-10
cronos next
cronos add --date 2025-04-10 "Lunch 12pm-1pm #green"
cronos delete --id event-12-1713456789
cronos delete --id event-<uuid>
`);
}
34 changes: 34 additions & 0 deletions src/data/db.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Database } from "bun:sqlite";
import { Effect, Schedule } from "effect";
import { ensureDatabaseDir, getDatabasePath } from "./config";
import { runMigrations } from "./migrations";

Expand Down Expand Up @@ -31,11 +32,44 @@ export function initDatabase(): void {

// Enable WAL mode for better concurrent read performance
db.run("PRAGMA journal_mode = WAL");
db.run("PRAGMA busy_timeout = 5000");

// Run any pending migrations
runMigrations(db);
}

const SQLITE_BUSY_RETRY_LIMIT = 5;
const SQLITE_BUSY_RETRY_BASE_DELAY = "50 millis";

function isSqliteBusyError(error: unknown): boolean {
if (!error) return false;
if (error instanceof Error && error.message.includes("SQLITE_BUSY")) {
return true;
}
if (typeof error === "object" && "code" in error) {
return (error as { code?: string }).code === "SQLITE_BUSY";
}
return false;
}

const sqliteBusyRetrySchedule = Schedule.recurWhile(isSqliteBusyError).pipe(
Schedule.intersect(Schedule.exponential(SQLITE_BUSY_RETRY_BASE_DELAY)),
Schedule.intersect(Schedule.recurs(SQLITE_BUSY_RETRY_LIMIT)),
);

export function withDbWrite<A>(
run: (db: Database) => A,
): Effect.Effect<A, Error> {
return Effect.retry(
Effect.try({
try: () => run(getDatabase()),
catch: (error) =>
error instanceof Error ? error : new Error(String(error)),
}),
sqliteBusyRetrySchedule,
);
}

/**
* Close the database connection.
* Useful for cleanup in tests or graceful shutdown.
Expand Down
36 changes: 4 additions & 32 deletions src/data/repository.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CalendarEvent, ColorName } from "@shared/types";
import { Effect } from "effect";
import { getDatabase } from "./db";
import { getDatabase, withDbWrite } from "./db";

interface EventRow {
id: string;
Expand Down Expand Up @@ -64,8 +64,7 @@ function rowToEvent(row: EventRow): CalendarEvent {
* Insert a new event into the database.
*/
export const insertEvent = (event: CalendarEvent) =>
Effect.sync(() => {
const db = getDatabase();
withDbWrite((db) => {
const now = event.updatedAt ?? new Date().toISOString();
const stmt = db.prepare(`
INSERT INTO events (
Expand Down Expand Up @@ -107,9 +106,7 @@ export const updateEventById = (
id: string,
updates: Partial<Omit<CalendarEvent, "id">>,
) =>
Effect.sync(() => {
const db = getDatabase();

withDbWrite((db) => {
// Build dynamic update query based on provided fields
const setClauses: string[] = [];
const values: (string | number | null)[] = [];
Expand Down Expand Up @@ -177,8 +174,7 @@ export const updateEventById = (
* Returns true if an event was deleted, false otherwise.
*/
export const deleteEventById = (id: string) =>
Effect.sync(() => {
const db = getDatabase();
withDbWrite((db) => {
const stmt = db.prepare("DELETE FROM events WHERE id = ?");
const result = stmt.run(id);
return result.changes > 0;
Expand Down Expand Up @@ -254,27 +250,3 @@ export const findEventsMissingGoogleId = () =>
const rows = stmt.all() as EventRow[];
return rows.map(rowToEvent);
});

/**
* Get the highest event ID counter value from existing events.
* Used to restore the counter on app startup.
*/
export const getMaxEventIdCounter = () =>
Effect.sync(() => {
const db = getDatabase();
const stmt = db.prepare("SELECT id FROM events");
const rows = stmt.all() as { id: string }[];

let maxCounter = 0;
for (const row of rows) {
// Parse event IDs like "event-123-1234567890"
const match = row.id.match(/^event-(\d+)-/);
if (match?.[1]) {
const counter = parseInt(match[1], 10);
if (counter > maxCounter) {
maxCounter = counter;
}
}
}
return maxCounter;
});
2 changes: 0 additions & 2 deletions src/features/events/eventsService.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import {
deleteEventById,
findAllEvents,
getMaxEventIdCounter,
insertEvent,
updateEventById,
} from "@data/repository";
import type { CalendarEvent } from "@shared/types";

export const loadAllEvents = findAllEvents;
export const loadMaxEventIdCounter = getMaxEventIdCounter;

export const persistEventCreate = (event: CalendarEvent) => insertEvent(event);

Expand Down
Loading