@blamejs/core 0.4.19 → 0.4.20

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/CHANGELOG.md CHANGED
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - **0.4.19** (2026-04-30) — b.router: schema-validated routes + OpenAPI gen
11
12
  - **0.4.18** (2026-04-30) — cookieJar forensic-test strengthening (real crypto, replay, nonce)
12
13
  - **0.4.17** (2026-04-30) — b.httpClient.cookieJar (encrypted) + wiki catch-up sweep
13
14
  - **0.4.16** (2026-04-30) — b.httpClient: interceptors + progress events
package/lib/db.js CHANGED
@@ -350,9 +350,13 @@ var FRAMEWORK_SCHEMA = [
350
350
  finishedAt: "INTEGER",
351
351
  traceId: "TEXT",
352
352
  classification: "TEXT",
353
+ priority: "INTEGER NOT NULL DEFAULT 0",
353
354
  },
354
355
  indexes: [
355
356
  { name: "idx_jobs_lease", columns: ["queueName", "status", "availableAt"] },
357
+ // Priority lease index — pickers walk this when ORDER BY priority DESC,
358
+ // availableAt ASC, enqueuedAt ASC; matches the queue.lease ordering.
359
+ { name: "idx_jobs_priority", columns: ["queueName", "status", "priority", "availableAt"] },
356
360
  "leaseExpiresAt",
357
361
  "finishedAt",
358
362
  ],
