llm-relay 0.68.8 → 0.69.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/accounting-store.d.ts +18 -1
- package/dist/accounting-store.js +130 -224
- package/dist/accounting-store.js.map +1 -1
- package/dist/backend.d.ts +11 -0
- package/dist/backend.js +7 -1
- package/dist/backend.js.map +1 -1
- package/dist/candidate-runner.d.ts +2 -2
- package/dist/candidate-runner.js +47 -21
- package/dist/candidate-runner.js.map +1 -1
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +77 -47
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/dispatch.d.ts +15 -0
- package/dist/dispatch.js +20 -0
- package/dist/dispatch.js.map +1 -1
- package/dist/dynamic-pools.d.ts +1 -1
- package/dist/dynamic-pools.js +2 -2
- package/dist/dynamic-pools.js.map +1 -1
- package/dist/mcp/lane-runner.d.ts +102 -2
- package/dist/mcp/lane-runner.js +49 -6
- package/dist/mcp/lane-runner.js.map +1 -1
- package/dist/mcp/server.d.ts +37 -1
- package/dist/mcp/server.js +183 -14
- package/dist/mcp/server.js.map +1 -1
- package/dist/routes/admin.js +2 -1
- package/dist/routes/admin.js.map +1 -1
- package/dist/routes/messages.js +5 -23
- package/dist/routes/messages.js.map +1 -1
- package/dist/routes/openai-front.js +5 -23
- package/dist/routes/openai-front.js.map +1 -1
- package/dist/server.js +12 -4
- package/dist/server.js.map +1 -1
- package/dist/setup-claude.d.ts +42 -0
- package/dist/setup-claude.js +98 -5
- package/dist/setup-claude.js.map +1 -1
- package/dist/target-facts.d.ts +10 -0
- package/dist/target-facts.js +11 -0
- package/dist/target-facts.js.map +1 -1
- package/package.json +1 -1
- package/skills/llm-relay/SKILL.md +15 -0
- package/skills/llm-relay/references/dispatch-lanes.md +26 -0
- package/dist/accounting-store-io.d.ts +0 -132
- package/dist/accounting-store-io.js +0 -1087
- package/dist/accounting-store-io.js.map +0 -1
|
@@ -9,7 +9,24 @@
|
|
|
9
9
|
* - Read-only queries (`readOnly: true`) never mutate or create writer leases.
|
|
10
10
|
*/
|
|
11
11
|
import { type AccountingRecorder } from "./accounting.js";
|
|
12
|
-
|
|
12
|
+
export interface SnapshotMutationResult {
|
|
13
|
+
readonly status: "none" | "committed" | "recovered" | "recovery-loss" | "invalid" | "failed";
|
|
14
|
+
readonly transactionId: string | null;
|
|
15
|
+
readonly lowerBoundLoss: boolean;
|
|
16
|
+
readonly error: string | null;
|
|
17
|
+
readonly quarantinedPath: string | null;
|
|
18
|
+
readonly retryable: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface SnapshotWriterResult {
|
|
21
|
+
readonly status: "acquired" | "released" | "closed" | "failed";
|
|
22
|
+
readonly error: string | null;
|
|
23
|
+
readonly retryable: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface SnapshotJournalHooks {
|
|
26
|
+
readonly beforeRead?: (path: string) => void;
|
|
27
|
+
readonly beforeStep?: (step: unknown) => void;
|
|
28
|
+
readonly afterStep?: (step: unknown) => void;
|
|
29
|
+
}
|
|
13
30
|
import { type AccountingDayShard, type AccountingLifetime, type AccountingRequestPacket } from "./accounting-store-schema.js";
|
|
14
31
|
export { ACCOUNTING_DAY_SCHEMA, ACCOUNTING_DEDUP_SCHEMA, ACCOUNTING_LIFETIME_SCHEMA, ACCOUNTING_MAX_DAY_ROWS, ACCOUNTING_MAX_DEDUP_IDS, ACCOUNTING_MAX_DETAIL_ATTEMPTS, ACCOUNTING_MAX_FILE_BYTES, ACCOUNTING_MAX_LOSS_MARKERS, ACCOUNTING_MAX_MONTHS, ACCOUNTING_MAX_PACKET_ATTEMPTS, ACCOUNTING_MAX_RECENT_ROWS, ACCOUNTING_MAX_ROWS_PER_CELL, ACCOUNTING_MAX_SAMPLES, ACCOUNTING_MINUTE_SCHEMA, ACCOUNTING_RECENT_SCHEMA, ACCOUNTING_STORE_VERSION, } from "./accounting-store-schema.js";
|
|
15
32
|
export type { AccountingAggregateTokenCellV1 as AggregateTokenCell, AccountingAggregateTokenTotalsV1 as AggregateTokenTotals, AccountingAggregateV1 as AccountingAggregate, AccountingAttemptPacketV1 as AccountingAttemptPacket, AccountingCoverageV1 as AccountingCoverage, AccountingDayShard, AccountingDimensionRowV1 as AccountingAggregateRow, AccountingLifetime, AccountingMetricCellV1 as AggregateMetric, AccountingMinuteShardV1 as AccountingMinuteCell, AccountingRequestPacket, } from "./accounting-store-schema.js";
|
package/dist/accounting-store.js
CHANGED
|
@@ -3,8 +3,9 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { ACCOUNTING_UNSPECIFIED_ESTIMATION_METHOD, } from "./accounting.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { readFileSync, unlinkSync } from "node:fs";
|
|
7
|
+
import { atomicWriteJsonSync } from "./storage/json-store.js";
|
|
8
|
+
import { emptyAccountingSpendCell, mergeAccountingSpendCells, ACCOUNTING_DAY_SCHEMA, ACCOUNTING_DEDUP_SCHEMA, ACCOUNTING_LIFETIME_SCHEMA, ACCOUNTING_MAX_DAY_ROWS, ACCOUNTING_MAX_DEDUP_IDS, ACCOUNTING_MAX_DETAIL_ATTEMPTS, ACCOUNTING_MAX_LOSS_MARKERS, ACCOUNTING_MAX_MONTHS, ACCOUNTING_MAX_PACKET_ATTEMPTS, ACCOUNTING_MAX_RECENT_ROWS, ACCOUNTING_MAX_ROWS_PER_CELL, ACCOUNTING_MAX_SAMPLES, ACCOUNTING_MINUTE_SCHEMA, ACCOUNTING_RECENT_SCHEMA, ACCOUNTING_STORE_VERSION, freezeDeep, parseAccountingAttemptPacketV1, parseAccountingDayShardV1, parseAccountingLifetimeV1, parseAccountingRecentV1, parseAccountingRequestPacketV1, isLoadableId, } from "./accounting-store-schema.js";
|
|
8
9
|
import { DASHBOARD_REQUEST_ID_PATTERN, isDashboardAttemptId, isDashboardFailureKind, isDashboardOutcome, isDashboardSafeId, isDashboardUtcTimestamp, } from "./dashboard-contract.js";
|
|
9
10
|
import { DEFAULT_FLUSH_DELAY_MS, MAX_FLUSH_DELAY_MS, WriteBehindTimer } from "./write-behind.js";
|
|
10
11
|
export { ACCOUNTING_DAY_SCHEMA, ACCOUNTING_DEDUP_SCHEMA, ACCOUNTING_LIFETIME_SCHEMA, ACCOUNTING_MAX_DAY_ROWS, ACCOUNTING_MAX_DEDUP_IDS, ACCOUNTING_MAX_DETAIL_ATTEMPTS, ACCOUNTING_MAX_FILE_BYTES, ACCOUNTING_MAX_LOSS_MARKERS, ACCOUNTING_MAX_MONTHS, ACCOUNTING_MAX_PACKET_ATTEMPTS, ACCOUNTING_MAX_RECENT_ROWS, ACCOUNTING_MAX_ROWS_PER_CELL, ACCOUNTING_MAX_SAMPLES, ACCOUNTING_MINUTE_SCHEMA, ACCOUNTING_RECENT_SCHEMA, ACCOUNTING_STORE_VERSION, } from "./accounting-store-schema.js";
|
|
@@ -481,7 +482,6 @@ function parsedLifetime(value) { const parsed = parseAccountingLifetimeV1(value)
|
|
|
481
482
|
function parsedRecent(value) { const parsed = parseAccountingRecentV1(value); return parsed.ok ? parsed.value : null; }
|
|
482
483
|
class AccountingStoreImpl {
|
|
483
484
|
directory;
|
|
484
|
-
io;
|
|
485
485
|
timer = new WriteBehindTimer();
|
|
486
486
|
retentionDays;
|
|
487
487
|
recentLimit;
|
|
@@ -493,6 +493,7 @@ class AccountingStoreImpl {
|
|
|
493
493
|
pendingAttemptLimit;
|
|
494
494
|
now;
|
|
495
495
|
readOnly;
|
|
496
|
+
ioHooks;
|
|
496
497
|
pending = new Map();
|
|
497
498
|
queued = new Array();
|
|
498
499
|
days = new Map();
|
|
@@ -504,10 +505,6 @@ class AccountingStoreImpl {
|
|
|
504
505
|
recentReadState = "missing";
|
|
505
506
|
dirtyLifetime = false;
|
|
506
507
|
dirtyRecent = false;
|
|
507
|
-
fixedLoadPending = false;
|
|
508
|
-
pendingFixedLoss = false;
|
|
509
|
-
pendingJournalLoss = false;
|
|
510
|
-
deferredGlobalLosses = [];
|
|
511
508
|
pendingRetention = null;
|
|
512
509
|
pendingRetentionPlan = null;
|
|
513
510
|
retryTimer = null;
|
|
@@ -529,28 +526,15 @@ class AccountingStoreImpl {
|
|
|
529
526
|
this.pendingAttemptLimit = positiveLimit(options.pendingAttemptLimit, ACCOUNTING_MAX_PENDING_ATTEMPTS_PER_REQUEST, ACCOUNTING_MAX_PENDING_ATTEMPTS_PER_REQUEST);
|
|
530
527
|
this.now = options.now ?? (() => Date.now());
|
|
531
528
|
this.readOnly = options.readOnly === true;
|
|
532
|
-
this.
|
|
533
|
-
rootDir: this.directory,
|
|
534
|
-
targets: [LIFETIME_TARGET, RECENT_TARGET],
|
|
535
|
-
acceptTarget: dayTarget,
|
|
536
|
-
maxFileBytes: ACCOUNTING_MAX_FILE_BYTES,
|
|
537
|
-
maxJournalBytes: SNAPSHOT_IO_HARD_MAX_JOURNAL_BYTES,
|
|
538
|
-
maxTargets: 128,
|
|
539
|
-
...(options.ioHooks === undefined ? {} : { hooks: options.ioHooks }),
|
|
540
|
-
});
|
|
529
|
+
this.ioHooks = options.ioHooks;
|
|
541
530
|
if (this.readOnly) {
|
|
542
531
|
this.writer = { status: "released", error: null, retryable: false };
|
|
543
532
|
this.last = result("none", null, false, false);
|
|
544
533
|
this.loadFixedSnapshotsReadOnly();
|
|
545
534
|
return;
|
|
546
535
|
}
|
|
547
|
-
this.writer =
|
|
548
|
-
|
|
549
|
-
this.recoverAndLoad();
|
|
550
|
-
else {
|
|
551
|
-
this.last = result("failed", this.writer.error ?? "writer-busy", this.writer.retryable);
|
|
552
|
-
this.markGlobalLoss("unknown", "truncated", "writer");
|
|
553
|
-
}
|
|
536
|
+
this.writer = { status: "acquired", error: null, retryable: false };
|
|
537
|
+
this.loadFixedSnapshots();
|
|
554
538
|
}
|
|
555
539
|
get closed() { return this._closed; }
|
|
556
540
|
get writerStatus() { return this.writer; }
|
|
@@ -590,48 +574,74 @@ class AccountingStoreImpl {
|
|
|
590
574
|
return result("none", null, false, false);
|
|
591
575
|
this.timer.clear();
|
|
592
576
|
this.clearRetry();
|
|
593
|
-
if (this.writer.status !== "acquired")
|
|
577
|
+
if (this.writer.status !== "acquired") {
|
|
594
578
|
return this.setLast(result("failed", this.writer.error ?? "writer-busy", this.writer.retryable));
|
|
595
|
-
const recovered = this.io.recover();
|
|
596
|
-
if (recovered.status === "failed" || recovered.status === "invalid")
|
|
597
|
-
return this.fail(recovered);
|
|
598
|
-
if (this.fixedLoadPending) {
|
|
599
|
-
this.recoverAndLoad();
|
|
600
|
-
if (this.fixedLoadPending)
|
|
601
|
-
return this.last ?? result("failed", "snapshot-read", true, this.lowerBoundLoss);
|
|
602
|
-
}
|
|
603
|
-
if (recovered.lowerBoundLoss || recovered.status === "recovery-loss")
|
|
604
|
-
this.consumeRecoveryLoss(recovered);
|
|
605
|
-
if (recovered.status === "recovered" && this.pendingRetentionPlan !== null) {
|
|
606
|
-
this.reconcileRecoveredRetention(this.pendingRetentionPlan);
|
|
607
579
|
}
|
|
608
580
|
this.drainQueued();
|
|
609
|
-
if (this.queued.length > 0)
|
|
581
|
+
if (this.queued.length > 0) {
|
|
610
582
|
return this.fail(result("failed", "day-read", true, this.lowerBoundLoss));
|
|
583
|
+
}
|
|
611
584
|
if (this.dirtyDays.size === 0 && !this.dirtyLifetime && !this.dirtyRecent) {
|
|
612
|
-
|
|
585
|
+
const retentionResult = this.commitRetention([]);
|
|
586
|
+
return retentionResult ?? this.setLast(result("none", null, false, this.lowerBoundLoss));
|
|
587
|
+
}
|
|
588
|
+
const daySnapshots = [];
|
|
589
|
+
for (const date of [...this.dirtyDays].sort()) {
|
|
590
|
+
const day = this.days.get(date);
|
|
591
|
+
const parsed = day === undefined ? null : parsedDay(day);
|
|
592
|
+
if (parsed === null)
|
|
593
|
+
return this.fail(result("invalid", "schema-or-size", false, this.lowerBoundLoss));
|
|
594
|
+
daySnapshots.push({ date, data: parsed });
|
|
595
|
+
}
|
|
596
|
+
let lifetimeSnapshot = null;
|
|
597
|
+
if (this.dirtyLifetime) {
|
|
598
|
+
lifetimeSnapshot = parsedLifetime(this.lifetime);
|
|
599
|
+
if (lifetimeSnapshot === null)
|
|
600
|
+
return this.fail(result("invalid", "schema-or-size", false, this.lowerBoundLoss));
|
|
601
|
+
}
|
|
602
|
+
let recentSnapshot = null;
|
|
603
|
+
if (this.dirtyRecent) {
|
|
604
|
+
recentSnapshot = parsedRecent(this.recent);
|
|
605
|
+
if (recentSnapshot === null)
|
|
606
|
+
return this.fail(result("invalid", "schema-or-size", false, this.lowerBoundLoss));
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
for (const { date, data } of daySnapshots) {
|
|
610
|
+
const filePath = join(this.directory, targetForDay(date));
|
|
611
|
+
if (!atomicWriteJsonSync(filePath, data, { strict: true })) {
|
|
612
|
+
throw new Error(`Failed to write ${targetForDay(date)}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (lifetimeSnapshot !== null) {
|
|
616
|
+
const filePath = join(this.directory, LIFETIME_TARGET);
|
|
617
|
+
if (!atomicWriteJsonSync(filePath, lifetimeSnapshot, { strict: true })) {
|
|
618
|
+
throw new Error(`Failed to write ${LIFETIME_TARGET}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (recentSnapshot !== null) {
|
|
622
|
+
const filePath = join(this.directory, RECENT_TARGET);
|
|
623
|
+
if (!atomicWriteJsonSync(filePath, recentSnapshot, { strict: true })) {
|
|
624
|
+
throw new Error(`Failed to write ${RECENT_TARGET}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
return this.fail(result("failed", err instanceof Error ? err.message : String(err), true, this.lowerBoundLoss));
|
|
613
630
|
}
|
|
614
|
-
if (this.dirtyDays.size + 2 > (this.io.maxTargets ?? 0))
|
|
615
|
-
return this.fail(result("invalid", "transaction-targets", false, this.lowerBoundLoss));
|
|
616
|
-
const snapshots = this.factSnapshots();
|
|
617
|
-
if (snapshots === null)
|
|
618
|
-
return this.fail(result("invalid", "schema-or-size", false, this.lowerBoundLoss));
|
|
619
631
|
const committedDates = [...this.dirtyDays];
|
|
620
|
-
const written = this.io.commit(snapshots);
|
|
621
|
-
this.last = written;
|
|
622
|
-
if (written.status !== "committed" && written.status !== "recovered")
|
|
623
|
-
return this.fail(written);
|
|
624
632
|
this.dirtyDays.clear();
|
|
625
633
|
this.dirtyLifetime = false;
|
|
626
634
|
this.dirtyRecent = false;
|
|
627
|
-
if (
|
|
635
|
+
if (lifetimeSnapshot !== null)
|
|
628
636
|
this.lifetimeReadState = "ok";
|
|
629
|
-
if (
|
|
637
|
+
if (recentSnapshot !== null)
|
|
630
638
|
this.recentReadState = "ok";
|
|
631
639
|
this.retryDelay = RETRY_MIN_MS;
|
|
632
|
-
const retention = this.commitRetention(committedDates);
|
|
633
640
|
for (const date of [...committedDates].sort())
|
|
634
641
|
this.rememberKnownDay(date);
|
|
642
|
+
const written = result("committed", null, false, this.lowerBoundLoss);
|
|
643
|
+
this.last = written;
|
|
644
|
+
const retention = this.commitRetention(committedDates);
|
|
635
645
|
return retention ?? written;
|
|
636
646
|
}
|
|
637
647
|
close() {
|
|
@@ -642,10 +652,8 @@ class AccountingStoreImpl {
|
|
|
642
652
|
const flushed = this.flush();
|
|
643
653
|
if (flushed.retryable)
|
|
644
654
|
return flushed;
|
|
645
|
-
|
|
646
|
-
this.
|
|
647
|
-
if (closed.status === "failed")
|
|
648
|
-
return this.setLast(result("failed", closed.error, closed.retryable, flushed.lowerBoundLoss));
|
|
655
|
+
this._closed = true;
|
|
656
|
+
this.writer = { status: "closed", error: null, retryable: false };
|
|
649
657
|
return flushed;
|
|
650
658
|
}
|
|
651
659
|
readDay(date) {
|
|
@@ -656,17 +664,39 @@ class AccountingStoreImpl {
|
|
|
656
664
|
const parsed = parsedDay(cached);
|
|
657
665
|
return parsed === null ? { status: "corrupt", value: null, error: "schema" } : { status: "ok", value: this.withDayLoss(parsed) };
|
|
658
666
|
}
|
|
659
|
-
const read = this.
|
|
667
|
+
const read = this.readJsonFile(targetForDay(date), parseAccountingDayShardV1);
|
|
660
668
|
if (read.status === "missing")
|
|
661
669
|
return { status: "missing", value: null };
|
|
662
670
|
if (read.status !== "ok" || read.value === null)
|
|
663
|
-
return { status: "corrupt", value: null, error: read.error
|
|
664
|
-
|
|
671
|
+
return { status: "corrupt", value: null, error: read.error };
|
|
672
|
+
this.days.set(date, mutableDay(read.value));
|
|
673
|
+
this.rememberKnownDay(date);
|
|
674
|
+
return { status: "ok", value: this.withDayLoss(read.value) };
|
|
675
|
+
}
|
|
676
|
+
readJsonFile(filename, parse) {
|
|
677
|
+
const filePath = join(this.directory, filename);
|
|
678
|
+
this.ioHooks?.beforeRead?.(filePath);
|
|
679
|
+
let text;
|
|
680
|
+
try {
|
|
681
|
+
text = readFileSync(filePath, "utf8");
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
const code = typeof err === "object" && err !== null && "code" in err ? err.code : undefined;
|
|
685
|
+
if (code === "ENOENT")
|
|
686
|
+
return { status: "missing", value: null };
|
|
687
|
+
return { status: "corrupt", value: null, error: err instanceof Error ? err.message : "read-error" };
|
|
688
|
+
}
|
|
689
|
+
let json;
|
|
690
|
+
try {
|
|
691
|
+
json = JSON.parse(text);
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
return { status: "corrupt", value: null, error: "invalid-json" };
|
|
695
|
+
}
|
|
696
|
+
const parsed = parse(json);
|
|
665
697
|
if (!parsed.ok)
|
|
666
698
|
return { status: "corrupt", value: null, error: "schema" };
|
|
667
|
-
|
|
668
|
-
this.rememberKnownDay(date);
|
|
669
|
-
return { status: "ok", value: this.withDayLoss(parsed.value) };
|
|
699
|
+
return { status: "ok", value: parsed.value };
|
|
670
700
|
}
|
|
671
701
|
readDays(datesOrFrom, toOrOptions, maybeOptions) {
|
|
672
702
|
const options = typeof toOrOptions === "object" ? toOrOptions : maybeOptions;
|
|
@@ -724,7 +754,7 @@ class AccountingStoreImpl {
|
|
|
724
754
|
readDetail(requestId) {
|
|
725
755
|
if (!DASHBOARD_REQUEST_ID_PATTERN.test(requestId))
|
|
726
756
|
return { status: "missing", value: null };
|
|
727
|
-
if (this.
|
|
757
|
+
if (this.recentReadState === "corrupt" && !this.dirtyRecent) {
|
|
728
758
|
return { status: "corrupt", value: null, error: "snapshot" };
|
|
729
759
|
}
|
|
730
760
|
if (this.recentReadState === "missing" && !this.dirtyRecent)
|
|
@@ -862,7 +892,7 @@ class AccountingStoreImpl {
|
|
|
862
892
|
const pending = this.pending.get(event.requestId) ?? null;
|
|
863
893
|
this.pending.delete(event.requestId);
|
|
864
894
|
const work = { event, pending };
|
|
865
|
-
if (
|
|
895
|
+
if (this.applyTerminal(work))
|
|
866
896
|
return;
|
|
867
897
|
if (this.queued.length >= this.pendingRequestLimit) {
|
|
868
898
|
this.markGlobalLoss("unknown", "truncated", "pending_terminals");
|
|
@@ -895,7 +925,7 @@ class AccountingStoreImpl {
|
|
|
895
925
|
return created;
|
|
896
926
|
}
|
|
897
927
|
drainQueued() {
|
|
898
|
-
if (this.
|
|
928
|
+
if (this.queued.length === 0)
|
|
899
929
|
return;
|
|
900
930
|
const pending = this.queued.splice(0);
|
|
901
931
|
for (const work of pending)
|
|
@@ -903,8 +933,6 @@ class AccountingStoreImpl {
|
|
|
903
933
|
this.queued.push(work);
|
|
904
934
|
}
|
|
905
935
|
applyTerminal(work) {
|
|
906
|
-
if (this.fixedLoadPending)
|
|
907
|
-
return false;
|
|
908
936
|
const date = dayFor(work.event.endedAt);
|
|
909
937
|
if (date === null || !this.ensureTransactionRoom(date))
|
|
910
938
|
return false;
|
|
@@ -1123,177 +1151,79 @@ class AccountingStoreImpl {
|
|
|
1123
1151
|
ensureTransactionRoom(date) {
|
|
1124
1152
|
if (this.dirtyDays.has(date))
|
|
1125
1153
|
return true;
|
|
1126
|
-
const maximum =
|
|
1127
|
-
if (maximum === null || maximum < 3)
|
|
1128
|
-
return false;
|
|
1154
|
+
const maximum = 128;
|
|
1129
1155
|
if (this.dirtyDays.size + 3 <= maximum)
|
|
1130
1156
|
return true;
|
|
1131
1157
|
const flushed = this.flush();
|
|
1132
|
-
return (flushed.status === "committed" || flushed.status === "
|
|
1158
|
+
return (flushed.status === "committed" || flushed.status === "none")
|
|
1133
1159
|
&& this.dirtyDays.size + 3 <= maximum;
|
|
1134
1160
|
}
|
|
1135
1161
|
loadDayForWrite(date) {
|
|
1136
1162
|
const cached = this.days.get(date);
|
|
1137
1163
|
if (cached !== undefined)
|
|
1138
1164
|
return cached;
|
|
1139
|
-
const read = this.
|
|
1165
|
+
const read = this.readJsonFile(targetForDay(date), parseAccountingDayShardV1);
|
|
1140
1166
|
if (read.status === "missing") {
|
|
1141
1167
|
const created = emptyDay(date);
|
|
1142
1168
|
this.days.set(date, created);
|
|
1143
1169
|
return created;
|
|
1144
1170
|
}
|
|
1145
1171
|
if (read.status !== "ok" || read.value === null) {
|
|
1146
|
-
if (read.status === "corrupt" || read.status === "oversize") {
|
|
1147
|
-
this.markGlobalLoss("corrupt_recovery", "corrupt", "day");
|
|
1148
|
-
}
|
|
1149
|
-
return null;
|
|
1150
|
-
}
|
|
1151
|
-
const parsed = parseAccountingDayShardV1(read.value);
|
|
1152
|
-
if (!parsed.ok) {
|
|
1153
1172
|
this.markGlobalLoss("corrupt_recovery", "corrupt", "day");
|
|
1154
1173
|
return null;
|
|
1155
1174
|
}
|
|
1156
|
-
const mutable = mutableDay(
|
|
1175
|
+
const mutable = mutableDay(read.value);
|
|
1157
1176
|
this.days.set(date, mutable);
|
|
1158
1177
|
return mutable;
|
|
1159
1178
|
}
|
|
1160
|
-
deferFixedLoad(value) {
|
|
1161
|
-
this.fixedLoadPending = true;
|
|
1162
|
-
this.lifetimeReadState = "corrupt";
|
|
1163
|
-
this.recentReadState = "corrupt";
|
|
1164
|
-
this.last = value;
|
|
1165
|
-
if (value.retryable)
|
|
1166
|
-
this.scheduleRetry();
|
|
1167
|
-
}
|
|
1168
|
-
flushDeferredGlobalLosses() {
|
|
1169
|
-
const losses = this.deferredGlobalLosses.splice(0);
|
|
1170
|
-
for (const loss of losses)
|
|
1171
|
-
this.markGlobalLoss(loss.reason, loss.kind, loss.field, loss.count);
|
|
1172
|
-
}
|
|
1173
1179
|
loadFixedSnapshotsReadOnly() {
|
|
1174
|
-
const lifetime = this.
|
|
1180
|
+
const lifetime = this.readJsonFile(LIFETIME_TARGET, parseAccountingLifetimeV1);
|
|
1175
1181
|
this.lifetimeReadState = lifetime.status === "ok" && lifetime.value !== null
|
|
1176
1182
|
? "ok"
|
|
1177
1183
|
: lifetime.status === "missing" ? "missing" : "corrupt";
|
|
1178
1184
|
if (this.lifetimeReadState === "ok")
|
|
1179
1185
|
this.lifetime = mutableLifetime(lifetime.value);
|
|
1180
|
-
const recent = this.
|
|
1186
|
+
const recent = this.readJsonFile(RECENT_TARGET, parseAccountingRecentV1);
|
|
1181
1187
|
this.recentReadState = recent.status === "ok" && recent.value !== null
|
|
1182
1188
|
? "ok"
|
|
1183
1189
|
: recent.status === "missing" ? "missing" : "corrupt";
|
|
1184
1190
|
if (this.recentReadState === "ok")
|
|
1185
1191
|
this.recent = mutableRecent(recent.value);
|
|
1186
1192
|
}
|
|
1187
|
-
|
|
1188
|
-
const recovered = this.io.recover();
|
|
1189
|
-
if (recovered.status === "failed" || recovered.status === "invalid") {
|
|
1190
|
-
this.pendingJournalLoss ||= recovered.lowerBoundLoss;
|
|
1191
|
-
this.deferFixedLoad(recovered);
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1193
|
+
loadFixedSnapshots() {
|
|
1194
1194
|
let fixedLoss = false;
|
|
1195
|
-
|
|
1196
|
-
const lifetime = this.io.readJson(LIFETIME_TARGET, (value) => parseAccountingLifetimeV1(value).ok, { quarantineCorrupt: true });
|
|
1195
|
+
const lifetime = this.readJsonFile(LIFETIME_TARGET, parseAccountingLifetimeV1);
|
|
1197
1196
|
if (lifetime.status === "ok" && lifetime.value !== null) {
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
this.lifetime = mutableLifetime(parsed.value);
|
|
1201
|
-
this.lifetimeReadState = "ok";
|
|
1202
|
-
}
|
|
1203
|
-
else {
|
|
1204
|
-
this.lifetimeReadState = "corrupt";
|
|
1205
|
-
fixedLoss = true;
|
|
1206
|
-
}
|
|
1197
|
+
this.lifetime = mutableLifetime(lifetime.value);
|
|
1198
|
+
this.lifetimeReadState = "ok";
|
|
1207
1199
|
}
|
|
1208
1200
|
else if (lifetime.status === "missing") {
|
|
1209
1201
|
this.lifetimeReadState = "missing";
|
|
1210
1202
|
}
|
|
1211
|
-
else if (lifetime.status === "failed") {
|
|
1212
|
-
this.lifetimeReadState = "corrupt";
|
|
1213
|
-
fixedReadFailed = true;
|
|
1214
|
-
}
|
|
1215
1203
|
else {
|
|
1216
1204
|
this.lifetimeReadState = "corrupt";
|
|
1217
1205
|
fixedLoss = true;
|
|
1218
1206
|
}
|
|
1219
|
-
const recent = this.
|
|
1207
|
+
const recent = this.readJsonFile(RECENT_TARGET, parseAccountingRecentV1);
|
|
1220
1208
|
if (recent.status === "ok" && recent.value !== null) {
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
this.recent = mutableRecent(parsed.value);
|
|
1224
|
-
this.recentReadState = "ok";
|
|
1225
|
-
}
|
|
1226
|
-
else {
|
|
1227
|
-
this.recentReadState = "corrupt";
|
|
1228
|
-
fixedLoss = true;
|
|
1229
|
-
}
|
|
1209
|
+
this.recent = mutableRecent(recent.value);
|
|
1210
|
+
this.recentReadState = "ok";
|
|
1230
1211
|
}
|
|
1231
1212
|
else if (recent.status === "missing") {
|
|
1232
1213
|
this.recentReadState = "missing";
|
|
1233
1214
|
}
|
|
1234
|
-
else if (recent.status === "failed") {
|
|
1235
|
-
this.recentReadState = "corrupt";
|
|
1236
|
-
fixedReadFailed = true;
|
|
1237
|
-
}
|
|
1238
1215
|
else {
|
|
1239
1216
|
this.recentReadState = "corrupt";
|
|
1240
1217
|
fixedLoss = true;
|
|
1241
1218
|
}
|
|
1242
|
-
if (
|
|
1243
|
-
this.pendingFixedLoss ||= fixedLoss;
|
|
1244
|
-
this.pendingJournalLoss ||= recovered.lowerBoundLoss || recovered.status === "recovery-loss";
|
|
1245
|
-
this.deferFixedLoad(result("failed", "snapshot-read", true, this.lowerBoundLoss || this.pendingJournalLoss));
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
fixedLoss ||= this.pendingFixedLoss;
|
|
1249
|
-
const journalLoss = this.pendingJournalLoss || recovered.lowerBoundLoss || recovered.status === "recovery-loss";
|
|
1250
|
-
this.pendingFixedLoss = false;
|
|
1251
|
-
this.pendingJournalLoss = false;
|
|
1252
|
-
this.fixedLoadPending = false;
|
|
1253
|
-
this.flushDeferredGlobalLosses();
|
|
1254
|
-
if (journalLoss)
|
|
1255
|
-
this.consumeRecoveryLoss(recovered);
|
|
1256
|
-
if (fixedLoss)
|
|
1219
|
+
if (fixedLoss) {
|
|
1257
1220
|
this.markGlobalLoss("corrupt_recovery", "corrupt", "snapshot");
|
|
1221
|
+
}
|
|
1258
1222
|
this.seedTrustedDateFromLifetime();
|
|
1259
1223
|
const today = new Date(this.now()).toISOString().slice(0, 10);
|
|
1260
1224
|
this.loadDayForWrite(today);
|
|
1261
1225
|
this.queueRecoveredRetention();
|
|
1262
|
-
this.last =
|
|
1263
|
-
}
|
|
1264
|
-
factSnapshots() {
|
|
1265
|
-
const snapshots = {};
|
|
1266
|
-
const encode = (name, value) => {
|
|
1267
|
-
let text;
|
|
1268
|
-
try {
|
|
1269
|
-
text = JSON.stringify(value);
|
|
1270
|
-
}
|
|
1271
|
-
catch {
|
|
1272
|
-
return false;
|
|
1273
|
-
}
|
|
1274
|
-
const ceiling = this.io.maxFileBytes;
|
|
1275
|
-
if (ceiling === null || Buffer.byteLength(text, "utf8") > ceiling)
|
|
1276
|
-
return false;
|
|
1277
|
-
snapshots[name] = text;
|
|
1278
|
-
return true;
|
|
1279
|
-
};
|
|
1280
|
-
if (this.dirtyLifetime) {
|
|
1281
|
-
const lifetime = parsedLifetime(this.lifetime);
|
|
1282
|
-
if (lifetime === null || !encode(LIFETIME_TARGET, lifetime))
|
|
1283
|
-
return null;
|
|
1284
|
-
}
|
|
1285
|
-
if (this.dirtyRecent) {
|
|
1286
|
-
const recent = parsedRecent(this.recent);
|
|
1287
|
-
if (recent === null || !encode(RECENT_TARGET, recent))
|
|
1288
|
-
return null;
|
|
1289
|
-
}
|
|
1290
|
-
for (const date of [...this.dirtyDays].sort()) {
|
|
1291
|
-
const day = this.days.get(date);
|
|
1292
|
-
const parsed = day === undefined ? null : parsedDay(day);
|
|
1293
|
-
if (parsed === null || !encode(targetForDay(date), parsed))
|
|
1294
|
-
return null;
|
|
1295
|
-
}
|
|
1296
|
-
return snapshots;
|
|
1226
|
+
this.last = result("none");
|
|
1297
1227
|
}
|
|
1298
1228
|
commitRetention(committedDates) {
|
|
1299
1229
|
if (this.retentionDays === null)
|
|
@@ -1310,15 +1240,12 @@ class AccountingStoreImpl {
|
|
|
1310
1240
|
this.pendingRetentionPlan = null;
|
|
1311
1241
|
return null;
|
|
1312
1242
|
}
|
|
1313
|
-
const maximum = this.io.maxTargets;
|
|
1314
|
-
if (maximum === null || maximum < 1)
|
|
1315
|
-
return this.fail(result("invalid", "retention-targets", false, this.lowerBoundLoss));
|
|
1316
1243
|
const current = parsedLifetime(this.lifetime);
|
|
1317
1244
|
if (current === null)
|
|
1318
1245
|
return this.fail(result("invalid", "lifetime-schema", false, this.lowerBoundLoss));
|
|
1319
1246
|
const progress = validDate(current.coverage.retentionFrom) ? current.coverage.retentionFrom : null;
|
|
1320
1247
|
const start = progress ?? dayFor(current.firstRequestAt ?? "");
|
|
1321
|
-
const retention = this.retentionCandidates(start, cutoff,
|
|
1248
|
+
const retention = this.retentionCandidates(start, cutoff, 127);
|
|
1322
1249
|
const candidates = retention.dates;
|
|
1323
1250
|
if (retention.truncated && candidates.length === 0) {
|
|
1324
1251
|
return this.fail(result("invalid", "retention-targets", false, this.lowerBoundLoss));
|
|
@@ -1335,23 +1262,28 @@ class AccountingStoreImpl {
|
|
|
1335
1262
|
markCoverage(next.coverage, "unknown", "truncated", "retention");
|
|
1336
1263
|
}
|
|
1337
1264
|
const plan = { cutoff, candidates, next, truncated: retention.truncated };
|
|
1338
|
-
const snapshots = {};
|
|
1339
1265
|
const retainedLifetime = parsedLifetime(next);
|
|
1340
1266
|
if (retainedLifetime === null)
|
|
1341
1267
|
return this.fail(result("invalid", "retention-schema-or-size", false, this.lowerBoundLoss));
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1268
|
+
try {
|
|
1269
|
+
const filePath = join(this.directory, LIFETIME_TARGET);
|
|
1270
|
+
if (!atomicWriteJsonSync(filePath, retainedLifetime, { strict: true })) {
|
|
1271
|
+
throw new Error(`Failed to write ${LIFETIME_TARGET}`);
|
|
1272
|
+
}
|
|
1273
|
+
for (const date of candidates) {
|
|
1274
|
+
try {
|
|
1275
|
+
unlinkSync(join(this.directory, targetForDay(date)));
|
|
1276
|
+
}
|
|
1277
|
+
catch (err) {
|
|
1278
|
+
const code = typeof err === "object" && err !== null && "code" in err ? err.code : undefined;
|
|
1279
|
+
if (code !== "ENOENT")
|
|
1280
|
+
throw err;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
catch (err) {
|
|
1285
|
+
return this.fail(result("failed", err instanceof Error ? err.message : String(err), true, this.lowerBoundLoss));
|
|
1346
1286
|
}
|
|
1347
|
-
snapshots[LIFETIME_TARGET] = serialized;
|
|
1348
|
-
for (const date of candidates)
|
|
1349
|
-
snapshots[targetForDay(date)] = null;
|
|
1350
|
-
this.pendingRetentionPlan = plan;
|
|
1351
|
-
const written = this.io.commit(snapshots);
|
|
1352
|
-
this.last = written;
|
|
1353
|
-
if (written.status !== "committed" && written.status !== "recovered")
|
|
1354
|
-
return this.fail(written);
|
|
1355
1287
|
this.applyRetentionPlan(plan);
|
|
1356
1288
|
this.pendingRetentionPlan = null;
|
|
1357
1289
|
if (plan.truncated) {
|
|
@@ -1362,6 +1294,8 @@ class AccountingStoreImpl {
|
|
|
1362
1294
|
else {
|
|
1363
1295
|
this.pendingRetention = null;
|
|
1364
1296
|
}
|
|
1297
|
+
const written = result("committed", null, false, this.lowerBoundLoss);
|
|
1298
|
+
this.last = written;
|
|
1365
1299
|
return written;
|
|
1366
1300
|
}
|
|
1367
1301
|
retentionCandidates(start, cutoff, limit) {
|
|
@@ -1500,35 +1434,7 @@ class AccountingStoreImpl {
|
|
|
1500
1434
|
const cutoff = new Date(at - (this.retentionDays - 1) * 86_400_000).toISOString().slice(0, 10);
|
|
1501
1435
|
return validDate(cutoff) ? cutoff : null;
|
|
1502
1436
|
}
|
|
1503
|
-
consumeRecoveryLoss(_recovered) {
|
|
1504
|
-
if (this.lowerBoundLoss)
|
|
1505
|
-
return;
|
|
1506
|
-
this.lowerBoundLoss = true;
|
|
1507
|
-
this.markGlobalLoss("corrupt_recovery", "corrupt", "journal");
|
|
1508
|
-
this.scheduleFlush();
|
|
1509
|
-
}
|
|
1510
1437
|
markGlobalLoss(reason, kind, field, count = 1) {
|
|
1511
|
-
if (this.fixedLoadPending) {
|
|
1512
|
-
const existing = this.deferredGlobalLosses.find((loss) => loss.reason === reason && loss.kind === kind && loss.field === field);
|
|
1513
|
-
if (existing !== undefined) {
|
|
1514
|
-
if (!increase(existing, "count", count))
|
|
1515
|
-
existing.count = Number.MAX_SAFE_INTEGER;
|
|
1516
|
-
}
|
|
1517
|
-
else if (this.deferredGlobalLosses.length < ACCOUNTING_MAX_LOSS_MARKERS) {
|
|
1518
|
-
this.deferredGlobalLosses.push({ reason, kind, field, count });
|
|
1519
|
-
}
|
|
1520
|
-
else {
|
|
1521
|
-
const summary = this.deferredGlobalLosses.find((loss) => loss.field === "deferred_loss");
|
|
1522
|
-
if (summary !== undefined) {
|
|
1523
|
-
if (!increase(summary, "count", count))
|
|
1524
|
-
summary.count = Number.MAX_SAFE_INTEGER;
|
|
1525
|
-
}
|
|
1526
|
-
else {
|
|
1527
|
-
this.deferredGlobalLosses[this.deferredGlobalLosses.length - 1] = { reason: "unknown", kind: "truncated", field: "deferred_loss", count };
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
return;
|
|
1531
|
-
}
|
|
1532
1438
|
if (reason === "corrupt_recovery")
|
|
1533
1439
|
this.lowerBoundLoss = true;
|
|
1534
1440
|
markCoverage(this.lifetime.coverage, reason, kind, field, count);
|