@openlimiter/core 0.4.0 → 1.3.1
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.
- package/dist/acquire/antigravity-probe.d.ts +139 -0
- package/dist/acquire/antigravity-probe.d.ts.map +1 -0
- package/dist/acquire/antigravity-probe.js +599 -0
- package/dist/acquire/antigravity-probe.js.map +1 -0
- package/dist/acquire/cadence.d.ts +74 -0
- package/dist/acquire/cadence.d.ts.map +1 -0
- package/dist/acquire/cadence.js +171 -0
- package/dist/acquire/cadence.js.map +1 -0
- package/dist/acquire/coordination.d.ts +157 -0
- package/dist/acquire/coordination.d.ts.map +1 -0
- package/dist/acquire/coordination.js +292 -0
- package/dist/acquire/coordination.js.map +1 -0
- package/dist/acquire/credentials.d.ts +113 -0
- package/dist/acquire/credentials.d.ts.map +1 -0
- package/dist/acquire/credentials.js +316 -0
- package/dist/acquire/credentials.js.map +1 -0
- package/dist/acquire/identity.d.ts +20 -0
- package/dist/acquire/identity.d.ts.map +1 -0
- package/dist/acquire/identity.js +20 -0
- package/dist/acquire/identity.js.map +1 -0
- package/dist/acquire/index.d.ts +10 -0
- package/dist/acquire/index.d.ts.map +1 -0
- package/dist/acquire/index.js +10 -0
- package/dist/acquire/index.js.map +1 -0
- package/dist/acquire/providers.d.ts +86 -0
- package/dist/acquire/providers.d.ts.map +1 -0
- package/dist/acquire/providers.js +175 -0
- package/dist/acquire/providers.js.map +1 -0
- package/dist/acquire/runner.d.ts +146 -0
- package/dist/acquire/runner.d.ts.map +1 -0
- package/dist/acquire/runner.js +345 -0
- package/dist/acquire/runner.js.map +1 -0
- package/dist/acquire/transport.d.ts +194 -0
- package/dist/acquire/transport.d.ts.map +1 -0
- package/dist/acquire/transport.js +434 -0
- package/dist/acquire/transport.js.map +1 -0
- package/dist/acquire/windows-credential.d.ts +74 -0
- package/dist/acquire/windows-credential.d.ts.map +1 -0
- package/dist/acquire/windows-credential.js +124 -0
- package/dist/acquire/windows-credential.js.map +1 -0
- package/dist/cache.d.ts +28 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +179 -25
- package/dist/cache.js.map +1 -1
- package/dist/connection-state.d.ts +53 -2
- package/dist/connection-state.d.ts.map +1 -1
- package/dist/connection-state.js +98 -9
- package/dist/connection-state.js.map +1 -1
- package/dist/failures.d.ts +2 -1
- package/dist/failures.d.ts.map +1 -1
- package/dist/failures.js +7 -3
- package/dist/failures.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/merge.d.ts.map +1 -1
- package/dist/merge.js +17 -1
- package/dist/merge.js.map +1 -1
- package/dist/normalizer.d.ts +2 -0
- package/dist/normalizer.d.ts.map +1 -1
- package/dist/normalizer.js +39 -4
- package/dist/normalizer.js.map +1 -1
- package/dist/types.d.ts +63 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AcquisitionOutcome } from "./transport.js";
|
|
2
|
+
/** The ordinary interval between two reads of one provider. */
|
|
3
|
+
export declare const ACQUISITION_INTERVAL_SECONDS = 900;
|
|
4
|
+
/** How long a rate limited provider is left alone. */
|
|
5
|
+
export declare const ACQUISITION_RATE_LIMIT_BACKOFF_SECONDS = 3600;
|
|
6
|
+
/** How long a provider that refused this client outright is left alone. */
|
|
7
|
+
export declare const ACQUISITION_BLOCKED_BACKOFF_SECONDS = 86400;
|
|
8
|
+
/**
|
|
9
|
+
* How long a desktop write keeps the command line tool from polling.
|
|
10
|
+
*
|
|
11
|
+
* The same fifteen minutes as one poll interval, because that is exactly the
|
|
12
|
+
* claim being made: a desktop that wrote inside the last interval is the
|
|
13
|
+
* refresher on this machine, and a second poll would only duplicate its work.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DESKTOP_OWNERSHIP_SECONDS = 900;
|
|
16
|
+
/** File the per provider schedule is kept in, beside the cache. */
|
|
17
|
+
export declare const ACQUISITION_STATE_FILE_NAME = "openlimiter-acquisition.json";
|
|
18
|
+
/** Document version of the schedule file. */
|
|
19
|
+
export declare const ACQUISITION_STATE_VERSION = 1;
|
|
20
|
+
export interface AcquisitionProviderSchedule {
|
|
21
|
+
/** When the last attempt was made. */
|
|
22
|
+
readonly lastAttemptAt: string;
|
|
23
|
+
/** The earliest instant the next attempt may be made. */
|
|
24
|
+
readonly nextAttemptAt: string;
|
|
25
|
+
/** What the last attempt achieved. */
|
|
26
|
+
readonly outcome: AcquisitionOutcome;
|
|
27
|
+
}
|
|
28
|
+
export type AcquisitionSchedule = Readonly<Record<string, AcquisitionProviderSchedule>>;
|
|
29
|
+
/**
|
|
30
|
+
* How long to wait after an attempt with this outcome.
|
|
31
|
+
*
|
|
32
|
+
* A provider that told us to go away is obeyed and then some: its Retry-After
|
|
33
|
+
* is a floor under our own backoff, never a replacement for it, so a header
|
|
34
|
+
* asking for less than our interval cannot talk us into polling sooner.
|
|
35
|
+
*/
|
|
36
|
+
export declare function backoffSecondsFor(outcome: AcquisitionOutcome, retryAfterSeconds?: number | null): number;
|
|
37
|
+
/** The instant an attempt with this outcome earns, or null on an unread clock. */
|
|
38
|
+
export declare function nextAttemptInstant(outcome: AcquisitionOutcome, now: string, retryAfterSeconds?: number | null): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Whether a provider may be read now.
|
|
41
|
+
*
|
|
42
|
+
* No schedule at all is due, which is what makes a first run work. A schedule
|
|
43
|
+
* this build cannot read is also due, because the alternative is a provider
|
|
44
|
+
* that never refreshes again on the strength of one unreadable value, and the
|
|
45
|
+
* first successful attempt replaces it with a readable one.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isProviderDue(schedule: AcquisitionProviderSchedule | undefined, now: string): boolean;
|
|
48
|
+
/** Providers a schedule document may name, bounded so a file cannot grow forever. */
|
|
49
|
+
export declare const MAX_SCHEDULE_ENTRIES = 32;
|
|
50
|
+
/**
|
|
51
|
+
* Read the schedule file.
|
|
52
|
+
*
|
|
53
|
+
* Every failure is an empty schedule rather than an exception: a schedule this
|
|
54
|
+
* build cannot read means every provider is due, which is the same place a
|
|
55
|
+
* fresh machine starts from and is never worse than not refreshing at all.
|
|
56
|
+
*/
|
|
57
|
+
export declare function readAcquisitionSchedule(directory?: string): Promise<AcquisitionSchedule>;
|
|
58
|
+
/**
|
|
59
|
+
* Replace the schedule file.
|
|
60
|
+
*
|
|
61
|
+
* Written atomically like every other state document, so a status line reading
|
|
62
|
+
* it while a refresh writes it sees one version or the other and never half of
|
|
63
|
+
* one. It carries no credential, no address and no response, only three fields
|
|
64
|
+
* per provider.
|
|
65
|
+
*/
|
|
66
|
+
export declare function writeAcquisitionSchedule(schedule: AcquisitionSchedule, directory?: string): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* One sentence per outcome, for the row a person reads.
|
|
69
|
+
*
|
|
70
|
+
* The backoff instant is rendered beside it by the caller; these sentences say
|
|
71
|
+
* what happened, in words that do not need the reader to know HTTP.
|
|
72
|
+
*/
|
|
73
|
+
export declare const ACQUISITION_OUTCOME_SENTENCE: Readonly<Record<AcquisitionOutcome, string>>;
|
|
74
|
+
//# sourceMappingURL=cadence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cadence.d.ts","sourceRoot":"","sources":["../../src/acquire/cadence.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEzD,+DAA+D;AAC/D,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD,sDAAsD;AACtD,eAAO,MAAM,sCAAsC,OAAQ,CAAC;AAE5D,2EAA2E;AAC3E,eAAO,MAAM,mCAAmC,QAAS,CAAC;AAE1D;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C,mEAAmE;AACnE,eAAO,MAAM,2BAA2B,iCAAiC,CAAC;AAE1E,6CAA6C;AAC7C,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C,MAAM,WAAW,2BAA2B;IAC1C,sCAAsC;IACtC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,yDAAyD;IACzD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;CACtC;AAED,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CACxC,MAAM,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAC5C,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,kBAAkB,EAC3B,iBAAiB,GAAE,MAAM,GAAG,IAAW,GACtC,MAAM,CAgBR;AAED,kFAAkF;AAClF,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,kBAAkB,EAC3B,GAAG,EAAE,MAAM,EACX,iBAAiB,GAAE,MAAM,GAAG,IAAW,GACtC,MAAM,GAAG,IAAI,CAOf;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,2BAA2B,GAAG,SAAS,EACjD,GAAG,EAAE,MAAM,GACV,OAAO,CAOT;AAwBD,qFAAqF;AACrF,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,SAA0B,GAClC,OAAO,CAAC,mBAAmB,CAAC,CA0B9B;AAED;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,mBAAmB,EAC7B,SAAS,SAA0B,GAClC,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,EACvC,QAAQ,CAAC,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAU5C,CAAC"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How often a terminal may ask a provider anything, and how long it stays away
|
|
3
|
+
* after being told to.
|
|
4
|
+
*
|
|
5
|
+
* The numbers are the desktop's, deliberately: two clients on one machine that
|
|
6
|
+
* disagree about cadence are two clients that between them poll twice as often
|
|
7
|
+
* as either intended, and the provider only sees the total. Mirrors
|
|
8
|
+
* `apps/desktop/src-tauri/src/claude_oauth.rs` lines 20 to 23.
|
|
9
|
+
*
|
|
10
|
+
* The arithmetic here is pure. The caller supplies the clock, so a whole
|
|
11
|
+
* backoff history replays to the same instants on every machine.
|
|
12
|
+
*/
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { MAX_JSON_FILE_BYTES, readJsonFileSafely, resolveStateDirectory, writeFileAtomically } from "../cache.js";
|
|
15
|
+
import { canonicalJson } from "../normalizer.js";
|
|
16
|
+
/** The ordinary interval between two reads of one provider. */
|
|
17
|
+
export const ACQUISITION_INTERVAL_SECONDS = 900;
|
|
18
|
+
/** How long a rate limited provider is left alone. */
|
|
19
|
+
export const ACQUISITION_RATE_LIMIT_BACKOFF_SECONDS = 3_600;
|
|
20
|
+
/** How long a provider that refused this client outright is left alone. */
|
|
21
|
+
export const ACQUISITION_BLOCKED_BACKOFF_SECONDS = 86_400;
|
|
22
|
+
/**
|
|
23
|
+
* How long a desktop write keeps the command line tool from polling.
|
|
24
|
+
*
|
|
25
|
+
* The same fifteen minutes as one poll interval, because that is exactly the
|
|
26
|
+
* claim being made: a desktop that wrote inside the last interval is the
|
|
27
|
+
* refresher on this machine, and a second poll would only duplicate its work.
|
|
28
|
+
*/
|
|
29
|
+
export const DESKTOP_OWNERSHIP_SECONDS = 900;
|
|
30
|
+
/** File the per provider schedule is kept in, beside the cache. */
|
|
31
|
+
export const ACQUISITION_STATE_FILE_NAME = "openlimiter-acquisition.json";
|
|
32
|
+
/** Document version of the schedule file. */
|
|
33
|
+
export const ACQUISITION_STATE_VERSION = 1;
|
|
34
|
+
/**
|
|
35
|
+
* How long to wait after an attempt with this outcome.
|
|
36
|
+
*
|
|
37
|
+
* A provider that told us to go away is obeyed and then some: its Retry-After
|
|
38
|
+
* is a floor under our own backoff, never a replacement for it, so a header
|
|
39
|
+
* asking for less than our interval cannot talk us into polling sooner.
|
|
40
|
+
*/
|
|
41
|
+
export function backoffSecondsFor(outcome, retryAfterSeconds = null) {
|
|
42
|
+
/* A provider that serves a field only to its own tools will answer the same
|
|
43
|
+
way in fifteen minutes, so that outcome waits a day like a refusal does.
|
|
44
|
+
Retrying it on the ordinary interval would be ninety six pointless requests
|
|
45
|
+
a day against an answer that cannot change until we change. */
|
|
46
|
+
const base = outcome === "rate_limited"
|
|
47
|
+
? ACQUISITION_RATE_LIMIT_BACKOFF_SECONDS
|
|
48
|
+
: outcome === "blocked" || outcome === "identity_refused"
|
|
49
|
+
? ACQUISITION_BLOCKED_BACKOFF_SECONDS
|
|
50
|
+
: ACQUISITION_INTERVAL_SECONDS;
|
|
51
|
+
if (retryAfterSeconds === null ||
|
|
52
|
+
!Number.isFinite(retryAfterSeconds) ||
|
|
53
|
+
retryAfterSeconds <= 0)
|
|
54
|
+
return base;
|
|
55
|
+
return Math.max(base, Math.min(retryAfterSeconds, ACQUISITION_BLOCKED_BACKOFF_SECONDS));
|
|
56
|
+
}
|
|
57
|
+
/** The instant an attempt with this outcome earns, or null on an unread clock. */
|
|
58
|
+
export function nextAttemptInstant(outcome, now, retryAfterSeconds = null) {
|
|
59
|
+
const current = Date.parse(now);
|
|
60
|
+
if (!Number.isFinite(current))
|
|
61
|
+
return null;
|
|
62
|
+
const instant = new Date(current + backoffSecondsFor(outcome, retryAfterSeconds) * 1_000);
|
|
63
|
+
return Number.isFinite(instant.getTime()) ? instant.toISOString() : null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether a provider may be read now.
|
|
67
|
+
*
|
|
68
|
+
* No schedule at all is due, which is what makes a first run work. A schedule
|
|
69
|
+
* this build cannot read is also due, because the alternative is a provider
|
|
70
|
+
* that never refreshes again on the strength of one unreadable value, and the
|
|
71
|
+
* first successful attempt replaces it with a readable one.
|
|
72
|
+
*/
|
|
73
|
+
export function isProviderDue(schedule, now) {
|
|
74
|
+
const current = Date.parse(now);
|
|
75
|
+
if (!Number.isFinite(current))
|
|
76
|
+
return false;
|
|
77
|
+
if (schedule === undefined)
|
|
78
|
+
return true;
|
|
79
|
+
const next = Date.parse(schedule.nextAttemptAt);
|
|
80
|
+
if (!Number.isFinite(next))
|
|
81
|
+
return true;
|
|
82
|
+
return current >= next;
|
|
83
|
+
}
|
|
84
|
+
function isRecord(value) {
|
|
85
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
86
|
+
}
|
|
87
|
+
function isIsoInstant(value) {
|
|
88
|
+
if (typeof value !== "string")
|
|
89
|
+
return false;
|
|
90
|
+
const parsed = Date.parse(value);
|
|
91
|
+
return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
|
|
92
|
+
}
|
|
93
|
+
const outcomeNames = new Set([
|
|
94
|
+
"ok",
|
|
95
|
+
"unauthorized",
|
|
96
|
+
"rate_limited",
|
|
97
|
+
"blocked",
|
|
98
|
+
"remote_error",
|
|
99
|
+
"transport",
|
|
100
|
+
"too_large",
|
|
101
|
+
"drift",
|
|
102
|
+
"identity_refused"
|
|
103
|
+
]);
|
|
104
|
+
/** Providers a schedule document may name, bounded so a file cannot grow forever. */
|
|
105
|
+
export const MAX_SCHEDULE_ENTRIES = 32;
|
|
106
|
+
/**
|
|
107
|
+
* Read the schedule file.
|
|
108
|
+
*
|
|
109
|
+
* Every failure is an empty schedule rather than an exception: a schedule this
|
|
110
|
+
* build cannot read means every provider is due, which is the same place a
|
|
111
|
+
* fresh machine starts from and is never worse than not refreshing at all.
|
|
112
|
+
*/
|
|
113
|
+
export async function readAcquisitionSchedule(directory = resolveStateDirectory()) {
|
|
114
|
+
const document = await readJsonFileSafely(path.join(directory, ACQUISITION_STATE_FILE_NAME), MAX_JSON_FILE_BYTES);
|
|
115
|
+
if (!document.ok || !isRecord(document.value))
|
|
116
|
+
return {};
|
|
117
|
+
const version = document.value["version"];
|
|
118
|
+
if (version !== undefined && version !== ACQUISITION_STATE_VERSION)
|
|
119
|
+
return {};
|
|
120
|
+
const providers = document.value["providers"];
|
|
121
|
+
if (!isRecord(providers))
|
|
122
|
+
return {};
|
|
123
|
+
const schedule = {};
|
|
124
|
+
for (const key of Object.keys(providers).slice(0, MAX_SCHEDULE_ENTRIES)) {
|
|
125
|
+
const entry = providers[key];
|
|
126
|
+
if (!isRecord(entry))
|
|
127
|
+
continue;
|
|
128
|
+
const lastAttemptAt = entry["lastAttemptAt"];
|
|
129
|
+
const nextAttemptAt = entry["nextAttemptAt"];
|
|
130
|
+
const outcome = entry["outcome"];
|
|
131
|
+
if (!isIsoInstant(lastAttemptAt) || !isIsoInstant(nextAttemptAt))
|
|
132
|
+
continue;
|
|
133
|
+
if (typeof outcome !== "string" || !outcomeNames.has(outcome))
|
|
134
|
+
continue;
|
|
135
|
+
schedule[key] = {
|
|
136
|
+
lastAttemptAt,
|
|
137
|
+
nextAttemptAt,
|
|
138
|
+
outcome: outcome
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return schedule;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Replace the schedule file.
|
|
145
|
+
*
|
|
146
|
+
* Written atomically like every other state document, so a status line reading
|
|
147
|
+
* it while a refresh writes it sees one version or the other and never half of
|
|
148
|
+
* one. It carries no credential, no address and no response, only three fields
|
|
149
|
+
* per provider.
|
|
150
|
+
*/
|
|
151
|
+
export async function writeAcquisitionSchedule(schedule, directory = resolveStateDirectory()) {
|
|
152
|
+
await writeFileAtomically(path.join(directory, ACQUISITION_STATE_FILE_NAME), canonicalJson({ providers: schedule, version: ACQUISITION_STATE_VERSION }));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* One sentence per outcome, for the row a person reads.
|
|
156
|
+
*
|
|
157
|
+
* The backoff instant is rendered beside it by the caller; these sentences say
|
|
158
|
+
* what happened, in words that do not need the reader to know HTTP.
|
|
159
|
+
*/
|
|
160
|
+
export const ACQUISITION_OUTCOME_SENTENCE = {
|
|
161
|
+
ok: "read",
|
|
162
|
+
unauthorized: "the provider refused this login, open its own tool once to refresh it",
|
|
163
|
+
rate_limited: "the provider asked for fewer requests, so this one is waiting",
|
|
164
|
+
blocked: "the provider refused this client, so this one is waiting a day",
|
|
165
|
+
remote_error: "the provider could not answer this time",
|
|
166
|
+
transport: "this machine could not reach the provider",
|
|
167
|
+
too_large: "the provider's answer was larger than this build accepts",
|
|
168
|
+
drift: "the provider answered in a shape this build does not understand",
|
|
169
|
+
identity_refused: "the provider serves this reading only to its own tools"
|
|
170
|
+
};
|
|
171
|
+
//# sourceMappingURL=cadence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cadence.js","sourceRoot":"","sources":["../../src/acquire/cadence.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Snapshot, SnapshotWriter } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* How old the cache may be before a render starts a refresh behind itself.
|
|
4
|
+
*
|
|
5
|
+
* A minute, which is the number Lucas's own status line has used for months. It
|
|
6
|
+
* is short enough that a person who opens a terminal sees current bars and long
|
|
7
|
+
* enough that a status line redrawing on every keystroke starts nothing.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CACHE_REFRESH_STALE_SECONDS = 60;
|
|
10
|
+
/** The lock one detached refresh holds, beside the cache lock and separate. */
|
|
11
|
+
export declare const REFRESH_LOCK_NAME = "openlimiter-refresh.lock";
|
|
12
|
+
/**
|
|
13
|
+
* How often a running refresh says it is still alive.
|
|
14
|
+
*
|
|
15
|
+
* The lock's modification time is the signal, so the heartbeat only touches the
|
|
16
|
+
* file and never rewrites it: the token inside has to stay byte for byte the
|
|
17
|
+
* same, because that token is what proves ownership at every write below.
|
|
18
|
+
*/
|
|
19
|
+
export declare const REFRESH_LOCK_HEARTBEAT_MILLISECONDS = 10000;
|
|
20
|
+
/**
|
|
21
|
+
* How long a silent lock is honoured before it is treated as abandoned.
|
|
22
|
+
*
|
|
23
|
+
* Six missed heartbeats. The bound used to be a flat two minutes measured from
|
|
24
|
+
* when the lock was TAKEN, which quietly assumed a whole round fits in two
|
|
25
|
+
* minutes; seven providers, two of them two hop, each with a fifteen second
|
|
26
|
+
* budget, does not fit, so a legitimate refresh could be declared dead and
|
|
27
|
+
* raced by the next status line render. Measuring from the last heartbeat
|
|
28
|
+
* instead means the bound is about liveness rather than about duration, and a
|
|
29
|
+
* round may take as long as it takes.
|
|
30
|
+
*/
|
|
31
|
+
export declare const REFRESH_LOCK_STALE_MILLISECONDS = 60000;
|
|
32
|
+
/** The newest observation in a set of rows, in epoch milliseconds, or null. */
|
|
33
|
+
export declare function newestObservedMilliseconds(snapshots: readonly Snapshot[]): number | null;
|
|
34
|
+
/**
|
|
35
|
+
* How old the freshest row is, in seconds.
|
|
36
|
+
*
|
|
37
|
+
* An empty cache has no age, which the caller reads as "refresh": there is
|
|
38
|
+
* nothing to be stale, and nothing to show either.
|
|
39
|
+
*/
|
|
40
|
+
export declare function cacheAgeSeconds(snapshots: readonly Snapshot[], now: string): number | null;
|
|
41
|
+
/**
|
|
42
|
+
* Whether a writer has kept this cache fresh inside the ownership window.
|
|
43
|
+
*
|
|
44
|
+
* Absent markers answer false, which is the backward compatible reading: a
|
|
45
|
+
* cache written before this field existed says nothing about who wrote it, and
|
|
46
|
+
* a reader that assumed a desktop was running would stop refreshing on a
|
|
47
|
+
* machine that has none.
|
|
48
|
+
*/
|
|
49
|
+
export declare function writerHoldsCache(snapshots: readonly Snapshot[], writer: SnapshotWriter, now: string, withinSeconds?: number): boolean;
|
|
50
|
+
/** Whether a running desktop is already keeping this cache fresh. */
|
|
51
|
+
export declare function desktopHoldsCache(snapshots: readonly Snapshot[], now: string, withinSeconds?: number): boolean;
|
|
52
|
+
/** Stamp rows with the process that is about to write them. */
|
|
53
|
+
export declare function withWriter(snapshots: readonly Snapshot[], writer: SnapshotWriter): Snapshot[];
|
|
54
|
+
export type RefreshDecision = {
|
|
55
|
+
refresh: true;
|
|
56
|
+
} | {
|
|
57
|
+
refresh: false;
|
|
58
|
+
reason: "fresh" | "desktop_running";
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Whether this render should start a refresh behind itself.
|
|
62
|
+
*
|
|
63
|
+
* Three inputs and no side effects, so the whole policy is one testable
|
|
64
|
+
* function rather than a condition spread across two commands.
|
|
65
|
+
*/
|
|
66
|
+
export declare function shouldStartRefresh(snapshots: readonly Snapshot[], now: string, staleSeconds?: number): RefreshDecision;
|
|
67
|
+
/**
|
|
68
|
+
* Whether a refresh is running right now.
|
|
69
|
+
*
|
|
70
|
+
* A lock file older than the stale bound is not a running refresh, it is a
|
|
71
|
+
* process that died, and it is reported as free so the next render can take
|
|
72
|
+
* over. Nothing is deleted here: reclaiming belongs to the acquisition below,
|
|
73
|
+
* which does it under the same create attempt that would have failed.
|
|
74
|
+
*/
|
|
75
|
+
export declare function refreshLockHeld(directory?: string, nowMilliseconds?: number): Promise<boolean>;
|
|
76
|
+
export interface RefreshLockHolder {
|
|
77
|
+
/** Give the lock up. Safe to call twice, and never removes somebody else's. */
|
|
78
|
+
readonly release: () => Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Whether this holder still owns the lock.
|
|
81
|
+
*
|
|
82
|
+
* Checked before every write a round makes, because a reclaim is always
|
|
83
|
+
* possible in principle and a round that lost its lock must not keep writing
|
|
84
|
+
* over the round that took it.
|
|
85
|
+
*/
|
|
86
|
+
readonly stillOwned: () => Promise<boolean>;
|
|
87
|
+
}
|
|
88
|
+
export type RefreshLockResult = ({
|
|
89
|
+
ok: true;
|
|
90
|
+
} & RefreshLockHolder) | {
|
|
91
|
+
ok: false;
|
|
92
|
+
reason: "held" | "unavailable";
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Take the refresh lock, or say why not.
|
|
96
|
+
*
|
|
97
|
+
* Exclusive creation is the whole mechanism: the operating system decides the
|
|
98
|
+
* winner, so two terminals starting a refresh in the same millisecond produce
|
|
99
|
+
* exactly one round of requests. The token inside the file is checked before
|
|
100
|
+
* the release, so a process that lost its lock to the stale reclaim cannot
|
|
101
|
+
* delete the lock its successor is holding.
|
|
102
|
+
*/
|
|
103
|
+
export declare function acquireRefreshLock(directory?: string, nowMilliseconds?: number): Promise<RefreshLockResult>;
|
|
104
|
+
/** The file that remembers a refresh this machine could not start. */
|
|
105
|
+
export declare const REFRESH_SPAWN_FAILURE_NAME = "openlimiter-refresh-spawn.json";
|
|
106
|
+
/**
|
|
107
|
+
* Remember that a background refresh could not be started.
|
|
108
|
+
*
|
|
109
|
+
* A detached spawn fails asynchronously: the executable is missing, or the
|
|
110
|
+
* platform refuses, and the error arrives after the status line has already
|
|
111
|
+
* drawn and returned. There is nobody left to tell at that point, so the fact
|
|
112
|
+
* is written down instead and doctor reads it back. Best effort throughout,
|
|
113
|
+
* because a machine that cannot start a refresh may well not be able to write
|
|
114
|
+
* this either, and neither failure may break a render.
|
|
115
|
+
*/
|
|
116
|
+
export declare function recordRefreshSpawnFailure(directory: string, now: string): Promise<void>;
|
|
117
|
+
/** When the last background refresh failed to start, if one did. */
|
|
118
|
+
export declare function readRefreshSpawnFailure(directory?: string): Promise<string | null>;
|
|
119
|
+
/** Forget it, which a refresh that actually ran is the proof of. */
|
|
120
|
+
export declare function clearRefreshSpawnFailure(directory?: string): Promise<void>;
|
|
121
|
+
export interface DetachedSpawn {
|
|
122
|
+
(executable: string, argumentsList: readonly string[], options: {
|
|
123
|
+
readonly cwd?: string;
|
|
124
|
+
/**
|
|
125
|
+
* Called when the child fails to start, which happens after this
|
|
126
|
+
* function has already returned. The spawner must swallow the error
|
|
127
|
+
* itself and call this, never let it reach the process as unhandled.
|
|
128
|
+
*/
|
|
129
|
+
readonly onError?: () => void;
|
|
130
|
+
}): void;
|
|
131
|
+
}
|
|
132
|
+
export interface SpawnRefreshOptions {
|
|
133
|
+
readonly snapshots: readonly Snapshot[];
|
|
134
|
+
readonly now: string;
|
|
135
|
+
readonly stateDirectory?: string;
|
|
136
|
+
readonly nodeExecutable: string;
|
|
137
|
+
readonly openLimiterScript: string;
|
|
138
|
+
readonly spawn: DetachedSpawn;
|
|
139
|
+
readonly staleSeconds?: number;
|
|
140
|
+
}
|
|
141
|
+
export type SpawnRefreshResult = {
|
|
142
|
+
spawned: true;
|
|
143
|
+
} | {
|
|
144
|
+
spawned: false;
|
|
145
|
+
reason: "fresh" | "desktop_running" | "already_running" | "no_script" | "failed";
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Start a refresh behind the caller, and never wait for it.
|
|
149
|
+
*
|
|
150
|
+
* Everything expensive happens in the child. This function creates one lock
|
|
151
|
+
* file and hands the operating system a detached process, so the render that
|
|
152
|
+
* called it returns in the time a file create takes. A failure to spawn is
|
|
153
|
+
* reported and never thrown: a status line that cannot start a refresh still
|
|
154
|
+
* has to draw the bars it already has.
|
|
155
|
+
*/
|
|
156
|
+
export declare function spawnDetachedRefresh(options: SpawnRefreshOptions): Promise<SpawnRefreshResult>;
|
|
157
|
+
//# sourceMappingURL=coordination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coordination.d.ts","sourceRoot":"","sources":["../../src/acquire/coordination.ts"],"names":[],"mappings":"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"}
|