@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 +1 -0
- package/lib/db.js +4 -0
- package/lib/framework-schema.js +3 -1
- package/lib/queue-local.js +5 -2
- package/lib/queue.js +77 -2
- package/package.json +1 -1
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
|
],
|
package/lib/framework-schema.js
CHANGED
|
@@ -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
|
],
|
package/lib/queue-local.js
CHANGED
|
@@ -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
|
|
232
|
-
//
|
|
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 },
|