Skip to content

g1t/packages/contracts/src/inbox.ts

242 lines8,954 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1/**
2 * The inbox: what needs a person, or what they follow, as it happens. The
Inbox: threads, reasons, subscriptions and watching3 * events service keeps it and brings each person's thread about a thing
4 * (an issue, a pull request, a workflow on a branch, a deployment) back to
5 * the top as things happen to it. Mirrors `crates/contracts/src/inbox.rs`,
6 * which says who is told of what, and why.
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all7 */
8import type { ServiceBinding } from "./clients";
9import type { User } from "./identity";
10
11/**
12 * How much an item matters: a failure, something a person must answer (an
Inbox: threads, reasons, subscriptions and watching13 * agent waiting on them, a review asked of them), something that went well,
14 * or something to know.
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all15 */
16export type InboxSeverity = "error" | "warning" | "success" | "info";
17
18export const INBOX_SEVERITIES: InboxSeverity[] = ["error", "warning", "success", "info"];
19
Inbox: threads, reasons, subscriptions and watching20/**
21 * Why a person was told: what the thread asked of them, or what ties them
22 * to it. Most specific first.
23 */
24export type InboxReason =
25 | "agent"
26 | "review_requested"
27 | "assign"
28 | "mention"
29 | "ci_activity"
30 | "security_alert"
31 | "state_change"
32 | "author"
33 | "comment"
34 | "manual"
35 | "subscribed";
36
37export const INBOX_REASONS: InboxReason[] = [
38 "agent",
39 "review_requested",
40 "assign",
41 "mention",
42 "ci_activity",
43 "security_alert",
44 "state_change",
45 "author",
46 "comment",
47 "manual",
48 "subscribed",
49];
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all50
Inbox: threads, reasons, subscriptions and watching51export type InboxSubjectKind = "issue" | "pull" | "run" | "deploy";
52
53/** One thread in a person's inbox: what it is about, and its latest activity. */
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all54export type InboxItem = {
Inbox: threads, reasons, subscriptions and watching55 /** The thread's id: the same for as long as the person has it. */
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all56 id: string;
Inbox: threads, reasons, subscriptions and watching57 /** Why they were told of the latest activity. */
58 reason: InboxReason;
59 /** While unread, the most urgent of what happened since it was last read. */
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all60 severity: InboxSeverity;
Inbox: threads, reasons, subscriptions and watching61 /** One line: what happened last, and where. */
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all62 title: string;
63 /** One line: what it happened to, such as the pull request's title. */
64 body: string;
Inbox: threads, reasons, subscriptions and watching65 /** The event behind the latest activity, such as `pull.merged`. */
66 event: string | null;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all67 /** `owner/name`. */
68 repo: string | null;
69 workspace: string | null;
70 subject: InboxSubjectKind | null;
71 number: number | null;
72 /** A path on g1t.sh, such as `/acme/rocket/pull/12`. */
73 url: string;
74 /** A username, or `g1t`. */
75 actor: string | null;
Inbox: threads, reasons, subscriptions and watching76 /** How many things have happened on the thread. */
77 count: number;
78 /** RFC 3339: when the person was first told of the thread. */
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all79 createdAt: string;
Inbox: threads, reasons, subscriptions and watching80 /** RFC 3339: its latest activity. */
81 updatedAt: string;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all82 readAt: string | null;
83 doneAt: string | null;
84 saved: boolean;
85 snoozedUntil: string | null;
86};
87
Inbox: threads, reasons, subscriptions and watching88/** One thing that happened on a thread, as the person was told of it. */
89export type InboxActivity = {
90 reason: InboxReason;
91 severity: InboxSeverity;
92 title: string;
93 body: string;
94 event: string | null;
95 actor: string | null;
96 createdAt: string;
97};
98
99/** A person's subscription to an issue or pull request. */
100export type ThreadSubscription = {
101 /** Whether they hear of what happens on it. */
102 subscribed: boolean;
103 /** Whether they hear of nothing on it at all, not even a mention. */
104 ignored: boolean;
105 /** Why they are subscribed; null when they are not. */
106 reason: InboxReason | null;
107 repo: string | null;
108 number: number | null;
109 /** When they last chose, or null if they never did. */
110 updatedAt: string | null;
111};
112
113/** A thread with its history, newest first, and the person's subscription. */
114export type InboxThread = InboxItem & {
115 activity: InboxActivity[];
116 subscription: ThreadSubscription | null;
117};
118
119/** How closely a person follows a repository. */
120export type WatchLevel = "participating" | "all" | "ignore" | "custom";
121
122export const WATCH_LEVELS: WatchLevel[] = ["participating", "all", "ignore", "custom"];
123
124/** The kinds of activity a custom watch can follow. */
125export type WatchEvent = "issues" | "pulls" | "deployments" | "security";
126
127export const WATCH_EVENTS: WatchEvent[] = ["issues", "pulls", "deployments", "security"];
128
129export type Watching = {
130 repoId: string;
131 repo: string | null;
132 level: WatchLevel;
133 /** With `custom`: what it follows. */
134 events: WatchEvent[];
135 updatedAt: string | null;
136};
137
138/** A person's choices about being told. */
139export type InboxSettings = {
140 /** The reasons they are also emailed for. */
141 email: InboxReason[];
142 /** How they watch a repository they create. */
143 defaultWatch: WatchLevel;
144};
145
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all146/** The inbox itself (not done, not snoozed), what was saved, or what is done. */
147export type InboxView = "inbox" | "saved" | "done";
148
149export type InboxQuery = {
150 view?: InboxView;
151 severity?: InboxSeverity | null;
Inbox: threads, reasons, subscriptions and watching152 reason?: InboxReason | null;
153 /** Only threads the person takes part in: not those followed by watching or by hand. */
154 participating?: boolean;
155 repoId?: string | null;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all156 unread?: boolean;
Inbox: threads, reasons, subscriptions and watching157 /** RFC 3339: only threads with activity at or after it. */
158 since?: string | null;
159 /** RFC 3339: only threads whose latest activity was before it. */
160 updatedBefore?: string | null;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all161 /** The `next` of the page before. */
162 before?: string | null;
163 /** At most 100. */
164 limit?: number;
165};
166
167export type InboxPage = { items: InboxItem[]; next: string | null };
168
169/** Unread items in the inbox, by severity. */
170export type InboxCounts = { unread: number } & Record<InboxSeverity, number>;
171
Inbox: threads, reasons, subscriptions and watching172export type InboxMark = "read" | "unread" | "done" | "undone" | "save" | "unsave" | "snooze" | "unsnooze";
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all173
174export type InboxMarkArgs = {
175 mark: InboxMark;
176 /** At most 100. */
177 ids?: string[];
178 /** Every item in the inbox, when `ids` is empty: Mark all read. */
179 all?: boolean;
180 severity?: InboxSeverity | null;
Inbox: threads, reasons, subscriptions and watching181 repoId?: string | null;
182 /** With `all`: only threads whose latest activity was at or before this. */
183 lastReadAt?: string | null;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all184 /** For `snooze`: RFC 3339, later than now. */
185 until?: string | null;
186};
187
Inbox: threads, reasons, subscriptions and watching188/** Which issue or pull request: a thread's id, or a repository and number. */
189export type SubscriptionTarget = { id: string } | { repoId: string; number: number };
190
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all191export interface InboxApi {
192 /**
Inbox: threads, reasons, subscriptions and watching193 * Latest activity first; in the inbox unfiltered, unread warnings first.
194 * Items about a repository the viewer can no longer read are dropped.
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all195 */
196 list(viewer: User, query?: InboxQuery): Promise<InboxPage>;
197 counts(username: string): Promise<InboxCounts>;
198 /** Changes the person's own items. Returns how many changed. */
199 mark(username: string, args: InboxMarkArgs): Promise<number>;
Inbox: threads, reasons, subscriptions and watching200 /** One of the viewer's threads with its history, or null. */
201 thread(viewer: User, id: string): Promise<InboxThread | null>;
202 /** The viewer's subscription to an issue or pull request; null when there is none to have. */
203 subscription(viewer: User, target: SubscriptionTarget): Promise<ThreadSubscription | null>;
204 /**
205 * Subscribes (`true`), unsubscribes (`false`), ignores, or goes back to
206 * the default (`null`, subscribed only while taking part).
207 */
208 subscribe(viewer: User, target: SubscriptionTarget, subscribed: boolean | null, ignored?: boolean): Promise<ThreadSubscription | null>;
209 watching(username: string, repoId: string): Promise<Watching>;
210 /** `null` goes back to the default, participating. */
211 watch(username: string, repoId: string, repo: string, level: WatchLevel | null, events?: WatchEvent[]): Promise<Watching>;
212 watched(username: string): Promise<Watching[]>;
213 settings(username: string): Promise<InboxSettings>;
214 updateSettings(username: string, changes: Partial<InboxSettings>): Promise<InboxSettings>;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all215}
216
217/** The inbox, which the events service keeps. */
218export function inboxClient(events: ServiceBinding): InboxApi {
219 const call = async <T>(method: string, args: object): Promise<T> => {
220 const response = await events.fetch(`https://service/rpc/${method}`, {
221 method: "POST",
222 headers: { "content-type": "application/json" },
223 body: JSON.stringify(args),
224 });
225 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
226 return (await response.json()) as T;
227 };
228 return {
229 list: (viewer, query = {}) => call("inbox_list", { viewer, ...query }),
230 counts: (username) => call("inbox_counts", { username }),
231 mark: (username, args) => call("inbox_mark", { username, ...args }),
Inbox: threads, reasons, subscriptions and watching232 thread: (viewer, id) => call("inbox_thread", { viewer, id }),
233 subscription: (viewer, target) => call("inbox_subscription", { viewer, ...target }),
234 subscribe: (viewer, target, subscribed, ignored = false) =>
235 call("inbox_subscribe", { viewer, ...target, subscribed, ignored }),
236 watching: (username, repoId) => call("inbox_watching", { username, repoId }),
237 watch: (username, repoId, repo, level, events = []) => call("inbox_watch", { username, repoId, repo, level, events }),
238 watched: (username) => call("inbox_watched", { username }),
239 settings: (username) => call("inbox_settings", { username }),
240 updateSettings: (username, changes) => call("inbox_update_settings", { username, ...changes }),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all241 };
242}