@@ -476,10 +476,12 @@ function _jobsDDL(dialect) {
476
476
  " lastError TEXT," +
477
477
  " finishedAt " + t.INT + "," +
478
478
  " traceId TEXT," +
479
- " classification TEXT" +
479
+ " classification TEXT," +
480
+ " priority " + t.INT + " NOT NULL DEFAULT 0" +
480
481
  ")",
481
482
  indexes: [
482
483
  "CREATE INDEX IF NOT EXISTS idx_" + name + "_lease ON " + name + " (queueName, status, availableAt)",
484
+ "CREATE INDEX IF NOT EXISTS idx_" + name + "_priority ON " + name + " (queueName, status, priority, availableAt)",
483
485
  "CREATE INDEX IF NOT EXISTS idx_" + name + "_leaseExpiresAt ON " + name + " (leaseExpiresAt)",
484
486
  "CREATE INDEX IF NOT EXISTS idx_" + name + "_finishedAt ON " + name + " (finishedAt)",
485
487
  ],
@@ -50,7 +50,7 @@ var JOB_COLS = [
50
50
  "_id", "queueName", "payload", "status",
51
51
  "enqueuedAt", "availableAt", "leasedAt", "leaseExpiresAt",
52
52
  "attempts", "maxAttempts", "lastError", "finishedAt",
53
- "traceId", "classification",
53
+ "traceId", "classification", "priority",
54
54
  ];
55
55
 
56
56
  // Columns returned by lease() / used by RETURNING. Subset of JOB_COLS
@@ -97,6 +97,8 @@ function create(_config) {
97
97
  var nowMs = Date.now();
98
98
  var availableAt = nowMs + (opts.delaySeconds ? opts.delaySeconds * 1000 : 0);
99
99
 
100
+ var priority = (typeof opts.priority === "number" && isFinite(opts.priority))
101
+ ? Math.floor(opts.priority) : 0;
100
102
  var row = {
101
103
  _id: generateToken(16),
102
104
  queueName: queueName,
@@ -112,6 +114,7 @@ function create(_config) {
112
114
  finishedAt: null,
113
115
  traceId: opts.traceId || null,
114
116
  classification: opts.classification || null,
117
+ priority: priority,
115
118
  };
116
119
  var sealed = cryptoField.sealRow("_blamejs_jobs", row);
117
120
  var values = JOB_COLS.map(function (c) { return c in sealed ? sealed[c] : null; });
@@ -148,7 +151,7 @@ function create(_config) {
148
151
  "WHERE _id IN (" +
149
152
  " SELECT _id FROM _blamejs_jobs " +
150
153
  " WHERE queueName = ? AND status = 'pending' AND availableAt <= ? " +
151
- " ORDER BY availableAt ASC, enqueuedAt ASC " +
154
+ " ORDER BY priority DESC, availableAt ASC, enqueuedAt ASC " +
152
155
  " LIMIT ?" +
153
156
  ") " +
154
157
  "RETURNING " + _quotedList(LEASE_RETURN_COLS);
package/lib/queue.js CHANGED
@@ -177,6 +177,42 @@ function consume(queueName, handler, opts) {
177
177
  var pollIntervalMs = opts.pollIntervalMs || 1000;
178
178
  var fastPollMs = opts.fastPollMs || 50;
179
179
 
180
+ // Rate-limit: { max, perSeconds } caps how many handler INVOCATIONS
181
+ // start within any rolling perSeconds window. Token-bucket-style
182
+ // accounting keeps it cheap (just a sliding deque of timestamps).
183
+ var rateLimit = null;
184
+ if (opts.rateLimit) {
185
+ if (!opts.rateLimit.max || !opts.rateLimit.perSeconds ||
186
+ typeof opts.rateLimit.max !== "number" ||
187
+ typeof opts.rateLimit.perSeconds !== "number") {
188
+ throw _err("BAD_RATE_LIMIT",
189
+ "consume({ rateLimit }): expected { max: number, perSeconds: number }, got " +
190
+ JSON.stringify(opts.rateLimit), true);
191
+ }
192
+ rateLimit = {
193
+ max: opts.rateLimit.max,
194
+ windowMs: opts.rateLimit.perSeconds * 1000,
195
+ timestamps: [],
196
+ };
197
+ }
198
+ function _rateLimitWaitMs() {
199
+ if (!rateLimit) return 0;
200
+ var now = Date.now();
201
+ var cutoff = now - rateLimit.windowMs;
202
+ while (rateLimit.timestamps.length > 0 && rateLimit.timestamps[0] <= cutoff) {
203
+ rateLimit.timestamps.shift();
204
+ }
205
+ if (rateLimit.timestamps.length < rateLimit.max) return 0;
206
+ return rateLimit.timestamps[0] + rateLimit.windowMs - now + 1;
207
+ }
208
+ function _rateLimitConsume() {
209
+ if (rateLimit) rateLimit.timestamps.push(Date.now());
210
+ }
211
+
212
+ // Progress audit-emit rate-limit — protect the audit chain from a
213
+ // chatty handler that calls progress() every loop iteration.
214
+ var PROGRESS_MIN_INTERVAL_MS = 250;
215
+
180
216
  // Each consumer has its own AbortController so cancel() unblocks any
181
217
  // in-flight poll-sleep immediately rather than waiting up to
182
218
  // pollIntervalMs (default 1s) for the next while-loop iteration.
@@ -209,6 +245,18 @@ function consume(queueName, handler, opts) {
209
245
  await _pollSleep(fastPollMs);
210
246
  continue;
211
247
  }
248
+ // If rate-limited and we'd exceed the budget, sleep until the
249
+ // next slot opens. We lease at most `max - currentTokens` jobs to
250
+ // stay under the cap.
251
+ if (rateLimit) {
252
+ var wait = _rateLimitWaitMs();
253
+ if (wait > 0) {
254
+ await _pollSleep(Math.min(wait, pollIntervalMs));
255
+ continue;
256
+ }
257
+ var remainingTokens = rateLimit.max - rateLimit.timestamps.length;
258
+ if (remainingTokens < slots) slots = Math.max(1, remainingTokens);
259
+ }
212
260
  var jobs;
213
261
  try { jobs = await b.lease(queueName, leaseDurationMs, slots); }
214
262
  catch {
@@ -227,9 +275,17 @@ function consume(queueName, handler, opts) {
227
275
  _emit("system.queue.consume.start", {
228
276
  metadata: { queue: queueName, backend: b.name, jobId: job.jobId, attempt: job.attempts, traceId: job.traceId },
229
277
  });
278
+ // Consume a rate-limit slot at handler-start so the budget
279
+ // tracks invocation rate, not lease rate (a single lease that
280
+ // splits work across many sub-units doesn't double-count).
281
+ _rateLimitConsume();
282
+
230
283
  // Handler context — second arg to handler. Carries
231
- // ctx.extendLease(ms) for long-running handlers that need to
232
- // bump their own lease before the sweeper reclaims the job.
284
+ // ctx.extendLease(ms) for long-running handlers and
285
+ // ctx.progress(0..100) for surfacing job progress to the
286
+ // audit chain (rate-limited so chatty handlers don't drown it).
287
+ var lastProgressEmitAt = 0;
288
+ var lastProgressValue = -1;
233
289
  var ctx = {
234
290
  extendLease: function (additionalMs) {
235
291
  if (typeof b.extendLease !== "function") {
@@ -246,6 +302,25 @@ function consume(queueName, handler, opts) {
246
302
  return ok;
247
303
  });
248
304
  },
305
+ progress: function (pct) {
306
+ if (typeof pct !== "number" || !isFinite(pct)) return;
307
+ var clamped = Math.max(0, Math.min(100, Math.floor(pct)));
308
+ var now = Date.now();
309
+ // Always emit 0 and 100 (start/done markers); throttle the rest.
310
+ var isMarker = clamped === 0 || clamped === 100;
311
+ if (!isMarker && (now - lastProgressEmitAt) < PROGRESS_MIN_INTERVAL_MS) return;
312
+ if (clamped === lastProgressValue && !isMarker) return;
313
+ lastProgressEmitAt = now;
314
+ lastProgressValue = clamped;
315
+ observability.event("queue.progress", clamped, { queueName: queueName });
316
+ _emit("system.queue.progress", {
317
+ metadata: {
318
+ queue: queueName, backend: b.name, jobId: job.jobId,
319
+ attempt: job.attempts, traceId: job.traceId,
320
+ percent: clamped,
321
+ },
322
+ });
323
+ },
249
324
  };
250
325
  observability.tap("queue.consume",
251
326
  { queueName: queueName, backend: b.name, jobId: job.jobId, attempt: job.attempts },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.4.19",
3
+ "version": "0.4.20",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",