ftown 0.19.65 → 0.19.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { appendFile, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { randomBytes } from 'node:crypto';
4
+ import { createHash, randomBytes, randomUUID } from 'node:crypto';
5
5
  /** Rewrites cap the inbox file at the newest N messages. */
6
6
  const MAX_STORED_MESSAGES = 500;
7
7
  export function createMailMessage(input) {
@@ -11,6 +11,11 @@ export function createMailMessage(input) {
11
11
  ...input,
12
12
  };
13
13
  }
14
+ function checkpoint(value) {
15
+ if (value.version !== 1 || typeof value.linkId !== 'string' || !value.linkId || !['in_progress', 'failed', 'passed', 'merged'].includes(String(value.state)) || !Number.isSafeInteger(value.ordinal) || Number(value.ordinal) < 0 || (value.state !== 'merged' && (typeof value.headOid !== 'string' || !value.headOid)) || (value.headOid !== undefined && typeof value.headOid !== 'string'))
16
+ throw new Error('Invalid PR event checkpoint');
17
+ return value;
18
+ }
14
19
  /**
15
20
  * Per-session append-only inbox (inbox.jsonl in the session's data dir).
16
21
  * Delivery marking rewrites the file atomically (tmp+rename); files are small.
@@ -31,33 +36,84 @@ export class MailStore {
31
36
  this.locks.set(sessionId, next.catch(() => undefined));
32
37
  return next;
33
38
  }
34
- /** All messages, oldest first. Missing file and corrupt lines are tolerated. */
35
- async readAll(sessionId) {
39
+ /** Old malformed message lines remain ignored; recognized checkpoints fail closed. */
40
+ async readInbox(sessionId) {
36
41
  const filePath = this.inboxPath(sessionId);
37
- if (!existsSync(filePath)) {
38
- return [];
39
- }
42
+ if (!existsSync(filePath))
43
+ return { messages: [] };
40
44
  const raw = await readFile(filePath, 'utf-8');
41
45
  const messages = [];
46
+ let state;
42
47
  for (const line of raw.split('\n')) {
43
48
  if (!line.trim())
44
49
  continue;
50
+ let value;
45
51
  try {
46
- const msg = JSON.parse(line);
47
- if (msg && typeof msg.id === 'string' && typeof msg.body === 'string') {
48
- messages.push(msg);
49
- }
52
+ value = JSON.parse(line);
50
53
  }
51
54
  catch {
52
- // Skip corrupt lines (e.g. partial write from a crashed bridge).
55
+ // A broken recognizable metadata record must not silently reset deduplication.
56
+ if (line.includes('pr-event-checkpoint'))
57
+ throw new Error('Invalid PR event checkpoint');
58
+ continue;
53
59
  }
60
+ if (!value || typeof value !== 'object' || Array.isArray(value))
61
+ continue;
62
+ const item = value;
63
+ if (item._ftown === 'pr-event-checkpoint') {
64
+ if (state)
65
+ throw new Error('Duplicate PR event checkpoint');
66
+ state = checkpoint(item);
67
+ }
68
+ else if (typeof item.id === 'string' && typeof item.body === 'string')
69
+ messages.push(item);
70
+ }
71
+ return { messages, checkpoint: state };
72
+ }
73
+ async readAll(sessionId) {
74
+ return (await this.readInbox(sessionId)).messages;
75
+ }
76
+ async replaceInbox(sessionId, inbox) {
77
+ const filePath = this.inboxPath(sessionId);
78
+ const tmp = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
79
+ const lines = [...(inbox.checkpoint ? [JSON.stringify(inbox.checkpoint)] : []), ...inbox.messages.map(message => JSON.stringify(message))];
80
+ try {
81
+ await writeFile(tmp, lines.join('\n') + '\n', { encoding: 'utf-8', mode: 0o600, flag: 'wx' });
82
+ await rename(tmp, filePath);
54
83
  }
55
- return messages;
84
+ catch (error) {
85
+ await import('node:fs/promises').then(fs => fs.rm(tmp, { force: true })).catch(() => { });
86
+ throw error;
87
+ }
88
+ }
89
+ /** Caller fences the link under the session mutation lock; never creates its directory. */
90
+ async commitPullRequestObservation(sessionId, linkId, observation) {
91
+ if (observation.stale || !observation.checkedAt || observation.reason || !['in_progress', 'failed', 'passed', 'merged'].includes(observation.state) || (observation.state !== 'merged' && !observation.headOid))
92
+ return { enqueued: false };
93
+ return this.withLock(sessionId, async () => {
94
+ const inbox = await this.readInbox(sessionId);
95
+ const old = inbox.checkpoint?.linkId === linkId ? inbox.checkpoint : undefined;
96
+ if (old?.state === 'merged' || (old && old.state === observation.state && old.headOid === observation.headOid))
97
+ return { enqueued: false };
98
+ const ordinal = (old?.ordinal ?? 0) + 1;
99
+ const state = observation.state;
100
+ inbox.checkpoint = { _ftown: 'pr-event-checkpoint', version: 1, linkId, state, ordinal, ...(observation.headOid ? { headOid: observation.headOid } : {}) };
101
+ let messageId;
102
+ if (state !== 'in_progress') {
103
+ messageId = `pr-${createHash('sha256').update(JSON.stringify([linkId, ordinal, state, observation.headOid ?? 'merged'])).digest('hex')}`;
104
+ const heading = state === 'merged' ? 'Pull request merged.' : state === 'failed' ? 'GitHub rollup checks failed.' : 'GitHub rollup checks passed.';
105
+ const advice = state === 'failed' ? 'Inspect the checks and reproduce failures before fixing them within your existing task authorization.' : 'Follow your existing task and user instructions; this notification does not authorize merging or other actions.';
106
+ inbox.messages.push({ id: messageId, ts: new Date().toISOString(), from: 'external', fromName: 'FTown PR updates', to: sessionId, type: 'message', body: `${heading}\nPR: ${observation.url}\nChecks: ${observation.url}/checks\n${observation.headOid ? `Observed head: ${observation.headOid}\n` : ''}Observed at: ${observation.checkedAt}\n${advice}` });
107
+ }
108
+ await this.replaceInbox(sessionId, inbox);
109
+ return { enqueued: !!messageId, ...(messageId ? { messageId } : {}) };
110
+ });
56
111
  }
57
112
  async append(msg) {
58
113
  await this.withLock(msg.to, async () => {
59
114
  const dir = this.resolveDir(msg.to);
60
115
  await mkdir(dir, { recursive: true });
116
+ await this.readInbox(msg.to);
61
117
  // Mail can carry task details; owner-only like the session registry.
62
118
  await appendFile(this.inboxPath(msg.to), `${JSON.stringify(msg)}\n`, {
63
119
  encoding: 'utf-8',
@@ -80,7 +136,8 @@ export class MailStore {
80
136
  if (ids.length === 0) {
81
137
  return [];
82
138
  }
83
- const all = await this.readAll(sessionId);
139
+ const inbox = await this.readInbox(sessionId);
140
+ const all = inbox.messages;
84
141
  const wanted = new Set(ids);
85
142
  const now = new Date().toISOString();
86
143
  const marked = [];
@@ -104,11 +161,7 @@ export class MailStore {
104
161
  }
105
162
  return true;
106
163
  });
107
- const filePath = this.inboxPath(sessionId);
108
- const tmp = `${filePath}.tmp`;
109
- const payload = kept.map((m) => JSON.stringify(m)).join('\n') + '\n';
110
- await writeFile(tmp, payload, { encoding: 'utf-8', mode: 0o600 });
111
- await rename(tmp, filePath);
164
+ await this.replaceInbox(sessionId, { ...inbox, messages: kept });
112
165
  return marked;
113
166
  });
114
167
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mail-store.js","sourceRoot":"","sources":["../src/mail-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mail-store.js","sourceRoot":"","sources":["../src/mail-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import type { SessionStore } from './session-store.js';
2
+ import type { Session } from './types.js';
3
+ import type { PullRequestStatusService } from './pull-request-status.js';
4
+ import type { MailStore } from './mail-store.js';
5
+ type Timer = ReturnType<typeof setTimeout>;
6
+ interface Options {
7
+ bridgeId: string;
8
+ store: Pick<SessionStore, 'listSessions' | 'loadSession' | 'saveSession'>;
9
+ withSessionMutation: (id: string, task: () => Promise<void>) => Promise<void>;
10
+ statusService: Pick<PullRequestStatusService, 'getObservations' | 'setBackgroundUrls'>;
11
+ mailStore: Pick<MailStore, 'commitPullRequestObservation'>;
12
+ notifyCommitted: (session: Session, sender: string) => Promise<void>;
13
+ now?: () => number;
14
+ setTimeout?: (fn: () => void, ms: number) => Timer;
15
+ clearTimeout?: (timer: Timer) => void;
16
+ }
17
+ /** Polls only observed changes using the same cache and quota as dashboard reads. */
18
+ export declare class PullRequestEventWatcher {
19
+ private readonly options;
20
+ private stopped;
21
+ private started;
22
+ private epoch;
23
+ private flight?;
24
+ private timer?;
25
+ private group;
26
+ private baseline;
27
+ private cursor;
28
+ private pendingWake;
29
+ private nextDelay;
30
+ private readonly now;
31
+ private readonly setTimer;
32
+ private readonly clearTimer;
33
+ constructor(options: Options);
34
+ start(): void;
35
+ private schedule;
36
+ stop(): Promise<void>;
37
+ runOnce(): Promise<void>;
38
+ private scan;
39
+ }
40
+ export {};
@@ -0,0 +1,205 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { normalizePullRequestUrl } from './pull-request-url.js';
3
+ function canonical(value) {
4
+ if (!value)
5
+ return undefined;
6
+ try {
7
+ const url = normalizePullRequestUrl(value);
8
+ return Number(url.slice(url.lastIndexOf('/') + 1)) <= 2_147_483_647 ? url : undefined;
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ }
14
+ function eligible(session, bridgeId) {
15
+ return !!session && session.bridgeId === bridgeId && session.shellType !== 'shell' && !!canonical(session.pullRequestUrl);
16
+ }
17
+ /** Polls only observed changes using the same cache and quota as dashboard reads. */
18
+ export class PullRequestEventWatcher {
19
+ options;
20
+ stopped = false;
21
+ started = false;
22
+ epoch = 0;
23
+ flight;
24
+ timer;
25
+ group = [];
26
+ baseline = new Map();
27
+ cursor = '';
28
+ pendingWake = new Map();
29
+ nextDelay = 60_000;
30
+ now;
31
+ setTimer;
32
+ clearTimer;
33
+ constructor(options) {
34
+ this.options = options;
35
+ this.now = options.now ?? Date.now;
36
+ this.setTimer = options.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
37
+ this.clearTimer = options.clearTimeout ?? (timer => clearTimeout(timer));
38
+ }
39
+ start() {
40
+ if (this.started || this.stopped)
41
+ return;
42
+ this.started = true;
43
+ void this.runOnce().catch(error => console.error('[PR events] Scan failed:', error)).finally(() => this.schedule());
44
+ }
45
+ schedule() {
46
+ if (!this.started || this.stopped || this.timer)
47
+ return;
48
+ this.timer = this.setTimer(() => { this.timer = undefined; void this.runOnce().catch(error => console.error('[PR events] Scan failed:', error)).finally(() => this.schedule()); }, Math.max(60_000, this.nextDelay));
49
+ this.timer.unref?.();
50
+ }
51
+ async stop() {
52
+ this.stopped = true;
53
+ this.epoch++;
54
+ if (this.timer) {
55
+ this.clearTimer(this.timer);
56
+ this.timer = undefined;
57
+ }
58
+ await this.flight?.catch(() => { });
59
+ this.options.statusService.setBackgroundUrls([]);
60
+ }
61
+ runOnce() {
62
+ if (this.stopped)
63
+ return Promise.resolve();
64
+ if (this.flight)
65
+ return this.flight;
66
+ const epoch = this.epoch;
67
+ this.flight = this.scan(epoch).finally(() => { this.flight = undefined; });
68
+ return this.flight;
69
+ }
70
+ async scan(epoch) {
71
+ const { store, bridgeId, withSessionMutation, statusService, mailStore, notifyCommitted } = this.options;
72
+ const active = () => !this.stopped && this.epoch === epoch;
73
+ const links = [];
74
+ for (const candidate of await store.listSessions()) {
75
+ if (!active())
76
+ return;
77
+ if (!eligible(candidate, bridgeId))
78
+ continue;
79
+ try {
80
+ await withSessionMutation(candidate.id, async () => {
81
+ if (!active())
82
+ return;
83
+ const session = await store.loadSession(candidate.id);
84
+ if (!eligible(session, bridgeId))
85
+ return;
86
+ const url = canonical(session.pullRequestUrl);
87
+ const linkId = session.pullRequestLinkId ?? randomUUID();
88
+ if (!session.pullRequestLinkId)
89
+ await store.saveSession({ ...session, pullRequestLinkId: linkId });
90
+ links.push({ id: session.id, url, key: url.toLowerCase(), linkId });
91
+ });
92
+ }
93
+ catch {
94
+ // One damaged session must not prevent unrelated links from being scanned.
95
+ console.error(`[PR events] Could not inspect session ${candidate.id}`);
96
+ }
97
+ }
98
+ if (!active())
99
+ return;
100
+ // A committed message survives a failed wake; retry without depending on a fresh gh result.
101
+ for (const [id, pending] of this.pendingWake) {
102
+ let sessionForWake;
103
+ try {
104
+ await withSessionMutation(id, async () => {
105
+ if (!active())
106
+ return;
107
+ const session = await store.loadSession(id);
108
+ if (eligible(session, bridgeId) && canonical(session.pullRequestUrl)?.toLowerCase() === pending.key && session.pullRequestLinkId === pending.linkId)
109
+ sessionForWake = session;
110
+ });
111
+ }
112
+ catch {
113
+ // Keep the retry pending; other sessions and status groups can still progress.
114
+ console.error(`[PR events] Could not inspect session ${id}`);
115
+ continue;
116
+ }
117
+ if (!active())
118
+ return;
119
+ if (!sessionForWake) {
120
+ this.pendingWake.delete(id);
121
+ continue;
122
+ }
123
+ try {
124
+ await notifyCommitted(sessionForWake, 'FTown PR updates');
125
+ this.pendingWake.delete(id);
126
+ }
127
+ catch { /* Retry on the next scan. */ }
128
+ }
129
+ const urls = [...new Map(links.map(link => [link.key, link.url])).entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
130
+ const keys = new Set(urls.map(([key]) => key));
131
+ if (this.group.some(key => !keys.has(key))) {
132
+ this.group = [];
133
+ this.baseline.clear();
134
+ }
135
+ if (!this.group.length && urls.length) {
136
+ let start = urls.findIndex(([key]) => key > this.cursor);
137
+ if (start < 0)
138
+ start = 0;
139
+ const selected = [...urls.slice(start), ...urls.slice(0, start)].slice(0, 20);
140
+ this.group = selected.map(([key]) => key);
141
+ // Capture attempt ordinals before pinning: a quota-blocked read must not advance the group.
142
+ const before = await statusService.getObservations(selected.map(([, url]) => url));
143
+ if (!active())
144
+ return;
145
+ this.baseline = new Map(before.observations.map(item => [item.url.toLowerCase(), item.attempt]));
146
+ statusService.setBackgroundUrls(selected.map(([, url]) => url));
147
+ }
148
+ if (!this.group.length) {
149
+ statusService.setBackgroundUrls([]);
150
+ this.nextDelay = 60_000;
151
+ return;
152
+ }
153
+ const selectedUrls = this.group.map(key => urls.find(([entry]) => entry === key)?.[1]).filter((url) => !!url);
154
+ const result = await statusService.getObservations(selectedUrls);
155
+ if (!active())
156
+ return;
157
+ this.nextDelay = Math.max(60_000, Date.parse(result.nextRefreshAt) - this.now() || 60_000);
158
+ const observed = new Map(result.observations.map(item => [item.url.toLowerCase(), item]));
159
+ for (const link of links) {
160
+ const observation = observed.get(link.key);
161
+ if (!observation || observation.stale || observation.reason || !observation.checkedAt || !['in_progress', 'failed', 'passed', 'merged'].includes(observation.state))
162
+ continue;
163
+ let sessionForWake;
164
+ let attemptingCommit = false;
165
+ try {
166
+ await withSessionMutation(link.id, async () => {
167
+ if (!active())
168
+ return;
169
+ const session = await store.loadSession(link.id);
170
+ if (!eligible(session, bridgeId) || canonical(session.pullRequestUrl)?.toLowerCase() !== link.key || session.pullRequestLinkId !== link.linkId)
171
+ return;
172
+ attemptingCommit = true;
173
+ await mailStore.commitPullRequestObservation(session.id, link.linkId, observation);
174
+ if (active())
175
+ sessionForWake = session;
176
+ });
177
+ }
178
+ catch {
179
+ // A failed atomic commit may have renamed successfully before reporting an error.
180
+ if (active() && attemptingCommit)
181
+ this.pendingWake.set(link.id, { key: link.key, linkId: link.linkId });
182
+ console.error(`[PR events] Could not commit observation for session ${link.id}`);
183
+ continue;
184
+ }
185
+ if (active() && sessionForWake) {
186
+ try {
187
+ await notifyCommitted(sessionForWake, 'FTown PR updates');
188
+ this.pendingWake.delete(link.id);
189
+ }
190
+ catch {
191
+ this.pendingWake.set(link.id, { key: link.key, linkId: link.linkId });
192
+ }
193
+ }
194
+ }
195
+ if (this.group.every(key => { const observation = observed.get(key); return !!observation && ((!observation.stale && !!observation.checkedAt && !observation.reason) || observation.attempt !== (this.baseline.get(key) ?? 0)); })) {
196
+ this.cursor = this.group.at(-1);
197
+ this.group = [];
198
+ this.baseline.clear();
199
+ statusService.setBackgroundUrls([]);
200
+ // Keep discovering new links and rotating other groups at the shared quota cadence.
201
+ this.nextDelay = 60_000;
202
+ }
203
+ }
204
+ }
205
+ //# sourceMappingURL=pull-request-events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull-request-events.js","sourceRoot":"","sources":["../src/pull-request-events.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,14 @@
1
- import type { PullRequestStatusesResponse } from './pull-request-status-protocol.js';
1
+ import type { PullRequestStatus, PullRequestStatusesResponse } from './pull-request-status-protocol.js';
2
2
  type RunnerResult = {
3
3
  stdout: string;
4
4
  stderr: string;
5
5
  exitCode: number;
6
6
  };
7
7
  type Runner = (input: string) => Promise<RunnerResult>;
8
+ export interface PullRequestObservation extends PullRequestStatus {
9
+ headOid?: string;
10
+ attempt: number;
11
+ }
8
12
  /** Run a single bounded gh process; credentials stay in gh's environment. */
9
13
  export declare function runGh(input: string): Promise<RunnerResult>;
10
14
  /** Process-wide, demand-driven PR status cache and quota gate. */
@@ -16,8 +20,19 @@ export declare class PullRequestStatusService {
16
20
  private cooldown;
17
21
  private flight;
18
22
  private sequence;
23
+ private attemptSequence;
24
+ private background;
25
+ private inFlight;
19
26
  constructor(now?: () => number, runner?: Runner);
27
+ private entry;
28
+ private evict;
29
+ setBackgroundUrls(urls: readonly string[]): void;
20
30
  get(urls: readonly string[]): Promise<Pick<PullRequestStatusesResponse, 'statuses' | 'nextRefreshAt'>>;
31
+ getObservations(urls: readonly string[]): Promise<{
32
+ observations: PullRequestObservation[];
33
+ nextRefreshAt: string;
34
+ }>;
35
+ private read;
21
36
  private refresh;
22
37
  }
23
38
  export {};
@@ -55,7 +55,9 @@ function stateOf(value) {
55
55
  if (!pr)
56
56
  return { state: 'unknown', reason: 'unavailable' };
57
57
  if (pr.merged === true || pr.state === 'MERGED')
58
- return { state: 'merged' };
58
+ return pr.merged === true && pr.state === 'MERGED' ? { state: 'merged' } : { state: 'unknown', reason: 'unavailable' };
59
+ if (pr.merged !== false)
60
+ return { state: 'unknown', reason: 'unavailable' };
59
61
  if (pr.state === 'CLOSED')
60
62
  return { state: 'closed' };
61
63
  if (pr.state !== 'OPEN' || typeof pr.headRefOid !== 'string' || !pr.headRefOid)
@@ -74,10 +76,10 @@ function stateOf(value) {
74
76
  return { state: 'unknown', reason: 'no_checks' };
75
77
  switch (rollup.state) {
76
78
  case 'FAILURE':
77
- case 'ERROR': return { state: 'failed' };
79
+ case 'ERROR': return { state: 'failed', headOid: pr.headRefOid };
78
80
  case 'PENDING':
79
- case 'EXPECTED': return { state: 'in_progress' };
80
- case 'SUCCESS': return { state: 'passed' };
81
+ case 'EXPECTED': return { state: 'in_progress', headOid: pr.headRefOid };
82
+ case 'SUCCESS': return { state: 'passed', headOid: pr.headRefOid };
81
83
  default: return { state: 'unknown', reason: 'unavailable' };
82
84
  }
83
85
  }
@@ -123,59 +125,102 @@ export class PullRequestStatusService {
123
125
  cooldown = 0;
124
126
  flight;
125
127
  sequence = 0;
128
+ attemptSequence = 0;
129
+ background = new Set();
130
+ inFlight = new Set();
126
131
  constructor(now = Date.now, runner = runGh) {
127
132
  this.now = now;
128
133
  this.runner = runner;
129
134
  }
130
- async get(urls) {
131
- const keys = [];
132
- for (const value of urls) {
133
- let url;
134
- try {
135
- url = normalizePullRequestUrl(value);
136
- }
137
- catch {
138
- continue;
139
- }
140
- if (Number(url.slice(url.lastIndexOf('/') + 1)) > 2_147_483_647)
141
- continue;
142
- const key = url.toLowerCase();
143
- if (keys.includes(key))
144
- continue;
145
- keys.push(key);
146
- let entry = this.cache.get(key);
147
- if (!entry) {
148
- entry = { url, state: 'unknown', checkedAt: null, lastAttempt: 0, nextAttempt: 0, used: 0 };
149
- this.cache.set(key, entry);
150
- }
151
- entry.used = ++this.sequence;
135
+ entry(value) {
136
+ let url;
137
+ try {
138
+ url = normalizePullRequestUrl(value);
139
+ }
140
+ catch {
141
+ return undefined;
142
+ }
143
+ if (Number(url.slice(url.lastIndexOf('/') + 1)) > 2_147_483_647)
144
+ return undefined;
145
+ const key = url.toLowerCase();
146
+ let entry = this.cache.get(key);
147
+ if (!entry) {
148
+ entry = { url, state: 'unknown', checkedAt: null, attempt: 0, lastAttempt: 0, nextAttempt: 0, used: 0 };
149
+ this.cache.set(key, entry);
152
150
  }
151
+ entry.used = ++this.sequence;
152
+ return key;
153
+ }
154
+ evict() {
153
155
  while (this.cache.size > 500) {
154
- const oldest = [...this.cache.entries()].sort((a, b) => a[1].used - b[1].used)[0];
156
+ const oldest = [...this.cache.entries()].filter(([key]) => !this.background.has(key) && !this.inFlight.has(key)).sort((a, b) => a[1].used - b[1].used)[0];
157
+ if (!oldest)
158
+ break;
155
159
  this.cache.delete(oldest[0]);
156
160
  }
161
+ }
162
+ setBackgroundUrls(urls) {
163
+ this.background.clear();
164
+ for (const url of urls) {
165
+ if (this.background.size >= 20)
166
+ break;
167
+ const key = this.entry(url);
168
+ if (key)
169
+ this.background.add(key);
170
+ }
171
+ this.evict();
172
+ }
173
+ async get(urls) {
174
+ const { observations, nextRefreshAt } = await this.read(urls, true);
175
+ return { statuses: observations.map(({ headOid: _headOid, attempt: _attempt, ...status }) => status), nextRefreshAt };
176
+ }
177
+ async getObservations(urls) {
178
+ return this.read(urls, false);
179
+ }
180
+ async read(urls, foreground) {
181
+ const keys = [];
182
+ for (const value of urls) {
183
+ const key = this.entry(value);
184
+ if (key && !keys.includes(key))
185
+ keys.push(key);
186
+ }
187
+ this.evict();
157
188
  if (this.flight)
158
189
  await this.flight;
159
190
  else {
160
191
  const now = this.now();
161
192
  this.launches = this.launches.filter(time => time > now - HOUR);
162
193
  const allowed = Math.max(this.cooldown, (this.launches.at(-1) ?? -Infinity) + MINUTE, this.launches.length >= 60 ? this.launches[0] + HOUR : 0);
163
- const due = keys.map(key => this.cache.get(key)).filter((entry) => !!entry && entry.state !== 'merged' && now >= entry.nextAttempt && (entry.checkedAt === null || now >= entry.checkedAt + ttl(entry.state)))
164
- .sort((a, b) => a.lastAttempt - b.lastAttempt || a.used - b.used).slice(0, 20);
165
- if (due.length && now >= allowed) {
194
+ const due = (key) => { const e = this.cache.get(key); return !!e && e.state !== 'merged' && now >= e.nextAttempt && (e.checkedAt === null || now >= e.checkedAt + ttl(e.state)); };
195
+ const sort = (a, b) => a.lastAttempt - b.lastAttempt || a.used - b.used;
196
+ const foregroundDue = (foreground ? keys : []).filter(due).map(key => this.cache.get(key)).sort(sort);
197
+ const backgroundDue = [...this.background].filter(due).map(key => this.cache.get(key)).sort(sort);
198
+ const selected = [];
199
+ const add = (entry) => { if (entry && !selected.includes(entry) && selected.length < 20)
200
+ selected.push(entry); };
201
+ if (foregroundDue.length && backgroundDue.length) {
202
+ add(foregroundDue[0]);
203
+ add(backgroundDue[0]);
204
+ }
205
+ for (const entry of [...new Set([...foregroundDue, ...backgroundDue])].sort(sort))
206
+ add(entry);
207
+ if (selected.length && now >= allowed) {
166
208
  this.launches.push(now);
167
- for (const entry of due)
209
+ for (const entry of selected) {
168
210
  entry.lastAttempt = now || 1;
169
- this.flight = this.refresh(due, now).finally(() => { this.flight = undefined; });
211
+ entry.attempt = ++this.attemptSequence;
212
+ this.inFlight.add(entry.url.toLowerCase());
213
+ }
214
+ this.flight = this.refresh(selected, now).finally(() => { this.inFlight.clear(); this.flight = undefined; this.evict(); });
170
215
  await this.flight;
171
216
  }
172
217
  }
173
218
  const now = this.now();
174
219
  const entries = keys.map(key => this.cache.get(key)).filter((entry) => !!entry);
175
- const statuses = entries.map(entry => ({ url: entry.url, state: entry.state, checkedAt: entry.checkedAt === null ? null : new Date(entry.checkedAt).toISOString(), stale: entry.checkedAt === null || now >= entry.checkedAt + ttl(entry.state) || (entry.reason !== undefined && entry.reason !== 'no_checks'), ...(entry.reason ? { reason: entry.reason } : {}) }));
220
+ const observations = entries.map(entry => ({ url: entry.url, state: entry.state, checkedAt: entry.checkedAt === null ? null : new Date(entry.checkedAt).toISOString(), stale: entry.checkedAt === null || now >= entry.checkedAt + ttl(entry.state) || (entry.reason !== undefined && entry.reason !== 'no_checks'), ...(entry.reason ? { reason: entry.reason } : {}), ...(entry.headOid ? { headOid: entry.headOid } : {}), attempt: entry.attempt }));
176
221
  const nextDue = entries.filter(entry => entry.state !== 'merged').map(entry => Math.max(entry.nextAttempt, entry.checkedAt === null ? 0 : entry.checkedAt + ttl(entry.state)));
177
222
  const next = nextDue.length ? Math.max(now + MINUTE, this.cooldown, (this.launches.at(-1) ?? -Infinity) + MINUTE, Math.min(...nextDue), this.launches.length >= 60 ? this.launches[0] + HOUR : 0) : now + HOUR;
178
- return { statuses, nextRefreshAt: new Date(next).toISOString() };
223
+ return { observations, nextRefreshAt: new Date(next).toISOString() };
179
224
  }
180
225
  async refresh(entries, now) {
181
226
  const fail = (reason, until) => {
@@ -238,6 +283,7 @@ export class PullRequestStatusService {
238
283
  else {
239
284
  entry.state = mapped.state;
240
285
  entry.reason = mapped.reason;
286
+ entry.headOid = mapped.headOid;
241
287
  entry.checkedAt = now;
242
288
  entry.nextAttempt = now + ttl(mapped.state);
243
289
  }