@rulvar/store-sqlite 1.138.0 → 1.140.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/index.d.ts +21 -0
- package/dist/index.js +53 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -156,10 +156,31 @@ declare class SqliteQuotaLimiter implements QuotaLimiter {
|
|
|
156
156
|
private readonly ordered;
|
|
157
157
|
private readonly now;
|
|
158
158
|
constructor(options: SqliteQuotaLimiterOptions);
|
|
159
|
+
/**
|
|
160
|
+
* Adds the `requests` column release() returns to the window
|
|
161
|
+
* (RV1103). Pre-release schemas lack it; the default 1 IS the exact
|
|
162
|
+
* value for every row an engine wrote, because the engine reserves
|
|
163
|
+
* exactly one request per admission. Serialized under BEGIN
|
|
164
|
+
* IMMEDIATE so N processes booting over one legacy file cannot race
|
|
165
|
+
* the ALTER; idempotent, and busy collisions retry with the rest of
|
|
166
|
+
* the bootstrap.
|
|
167
|
+
*/
|
|
168
|
+
private migrateReservationRequests;
|
|
159
169
|
reserve(request: QuotaReservationRequest): Promise<QuotaDecision>;
|
|
160
170
|
reconcile(reservationId: string, usage: Usage, actual?: {
|
|
161
171
|
requests?: number;
|
|
162
172
|
}): Promise<void>;
|
|
173
|
+
/**
|
|
174
|
+
* Cancels an UNUSED admission (RV1103, the optional SPI method from
|
|
175
|
+
* RV1013): exactly what admission consumed, the admitted requests
|
|
176
|
+
* and the token estimate, returns to the window, from any process
|
|
177
|
+
* sharing the file. Unknown ids, a double release, and a release
|
|
178
|
+
* after reconcile are no-ops (the row is gone); a rolled-over window
|
|
179
|
+
* already aged the estimate out, so only the row is deleted; a
|
|
180
|
+
* released id settles nothing afterwards. Mirrors
|
|
181
|
+
* `memoryQuotaLimiter.release` verdict for verdict.
|
|
182
|
+
*/
|
|
183
|
+
release(reservationId: string): Promise<void>;
|
|
163
184
|
/** Current-window counters per rule, for telemetry and referees. */
|
|
164
185
|
snapshot(): Array<{
|
|
165
186
|
rule: QuotaRule;
|
package/dist/index.js
CHANGED
|
@@ -460,12 +460,32 @@ var SqliteQuotaLimiter = class {
|
|
|
460
460
|
const bootDeadline = wallClock() + BOOT_BUSY_TIMEOUT_MS;
|
|
461
461
|
for (;;) try {
|
|
462
462
|
this.db.exec(schema);
|
|
463
|
+
this.migrateReservationRequests();
|
|
463
464
|
break;
|
|
464
465
|
} catch (thrown) {
|
|
465
466
|
if (!isSqliteBusy(thrown) || wallClock() > bootDeadline) throw thrown;
|
|
466
467
|
sleepSync(25);
|
|
467
468
|
}
|
|
468
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* Adds the `requests` column release() returns to the window
|
|
472
|
+
* (RV1103). Pre-release schemas lack it; the default 1 IS the exact
|
|
473
|
+
* value for every row an engine wrote, because the engine reserves
|
|
474
|
+
* exactly one request per admission. Serialized under BEGIN
|
|
475
|
+
* IMMEDIATE so N processes booting over one legacy file cannot race
|
|
476
|
+
* the ALTER; idempotent, and busy collisions retry with the rest of
|
|
477
|
+
* the bootstrap.
|
|
478
|
+
*/
|
|
479
|
+
migrateReservationRequests() {
|
|
480
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
481
|
+
try {
|
|
482
|
+
if (!this.db.prepare("PRAGMA table_info(quota_reservations)").all().some((column) => column.name === "requests")) this.db.exec("ALTER TABLE quota_reservations ADD COLUMN requests INTEGER NOT NULL DEFAULT 1");
|
|
483
|
+
this.db.exec("COMMIT");
|
|
484
|
+
} catch (thrown) {
|
|
485
|
+
this.rollbackQuietly();
|
|
486
|
+
throw thrown;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
469
489
|
reserve(request) {
|
|
470
490
|
const at = this.now();
|
|
471
491
|
const windowStart = at - at % QUOTA_WINDOW_MS;
|
|
@@ -498,7 +518,7 @@ var SqliteQuotaLimiter = class {
|
|
|
498
518
|
const consume = this.db.prepare("INSERT INTO quota_buckets (rule_key, window_start, requests, tokens) VALUES (?, ?, ?, ?) ON CONFLICT (rule_key, window_start) DO UPDATE SET requests = requests + excluded.requests, tokens = tokens + excluded.tokens");
|
|
499
519
|
for (const key of matched) consume.run(key, windowStart, request.estimate.requests, estimateTokens);
|
|
500
520
|
const reservationId = randomUUID();
|
|
501
|
-
this.db.prepare("INSERT INTO quota_reservations (id, window_start, estimate_tokens, rule_keys) VALUES (?, ?, ?, ?)").run(reservationId, windowStart, estimateTokens, JSON.stringify(matched));
|
|
521
|
+
this.db.prepare("INSERT INTO quota_reservations (id, window_start, estimate_tokens, rule_keys, requests) VALUES (?, ?, ?, ?, ?)").run(reservationId, windowStart, estimateTokens, JSON.stringify(matched), request.estimate.requests);
|
|
502
522
|
this.db.exec("COMMIT");
|
|
503
523
|
return Promise.resolve({
|
|
504
524
|
granted: true,
|
|
@@ -533,6 +553,38 @@ var SqliteQuotaLimiter = class {
|
|
|
533
553
|
throw thrown;
|
|
534
554
|
}
|
|
535
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* Cancels an UNUSED admission (RV1103, the optional SPI method from
|
|
558
|
+
* RV1013): exactly what admission consumed, the admitted requests
|
|
559
|
+
* and the token estimate, returns to the window, from any process
|
|
560
|
+
* sharing the file. Unknown ids, a double release, and a release
|
|
561
|
+
* after reconcile are no-ops (the row is gone); a rolled-over window
|
|
562
|
+
* already aged the estimate out, so only the row is deleted; a
|
|
563
|
+
* released id settles nothing afterwards. Mirrors
|
|
564
|
+
* `memoryQuotaLimiter.release` verdict for verdict.
|
|
565
|
+
*/
|
|
566
|
+
release(reservationId) {
|
|
567
|
+
const at = this.now();
|
|
568
|
+
const windowStart = at - at % QUOTA_WINDOW_MS;
|
|
569
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
570
|
+
try {
|
|
571
|
+
const row = this.db.prepare("SELECT window_start, estimate_tokens, rule_keys, requests FROM quota_reservations WHERE id = ?").get(reservationId);
|
|
572
|
+
if (row === void 0) {
|
|
573
|
+
this.db.exec("COMMIT");
|
|
574
|
+
return Promise.resolve();
|
|
575
|
+
}
|
|
576
|
+
this.db.prepare("DELETE FROM quota_reservations WHERE id = ?").run(reservationId);
|
|
577
|
+
if (row.window_start === windowStart) {
|
|
578
|
+
const giveBack = this.db.prepare("UPDATE quota_buckets SET requests = MAX(0, requests - ?), tokens = MAX(0, tokens - ?) WHERE rule_key = ? AND window_start = ?");
|
|
579
|
+
for (const key of JSON.parse(row.rule_keys)) giveBack.run(row.requests, row.estimate_tokens, key, windowStart);
|
|
580
|
+
}
|
|
581
|
+
this.db.exec("COMMIT");
|
|
582
|
+
return Promise.resolve();
|
|
583
|
+
} catch (thrown) {
|
|
584
|
+
this.rollbackQuietly();
|
|
585
|
+
throw thrown;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
536
588
|
/** Current-window counters per rule, for telemetry and referees. */
|
|
537
589
|
snapshot() {
|
|
538
590
|
const at = this.now();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/store-sqlite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.140.0",
|
|
4
4
|
"description": "Rulvar SQLite store implementing JournalStore and LeasableStore with a fencing epoch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@rulvar/core": "1.
|
|
25
|
+
"@rulvar/core": "1.140.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.20.1",
|
|
29
29
|
"tsdown": "^0.22.14",
|
|
30
30
|
"typescript": "~6.0.3",
|
|
31
|
-
"@rulvar/store-conformance": "1.
|
|
31
|
+
"@rulvar/store-conformance": "1.140.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|