Code Inbox
model.js 199 lines
Files · 8
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199import ddl from "./schema.js";
import manifest from "./manifest.json";
import { database } from "../../shared/database.js";
export function fromGitHub(item) {
if (!item || typeof item !== "object" || Array.isArray(item))
throw Error("Each notification must be an object.");
if (typeof item.id !== "string")
throw Error("GitHub notification IDs must be strings.");
if (item.unread !== undefined && typeof item.unread !== "boolean")
throw Error("GitHub unread must be a boolean.");
return {
id: item.id,
repository_full_name: item.repository?.full_name,
repository_html_url: item.repository?.html_url ?? null,
subject_title: item.subject?.title,
subject_type: item.subject?.type,
subject_url: item.subject?.url ?? null,
subject_latest_comment_url: item.subject?.latest_comment_url ?? null,
reason: item.reason,
unread: item.unread === undefined ? 1 : Number(item.unread),
updated_at: item.updated_at,
last_read_at: item.last_read_at ?? null,
url: item.url,
subscription_url: item.subscription_url ?? null,
};
}
export function createCodeInbox(L) {
const db = database(L, ddl, manifest);
const normalize = (data) => {
db.check("receive-thread", data);
for (const field of [
"id",
"repository_full_name",
"subject_title",
"subject_type",
"reason",
"updated_at",
"url",
])
if (!data[field]?.trim()) throw Error(`${field} cannot be empty.`);
for (const field of ["updated_at", "last_read_at"])
if (
data[field] != null &&
(!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(
data[field],
) ||
!Number.isFinite(Date.parse(data[field])))
)
throw Error(`${field} must be an ISO timestamp.`);
return data;
};
const save = async (data, existing) => {
const row = existing
? await db.update("threads", data.id, data)
: await db.insert("threads", data);
await db.emit(existing ? "thread-updated" : "thread-received", row);
return row;
};
return {
...db,
list: () =>
db.ready.then(() =>
db.query(
"SELECT * FROM threads ORDER BY julianday(updated_at) DESC, id",
),
),
receive: (data) =>
db.run(async () => {
normalize(data);
if (await db.get("threads", data.id)) return;
return save(data, false);
}),
save: (data, id) =>
db.run(async () => {
normalize(data);
if (id !== undefined && data.id !== id)
throw Error("A thread keeps its source ID.");
const existing = await db.get("threads", data.id);
if (id === undefined && existing)
throw Error(
"That thread ID already exists. Edit it or import an update.",
);
if (id !== undefined && !existing)
throw Error("Thread no longer exists.");
const row = await save(data, !!existing);
if (existing?.unread && row.unread === 0)
await db.emit("thread-read", row);
return row;
}),
import: (items) =>
db.run(async () => {
const list = (Array.isArray(items) ? items : [items])
.map(fromGitHub)
.map(normalize);
for (const data of list) {
const existing = await db.get("threads", data.id);
if (
existing &&
Object.entries(data).every(([k, v]) => existing[k] === v)
)
continue;
await save(data, !!existing);
}
return list.length;
}),
read: (id) =>
db.run(async () => {
const row = await db.get("threads", id);
if (!row || !row.unread) return;
const updated = await db.update("threads", id, {
unread: 0,
last_read_at: new Date().toISOString(),
});
await db.emit("thread-read", updated);
}),
remove: (id) =>
db.run(() => db.exec("DELETE FROM threads WHERE id = ?", [id])),
};
}
export const samples = [
{
id: "sample-851",
repository: {
full_name: "northstar/design-system",
html_url: "https://example.invalid/resource",
},
subject: {
title: "Add keyboard navigation to the command menu",
type: "PullRequest",
url: "https://example.invalid/resource",
latest_comment_url: null,
},
reason: "review_requested",
unread: true,
updated_at: "2026-10-15T10:42:00Z",
last_read_at: null,
url: "https://example.invalid/resource",
subscription_url: null,
},
{
id: "sample-852",
repository: {
full_name: "northstar/mobile",
html_url: null,
},
subject: {
title: "Offline changes disappear after reconnecting",
type: "Issue",
url: null,
latest_comment_url: null,
},
reason: "mention",
unread: true,
updated_at: "2026-10-15T09:20:00Z",
last_read_at: null,
url: "https://example.invalid/resource",
subscription_url: null,
},
{
id: "sample-853",
repository: {
full_name: "northstar/design-system",
html_url: null,
},
subject: {
title: "Version 2.4.0",
type: "Release",
url: null,
latest_comment_url: null,
},
reason: "subscribed",
unread: false,
updated_at: "2026-10-14T16:10:00Z",
last_read_at: "2026-10-14T17:00:00Z",
url: "https://example.invalid/resource",
subscription_url: null,
},
{
id: "sample-854",
repository: {
full_name: "northstar/docs",
html_url: null,
},
subject: {
title: "Document the new import format",
type: "Issue",
url: null,
latest_comment_url: null,
},
reason: "assign",
unread: true,
updated_at: "2026-10-14T14:35:00Z",
last_read_at: null,
url: "https://example.invalid/resource",
subscription_url: null,
},
];