@rulvar/store-sqlite 1.139.0 → 1.141.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 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.139.0",
3
+ "version": "1.141.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.139.0"
25
+ "@rulvar/core": "1.141.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.139.0"
31
+ "@rulvar/store-conformance": "1.141.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",