@holo-js/queue-db 0.3.27 → 0.4.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Holo-JS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ 30d0df260f628005bfe477f720793dcd4215e7fbc0ddc40115301d322466634a
package/dist/index.d.ts CHANGED
@@ -64,12 +64,15 @@ declare class DatabaseQueueDriver implements QueueAsyncDriver {
64
64
  readonly name: string;
65
65
  readonly driver: "database";
66
66
  readonly mode: "async";
67
+ readonly acceptsJobId = true;
67
68
  private readonly tableName;
68
69
  constructor(connection: NormalizedQueueDatabaseConnectionConfig, context: QueueDriverFactoryContext);
69
70
  private getConnection;
70
71
  private getQuotedTable;
71
72
  private getExpiredReservationCutoff;
72
73
  private createReservedJob;
74
+ private lockNextJob;
75
+ private selectReservationCandidates;
73
76
  dispatch<TPayload extends QueueJsonValue = QueueJsonValue, TResult = unknown>(job: QueueJobEnvelope<TPayload>): Promise<QueueDriverDispatchResult<TResult>>;
74
77
  reserve<TPayload extends QueueJsonValue = QueueJsonValue>(input: {
75
78
  readonly queueNames: readonly string[];
@@ -112,4 +115,5 @@ declare function createQueueDbRuntimeOptions(): Readonly<{
112
115
  failedJobStore: _holo_js_queue.QueueFailedJobStore;
113
116
  }>;
114
117
 
115
- export { DatabaseQueueDriver, DatabaseQueueDriverError, type StoredFailedQueueJobRow, type StoredQueueJobRow, createQueueDbRuntimeOptions, databaseQueueDriverFactory, databaseQueueDriverInternals, queueDatabaseInternals, queueDbFailedJobStore, queueDbFailedStoreInternals };
118
+ export { DatabaseQueueDriver, DatabaseQueueDriverError, createQueueDbRuntimeOptions, databaseQueueDriverFactory, databaseQueueDriverInternals, queueDatabaseInternals, queueDbFailedJobStore, queueDbFailedStoreInternals };
119
+ export type { StoredFailedQueueJobRow, StoredQueueJobRow };
package/dist/index.mjs CHANGED
@@ -190,6 +190,7 @@ function wrapDatabaseError(connectionName, action, error) {
190
190
  }
191
191
  return new DatabaseQueueDriverError(connectionName, action, error);
192
192
  }
193
+ var RESERVATION_CANDIDATE_PAGE_SIZE = 100;
193
194
  function normalizeQueueNames(queueNames, fallbackQueueName) {
194
195
  if (!queueNames || queueNames.length === 0) {
195
196
  return Object.freeze([fallbackQueueName]);
@@ -207,9 +208,12 @@ var DatabaseQueueDriver = class {
207
208
  this.name = connection.name;
208
209
  this.tableName = normalizeIdentifierPath(connection.table, "Queue table name");
209
210
  }
211
+ connection;
212
+ context;
210
213
  name;
211
214
  driver = "database";
212
215
  mode = "async";
216
+ acceptsJobId = true;
213
217
  tableName;
214
218
  async getConnection() {
215
219
  return ensureConnectionReady(resolveDatabaseConnection(this.connection.connection));
@@ -227,6 +231,45 @@ var DatabaseQueueDriver = class {
227
231
  envelope: parseStoredQueueJobRow(row)
228
232
  });
229
233
  }
234
+ async lockNextJob(tx, quotedTable, queueName, now) {
235
+ const expiredReservationCutoff = this.getExpiredReservationCutoff(now);
236
+ const lockClause = tx.getCapabilities().lockForUpdate ? " FOR UPDATE SKIP LOCKED" : "";
237
+ let after;
238
+ while (true) {
239
+ const candidates = await this.selectReservationCandidates(tx, quotedTable, queueName, now, expiredReservationCutoff, after);
240
+ for (const candidate of candidates) {
241
+ const [idPlaceholder, availablePlaceholder, expiredPlaceholder] = createPlaceholders(tx, 3);
242
+ const locked = await tx.queryCompiled({
243
+ sql: `SELECT id, job, connection, queue, payload, attempts, max_attempts, available_at, created_at FROM ${quotedTable} WHERE id = ${idPlaceholder} AND available_at <= ${availablePlaceholder} AND (reserved_at IS NULL OR reserved_at <= ${expiredPlaceholder})${lockClause}`,
244
+ bindings: [candidate.id, now, expiredReservationCutoff],
245
+ source: `queue:${this.name}:reserve:lock`
246
+ });
247
+ if (locked.rows[0]) {
248
+ return locked.rows[0];
249
+ }
250
+ }
251
+ after = candidates.at(-1);
252
+ if (!after || candidates.length < RESERVATION_CANDIDATE_PAGE_SIZE) {
253
+ return void 0;
254
+ }
255
+ }
256
+ }
257
+ async selectReservationCandidates(tx, quotedTable, queueName, now, expiredReservationCutoff, after) {
258
+ const placeholders = createPlaceholders(tx, after ? 6 : 3);
259
+ const [queuePlaceholder, availablePlaceholder, expiredPlaceholder] = placeholders;
260
+ const cursorCondition = after ? `AND (available_at, created_at, id) > (${placeholders.slice(3).join(", ")}) ` : "";
261
+ const result = await tx.queryCompiled({
262
+ sql: `SELECT id, available_at, created_at FROM ${quotedTable} WHERE queue = ${queuePlaceholder} AND available_at <= ${availablePlaceholder} AND (reserved_at IS NULL OR reserved_at <= ${expiredPlaceholder}) ` + cursorCondition + `ORDER BY available_at ASC, created_at ASC, id ASC LIMIT ${RESERVATION_CANDIDATE_PAGE_SIZE}`,
263
+ bindings: [
264
+ queueName,
265
+ now,
266
+ expiredReservationCutoff,
267
+ ...after ? [after.available_at, after.created_at, after.id] : []
268
+ ],
269
+ source: `queue:${this.name}:reserve:candidates`
270
+ });
271
+ return result.rows;
272
+ }
230
273
  async dispatch(job) {
231
274
  try {
232
275
  const connection = await this.getConnection();
@@ -262,38 +305,20 @@ var DatabaseQueueDriver = class {
262
305
  const queueNames = normalizeQueueNames(input.queueNames, this.connection.queue);
263
306
  return await connection.transaction(async (tx) => {
264
307
  for (const queueName of queueNames) {
265
- while (true) {
266
- const now = Date.now();
267
- const expiredReservationCutoff = this.getExpiredReservationCutoff(now);
268
- const [queuePlaceholder, availablePlaceholder, expiredPlaceholder] = createPlaceholders(tx, 3);
269
- const selected = await tx.queryCompiled({
270
- sql: `SELECT id, job, connection, queue, payload, attempts, max_attempts, available_at, created_at FROM ${quotedTable} WHERE queue = ${queuePlaceholder} AND available_at <= ${availablePlaceholder} AND (reserved_at IS NULL OR reserved_at <= ${expiredPlaceholder}) ORDER BY available_at ASC, created_at ASC, id ASC LIMIT 1`,
271
- bindings: [queueName, now, expiredReservationCutoff],
272
- source: `queue:${this.name}:reserve:select`
273
- });
274
- const row = selected.rows[0];
275
- if (!row) {
276
- break;
277
- }
278
- const reservationId = `${input.workerId}:${randomUUID()}`;
279
- const nextAttempts = coerceRequiredInteger(row.attempts, "Stored queue job attempts") + 1;
280
- const [
281
- reservedAtPlaceholder,
282
- reservationPlaceholder,
283
- attemptsPlaceholder,
284
- idPlaceholder,
285
- queueNamePlaceholder,
286
- expiredCutoffPlaceholder
287
- ] = createPlaceholders(tx, 6);
288
- const updated = await tx.executeCompiled({
289
- sql: `UPDATE ${quotedTable} SET reserved_at = ${reservedAtPlaceholder}, reservation_id = ${reservationPlaceholder}, attempts = ${attemptsPlaceholder} WHERE id = ${idPlaceholder} AND queue = ${queueNamePlaceholder} AND (reserved_at IS NULL OR reserved_at <= ${expiredCutoffPlaceholder})`,
290
- bindings: [now, reservationId, nextAttempts, row.id, queueName, expiredReservationCutoff],
291
- source: `queue:${this.name}:reserve:update`
292
- });
293
- if ((updated.affectedRows ?? 0) === 1) {
294
- return this.createReservedJob(row, reservationId, now);
295
- }
308
+ const now = Date.now();
309
+ const row = await this.lockNextJob(tx, quotedTable, queueName, now);
310
+ if (!row) {
311
+ continue;
296
312
  }
313
+ const reservationId = `${input.workerId}:${randomUUID()}`;
314
+ const nextAttempts = coerceRequiredInteger(row.attempts, "Stored queue job attempts") + 1;
315
+ const [reservedAtPlaceholder, reservationPlaceholder, attemptsPlaceholder, idPlaceholder] = createPlaceholders(tx, 4);
316
+ await tx.executeCompiled({
317
+ sql: `UPDATE ${quotedTable} SET reserved_at = ${reservedAtPlaceholder}, reservation_id = ${reservationPlaceholder}, attempts = ${attemptsPlaceholder} WHERE id = ${idPlaceholder}`,
318
+ bindings: [now, reservationId, nextAttempts, row.id],
319
+ source: `queue:${this.name}:reserve:update`
320
+ });
321
+ return this.createReservedJob(row, reservationId, now);
297
322
  }
298
323
  return null;
299
324
  });
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@holo-js/queue-db",
3
- "version": "0.3.27",
3
+ "version": "0.4.0",
4
4
  "description": "Holo-JS Framework - DB-backed integrations for @holo-js/queue",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/cobraprojects/holo-js.git",
10
+ "directory": "packages/queue-db"
11
+ },
7
12
  "exports": {
8
13
  ".": {
9
14
  "types": "./dist/index.d.ts",
@@ -14,7 +19,8 @@
14
19
  "main": "./dist/index.mjs",
15
20
  "types": "./dist/index.d.ts",
16
21
  "files": [
17
- "dist"
22
+ "dist",
23
+ "LICENSE"
18
24
  ],
19
25
  "scripts": {
20
26
  "build": "tsup",
@@ -23,17 +29,19 @@
23
29
  "test": "vitest --run"
24
30
  },
25
31
  "dependencies": {
26
- "@holo-js/db": "^0.3.27",
27
- "@holo-js/queue": "^0.3.27"
32
+ "@holo-js/db": "^0.4.0",
33
+ "@holo-js/queue": "^0.4.0"
28
34
  },
29
35
  "devDependencies": {
30
- "@holo-js/db-sqlite": "^0.3.27",
31
- "@types/node": "^22.10.2",
32
- "tsup": "^8.3.5",
33
- "typescript": "^5.7.2",
34
- "vitest": "^4.1.5"
36
+ "@holo-js/db-mysql": "^0.4.0",
37
+ "@holo-js/db-postgres": "^0.4.0",
38
+ "@holo-js/db-sqlite": "^0.4.0",
39
+ "@types/node": "^22.20.5",
40
+ "tsup": "^9.0.0",
41
+ "typescript": "^6.0.3",
42
+ "vitest": "^5.0.5"
35
43
  },
36
44
  "engines": {
37
- "node": "^22.12.0 || ^24.0.0 || ^26.0.0"
45
+ "node": "^22.18.0 || ^24.0.0 || ^26.0.0"
38
46
  }
39
47
  }