@gobing-ai/ts-db 0.5.4 → 0.5.7

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.
@@ -1 +1 @@
1
- {"version":3,"file":"queue-job-dao.d.ts","sourceRoot":"","sources":["../src/queue-job-dao.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"queue-job-dao.d.ts","sourceRoot":"","sources":["../src/queue-job-dao.ts"],"names":[],"mappings":"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"}
@@ -155,6 +155,21 @@ export class QueueJobDao extends EntityDao {
155
155
  const leaseMs = options?.leaseMs;
156
156
  const leased = leaseMs !== undefined;
157
157
  const leaseExpiresAt = leased ? now + leaseMs : null;
158
+ // Task 0086 R7: attempts exhausted on an expired lease — fail the job
159
+ // instead of reclaiming it, mirroring exactly what markFailed writes.
160
+ await this.db
161
+ .update(queueJobs)
162
+ .set({
163
+ status: 'failed',
164
+ attempts: sql `${queueJobs.attempts} + 1`,
165
+ lastError: 'lease expired: attempts exhausted',
166
+ processingAt: null,
167
+ attemptToken: null,
168
+ leaseExpiresAt: null,
169
+ updatedAt: now,
170
+ })
171
+ .where(sql `${queueJobs.status} = 'processing' AND ${queueJobs.attemptToken} IS NOT NULL AND ${queueJobs.leaseExpiresAt} IS NOT NULL
172
+ AND ${queueJobs.leaseExpiresAt} <= ${now} AND ${queueJobs.attempts} + 1 >= ${queueJobs.maxRetries}`);
158
173
  const reclaimable = sql `(
159
174
  ${queueJobs.status} = 'pending'
160
175
  AND (${queueJobs.nextRetryAt} IS NULL OR ${queueJobs.nextRetryAt} <= ${now})
@@ -172,6 +187,11 @@ export class QueueJobDao extends EntityDao {
172
187
  updatedAt: now,
173
188
  attemptToken: leased ? sql `lower(hex(randomblob(16)))` : null,
174
189
  leaseExpiresAt,
190
+ // Task 0086 R7: a reclaim of an expired lease counts as an
191
+ // attempt; SQLite evaluates SET against the pre-update row, so
192
+ // status here still reads 'processing'. Fresh pending claims
193
+ // keep their attempt count (attempt 0 on first claim).
194
+ attempts: sql `CASE WHEN ${queueJobs.status} = 'processing' THEN ${queueJobs.attempts} + 1 ELSE ${queueJobs.attempts} END`,
175
195
  })
176
196
  .where(sql `${queueJobs.id} IN (
177
197
  SELECT id
@@ -286,9 +306,30 @@ export class QueueJobDao extends EntityDao {
286
306
  */
287
307
  async resetStuckJobs(visibilityTimeout) {
288
308
  const cutoff = this.now() - visibilityTimeout;
309
+ // Task 0086 R7: age sweep exhaustion — a stuck legacy (token-less) row
310
+ // whose next increment would hit maxRetries fails outright instead of
311
+ // being reset to pending forever.
312
+ await this.db
313
+ .update(queueJobs)
314
+ .set({
315
+ status: 'failed',
316
+ attempts: sql `${queueJobs.attempts} + 1`,
317
+ lastError: 'stuck job: attempts exhausted',
318
+ processingAt: null,
319
+ attemptToken: null,
320
+ leaseExpiresAt: null,
321
+ updatedAt: this.now(),
322
+ })
323
+ .where(sql `${queueJobs.status} = 'processing' AND ${queueJobs.processingAt} IS NOT NULL AND ${queueJobs.processingAt} <= ${cutoff}
324
+ AND ${queueJobs.attemptToken} IS NULL AND ${queueJobs.attempts} + 1 >= ${queueJobs.maxRetries}`);
289
325
  const result = await this.db
290
326
  .update(queueJobs)
291
- .set({ status: 'pending', processingAt: null, updatedAt: this.now() })
327
+ .set({
328
+ status: 'pending',
329
+ processingAt: null,
330
+ updatedAt: this.now(),
331
+ attempts: sql `${queueJobs.attempts} + 1`,
332
+ })
292
333
  .where(sql `${queueJobs.status} = 'processing' AND ${queueJobs.processingAt} IS NOT NULL AND ${queueJobs.processingAt} <= ${cutoff}
293
334
  AND ${queueJobs.attemptToken} IS NULL`);
294
335
  return result.changes;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-db",
3
- "version": "0.5.4",
3
+ "version": "0.5.7",
4
4
  "description": "@gobing-ai/ts-db — a drizzle-free database facade: typed DAOs over Bun SQLite / Cloudflare D1, a small predicate query spec, single-source-of-truth tables, and migrations. Drizzle stays an internal detail.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -66,8 +66,8 @@
66
66
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-db-v<version> && git push --tags' && exit 1"
67
67
  },
68
68
  "dependencies": {
69
- "@gobing-ai/ts-runtime": "^0.5.4",
70
- "@gobing-ai/ts-utils": "^0.5.4"
69
+ "@gobing-ai/ts-runtime": "^0.5.7",
70
+ "@gobing-ai/ts-utils": "^0.5.7"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "drizzle-orm": ">=0.38.0",
@@ -220,6 +220,23 @@ export class QueueJobDao extends EntityDao<typeof queueJobs, typeof queueJobs.id
220
220
  const leaseMs = options?.leaseMs;
221
221
  const leased = leaseMs !== undefined;
222
222
  const leaseExpiresAt = leased ? now + leaseMs : null;
223
+ // Task 0086 R7: attempts exhausted on an expired lease — fail the job
224
+ // instead of reclaiming it, mirroring exactly what markFailed writes.
225
+ await (this.db as UpdateChangesDb)
226
+ .update(queueJobs)
227
+ .set({
228
+ status: 'failed',
229
+ attempts: sql`${queueJobs.attempts} + 1`,
230
+ lastError: 'lease expired: attempts exhausted',
231
+ processingAt: null,
232
+ attemptToken: null,
233
+ leaseExpiresAt: null,
234
+ updatedAt: now,
235
+ })
236
+ .where(
237
+ sql`${queueJobs.status} = 'processing' AND ${queueJobs.attemptToken} IS NOT NULL AND ${queueJobs.leaseExpiresAt} IS NOT NULL
238
+ AND ${queueJobs.leaseExpiresAt} <= ${now} AND ${queueJobs.attempts} + 1 >= ${queueJobs.maxRetries}`,
239
+ );
223
240
  const reclaimable = sql`(
224
241
  ${queueJobs.status} = 'pending'
225
242
  AND (${queueJobs.nextRetryAt} IS NULL OR ${queueJobs.nextRetryAt} <= ${now})
@@ -238,6 +255,11 @@ export class QueueJobDao extends EntityDao<typeof queueJobs, typeof queueJobs.id
238
255
  updatedAt: now,
239
256
  attemptToken: leased ? sql`lower(hex(randomblob(16)))` : null,
240
257
  leaseExpiresAt,
258
+ // Task 0086 R7: a reclaim of an expired lease counts as an
259
+ // attempt; SQLite evaluates SET against the pre-update row, so
260
+ // status here still reads 'processing'. Fresh pending claims
261
+ // keep their attempt count (attempt 0 on first claim).
262
+ attempts: sql`CASE WHEN ${queueJobs.status} = 'processing' THEN ${queueJobs.attempts} + 1 ELSE ${queueJobs.attempts} END`,
241
263
  })
242
264
  .where(
243
265
  sql`${queueJobs.id} IN (
@@ -374,9 +396,33 @@ export class QueueJobDao extends EntityDao<typeof queueJobs, typeof queueJobs.id
374
396
  async resetStuckJobs(visibilityTimeout: number): Promise<number> {
375
397
  const cutoff = this.now() - visibilityTimeout;
376
398
 
399
+ // Task 0086 R7: age sweep exhaustion — a stuck legacy (token-less) row
400
+ // whose next increment would hit maxRetries fails outright instead of
401
+ // being reset to pending forever.
402
+ await (this.db as UpdateChangesDb)
403
+ .update(queueJobs)
404
+ .set({
405
+ status: 'failed',
406
+ attempts: sql`${queueJobs.attempts} + 1`,
407
+ lastError: 'stuck job: attempts exhausted',
408
+ processingAt: null,
409
+ attemptToken: null,
410
+ leaseExpiresAt: null,
411
+ updatedAt: this.now(),
412
+ })
413
+ .where(
414
+ sql`${queueJobs.status} = 'processing' AND ${queueJobs.processingAt} IS NOT NULL AND ${queueJobs.processingAt} <= ${cutoff}
415
+ AND ${queueJobs.attemptToken} IS NULL AND ${queueJobs.attempts} + 1 >= ${queueJobs.maxRetries}`,
416
+ );
417
+
377
418
  const result = await (this.db as UpdateChangesDb)
378
419
  .update(queueJobs)
379
- .set({ status: 'pending', processingAt: null, updatedAt: this.now() })
420
+ .set({
421
+ status: 'pending',
422
+ processingAt: null,
423
+ updatedAt: this.now(),
424
+ attempts: sql`${queueJobs.attempts} + 1`,
425
+ })
380
426
  .where(
381
427
  sql`${queueJobs.status} = 'processing' AND ${queueJobs.processingAt} IS NOT NULL AND ${queueJobs.processingAt} <= ${cutoff}
382
428
  AND ${queueJobs.attemptToken} IS NULL`,