tina4-nodejs 3.13.104 → 3.13.105
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/CLAUDE.md +16 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +157 -38
- package/packages/core/dist/index.js +157 -38
- package/packages/core/src/queue.ts +75 -18
- package/packages/core/src/queueBackends/liteBackend.ts +17 -1
- package/packages/core/src/queueBackends/mongoBackend.ts +80 -14
- package/packages/orm/dist/index.js +157 -38
- package/packages/orm/src/baseModel.ts +18 -5
- package/packages/orm/src/migration.ts +14 -5
- package/types/core/src/queue.d.ts +41 -4
- package/types/core/src/queueBackends/liteBackend.d.ts +7 -1
- package/types/core/src/queueBackends/mongoBackend.d.ts +7 -2
- package/types/orm/src/baseModel.d.ts +10 -5
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.
|
|
1
|
+
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.105)
|
|
2
2
|
|
|
3
3
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
4
4
|
|
|
5
5
|
## What This Project Is
|
|
6
6
|
|
|
7
|
-
Tina4 for Node.js/TypeScript v3.13.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.13.105 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
|
|
8
8
|
|
|
9
9
|
The philosophy: zero ceremony, batteries included, file system as source of truth.
|
|
10
10
|
|
|
@@ -309,6 +309,20 @@ queue.consume(topic?, id?, pollInterval=1000): AsyncGenerator<QueueJob>
|
|
|
309
309
|
// Usage: for await (const job of queue.consume("emails")) { ... }
|
|
310
310
|
```
|
|
311
311
|
|
|
312
|
+
**size / failed / deadLetters — three surfaces, one distinction (3.13.105 parity).**
|
|
313
|
+
- `queue.size(status)` — `"pending"` counts jobs waiting to be popped AND retryable-
|
|
314
|
+
but-attempted ones (they live in the pending queue under the auto-retry lifecycle).
|
|
315
|
+
`"reserved"` counts in-flight jobs against the visibility timeout. `"completed"`
|
|
316
|
+
counts finished jobs. `"failed"` / `"dead"` / `"dead_letter"` are ALIASES — all
|
|
317
|
+
three count the dead-letter store (== `queue.deadLetters().length`).
|
|
318
|
+
- `queue.failed()` — retryable-but-attempted jobs (0 < attempts < maxRetries) that
|
|
319
|
+
live in the pending queue and are still being auto-retried. They are counted under
|
|
320
|
+
`size("pending")`, NOT `size("failed")`. Use this to LIST them; use
|
|
321
|
+
`size("pending")` to include them in a total.
|
|
322
|
+
- `queue.deadLetters()` — terminal failures (attempts >= maxRetries). Same set that
|
|
323
|
+
`size("failed")` / `size("dead")` / `size("dead_letter")` count. Returns wrapped
|
|
324
|
+
`Job` objects so callers can iterate uniformly and call `.retry()` on each.
|
|
325
|
+
|
|
312
326
|
### @tina4/swagger (`packages/swagger/`)
|
|
313
327
|
Auto-generates OpenAPI 3.0.3 docs.
|
|
314
328
|
|
package/package.json
CHANGED
package/packages/cli/dist/bin.js
CHANGED
|
@@ -12352,7 +12352,9 @@ function buildAddColumnSql(adapter, table2, colName, def) {
|
|
|
12352
12352
|
return sql;
|
|
12353
12353
|
}
|
|
12354
12354
|
function mt(db) {
|
|
12355
|
-
|
|
12355
|
+
const engine = engineOf(db);
|
|
12356
|
+
if (engine === "firebird") return MIGRATION_TABLE;
|
|
12357
|
+
return engine === "mysql" ? `\`${MIGRATION_TABLE}\`` : `"${MIGRATION_TABLE}"`;
|
|
12356
12358
|
}
|
|
12357
12359
|
function deriveDescription(name) {
|
|
12358
12360
|
return name.replace(/^\d+_/, "").replace(/_/g, " ");
|
|
@@ -12375,9 +12377,9 @@ async function ensureMigrationTableOn(db) {
|
|
|
12375
12377
|
id INTEGER NOT NULL PRIMARY KEY,
|
|
12376
12378
|
migration_name VARCHAR(500) NOT NULL UNIQUE,
|
|
12377
12379
|
description VARCHAR(500),
|
|
12378
|
-
batch INTEGER NOT NULL
|
|
12380
|
+
batch INTEGER DEFAULT 1 NOT NULL,
|
|
12379
12381
|
executed_at VARCHAR(50) NOT NULL,
|
|
12380
|
-
passed INTEGER NOT NULL
|
|
12382
|
+
passed INTEGER DEFAULT 1 NOT NULL
|
|
12381
12383
|
)`);
|
|
12382
12384
|
} else {
|
|
12383
12385
|
const idCol = migrationIdColumn(db);
|
|
@@ -12474,7 +12476,7 @@ async function recordApplied(db, name, batch, passed = 1) {
|
|
|
12474
12476
|
"SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS NEXT_ID FROM RDB$DATABASE"
|
|
12475
12477
|
);
|
|
12476
12478
|
insertCols.unshift("id");
|
|
12477
|
-
values.unshift(rows[0]?.
|
|
12479
|
+
values.unshift(rows[0]?.next_id ?? 1);
|
|
12478
12480
|
}
|
|
12479
12481
|
const placeholders = insertCols.map(() => "?").join(", ");
|
|
12480
12482
|
await adapterExecute(
|
|
@@ -14888,15 +14890,25 @@ var init_baseModel = __esm({
|
|
|
14888
14890
|
/**
|
|
14889
14891
|
* Invalidate every cached query that touches this model's table.
|
|
14890
14892
|
*
|
|
14891
|
-
* Tag-scoped
|
|
14892
|
-
* this table is busted too
|
|
14893
|
-
* never touches this table is left intact
|
|
14894
|
-
*
|
|
14895
|
-
*
|
|
14893
|
+
* Tag-scoped in the ORM layer (a cached JOIN on another model that reads
|
|
14894
|
+
* this table is busted too because it carries this table's tag; a query
|
|
14895
|
+
* that never touches this table is left intact), then cascaded to the
|
|
14896
|
+
* DB layer on this model's bound connection so an out-of-band write /
|
|
14897
|
+
* deliberate refresh / race-with-another-process cannot leave stale rows
|
|
14898
|
+
* in db.fetch()'s persistent cache. Called after every ORM write
|
|
14899
|
+
* (save/delete/forceDelete/restore) so a read-after-write never serves
|
|
14900
|
+
* a stale/deleted row (CACHE-DEC-01). PY-06-22 (3.13.105) added the
|
|
14901
|
+
* DB-layer cascade -- previously the two cache layers disagreed under
|
|
14902
|
+
* TINA4_AUTO_CACHING=true + TINA4_DB_CACHE=true.
|
|
14896
14903
|
*/
|
|
14897
14904
|
static clearCache() {
|
|
14898
14905
|
const ModelClass = this;
|
|
14899
14906
|
modelQueryCache.clearTag((ModelClass.tableName ?? "").toLowerCase());
|
|
14907
|
+
try {
|
|
14908
|
+
const db = ModelClass.getDb();
|
|
14909
|
+
if (typeof db?.cacheClear === "function") db.cacheClear();
|
|
14910
|
+
} catch {
|
|
14911
|
+
}
|
|
14900
14912
|
}
|
|
14901
14913
|
/**
|
|
14902
14914
|
* Execute a raw SQL SELECT and return results as model instances.
|
|
@@ -28115,17 +28127,67 @@ var init_mongoBackend = __esm({
|
|
|
28115
28127
|
process.stdout.write("__OK__");
|
|
28116
28128
|
}
|
|
28117
28129
|
else if (operation === "retry") {
|
|
28118
|
-
// Explicit manual re-queue (
|
|
28119
|
-
//
|
|
28130
|
+
// Explicit manual re-queue. Serves BOTH Queue.retry(id) (revive
|
|
28131
|
+
// a dead-letter job) AND job.retry() (manual re-queue of a live
|
|
28132
|
+
// reserved/pending job) so the Mongo backend matches
|
|
28133
|
+
// LiteBackend's dual behaviour.
|
|
28134
|
+
//
|
|
28135
|
+
// 1) DL revival (Queue.retry(id) after fail exhausted retries).
|
|
28136
|
+
// Pre-3.13.105 this branch was BROKEN: the search filter was
|
|
28137
|
+
// { queue: queueName, id, status: "failed" } -- three separate
|
|
28138
|
+
// reasons it could never match. dead_letter() inserts under
|
|
28139
|
+
// queueName + ".dead_letter" (not queueName), carries
|
|
28140
|
+
// status "dead" (not "failed"), and the original under
|
|
28141
|
+
// queueName was already acked to "completed" by the time the
|
|
28142
|
+
// DL was written. Now we look up in the DL namespace by id,
|
|
28143
|
+
// delete the DL doc first (so an interrupted retry never
|
|
28144
|
+
// leaves both a DL and a fresh pending doc), and upsert the
|
|
28145
|
+
// original back to pending -- re-hydrating if the original
|
|
28146
|
+
// was purged (housekeeping) so a retry always works.
|
|
28147
|
+
// 2) Live-doc manual re-queue (job.retry() on a job the caller
|
|
28148
|
+
// just popped and wants back in pending). The live-doc path
|
|
28149
|
+
// is preserved from before 3.13.105.
|
|
28150
|
+
//
|
|
28151
|
+
// Returns __OK__ when either path acted; __NOT_FOUND__ when
|
|
28152
|
+
// neither the DL nor the live doc existed, so Queue.retry(id)
|
|
28153
|
+
// can now report the pre-3.13.105 blanket-true as false for
|
|
28154
|
+
// unknown ids. data = JSON { id, delaySeconds }.
|
|
28120
28155
|
const info = JSON.parse(data);
|
|
28156
|
+
const dlTopic = queueName + ".dead_letter";
|
|
28157
|
+
const now = new Date().toISOString();
|
|
28121
28158
|
const avail = info.delaySeconds > 0
|
|
28122
28159
|
? new Date(Date.now() + info.delaySeconds * 1000).toISOString()
|
|
28123
|
-
:
|
|
28124
|
-
await col.
|
|
28125
|
-
|
|
28126
|
-
|
|
28127
|
-
|
|
28128
|
-
|
|
28160
|
+
: now;
|
|
28161
|
+
const dlDoc = await col.findOne({ queue: dlTopic, id: info.id });
|
|
28162
|
+
if (dlDoc !== null) {
|
|
28163
|
+
await col.deleteOne({ _id: dlDoc._id });
|
|
28164
|
+
const payload = dlDoc.payload ?? {};
|
|
28165
|
+
const priority = dlDoc.priority ?? 0;
|
|
28166
|
+
await col.updateOne(
|
|
28167
|
+
{ queue: queueName, id: info.id },
|
|
28168
|
+
{
|
|
28169
|
+
$set: {
|
|
28170
|
+
status: "pending",
|
|
28171
|
+
availableAt: avail,
|
|
28172
|
+
reservedAt: null,
|
|
28173
|
+
error: null,
|
|
28174
|
+
payload,
|
|
28175
|
+
priority,
|
|
28176
|
+
id: info.id,
|
|
28177
|
+
createdAt: dlDoc.createdAt ?? now,
|
|
28178
|
+
},
|
|
28179
|
+
$inc: { attempts: 1 },
|
|
28180
|
+
},
|
|
28181
|
+
{ upsert: true },
|
|
28182
|
+
);
|
|
28183
|
+
process.stdout.write("__OK__");
|
|
28184
|
+
} else {
|
|
28185
|
+
const result = await col.updateOne(
|
|
28186
|
+
{ queue: queueName, id: info.id },
|
|
28187
|
+
{ $set: { status: "pending", availableAt: avail, reservedAt: null }, $inc: { attempts: 1 } },
|
|
28188
|
+
);
|
|
28189
|
+
process.stdout.write(result.matchedCount > 0 ? "__OK__" : "__NOT_FOUND__");
|
|
28190
|
+
}
|
|
28129
28191
|
}
|
|
28130
28192
|
else if (operation === "deadLetters") {
|
|
28131
28193
|
const docs = await col.find({ queue: queueName + ".dead_letter" }).toArray();
|
|
@@ -28170,10 +28232,20 @@ var init_mongoBackend = __esm({
|
|
|
28170
28232
|
process.stdout.write(String(revived));
|
|
28171
28233
|
}
|
|
28172
28234
|
else if (operation === "purge") {
|
|
28173
|
-
// Delete docs by status (default:
|
|
28235
|
+
// Delete docs by status (default: every doc for the topic).
|
|
28236
|
+
// Pre-3.13.105 this filtered by { queue: queueName, status } for
|
|
28237
|
+
// EVERY status -- correct for pending/reserved/completed, wrong
|
|
28238
|
+
// for the dead-letter states (dead/failed/dead_letter) which
|
|
28239
|
+
// live under queueName + ".dead_letter" and carry status "dead".
|
|
28240
|
+
// A purge("dead") therefore deleted nothing and returned 0.
|
|
28241
|
+
// data = JSON { status }.
|
|
28174
28242
|
const info = data ? JSON.parse(data) : {};
|
|
28175
|
-
const
|
|
28176
|
-
|
|
28243
|
+
const isDead = info.status && ["dead", "failed", "dead_letter"].includes(info.status);
|
|
28244
|
+
const filter = isDead
|
|
28245
|
+
? { queue: queueName + ".dead_letter" }
|
|
28246
|
+
: (info.status
|
|
28247
|
+
? { queue: queueName, status: info.status }
|
|
28248
|
+
: { queue: queueName });
|
|
28177
28249
|
const res = await col.deleteMany(filter);
|
|
28178
28250
|
process.stdout.write(String(res.deletedCount || 0));
|
|
28179
28251
|
}
|
|
@@ -28270,9 +28342,15 @@ var init_mongoBackend = __esm({
|
|
|
28270
28342
|
fail(queue, id, error, maxRetries, retryBackoff = 0) {
|
|
28271
28343
|
this.execSync("fail", queue, JSON.stringify({ id, error, maxRetries, retryBackoff }));
|
|
28272
28344
|
}
|
|
28273
|
-
/**
|
|
28345
|
+
/**
|
|
28346
|
+
* Revive a specific dead-letter job by id. Returns true if the DL was found
|
|
28347
|
+
* and revived, false otherwise (parity with LiteBackend.retry(queue, id)
|
|
28348
|
+
* and Python's mongo_backend.retry_job()). Pre-3.13.105 this returned void
|
|
28349
|
+
* and Queue.retry(id) reported success for every call, even for unknown ids.
|
|
28350
|
+
*/
|
|
28274
28351
|
retry(queue, id, delaySeconds = 0) {
|
|
28275
|
-
this.execSync("retry", queue, JSON.stringify({ id, delaySeconds }));
|
|
28352
|
+
const out = this.execSync("retry", queue, JSON.stringify({ id, delaySeconds }));
|
|
28353
|
+
return out.includes("__OK__");
|
|
28276
28354
|
}
|
|
28277
28355
|
/** Jobs that exceeded max retries (the `<queue>.dead_letter` collection topic). */
|
|
28278
28356
|
deadLetters(queue, maxRetries) {
|
|
@@ -28941,10 +29019,20 @@ var init_liteBackend = __esm({
|
|
|
28941
29019
|
* Explicit re-queue requested by the caller (job.retry()).
|
|
28942
29020
|
*
|
|
28943
29021
|
* Always re-enqueues regardless of the retry limit — manual override,
|
|
28944
|
-
* distinct from the automatic failJob() path.
|
|
29022
|
+
* distinct from the automatic failJob() path. Cleans up BOTH the
|
|
29023
|
+
* reservation record AND any dead-letter file for this id, so a caller
|
|
29024
|
+
* that iterates deadLetters() and calls .retry() on each doesn't leave
|
|
29025
|
+
* the failed/ directory carrying duplicates (PY-12-05, 3.13.105).
|
|
29026
|
+
* Aligns with retry(queue, jobId) which had always unlinked the
|
|
29027
|
+
* dead-letter file -- two spellings of the same intent that previously
|
|
29028
|
+
* diverged.
|
|
28945
29029
|
*/
|
|
28946
29030
|
retryJob(queue, job, delaySeconds) {
|
|
28947
29031
|
this.clearReservation(queue, job.id);
|
|
29032
|
+
try {
|
|
29033
|
+
unlinkSync7(join23(this.ensureFailedDir(queue), `${job.id}.queue-data`));
|
|
29034
|
+
} catch {
|
|
29035
|
+
}
|
|
28948
29036
|
job.attempts = (job.attempts || 0) + 1;
|
|
28949
29037
|
job.error = void 0;
|
|
28950
29038
|
this.requeue(queue, job, delaySeconds ?? 0, void 0);
|
|
@@ -29143,7 +29231,19 @@ var init_queue = __esm({
|
|
|
29143
29231
|
}
|
|
29144
29232
|
}
|
|
29145
29233
|
/**
|
|
29146
|
-
* Count jobs
|
|
29234
|
+
* Count jobs by status. Defaults to "pending".
|
|
29235
|
+
*
|
|
29236
|
+
* ``"pending"`` counts jobs waiting to be popped -- INCLUDES retryable-
|
|
29237
|
+
* but-attempted ones, because they live in the pending queue under the
|
|
29238
|
+
* auto-retry lifecycle (see failed()).
|
|
29239
|
+
* ``"reserved"`` counts jobs a consumer has popped but not yet
|
|
29240
|
+
* completed/failed (in-flight against the visibility timeout).
|
|
29241
|
+
* ``"completed"`` counts jobs the consumer has finished successfully.
|
|
29242
|
+
* ``"failed"``, ``"dead"``, ``"dead_letter"`` are ALIASES that all count
|
|
29243
|
+
* the dead-letter store -- jobs whose attempts >= maxRetries and that
|
|
29244
|
+
* have given up. Use deadLetters() to list them. Retryable-but-attempted
|
|
29245
|
+
* jobs are NOT counted by size("failed"); use failed() to list them or
|
|
29246
|
+
* size("pending") to include them in a total.
|
|
29147
29247
|
*/
|
|
29148
29248
|
size(status2 = "pending") {
|
|
29149
29249
|
const q = this.topic;
|
|
@@ -29193,13 +29293,17 @@ var init_queue = __esm({
|
|
|
29193
29293
|
/**
|
|
29194
29294
|
* Get jobs that failed at least once but are still being retried
|
|
29195
29295
|
* (0 < attempts < maxRetries). These live in the pending queue under the
|
|
29196
|
-
* auto-retry lifecycle
|
|
29296
|
+
* auto-retry lifecycle (fail() re-queues them with an incremented attempts
|
|
29297
|
+
* count and a retryBackoff delay) so pop() picks them up again. They are
|
|
29298
|
+
* NOT counted by size("failed") -- that alias counts the dead-letter store,
|
|
29299
|
+
* matching deadLetters(). To include retryable-failed jobs in a total, use
|
|
29300
|
+
* size("pending"). Terminal failures are returned by deadLetters().
|
|
29197
29301
|
*/
|
|
29198
29302
|
failed() {
|
|
29199
|
-
|
|
29200
|
-
|
|
29201
|
-
|
|
29202
|
-
|
|
29303
|
+
const raw = this.externalBackend?.failed ? this.externalBackend.failed(this.topic, this._maxRetries) : this.liteBackend.failed(this.topic, this._maxRetries);
|
|
29304
|
+
return raw.map(
|
|
29305
|
+
(data) => createJob({ ...data, topic: data.topic ?? this.topic }, this)
|
|
29306
|
+
);
|
|
29203
29307
|
}
|
|
29204
29308
|
/**
|
|
29205
29309
|
* Retry all dead letter jobs for this queue's topic.
|
|
@@ -29211,8 +29315,8 @@ var init_queue = __esm({
|
|
|
29211
29315
|
retry(jobId, delaySeconds) {
|
|
29212
29316
|
if (jobId) {
|
|
29213
29317
|
if (this.externalBackend?.retry) {
|
|
29214
|
-
this.externalBackend.retry(this.topic, jobId, delaySeconds);
|
|
29215
|
-
return true;
|
|
29318
|
+
const result = this.externalBackend.retry(this.topic, jobId, delaySeconds);
|
|
29319
|
+
return result === void 0 ? true : Boolean(result);
|
|
29216
29320
|
}
|
|
29217
29321
|
return this.liteBackend.retry(this.topic, jobId, delaySeconds);
|
|
29218
29322
|
}
|
|
@@ -29221,8 +29325,8 @@ var init_queue = __esm({
|
|
|
29221
29325
|
let retried = false;
|
|
29222
29326
|
for (const job of deadJobs) {
|
|
29223
29327
|
if (this.externalBackend?.retry) {
|
|
29224
|
-
this.externalBackend.retry(this.topic, job.id, delaySeconds);
|
|
29225
|
-
retried = true;
|
|
29328
|
+
const result = this.externalBackend.retry(this.topic, job.id, delaySeconds);
|
|
29329
|
+
if (result === void 0 || Boolean(result)) retried = true;
|
|
29226
29330
|
} else if (this.liteBackend.retry(this.topic, job.id, delaySeconds)) {
|
|
29227
29331
|
retried = true;
|
|
29228
29332
|
}
|
|
@@ -29230,13 +29334,28 @@ var init_queue = __esm({
|
|
|
29230
29334
|
return retried;
|
|
29231
29335
|
}
|
|
29232
29336
|
/**
|
|
29233
|
-
* Get
|
|
29337
|
+
* Get jobs that exceeded max_retries -- terminal failures.
|
|
29338
|
+
*
|
|
29339
|
+
* Same set counted by size("failed") / size("dead") / size("dead_letter")
|
|
29340
|
+
* (three aliases for the dead-letter store). To LIST retryable-but-
|
|
29341
|
+
* attempted jobs (attempts > 0 AND attempts < maxRetries) that are still
|
|
29342
|
+
* being auto-retried, use failed() -- those live in the pending queue and
|
|
29343
|
+
* are NOT dead letters.
|
|
29344
|
+
*
|
|
29345
|
+
* Returns Job objects with the failure reason on ``.error`` (not raw dicts)
|
|
29346
|
+
* so callers can iterate uniformly with the rest of the queue API and, in
|
|
29347
|
+
* particular, call ``.retry()`` on each to manually revive it:
|
|
29348
|
+
*
|
|
29349
|
+
* for (const job of queue.deadLetters()) {
|
|
29350
|
+
* Log.warn(`revived ${job.id}: ${job.error}`);
|
|
29351
|
+
* job.retry();
|
|
29352
|
+
* }
|
|
29234
29353
|
*/
|
|
29235
29354
|
deadLetters(maxRetries) {
|
|
29236
|
-
|
|
29237
|
-
|
|
29238
|
-
|
|
29239
|
-
|
|
29355
|
+
const raw = this.externalBackend?.deadLetters ? this.externalBackend.deadLetters(this.topic, maxRetries ?? this._maxRetries) : this.liteBackend.deadLetters(this.topic, maxRetries ?? this._maxRetries);
|
|
29356
|
+
return raw.map(
|
|
29357
|
+
(data) => createJob({ ...data, topic: data.topic ?? this.topic }, this)
|
|
29358
|
+
);
|
|
29240
29359
|
}
|
|
29241
29360
|
/**
|
|
29242
29361
|
* Delete messages by status (e.g. "completed", "failed", "dead").
|
|
@@ -12351,7 +12351,9 @@ function buildAddColumnSql(adapter, table2, colName, def) {
|
|
|
12351
12351
|
return sql;
|
|
12352
12352
|
}
|
|
12353
12353
|
function mt(db) {
|
|
12354
|
-
|
|
12354
|
+
const engine = engineOf(db);
|
|
12355
|
+
if (engine === "firebird") return MIGRATION_TABLE;
|
|
12356
|
+
return engine === "mysql" ? `\`${MIGRATION_TABLE}\`` : `"${MIGRATION_TABLE}"`;
|
|
12355
12357
|
}
|
|
12356
12358
|
function deriveDescription(name) {
|
|
12357
12359
|
return name.replace(/^\d+_/, "").replace(/_/g, " ");
|
|
@@ -12374,9 +12376,9 @@ async function ensureMigrationTableOn(db) {
|
|
|
12374
12376
|
id INTEGER NOT NULL PRIMARY KEY,
|
|
12375
12377
|
migration_name VARCHAR(500) NOT NULL UNIQUE,
|
|
12376
12378
|
description VARCHAR(500),
|
|
12377
|
-
batch INTEGER NOT NULL
|
|
12379
|
+
batch INTEGER DEFAULT 1 NOT NULL,
|
|
12378
12380
|
executed_at VARCHAR(50) NOT NULL,
|
|
12379
|
-
passed INTEGER NOT NULL
|
|
12381
|
+
passed INTEGER DEFAULT 1 NOT NULL
|
|
12380
12382
|
)`);
|
|
12381
12383
|
} else {
|
|
12382
12384
|
const idCol = migrationIdColumn(db);
|
|
@@ -12473,7 +12475,7 @@ async function recordApplied(db, name, batch, passed = 1) {
|
|
|
12473
12475
|
"SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS NEXT_ID FROM RDB$DATABASE"
|
|
12474
12476
|
);
|
|
12475
12477
|
insertCols.unshift("id");
|
|
12476
|
-
values.unshift(rows[0]?.
|
|
12478
|
+
values.unshift(rows[0]?.next_id ?? 1);
|
|
12477
12479
|
}
|
|
12478
12480
|
const placeholders = insertCols.map(() => "?").join(", ");
|
|
12479
12481
|
await adapterExecute(
|
|
@@ -14887,15 +14889,25 @@ var init_baseModel = __esm({
|
|
|
14887
14889
|
/**
|
|
14888
14890
|
* Invalidate every cached query that touches this model's table.
|
|
14889
14891
|
*
|
|
14890
|
-
* Tag-scoped
|
|
14891
|
-
* this table is busted too
|
|
14892
|
-
* never touches this table is left intact
|
|
14893
|
-
*
|
|
14894
|
-
*
|
|
14892
|
+
* Tag-scoped in the ORM layer (a cached JOIN on another model that reads
|
|
14893
|
+
* this table is busted too because it carries this table's tag; a query
|
|
14894
|
+
* that never touches this table is left intact), then cascaded to the
|
|
14895
|
+
* DB layer on this model's bound connection so an out-of-band write /
|
|
14896
|
+
* deliberate refresh / race-with-another-process cannot leave stale rows
|
|
14897
|
+
* in db.fetch()'s persistent cache. Called after every ORM write
|
|
14898
|
+
* (save/delete/forceDelete/restore) so a read-after-write never serves
|
|
14899
|
+
* a stale/deleted row (CACHE-DEC-01). PY-06-22 (3.13.105) added the
|
|
14900
|
+
* DB-layer cascade -- previously the two cache layers disagreed under
|
|
14901
|
+
* TINA4_AUTO_CACHING=true + TINA4_DB_CACHE=true.
|
|
14895
14902
|
*/
|
|
14896
14903
|
static clearCache() {
|
|
14897
14904
|
const ModelClass = this;
|
|
14898
14905
|
modelQueryCache.clearTag((ModelClass.tableName ?? "").toLowerCase());
|
|
14906
|
+
try {
|
|
14907
|
+
const db = ModelClass.getDb();
|
|
14908
|
+
if (typeof db?.cacheClear === "function") db.cacheClear();
|
|
14909
|
+
} catch {
|
|
14910
|
+
}
|
|
14899
14911
|
}
|
|
14900
14912
|
/**
|
|
14901
14913
|
* Execute a raw SQL SELECT and return results as model instances.
|
|
@@ -28094,17 +28106,67 @@ var init_mongoBackend = __esm({
|
|
|
28094
28106
|
process.stdout.write("__OK__");
|
|
28095
28107
|
}
|
|
28096
28108
|
else if (operation === "retry") {
|
|
28097
|
-
// Explicit manual re-queue (
|
|
28098
|
-
//
|
|
28109
|
+
// Explicit manual re-queue. Serves BOTH Queue.retry(id) (revive
|
|
28110
|
+
// a dead-letter job) AND job.retry() (manual re-queue of a live
|
|
28111
|
+
// reserved/pending job) so the Mongo backend matches
|
|
28112
|
+
// LiteBackend's dual behaviour.
|
|
28113
|
+
//
|
|
28114
|
+
// 1) DL revival (Queue.retry(id) after fail exhausted retries).
|
|
28115
|
+
// Pre-3.13.105 this branch was BROKEN: the search filter was
|
|
28116
|
+
// { queue: queueName, id, status: "failed" } -- three separate
|
|
28117
|
+
// reasons it could never match. dead_letter() inserts under
|
|
28118
|
+
// queueName + ".dead_letter" (not queueName), carries
|
|
28119
|
+
// status "dead" (not "failed"), and the original under
|
|
28120
|
+
// queueName was already acked to "completed" by the time the
|
|
28121
|
+
// DL was written. Now we look up in the DL namespace by id,
|
|
28122
|
+
// delete the DL doc first (so an interrupted retry never
|
|
28123
|
+
// leaves both a DL and a fresh pending doc), and upsert the
|
|
28124
|
+
// original back to pending -- re-hydrating if the original
|
|
28125
|
+
// was purged (housekeeping) so a retry always works.
|
|
28126
|
+
// 2) Live-doc manual re-queue (job.retry() on a job the caller
|
|
28127
|
+
// just popped and wants back in pending). The live-doc path
|
|
28128
|
+
// is preserved from before 3.13.105.
|
|
28129
|
+
//
|
|
28130
|
+
// Returns __OK__ when either path acted; __NOT_FOUND__ when
|
|
28131
|
+
// neither the DL nor the live doc existed, so Queue.retry(id)
|
|
28132
|
+
// can now report the pre-3.13.105 blanket-true as false for
|
|
28133
|
+
// unknown ids. data = JSON { id, delaySeconds }.
|
|
28099
28134
|
const info = JSON.parse(data);
|
|
28135
|
+
const dlTopic = queueName + ".dead_letter";
|
|
28136
|
+
const now = new Date().toISOString();
|
|
28100
28137
|
const avail = info.delaySeconds > 0
|
|
28101
28138
|
? new Date(Date.now() + info.delaySeconds * 1000).toISOString()
|
|
28102
|
-
:
|
|
28103
|
-
await col.
|
|
28104
|
-
|
|
28105
|
-
|
|
28106
|
-
|
|
28107
|
-
|
|
28139
|
+
: now;
|
|
28140
|
+
const dlDoc = await col.findOne({ queue: dlTopic, id: info.id });
|
|
28141
|
+
if (dlDoc !== null) {
|
|
28142
|
+
await col.deleteOne({ _id: dlDoc._id });
|
|
28143
|
+
const payload = dlDoc.payload ?? {};
|
|
28144
|
+
const priority = dlDoc.priority ?? 0;
|
|
28145
|
+
await col.updateOne(
|
|
28146
|
+
{ queue: queueName, id: info.id },
|
|
28147
|
+
{
|
|
28148
|
+
$set: {
|
|
28149
|
+
status: "pending",
|
|
28150
|
+
availableAt: avail,
|
|
28151
|
+
reservedAt: null,
|
|
28152
|
+
error: null,
|
|
28153
|
+
payload,
|
|
28154
|
+
priority,
|
|
28155
|
+
id: info.id,
|
|
28156
|
+
createdAt: dlDoc.createdAt ?? now,
|
|
28157
|
+
},
|
|
28158
|
+
$inc: { attempts: 1 },
|
|
28159
|
+
},
|
|
28160
|
+
{ upsert: true },
|
|
28161
|
+
);
|
|
28162
|
+
process.stdout.write("__OK__");
|
|
28163
|
+
} else {
|
|
28164
|
+
const result = await col.updateOne(
|
|
28165
|
+
{ queue: queueName, id: info.id },
|
|
28166
|
+
{ $set: { status: "pending", availableAt: avail, reservedAt: null }, $inc: { attempts: 1 } },
|
|
28167
|
+
);
|
|
28168
|
+
process.stdout.write(result.matchedCount > 0 ? "__OK__" : "__NOT_FOUND__");
|
|
28169
|
+
}
|
|
28108
28170
|
}
|
|
28109
28171
|
else if (operation === "deadLetters") {
|
|
28110
28172
|
const docs = await col.find({ queue: queueName + ".dead_letter" }).toArray();
|
|
@@ -28149,10 +28211,20 @@ var init_mongoBackend = __esm({
|
|
|
28149
28211
|
process.stdout.write(String(revived));
|
|
28150
28212
|
}
|
|
28151
28213
|
else if (operation === "purge") {
|
|
28152
|
-
// Delete docs by status (default:
|
|
28214
|
+
// Delete docs by status (default: every doc for the topic).
|
|
28215
|
+
// Pre-3.13.105 this filtered by { queue: queueName, status } for
|
|
28216
|
+
// EVERY status -- correct for pending/reserved/completed, wrong
|
|
28217
|
+
// for the dead-letter states (dead/failed/dead_letter) which
|
|
28218
|
+
// live under queueName + ".dead_letter" and carry status "dead".
|
|
28219
|
+
// A purge("dead") therefore deleted nothing and returned 0.
|
|
28220
|
+
// data = JSON { status }.
|
|
28153
28221
|
const info = data ? JSON.parse(data) : {};
|
|
28154
|
-
const
|
|
28155
|
-
|
|
28222
|
+
const isDead = info.status && ["dead", "failed", "dead_letter"].includes(info.status);
|
|
28223
|
+
const filter = isDead
|
|
28224
|
+
? { queue: queueName + ".dead_letter" }
|
|
28225
|
+
: (info.status
|
|
28226
|
+
? { queue: queueName, status: info.status }
|
|
28227
|
+
: { queue: queueName });
|
|
28156
28228
|
const res = await col.deleteMany(filter);
|
|
28157
28229
|
process.stdout.write(String(res.deletedCount || 0));
|
|
28158
28230
|
}
|
|
@@ -28249,9 +28321,15 @@ var init_mongoBackend = __esm({
|
|
|
28249
28321
|
fail(queue, id, error, maxRetries, retryBackoff = 0) {
|
|
28250
28322
|
this.execSync("fail", queue, JSON.stringify({ id, error, maxRetries, retryBackoff }));
|
|
28251
28323
|
}
|
|
28252
|
-
/**
|
|
28324
|
+
/**
|
|
28325
|
+
* Revive a specific dead-letter job by id. Returns true if the DL was found
|
|
28326
|
+
* and revived, false otherwise (parity with LiteBackend.retry(queue, id)
|
|
28327
|
+
* and Python's mongo_backend.retry_job()). Pre-3.13.105 this returned void
|
|
28328
|
+
* and Queue.retry(id) reported success for every call, even for unknown ids.
|
|
28329
|
+
*/
|
|
28253
28330
|
retry(queue, id, delaySeconds = 0) {
|
|
28254
|
-
this.execSync("retry", queue, JSON.stringify({ id, delaySeconds }));
|
|
28331
|
+
const out = this.execSync("retry", queue, JSON.stringify({ id, delaySeconds }));
|
|
28332
|
+
return out.includes("__OK__");
|
|
28255
28333
|
}
|
|
28256
28334
|
/** Jobs that exceeded max retries (the `<queue>.dead_letter` collection topic). */
|
|
28257
28335
|
deadLetters(queue, maxRetries) {
|
|
@@ -28920,10 +28998,20 @@ var init_liteBackend = __esm({
|
|
|
28920
28998
|
* Explicit re-queue requested by the caller (job.retry()).
|
|
28921
28999
|
*
|
|
28922
29000
|
* Always re-enqueues regardless of the retry limit — manual override,
|
|
28923
|
-
* distinct from the automatic failJob() path.
|
|
29001
|
+
* distinct from the automatic failJob() path. Cleans up BOTH the
|
|
29002
|
+
* reservation record AND any dead-letter file for this id, so a caller
|
|
29003
|
+
* that iterates deadLetters() and calls .retry() on each doesn't leave
|
|
29004
|
+
* the failed/ directory carrying duplicates (PY-12-05, 3.13.105).
|
|
29005
|
+
* Aligns with retry(queue, jobId) which had always unlinked the
|
|
29006
|
+
* dead-letter file -- two spellings of the same intent that previously
|
|
29007
|
+
* diverged.
|
|
28924
29008
|
*/
|
|
28925
29009
|
retryJob(queue, job, delaySeconds) {
|
|
28926
29010
|
this.clearReservation(queue, job.id);
|
|
29011
|
+
try {
|
|
29012
|
+
unlinkSync7(join22(this.ensureFailedDir(queue), `${job.id}.queue-data`));
|
|
29013
|
+
} catch {
|
|
29014
|
+
}
|
|
28927
29015
|
job.attempts = (job.attempts || 0) + 1;
|
|
28928
29016
|
job.error = void 0;
|
|
28929
29017
|
this.requeue(queue, job, delaySeconds ?? 0, void 0);
|
|
@@ -29122,7 +29210,19 @@ var init_queue = __esm({
|
|
|
29122
29210
|
}
|
|
29123
29211
|
}
|
|
29124
29212
|
/**
|
|
29125
|
-
* Count jobs
|
|
29213
|
+
* Count jobs by status. Defaults to "pending".
|
|
29214
|
+
*
|
|
29215
|
+
* ``"pending"`` counts jobs waiting to be popped -- INCLUDES retryable-
|
|
29216
|
+
* but-attempted ones, because they live in the pending queue under the
|
|
29217
|
+
* auto-retry lifecycle (see failed()).
|
|
29218
|
+
* ``"reserved"`` counts jobs a consumer has popped but not yet
|
|
29219
|
+
* completed/failed (in-flight against the visibility timeout).
|
|
29220
|
+
* ``"completed"`` counts jobs the consumer has finished successfully.
|
|
29221
|
+
* ``"failed"``, ``"dead"``, ``"dead_letter"`` are ALIASES that all count
|
|
29222
|
+
* the dead-letter store -- jobs whose attempts >= maxRetries and that
|
|
29223
|
+
* have given up. Use deadLetters() to list them. Retryable-but-attempted
|
|
29224
|
+
* jobs are NOT counted by size("failed"); use failed() to list them or
|
|
29225
|
+
* size("pending") to include them in a total.
|
|
29126
29226
|
*/
|
|
29127
29227
|
size(status2 = "pending") {
|
|
29128
29228
|
const q = this.topic;
|
|
@@ -29172,13 +29272,17 @@ var init_queue = __esm({
|
|
|
29172
29272
|
/**
|
|
29173
29273
|
* Get jobs that failed at least once but are still being retried
|
|
29174
29274
|
* (0 < attempts < maxRetries). These live in the pending queue under the
|
|
29175
|
-
* auto-retry lifecycle
|
|
29275
|
+
* auto-retry lifecycle (fail() re-queues them with an incremented attempts
|
|
29276
|
+
* count and a retryBackoff delay) so pop() picks them up again. They are
|
|
29277
|
+
* NOT counted by size("failed") -- that alias counts the dead-letter store,
|
|
29278
|
+
* matching deadLetters(). To include retryable-failed jobs in a total, use
|
|
29279
|
+
* size("pending"). Terminal failures are returned by deadLetters().
|
|
29176
29280
|
*/
|
|
29177
29281
|
failed() {
|
|
29178
|
-
|
|
29179
|
-
|
|
29180
|
-
|
|
29181
|
-
|
|
29282
|
+
const raw = this.externalBackend?.failed ? this.externalBackend.failed(this.topic, this._maxRetries) : this.liteBackend.failed(this.topic, this._maxRetries);
|
|
29283
|
+
return raw.map(
|
|
29284
|
+
(data) => createJob({ ...data, topic: data.topic ?? this.topic }, this)
|
|
29285
|
+
);
|
|
29182
29286
|
}
|
|
29183
29287
|
/**
|
|
29184
29288
|
* Retry all dead letter jobs for this queue's topic.
|
|
@@ -29190,8 +29294,8 @@ var init_queue = __esm({
|
|
|
29190
29294
|
retry(jobId, delaySeconds) {
|
|
29191
29295
|
if (jobId) {
|
|
29192
29296
|
if (this.externalBackend?.retry) {
|
|
29193
|
-
this.externalBackend.retry(this.topic, jobId, delaySeconds);
|
|
29194
|
-
return true;
|
|
29297
|
+
const result = this.externalBackend.retry(this.topic, jobId, delaySeconds);
|
|
29298
|
+
return result === void 0 ? true : Boolean(result);
|
|
29195
29299
|
}
|
|
29196
29300
|
return this.liteBackend.retry(this.topic, jobId, delaySeconds);
|
|
29197
29301
|
}
|
|
@@ -29200,8 +29304,8 @@ var init_queue = __esm({
|
|
|
29200
29304
|
let retried = false;
|
|
29201
29305
|
for (const job of deadJobs) {
|
|
29202
29306
|
if (this.externalBackend?.retry) {
|
|
29203
|
-
this.externalBackend.retry(this.topic, job.id, delaySeconds);
|
|
29204
|
-
retried = true;
|
|
29307
|
+
const result = this.externalBackend.retry(this.topic, job.id, delaySeconds);
|
|
29308
|
+
if (result === void 0 || Boolean(result)) retried = true;
|
|
29205
29309
|
} else if (this.liteBackend.retry(this.topic, job.id, delaySeconds)) {
|
|
29206
29310
|
retried = true;
|
|
29207
29311
|
}
|
|
@@ -29209,13 +29313,28 @@ var init_queue = __esm({
|
|
|
29209
29313
|
return retried;
|
|
29210
29314
|
}
|
|
29211
29315
|
/**
|
|
29212
|
-
* Get
|
|
29316
|
+
* Get jobs that exceeded max_retries -- terminal failures.
|
|
29317
|
+
*
|
|
29318
|
+
* Same set counted by size("failed") / size("dead") / size("dead_letter")
|
|
29319
|
+
* (three aliases for the dead-letter store). To LIST retryable-but-
|
|
29320
|
+
* attempted jobs (attempts > 0 AND attempts < maxRetries) that are still
|
|
29321
|
+
* being auto-retried, use failed() -- those live in the pending queue and
|
|
29322
|
+
* are NOT dead letters.
|
|
29323
|
+
*
|
|
29324
|
+
* Returns Job objects with the failure reason on ``.error`` (not raw dicts)
|
|
29325
|
+
* so callers can iterate uniformly with the rest of the queue API and, in
|
|
29326
|
+
* particular, call ``.retry()`` on each to manually revive it:
|
|
29327
|
+
*
|
|
29328
|
+
* for (const job of queue.deadLetters()) {
|
|
29329
|
+
* Log.warn(`revived ${job.id}: ${job.error}`);
|
|
29330
|
+
* job.retry();
|
|
29331
|
+
* }
|
|
29213
29332
|
*/
|
|
29214
29333
|
deadLetters(maxRetries) {
|
|
29215
|
-
|
|
29216
|
-
|
|
29217
|
-
|
|
29218
|
-
|
|
29334
|
+
const raw = this.externalBackend?.deadLetters ? this.externalBackend.deadLetters(this.topic, maxRetries ?? this._maxRetries) : this.liteBackend.deadLetters(this.topic, maxRetries ?? this._maxRetries);
|
|
29335
|
+
return raw.map(
|
|
29336
|
+
(data) => createJob({ ...data, topic: data.topic ?? this.topic }, this)
|
|
29337
|
+
);
|
|
29219
29338
|
}
|
|
29220
29339
|
/**
|
|
29221
29340
|
* Delete messages by status (e.g. "completed", "failed", "dead").
|