@cortexkit/opencode-openai-auth 1.3.0 → 1.4.0
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/bin.js +8 -8
- package/dist/{codex-request-B9D0e97Q.d.ts → codex-request-hw0XcldE.d.ts} +27 -27
- package/dist/index-1eq2s1cp.js +2 -0
- package/dist/{index-m3ctrabv.js → index-1tdnrt68.js} +1 -1
- package/dist/{index-8x1aj7q3.js → index-5230x6s8.js} +2 -2
- package/dist/{index-gn2b097f.js → index-ag04nyv7.js} +7 -7
- package/dist/index-brbqqer5.js +90 -0
- package/dist/{index-xwvze96g.js → index-d98f1xdp.js} +1 -1
- package/dist/index-jcyrm58f.js +65 -0
- package/dist/{index-2pgkrebq.js → index-rsvnabbe.js} +2 -2
- package/dist/{index-ftkhhr6d.js → index-s88n2bjn.js} +1 -1
- package/dist/{index-bc89akak.js → index-t5m0dhmq.js} +1 -1
- package/dist/index-vy729a0b.js +69 -0
- package/dist/index-wgwze391.js +2 -0
- package/dist/{index-tddr876f.js → index-yhw3hnsh.js} +2 -2
- package/dist/index.d.ts +11 -3
- package/dist/index.js +1 -1
- package/dist/{model-BdITTmeb.d.ts → model-DphbMQce.d.ts} +1 -1
- package/dist/{port-file-DbHIVC9a.d.ts → port-file-Cx-wwe5Q.d.ts} +1 -1
- package/dist/{protocol-MysYPAho.d.ts → protocol-CDqdjh74.d.ts} +1 -1
- package/dist/rpc/port-file.d.ts +1 -1
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/protocol.d.ts +1 -1
- package/dist/rpc/protocol.js +1 -1
- package/dist/rpc/rpc-client.d.ts +3 -3
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/{sidebar-state-CVSok7zv.d.ts → sidebar-state-pTd9gg9W.d.ts} +55 -7
- package/dist/sidebar-state.d.ts +2 -2
- package/dist/sidebar-state.js +1 -1
- package/dist/{token-z6qq7a1d.js → token-0z5g59p8.js} +1 -1
- package/dist/token-util-jf9g6stk.js +1 -0
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +30 -3
- package/dist/v2/server.d.ts +2 -2
- package/dist/v2/server.js +2 -2
- package/package.json +4 -4
- package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +50 -5
- package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +70 -1
- package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +88 -61
- package/src/tui-compiled/raw/shared/2577ec69f9973ae9-quota-normalize.js +45 -1
- package/src/tui-compiled/raw/shared/30f5bc480f31f51c-roster.js +100 -43
- package/src/tui-compiled/raw/shared/39c3cb05738fcf6d-projection.js +3 -1
- package/src/tui-compiled/raw/shared/42ae93ab554fcec2-open-url.js +7 -9
- package/src/tui-compiled/raw/shared/4825e03209d6ae3e-with-lock.js +2 -0
- package/src/tui-compiled/raw/shared/4f36d333d081d040-merge.js +17 -3
- package/src/tui-compiled/raw/shared/81dddd203f1b92a3-quota-writer.js +253 -0
- package/src/tui-compiled/raw/shared/905bffc0220f43d0-map.js +31 -6
- package/src/tui-compiled/raw/shared/abd062d633e29fd2-pool-routing.js +116 -12
- package/src/tui-compiled/raw/shared/bfbdd0f9aae56b47-pool-quota.js +15 -3
- package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +64 -2
- package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +2 -2
- package/src/tui-compiled/raw/shared/dbc61cd405941828-accounts.js +5 -4
- package/src/tui-compiled/raw/shared/dc9290c2a7552ba6-watch.js +2 -1
- package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +1 -0
- package/src/tui-compiled/raw/sidebar-state.js +32 -3
- package/src/tui-compiled/raw/tui/v2-dialog.js +2 -1
- package/src/tui-compiled/raw/tui.tsx +170 -26
- package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +50 -5
- package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +70 -1
- package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +88 -61
- package/src/tui-compiled/runtime/shared/2577ec69f9973ae9-quota-normalize.js +45 -1
- package/src/tui-compiled/runtime/shared/30f5bc480f31f51c-roster.js +100 -43
- package/src/tui-compiled/runtime/shared/39c3cb05738fcf6d-projection.js +3 -1
- package/src/tui-compiled/runtime/shared/42ae93ab554fcec2-open-url.js +7 -9
- package/src/tui-compiled/runtime/shared/4825e03209d6ae3e-with-lock.js +2 -0
- package/src/tui-compiled/runtime/shared/4f36d333d081d040-merge.js +17 -3
- package/src/tui-compiled/runtime/shared/81dddd203f1b92a3-quota-writer.js +253 -0
- package/src/tui-compiled/runtime/shared/905bffc0220f43d0-map.js +31 -6
- package/src/tui-compiled/runtime/shared/abd062d633e29fd2-pool-routing.js +116 -12
- package/src/tui-compiled/runtime/shared/bfbdd0f9aae56b47-pool-quota.js +15 -3
- package/src/tui-compiled/runtime/shared/d0f0b6be200311af-insula.js +64 -2
- package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +2 -2
- package/src/tui-compiled/runtime/shared/dbc61cd405941828-accounts.js +5 -4
- package/src/tui-compiled/runtime/shared/dc9290c2a7552ba6-watch.js +2 -1
- package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +1 -0
- package/src/tui-compiled/runtime/sidebar-state.js +32 -3
- package/src/tui-compiled/runtime/tui/v2-dialog.js +2 -1
- package/src/tui-compiled/runtime/tui.js +350 -219
- package/dist/index-1mbgsxsr.js +0 -2
- package/dist/index-4v2fs42x.js +0 -65
- package/dist/index-ex6nx3mq.js +0 -2
- package/dist/index-psjyexj3.js +0 -69
- package/dist/index-tn8x5tz8.js +0 -90
- package/dist/token-util-cf0nyh7n.js +0 -1
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// no entry gets an absence record. A pair outside the coverage is never
|
|
12
12
|
// touched, so a header-shaped partial observation (which covers only what it
|
|
13
13
|
// carries) cannot erase a family limit or the budget.
|
|
14
|
-
import { ALL_SCOPE, decodeQuotaMap, entryTime, isQuotaCounters, limitKey, } from "./905bffc0220f43d0-map.js";
|
|
14
|
+
import { ALL_SCOPE, decodeQuotaMap, entryTime, isQuotaCounters, isQuotaCredits, limitKey, } from "./905bffc0220f43d0-map.js";
|
|
15
15
|
/** Thrown by `mergeQuotaObservation` when either argument is malformed. */
|
|
16
16
|
export class QuotaCodecError extends TypeError {
|
|
17
17
|
constructor(message) {
|
|
@@ -84,7 +84,8 @@ export function isQuotaObservation(value) {
|
|
|
84
84
|
if (!coverage.every(isObservedPair))
|
|
85
85
|
return false;
|
|
86
86
|
return ((value.budget === undefined || isObservedBudget(value.budget)) &&
|
|
87
|
-
(value.counters === undefined || isQuotaCounters(value.counters))
|
|
87
|
+
(value.counters === undefined || isQuotaCounters(value.counters)) &&
|
|
88
|
+
(value.credits === undefined || isQuotaCredits(value.credits)));
|
|
88
89
|
}
|
|
89
90
|
function compareEntries(left, right) {
|
|
90
91
|
if (left.scope !== right.scope)
|
|
@@ -110,6 +111,17 @@ function mergeCounters(stored, observed) {
|
|
|
110
111
|
}
|
|
111
112
|
return counters.size === 0 ? undefined : Object.fromEntries(counters);
|
|
112
113
|
}
|
|
114
|
+
// Credits merge like a single counter: the newer record wins, an equal time
|
|
115
|
+
// applies (the observation applied last wins), and an observation without
|
|
116
|
+
// credits leaves the stored record alone.
|
|
117
|
+
function mergeCredits(stored, observed) {
|
|
118
|
+
if (observed === undefined)
|
|
119
|
+
return stored;
|
|
120
|
+
if (stored !== undefined && observed.checkedAt < stored.checkedAt) {
|
|
121
|
+
return stored;
|
|
122
|
+
}
|
|
123
|
+
return { ...observed };
|
|
124
|
+
}
|
|
113
125
|
/**
|
|
114
126
|
* Applies `observation` to `stored` (undefined for a row with no map yet) and
|
|
115
127
|
* returns the new map. Pure: neither argument is modified. Throws
|
|
@@ -166,13 +178,15 @@ export function mergeQuotaObservation(stored, observation) {
|
|
|
166
178
|
: { scope, label: pair.label, kind: 'retired', retiredAt: at });
|
|
167
179
|
}
|
|
168
180
|
// Keys this module does not recognise are carried over untouched.
|
|
169
|
-
const { limits: _limits, budget: _budget, counters: _counters, ...unknownKeys } = base;
|
|
181
|
+
const { limits: _limits, budget: _budget, counters: _counters, credits: _credits, ...unknownKeys } = base;
|
|
170
182
|
const budget = mergeBudget(base.budget, observation.budget, at);
|
|
171
183
|
const counters = mergeCounters(base.counters, observation.counters);
|
|
184
|
+
const credits = mergeCredits(base.credits, observation.credits);
|
|
172
185
|
return {
|
|
173
186
|
...unknownKeys,
|
|
174
187
|
limits: [...entries.values()].sort(compareEntries),
|
|
175
188
|
...(budget === undefined ? {} : { budget }),
|
|
176
189
|
...(counters === undefined ? {} : { counters }),
|
|
190
|
+
...(credits === undefined ? {} : { credits }),
|
|
177
191
|
};
|
|
178
192
|
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { createLogger } from "./cc9e0702a752cea7-index.js";
|
|
3
|
+
import { isQuotaObservation } from "./d5a8f1061ee2f114-index.js";
|
|
4
|
+
import { applyVaultQuota, mutateVaultRoster, } from "./30f5bc480f31f51c-roster.js";
|
|
5
|
+
// Every consumer of the same file shares one mailbox and one lock waiter.
|
|
6
|
+
const writers = new Map();
|
|
7
|
+
const logger = createLogger('claustrum');
|
|
8
|
+
const COALESCE_MS = 100;
|
|
9
|
+
function writerFor(path) {
|
|
10
|
+
const key = resolve(path);
|
|
11
|
+
let writer = writers.get(key);
|
|
12
|
+
if (!writer) {
|
|
13
|
+
writer = {
|
|
14
|
+
path: key,
|
|
15
|
+
pending: new Map(),
|
|
16
|
+
listeners: new Set(),
|
|
17
|
+
wake: new Set(),
|
|
18
|
+
abort: new AbortController(),
|
|
19
|
+
failures: 0,
|
|
20
|
+
draining: false,
|
|
21
|
+
order: 0,
|
|
22
|
+
};
|
|
23
|
+
writers.set(key, writer);
|
|
24
|
+
}
|
|
25
|
+
return writer;
|
|
26
|
+
}
|
|
27
|
+
/** Retain the shared writer; the last consumer's close drains it. */
|
|
28
|
+
export function retainVaultQuotaWriter(path, listener) {
|
|
29
|
+
const writer = writerFor(path);
|
|
30
|
+
writer.listeners.add(listener);
|
|
31
|
+
let released;
|
|
32
|
+
return () => (released ??= (() => {
|
|
33
|
+
writer.listeners.delete(listener);
|
|
34
|
+
return writer.listeners.size ? Promise.resolve() : drainVaultQuota(path);
|
|
35
|
+
})());
|
|
36
|
+
}
|
|
37
|
+
// Split one observation into its independently dated parts: each rate-limit
|
|
38
|
+
// window (scope and label), each covered-but-unread window, the budget, each
|
|
39
|
+
// counter and the credits record. The mailbox keeps the newest of each part on
|
|
40
|
+
// its own, so a later observation that carries only some parts can't erase an
|
|
41
|
+
// older but still newest reading of another part, which is how
|
|
42
|
+
// mergeQuotaObservation treats them on disk. Each key also carries the
|
|
43
|
+
// receipt's identity, so the identity fence still applies to every part.
|
|
44
|
+
function fields(publication) {
|
|
45
|
+
const result = new Map();
|
|
46
|
+
const { observation } = publication;
|
|
47
|
+
const witness = JSON.stringify([
|
|
48
|
+
publication.routeId,
|
|
49
|
+
publication.receipt?.credentialId,
|
|
50
|
+
publication.receipt?.accountIdentitySource,
|
|
51
|
+
publication.receipt?.expectedAccountIdentity,
|
|
52
|
+
]);
|
|
53
|
+
const add = (key, at, part) => result.set(`${witness}:${key}`, {
|
|
54
|
+
at,
|
|
55
|
+
order: 0,
|
|
56
|
+
publication: { ...publication, observation: part },
|
|
57
|
+
});
|
|
58
|
+
const keyFor = (scope, label) => JSON.stringify([scope ?? 'all', label]);
|
|
59
|
+
const read = new Set();
|
|
60
|
+
for (const reading of observation.readings ?? []) {
|
|
61
|
+
const key = keyFor(reading.scope, reading.label);
|
|
62
|
+
read.add(key);
|
|
63
|
+
add(key, observation.checkedAt, {
|
|
64
|
+
checkedAt: observation.checkedAt,
|
|
65
|
+
readings: [{ ...reading }],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
for (const pair of observation.coverage ?? []) {
|
|
69
|
+
const key = keyFor(pair.scope, pair.label);
|
|
70
|
+
if (!read.has(key))
|
|
71
|
+
add(key, observation.checkedAt, {
|
|
72
|
+
checkedAt: observation.checkedAt,
|
|
73
|
+
coverage: [{ ...pair }],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (observation.budget)
|
|
77
|
+
add('budget', observation.checkedAt, {
|
|
78
|
+
checkedAt: observation.checkedAt,
|
|
79
|
+
budget: { ...observation.budget },
|
|
80
|
+
});
|
|
81
|
+
for (const [name, counter] of Object.entries(observation.counters ?? {})) {
|
|
82
|
+
add(`counter:${name}`, counter.checkedAt, {
|
|
83
|
+
checkedAt: observation.checkedAt,
|
|
84
|
+
counters: { [name]: { ...counter } },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (observation.credits)
|
|
88
|
+
add('credits', observation.credits.checkedAt, {
|
|
89
|
+
checkedAt: observation.checkedAt,
|
|
90
|
+
credits: { ...observation.credits },
|
|
91
|
+
});
|
|
92
|
+
// Even an empty observation can bind an account proved by its receipt.
|
|
93
|
+
if (!result.size)
|
|
94
|
+
add('empty', observation.checkedAt, { checkedAt: observation.checkedAt });
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
/** Enqueue without filesystem work; each quota field retains its own receipt fence. */
|
|
98
|
+
export function enqueueVaultQuota(path, publication) {
|
|
99
|
+
if (!isQuotaObservation(publication.observation))
|
|
100
|
+
return Promise.resolve(false);
|
|
101
|
+
const writer = writerFor(path);
|
|
102
|
+
const account = JSON.stringify([
|
|
103
|
+
publication.accountIdentity ?? null,
|
|
104
|
+
publication.accountIdentity === undefined ? publication.routeId : null,
|
|
105
|
+
]);
|
|
106
|
+
const pending = writer.pending.get(account) ?? new Map();
|
|
107
|
+
for (const [key, field] of fields(publication)) {
|
|
108
|
+
const previous = pending.get(key);
|
|
109
|
+
if (!previous || field.at >= previous.at) {
|
|
110
|
+
field.order = ++writer.order;
|
|
111
|
+
// A covered disappearance after an unflushed reading is a retirement,
|
|
112
|
+
// not an absence. Retain just enough history to preserve that distinction.
|
|
113
|
+
if (field.publication.observation.coverage)
|
|
114
|
+
field.priorReading = previous?.publication.observation.readings
|
|
115
|
+
? previous.publication
|
|
116
|
+
: previous?.priorReading;
|
|
117
|
+
pending.set(key, field);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (pending.size)
|
|
121
|
+
writer.pending.set(account, pending);
|
|
122
|
+
for (const listener of writer.listeners)
|
|
123
|
+
listener(publication);
|
|
124
|
+
schedule(writer, COALESCE_MS);
|
|
125
|
+
return Promise.resolve(true);
|
|
126
|
+
}
|
|
127
|
+
function orderedFields(pending) {
|
|
128
|
+
// Equal observation times are last-enqueue-wins even across different
|
|
129
|
+
// credentials or account-only readings. Map insertion order cannot prove it.
|
|
130
|
+
return [...pending.values()]
|
|
131
|
+
.flatMap((account) => [...account.values()])
|
|
132
|
+
.sort((a, b) => a.order - b.order);
|
|
133
|
+
}
|
|
134
|
+
/** Overlay uncommitted observations when a consumer reloads the durable roster. */
|
|
135
|
+
export function pendingVaultQuota(path, roster) {
|
|
136
|
+
const writer = writers.get(resolve(path));
|
|
137
|
+
if (writer) {
|
|
138
|
+
for (const field of orderedFields(writer.pending)) {
|
|
139
|
+
if (field.priorReading)
|
|
140
|
+
roster = applyVaultQuota(roster, field.priorReading).roster;
|
|
141
|
+
roster = applyVaultQuota(roster, field.publication).roster;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return roster;
|
|
145
|
+
}
|
|
146
|
+
function schedule(writer, delay) {
|
|
147
|
+
if (writer.running ||
|
|
148
|
+
writer.timer ||
|
|
149
|
+
!writer.pending.size ||
|
|
150
|
+
writer.abort.signal.aborted)
|
|
151
|
+
return;
|
|
152
|
+
writer.timer = setTimeout(() => {
|
|
153
|
+
writer.timer = undefined;
|
|
154
|
+
flush(writer);
|
|
155
|
+
}, delay);
|
|
156
|
+
if (!writer.draining)
|
|
157
|
+
writer.timer.unref?.();
|
|
158
|
+
}
|
|
159
|
+
function flush(writer) {
|
|
160
|
+
if (writer.running || writer.abort.signal.aborted)
|
|
161
|
+
return;
|
|
162
|
+
const batch = new Map([...writer.pending].map(([account, pending]) => [
|
|
163
|
+
account,
|
|
164
|
+
new Map(pending),
|
|
165
|
+
]));
|
|
166
|
+
writer.running = (async () => {
|
|
167
|
+
try {
|
|
168
|
+
await mutateVaultRoster(writer.path, (current) => {
|
|
169
|
+
let next = current;
|
|
170
|
+
for (const field of orderedFields(batch)) {
|
|
171
|
+
if (field.priorReading)
|
|
172
|
+
next = applyVaultQuota(next, field.priorReading).roster;
|
|
173
|
+
next = applyVaultQuota(next, field.publication).roster;
|
|
174
|
+
}
|
|
175
|
+
return { ...(next !== current && { next }), result: undefined };
|
|
176
|
+
}, { signal: writer.abort.signal });
|
|
177
|
+
for (const [account, fields] of batch) {
|
|
178
|
+
const pending = writer.pending.get(account);
|
|
179
|
+
for (const [key, field] of fields) {
|
|
180
|
+
// A newer enqueue must survive completion of an older flush.
|
|
181
|
+
if (pending?.get(key) === field)
|
|
182
|
+
pending.delete(key);
|
|
183
|
+
}
|
|
184
|
+
if (!pending?.size)
|
|
185
|
+
writer.pending.delete(account);
|
|
186
|
+
}
|
|
187
|
+
writer.failures = 0;
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (!writer.abort.signal.aborted && writer.failures++ === 0) {
|
|
191
|
+
logger.warn('vault quota publication delayed; retrying', {
|
|
192
|
+
error: error instanceof Error ? error.message : String(error),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
// Keep the batch in the mailbox, including any fresher fields enqueued meanwhile.
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
writer.running = undefined;
|
|
199
|
+
for (const wake of writer.wake)
|
|
200
|
+
wake();
|
|
201
|
+
const backoff = writer.failures
|
|
202
|
+
? Math.min(5_000, 100 * 2 ** Math.min(writer.failures, 6)) *
|
|
203
|
+
(0.5 + Math.random())
|
|
204
|
+
: COALESCE_MS;
|
|
205
|
+
schedule(writer, backoff);
|
|
206
|
+
}
|
|
207
|
+
})();
|
|
208
|
+
}
|
|
209
|
+
/** Wait at most timeoutMs for detached publication; shutdown loss is acceptable. */
|
|
210
|
+
export async function drainVaultQuota(path, timeoutMs = 1_000) {
|
|
211
|
+
const key = resolve(path);
|
|
212
|
+
const writer = writers.get(key);
|
|
213
|
+
if (!writer)
|
|
214
|
+
return;
|
|
215
|
+
writer.draining = true;
|
|
216
|
+
clearTimeout(writer.timer);
|
|
217
|
+
writer.timer = undefined;
|
|
218
|
+
let deadline;
|
|
219
|
+
let wake;
|
|
220
|
+
const drained = new Promise((done) => {
|
|
221
|
+
wake = () => {
|
|
222
|
+
if (!writer.pending.size || writer.abort.signal.aborted)
|
|
223
|
+
done();
|
|
224
|
+
};
|
|
225
|
+
writer.wake.add(wake);
|
|
226
|
+
wake();
|
|
227
|
+
});
|
|
228
|
+
try {
|
|
229
|
+
if (writer.pending.size)
|
|
230
|
+
flush(writer);
|
|
231
|
+
await Promise.race([
|
|
232
|
+
drained,
|
|
233
|
+
new Promise((done) => {
|
|
234
|
+
deadline = setTimeout(done, Math.max(0, timeoutMs));
|
|
235
|
+
}),
|
|
236
|
+
]);
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
clearTimeout(deadline);
|
|
240
|
+
if (wake)
|
|
241
|
+
writer.wake.delete(wake);
|
|
242
|
+
writer.draining = false;
|
|
243
|
+
// A still-open consumer owns the mailbox, so another consumer's disposal
|
|
244
|
+
// must not discard its observations or stop its retries.
|
|
245
|
+
if (!writer.listeners.size) {
|
|
246
|
+
writer.abort.abort();
|
|
247
|
+
clearTimeout(writer.timer);
|
|
248
|
+
writer.pending.clear();
|
|
249
|
+
if (writers.get(key) === writer)
|
|
250
|
+
writers.delete(key);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
// reset clock. It is a tri-state: absent (never reported), a reading, or
|
|
15
15
|
// cleared (the provider reported that no budget exists).
|
|
16
16
|
//
|
|
17
|
+
// Provider-named counters and the prepaid credits record sit beside them too,
|
|
18
|
+
// each judged by its own observation time rather than the limits'.
|
|
19
|
+
//
|
|
17
20
|
// The map is plain JSON so the store can persist it without interpreting it.
|
|
18
21
|
/** The scope of a limit that binds every request regardless of model. */
|
|
19
22
|
export const ALL_SCOPE = 'all';
|
|
@@ -93,6 +96,18 @@ function isQuotaCounter(value) {
|
|
|
93
96
|
export function isQuotaCounters(value) {
|
|
94
97
|
return isRecord(value) && Object.values(value).every(isQuotaCounter);
|
|
95
98
|
}
|
|
99
|
+
function isBoolean(value) {
|
|
100
|
+
return typeof value === 'boolean';
|
|
101
|
+
}
|
|
102
|
+
/** True when `value` is a well-formed prepaid-credits record. */
|
|
103
|
+
export function isQuotaCredits(value) {
|
|
104
|
+
return (isRecord(value) &&
|
|
105
|
+
typeof value.hasCredits === 'boolean' &&
|
|
106
|
+
typeof value.unlimited === 'boolean' &&
|
|
107
|
+
optional(value, 'balance', isFiniteNumber) &&
|
|
108
|
+
optional(value, 'overageLimitReached', isBoolean) &&
|
|
109
|
+
isFiniteNumber(value.checkedAt));
|
|
110
|
+
}
|
|
96
111
|
/** Keep usable counters without letting one malformed count hide a quota map. */
|
|
97
112
|
export function readQuotaCounters(value) {
|
|
98
113
|
if (!isRecord(value))
|
|
@@ -102,20 +117,29 @@ export function readQuotaCounters(value) {
|
|
|
102
117
|
? undefined
|
|
103
118
|
: Object.fromEntries(entries);
|
|
104
119
|
}
|
|
105
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Decode persisted quota, dropping malformed counters or a malformed credits
|
|
122
|
+
* record but not limit or budget evidence.
|
|
123
|
+
*/
|
|
106
124
|
export function decodeQuotaMap(value) {
|
|
107
125
|
// A valid map is returned as the same object, so stored quota keeps the
|
|
108
|
-
// identity it had before counters existed
|
|
109
|
-
// counter is copied
|
|
126
|
+
// identity it had before counters and credits existed. A map with a
|
|
127
|
+
// malformed counter or credits record is copied with those malformed
|
|
128
|
+
// fields left out.
|
|
110
129
|
if (isQuotaMap(value))
|
|
111
130
|
return value;
|
|
112
131
|
if (!isRecord(value))
|
|
113
132
|
return undefined;
|
|
114
|
-
const { counters: rawCounters, ...rest } = value;
|
|
133
|
+
const { counters: rawCounters, credits: rawCredits, ...rest } = value;
|
|
115
134
|
if (!isQuotaMap(rest))
|
|
116
135
|
return undefined;
|
|
117
136
|
const counters = readQuotaCounters(rawCounters);
|
|
118
|
-
|
|
137
|
+
const credits = isQuotaCredits(rawCredits) ? rawCredits : undefined;
|
|
138
|
+
return {
|
|
139
|
+
...rest,
|
|
140
|
+
...(counters === undefined ? {} : { counters }),
|
|
141
|
+
...(credits === undefined ? {} : { credits }),
|
|
142
|
+
};
|
|
119
143
|
}
|
|
120
144
|
/**
|
|
121
145
|
* True when `value` is a well-formed quota map. Unrecognised top-level keys
|
|
@@ -136,5 +160,6 @@ export function isQuotaMap(value) {
|
|
|
136
160
|
seen.add(key);
|
|
137
161
|
}
|
|
138
162
|
return (optional(value, 'budget', isCreditBudgetEntry) &&
|
|
139
|
-
optional(value, 'counters', isQuotaCounters)
|
|
163
|
+
optional(value, 'counters', isQuotaCounters) &&
|
|
164
|
+
optional(value, 'credits', isQuotaCredits));
|
|
140
165
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
budgetExhaustedResetAt,
|
|
3
|
+
isQuotaCredits,
|
|
4
|
+
projectQuota
|
|
5
|
+
} from "./d5a8f1061ee2f114-index.js";
|
|
2
6
|
import {
|
|
3
7
|
admit,
|
|
4
8
|
decideStickyBreak,
|
|
5
9
|
orderForPlacement,
|
|
10
|
+
QUOTA_STALENESS_MS,
|
|
6
11
|
routeOrdered,
|
|
7
12
|
routeSticky
|
|
8
13
|
} from "./3e7d45a77c6ad4d5-index.js";
|
|
@@ -65,6 +70,70 @@ export function waitForFirstQuotaReading(input, waitMs = FIRST_QUOTA_READING_WAI
|
|
|
65
70
|
function isConfirmedExhaustion(refusal) {
|
|
66
71
|
return refusal.reason === "exhausted" || refusal.reason === "budget-spent";
|
|
67
72
|
}
|
|
73
|
+
export function creditsUsable(credits, now) {
|
|
74
|
+
if (!isQuotaCredits(credits))
|
|
75
|
+
return !1;
|
|
76
|
+
const record = credits;
|
|
77
|
+
if (!(now - record.checkedAt <= QUOTA_STALENESS_MS))
|
|
78
|
+
return !1;
|
|
79
|
+
if (record.overageLimitReached === !0)
|
|
80
|
+
return !1;
|
|
81
|
+
if (record.unlimited)
|
|
82
|
+
return !0;
|
|
83
|
+
return record.hasCredits && (record.balance === void 0 || record.balance > 0);
|
|
84
|
+
}
|
|
85
|
+
function creditsTiers(rows, input, admission) {
|
|
86
|
+
const passes = (id) => input.killswitch.get(id) !== !1, free = new Set(admission.admitted.filter((row) => row.lastPath !== !0 && passes(row.id)).map((row) => row.id)), refusals = new Map(admission.refused.map((refusal) => [refusal.id, refusal])), credits = rows.filter((row) => {
|
|
87
|
+
if (row.kind !== "oauth" || !passes(row.id))
|
|
88
|
+
return !1;
|
|
89
|
+
if (refusals.get(row.id)?.reason !== "exhausted")
|
|
90
|
+
return !1;
|
|
91
|
+
const projection = projectQuota(row.quota);
|
|
92
|
+
if (budgetExhaustedResetAt(projection, input.now))
|
|
93
|
+
return !1;
|
|
94
|
+
return creditsUsable(projection.credits, input.now);
|
|
95
|
+
}).map((row) => row.id);
|
|
96
|
+
return { free, credits };
|
|
97
|
+
}
|
|
98
|
+
function admitQuietly(input, rows = input.rows) {
|
|
99
|
+
return admit({
|
|
100
|
+
rows,
|
|
101
|
+
now: input.now,
|
|
102
|
+
rateLimitMarks: input.rateLimitMarks,
|
|
103
|
+
refreshBackoff: input.refreshBackoff,
|
|
104
|
+
requestPull: () => {}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
export function creditsFallbackIds(input) {
|
|
108
|
+
return [...creditsTiers(input.rows, input, admitQuietly(input)).credits];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class CreditsServing {
|
|
112
|
+
#ids = new Set;
|
|
113
|
+
served(input, id, log) {
|
|
114
|
+
const admission = admitQuietly(input), tiers = creditsTiers(input.rows, input, admission), credits = new Set(tiers.credits);
|
|
115
|
+
if (tiers.free.has(id)) {
|
|
116
|
+
this.#ids.clear();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const marked of [...this.#ids])
|
|
120
|
+
if (!credits.has(marked))
|
|
121
|
+
this.#ids.delete(marked);
|
|
122
|
+
if (!credits.has(id) || this.#ids.has(id))
|
|
123
|
+
return;
|
|
124
|
+
this.#ids.add(id);
|
|
125
|
+
log?.info("serving requests on prepaid credits", {
|
|
126
|
+
accountId: id,
|
|
127
|
+
reason: "free quota used up"
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
has(id) {
|
|
131
|
+
return this.#ids.has(id);
|
|
132
|
+
}
|
|
133
|
+
ids() {
|
|
134
|
+
return new Set(this.#ids);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
68
137
|
export function blockFor(id, admission, killswitch) {
|
|
69
138
|
if (id === void 0)
|
|
70
139
|
return { reason: "no-credential" };
|
|
@@ -97,11 +166,13 @@ export function planOrdered(input) {
|
|
|
97
166
|
refreshBackoff: input.refreshBackoff,
|
|
98
167
|
now: input.now,
|
|
99
168
|
requestPull: input.requestPull
|
|
100
|
-
}), ids = orderForPlacement(input.rows.map((row) => row.id), placement, mainId), confirmed = new Set(route.admission.refused.filter((refusal) => isConfirmedExhaustion(refusal) && input.killswitch.get(refusal.id) !== !1).map((refusal) => refusal.id)), lastPath = ids.filter((id) => confirmed.has(id));
|
|
169
|
+
}), ids = orderForPlacement(input.rows.map((row) => row.id), placement, mainId), confirmed = new Set(route.admission.refused.filter((refusal) => isConfirmedExhaustion(refusal) && input.killswitch.get(refusal.id) !== !1).map((refusal) => refusal.id)), lastPath = ids.filter((id) => confirmed.has(id)), tiers = creditsTiers(input.rows, input, route.admission), free = route.order.filter((id) => tiers.free.has(id)), credits = ids.filter((id) => tiers.credits.includes(id));
|
|
101
170
|
if (!replayable) {
|
|
102
|
-
const hasMain = ids.includes(mainId), target = hasMain ? mainId : route.order[0] ?? lastPath[0];
|
|
171
|
+
const hasMain = ids.includes(mainId), target = hasMain ? mainId : free[0] ?? credits[0] ?? route.order[0] ?? lastPath[0];
|
|
103
172
|
if (target !== void 0 && route.order.includes(target))
|
|
104
173
|
return { kind: "send", order: [target], lastPath: !1 };
|
|
174
|
+
if (target !== void 0 && credits.includes(target))
|
|
175
|
+
return { kind: "send", order: [target], lastPath: !1 };
|
|
105
176
|
if (target !== void 0 && confirmed.has(target))
|
|
106
177
|
return { kind: "send", order: [target], lastPath: !0 };
|
|
107
178
|
return {
|
|
@@ -109,6 +180,16 @@ export function planOrdered(input) {
|
|
|
109
180
|
block: hasMain ? blockFor(mainId, route.admission, input.killswitch) : routeBlock(ids, route.admission, input.killswitch)
|
|
110
181
|
};
|
|
111
182
|
}
|
|
183
|
+
if (free.length > 0)
|
|
184
|
+
return { kind: "send", order: free, lastPath: !1 };
|
|
185
|
+
if (credits.length > 0) {
|
|
186
|
+
const rest = route.order.length > 0 ? route.order : lastPath;
|
|
187
|
+
return {
|
|
188
|
+
kind: "send",
|
|
189
|
+
order: [...credits, ...rest.filter((id) => !credits.includes(id))],
|
|
190
|
+
lastPath: !1
|
|
191
|
+
};
|
|
192
|
+
}
|
|
112
193
|
if (route.order.length > 0)
|
|
113
194
|
return { kind: "send", order: route.order, lastPath: !1 };
|
|
114
195
|
if (lastPath.length > 0)
|
|
@@ -160,23 +241,46 @@ function routePoolSticky(input, options, extra) {
|
|
|
160
241
|
});
|
|
161
242
|
}
|
|
162
243
|
export function selectStickyRow(input, options, exclude, pendingBytes) {
|
|
163
|
-
const route = routePoolSticky(input, options, { exclude, pendingBytes })
|
|
164
|
-
if (route.outcome !== "dispatch" || route.source === "pin")
|
|
165
|
-
return;
|
|
166
|
-
return {
|
|
244
|
+
const route = routePoolSticky(input, options, { exclude, pendingBytes }), rows = input.rows.filter((row) => !exclude.has(row.id)), tiers = creditsTiers(rows, input, route.admission), selected = route.outcome === "dispatch" && route.source !== "pin" ? {
|
|
167
245
|
accountId: route.accountId,
|
|
168
246
|
source: route.source,
|
|
169
247
|
...route.quotaCheckedAt === void 0 ? {} : { quotaCheckedAt: route.quotaCheckedAt }
|
|
248
|
+
} : void 0;
|
|
249
|
+
if (selected && tiers.free.has(selected.accountId))
|
|
250
|
+
return selected;
|
|
251
|
+
const credits = tiers.credits[0];
|
|
252
|
+
if (credits === void 0)
|
|
253
|
+
return selected;
|
|
254
|
+
const checkedAt = projectQuota(rows.find((row) => row.id === credits)?.quota).checkedAt;
|
|
255
|
+
return {
|
|
256
|
+
accountId: credits,
|
|
257
|
+
source: "mode-fallback",
|
|
258
|
+
...checkedAt === void 0 ? {} : { quotaCheckedAt: checkedAt }
|
|
170
259
|
};
|
|
171
260
|
}
|
|
172
261
|
export function routePinnedRow(input, options, id) {
|
|
173
|
-
const killed = input.killswitch.get(id) === !1,
|
|
262
|
+
const killed = input.killswitch.get(id) === !1, move = {
|
|
263
|
+
kind: "move",
|
|
264
|
+
reason: killed ? "killswitch" : "exhausted"
|
|
265
|
+
}, route = routePoolSticky(input, options, { pin: id }), tiers = creditsTiers(input.rows, input, route.admission), onCredits = tiers.credits.includes(id), othersOnCredits = tiers.credits.some((other) => other !== id);
|
|
174
266
|
if (route.outcome === "dispatch") {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
267
|
+
const toLastPath = !tiers.free.has(route.accountId);
|
|
268
|
+
if (route.pin.action === "assign") {
|
|
269
|
+
if (toLastPath && onCredits)
|
|
270
|
+
return { kind: "serve" };
|
|
271
|
+
return move;
|
|
272
|
+
}
|
|
273
|
+
if (route.accountId !== id)
|
|
274
|
+
return { kind: "detour" };
|
|
275
|
+
if (toLastPath && othersOnCredits)
|
|
276
|
+
return move;
|
|
277
|
+
return { kind: "serve" };
|
|
178
278
|
}
|
|
179
|
-
const refusal = route.admission.refused.find((entry) => entry.id === id);
|
|
279
|
+
const refusal = route.admission.refused.find((entry) => entry.id === id), confirmed = killed || refusal !== void 0 && isConfirmedExhaustion(refusal);
|
|
280
|
+
if (onCredits)
|
|
281
|
+
return { kind: "serve" };
|
|
282
|
+
if (othersOnCredits)
|
|
283
|
+
return confirmed ? move : { kind: "detour" };
|
|
180
284
|
if (!killed && refusal && isConfirmedExhaustion(refusal))
|
|
181
285
|
return { kind: "last-path" };
|
|
182
286
|
return { kind: "none" };
|
|
@@ -42,6 +42,17 @@ function budgetFor(snapshot) {
|
|
|
42
42
|
return { kind: "cleared" };
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
|
+
function prepaidCreditsFor(value, checkedAt) {
|
|
46
|
+
if (!isRecord(value) || typeof value.hasCredits !== "boolean" || typeof value.unlimited !== "boolean")
|
|
47
|
+
return;
|
|
48
|
+
return {
|
|
49
|
+
hasCredits: value.hasCredits,
|
|
50
|
+
unlimited: value.unlimited,
|
|
51
|
+
...finite(value.balance) && value.balance >= 0 ? { balance: value.balance } : {},
|
|
52
|
+
...typeof value.overageLimitReached === "boolean" ? { overageLimitReached: value.overageLimitReached } : {},
|
|
53
|
+
checkedAt
|
|
54
|
+
};
|
|
55
|
+
}
|
|
45
56
|
export function observationFromSnapshot(snapshot, checkedAt, complete) {
|
|
46
57
|
if (!isRecord(snapshot) || !finite(checkedAt))
|
|
47
58
|
return;
|
|
@@ -53,15 +64,16 @@ export function observationFromSnapshot(snapshot, checkedAt, complete) {
|
|
|
53
64
|
else if (complete)
|
|
54
65
|
coverage.push({ label });
|
|
55
66
|
}
|
|
56
|
-
const budget = budgetFor(snapshot), credits = snapshot.resetCreditsAvailable, counters = typeof credits === "number" && Number.isSafeInteger(credits) && credits >= 0 ? { resetCredits: { value: credits, checkedAt } } : void 0;
|
|
57
|
-
if (readings.length === 0 && coverage.length === 0 && !budget && !counters)
|
|
67
|
+
const budget = budgetFor(snapshot), credits = snapshot.resetCreditsAvailable, counters = typeof credits === "number" && Number.isSafeInteger(credits) && credits >= 0 ? { resetCredits: { value: credits, checkedAt } } : void 0, prepaid = prepaidCreditsFor(snapshot.credits, checkedAt);
|
|
68
|
+
if (readings.length === 0 && coverage.length === 0 && !budget && !counters && !prepaid)
|
|
58
69
|
return;
|
|
59
70
|
return {
|
|
60
71
|
checkedAt,
|
|
61
72
|
...readings.length > 0 ? { readings } : {},
|
|
62
73
|
...coverage.length > 0 ? { coverage } : {},
|
|
63
74
|
...budget ? { budget } : {},
|
|
64
|
-
...counters ? { counters } : {}
|
|
75
|
+
...counters ? { counters } : {},
|
|
76
|
+
...prepaid ? { credits: prepaid } : {}
|
|
65
77
|
};
|
|
66
78
|
}
|
|
67
79
|
export function windowsFromQuotaMap(map) {
|