@fedify/sqlite 2.0.0-dev.237 → 2.0.0-dev.279

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/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/sqlite",
3
- "version": "2.0.0-dev.237+7f2bb1de",
3
+ "version": "2.0.0-dev.279+ce1bdc22",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./src/mod.ts",
@@ -18,7 +18,8 @@
18
18
  "publish": {
19
19
  "exclude": [
20
20
  "**/*.test.ts",
21
- "!dist/"
21
+ "!dist/",
22
+ "tsdown.config.ts"
22
23
  ]
23
24
  },
24
25
  "tasks": {
package/dist/mq.cjs CHANGED
@@ -106,10 +106,14 @@ var SqliteMessageQueue = class SqliteMessageQueue {
106
106
  message
107
107
  });
108
108
  else logger.debug("Enqueuing a message...", { message });
109
+ const orderingKey = options?.orderingKey ?? null;
109
110
  return this.#retryOnBusy(() => {
110
- this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled)
111
- VALUES (?, ?, ?, ?)`).run(id, encodedMessage, now, scheduled);
112
- logger.debug("Enqueued a message.", { message });
111
+ this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled, ordering_key)
112
+ VALUES (?, ?, ?, ?, ?)`).run(id, encodedMessage, now, scheduled, orderingKey);
113
+ logger.debug("Enqueued a message.", {
114
+ message,
115
+ orderingKey
116
+ });
113
117
  const delayMs = delay.total("millisecond");
114
118
  SqliteMessageQueue.#getNotifyChannel(this.#tableName).dispatchEvent(new EnqueueEvent(delayMs));
115
119
  });
@@ -128,13 +132,14 @@ var SqliteMessageQueue = class SqliteMessageQueue {
128
132
  messages
129
133
  });
130
134
  else logger.debug("Enqueuing messages...", { messages });
135
+ const orderingKey = options?.orderingKey ?? null;
131
136
  return this.#withTransactionRetries(() => {
132
- const stmt = this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled)
133
- VALUES (?, ?, ?, ?)`);
137
+ const stmt = this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled, ordering_key)
138
+ VALUES (?, ?, ?, ?, ?)`);
134
139
  for (const message of messages) {
135
140
  const id = crypto.randomUUID();
136
141
  const encodedMessage = this.#encodeMessage(message);
137
- stmt.run(id, encodedMessage, now, scheduled);
142
+ stmt.run(id, encodedMessage, now, scheduled, orderingKey);
138
143
  }
139
144
  logger.debug("Enqueued messages.", { messages });
140
145
  const delayMs = delay.total("millisecond");
@@ -157,6 +162,7 @@ var SqliteMessageQueue = class SqliteMessageQueue {
157
162
  logger.debug("Starting to listen for messages on table {tableName}...", { tableName: this.#tableName });
158
163
  const channel = SqliteMessageQueue.#getNotifyChannel(this.#tableName);
159
164
  const timeouts = /* @__PURE__ */ new Set();
165
+ const lockTableName = `${this.#tableName}_locks`;
160
166
  const poll = async () => {
161
167
  while (signal == null || !signal.aborted) {
162
168
  const now = Temporal.Now.instant().epochMilliseconds;
@@ -165,16 +171,23 @@ var SqliteMessageQueue = class SqliteMessageQueue {
165
171
  WHERE id = (
166
172
  SELECT id FROM "${this.#tableName}"
167
173
  WHERE scheduled <= ?
174
+ AND (ordering_key IS NULL
175
+ OR ordering_key NOT IN (SELECT ordering_key FROM "${lockTableName}"))
168
176
  ORDER BY scheduled
169
177
  LIMIT 1
170
178
  )
171
- RETURNING id, message`).get(now);
179
+ RETURNING id, message, ordering_key`).get(now);
172
180
  });
173
181
  if (result) {
174
182
  const message = this.#decodeMessage(result.message);
183
+ const orderingKey = result.ordering_key;
184
+ if (orderingKey != null) await this.#retryOnBusy(() => {
185
+ this.#db.prepare(`INSERT OR IGNORE INTO "${lockTableName}" (ordering_key) VALUES (?)`).run(orderingKey);
186
+ });
175
187
  logger.debug("Processing message {id}...", {
176
188
  id: result.id,
177
- message
189
+ message,
190
+ orderingKey
178
191
  });
179
192
  try {
180
193
  await handler(message);
@@ -186,6 +199,10 @@ var SqliteMessageQueue = class SqliteMessageQueue {
186
199
  message,
187
200
  error
188
201
  });
202
+ } finally {
203
+ if (orderingKey != null) await this.#retryOnBusy(() => {
204
+ this.#db.prepare(`DELETE FROM "${lockTableName}" WHERE ordering_key = ?`).run(orderingKey);
205
+ });
189
206
  }
190
207
  continue;
191
208
  }
@@ -240,23 +257,30 @@ var SqliteMessageQueue = class SqliteMessageQueue {
240
257
  id TEXT PRIMARY KEY,
241
258
  message TEXT NOT NULL,
242
259
  created INTEGER NOT NULL,
243
- scheduled INTEGER NOT NULL
260
+ scheduled INTEGER NOT NULL,
261
+ ordering_key TEXT
244
262
  )
245
263
  `);
246
264
  this.#db.exec(`
247
265
  CREATE INDEX IF NOT EXISTS "idx_${this.#tableName}_scheduled"
248
266
  ON "${this.#tableName}" (scheduled)
267
+ `);
268
+ this.#db.exec(`
269
+ CREATE TABLE IF NOT EXISTS "${this.#tableName}_locks" (
270
+ ordering_key TEXT PRIMARY KEY
271
+ )
249
272
  `);
250
273
  });
251
274
  this.#initialized = true;
252
275
  logger.debug("Initialized the message queue table {tableName}.", { tableName: this.#tableName });
253
276
  }
254
277
  /**
255
- * Drops the table used by the message queue. Does nothing if the table
256
- * does not exist.
278
+ * Drops the tables used by the message queue. Does nothing if the tables
279
+ * do not exist.
257
280
  */
258
281
  drop() {
259
282
  this.#db.exec(`DROP TABLE IF EXISTS "${this.#tableName}"`);
283
+ this.#db.exec(`DROP TABLE IF EXISTS "${this.#tableName}_locks"`);
260
284
  this.#initialized = false;
261
285
  }
262
286
  /**
package/dist/mq.d.cts CHANGED
@@ -99,8 +99,8 @@ declare class SqliteMessageQueue implements MessageQueue, Disposable {
99
99
  */
100
100
  initialize(): void;
101
101
  /**
102
- * Drops the table used by the message queue. Does nothing if the table
103
- * does not exist.
102
+ * Drops the tables used by the message queue. Does nothing if the tables
103
+ * do not exist.
104
104
  */
105
105
  drop(): void;
106
106
  /**
package/dist/mq.d.ts CHANGED
@@ -100,8 +100,8 @@ declare class SqliteMessageQueue implements MessageQueue, Disposable {
100
100
  */
101
101
  initialize(): void;
102
102
  /**
103
- * Drops the table used by the message queue. Does nothing if the table
104
- * does not exist.
103
+ * Drops the tables used by the message queue. Does nothing if the tables
104
+ * do not exist.
105
105
  */
106
106
  drop(): void;
107
107
  /**
package/dist/mq.js CHANGED
@@ -105,10 +105,14 @@ var SqliteMessageQueue = class SqliteMessageQueue {
105
105
  message
106
106
  });
107
107
  else logger.debug("Enqueuing a message...", { message });
108
+ const orderingKey = options?.orderingKey ?? null;
108
109
  return this.#retryOnBusy(() => {
109
- this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled)
110
- VALUES (?, ?, ?, ?)`).run(id, encodedMessage, now, scheduled);
111
- logger.debug("Enqueued a message.", { message });
110
+ this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled, ordering_key)
111
+ VALUES (?, ?, ?, ?, ?)`).run(id, encodedMessage, now, scheduled, orderingKey);
112
+ logger.debug("Enqueued a message.", {
113
+ message,
114
+ orderingKey
115
+ });
112
116
  const delayMs = delay.total("millisecond");
113
117
  SqliteMessageQueue.#getNotifyChannel(this.#tableName).dispatchEvent(new EnqueueEvent(delayMs));
114
118
  });
@@ -127,13 +131,14 @@ var SqliteMessageQueue = class SqliteMessageQueue {
127
131
  messages
128
132
  });
129
133
  else logger.debug("Enqueuing messages...", { messages });
134
+ const orderingKey = options?.orderingKey ?? null;
130
135
  return this.#withTransactionRetries(() => {
131
- const stmt = this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled)
132
- VALUES (?, ?, ?, ?)`);
136
+ const stmt = this.#db.prepare(`INSERT INTO "${this.#tableName}" (id, message, created, scheduled, ordering_key)
137
+ VALUES (?, ?, ?, ?, ?)`);
133
138
  for (const message of messages) {
134
139
  const id = crypto.randomUUID();
135
140
  const encodedMessage = this.#encodeMessage(message);
136
- stmt.run(id, encodedMessage, now, scheduled);
141
+ stmt.run(id, encodedMessage, now, scheduled, orderingKey);
137
142
  }
138
143
  logger.debug("Enqueued messages.", { messages });
139
144
  const delayMs = delay.total("millisecond");
@@ -156,6 +161,7 @@ var SqliteMessageQueue = class SqliteMessageQueue {
156
161
  logger.debug("Starting to listen for messages on table {tableName}...", { tableName: this.#tableName });
157
162
  const channel = SqliteMessageQueue.#getNotifyChannel(this.#tableName);
158
163
  const timeouts = /* @__PURE__ */ new Set();
164
+ const lockTableName = `${this.#tableName}_locks`;
159
165
  const poll = async () => {
160
166
  while (signal == null || !signal.aborted) {
161
167
  const now = Temporal.Now.instant().epochMilliseconds;
@@ -164,16 +170,23 @@ var SqliteMessageQueue = class SqliteMessageQueue {
164
170
  WHERE id = (
165
171
  SELECT id FROM "${this.#tableName}"
166
172
  WHERE scheduled <= ?
173
+ AND (ordering_key IS NULL
174
+ OR ordering_key NOT IN (SELECT ordering_key FROM "${lockTableName}"))
167
175
  ORDER BY scheduled
168
176
  LIMIT 1
169
177
  )
170
- RETURNING id, message`).get(now);
178
+ RETURNING id, message, ordering_key`).get(now);
171
179
  });
172
180
  if (result) {
173
181
  const message = this.#decodeMessage(result.message);
182
+ const orderingKey = result.ordering_key;
183
+ if (orderingKey != null) await this.#retryOnBusy(() => {
184
+ this.#db.prepare(`INSERT OR IGNORE INTO "${lockTableName}" (ordering_key) VALUES (?)`).run(orderingKey);
185
+ });
174
186
  logger.debug("Processing message {id}...", {
175
187
  id: result.id,
176
- message
188
+ message,
189
+ orderingKey
177
190
  });
178
191
  try {
179
192
  await handler(message);
@@ -185,6 +198,10 @@ var SqliteMessageQueue = class SqliteMessageQueue {
185
198
  message,
186
199
  error
187
200
  });
201
+ } finally {
202
+ if (orderingKey != null) await this.#retryOnBusy(() => {
203
+ this.#db.prepare(`DELETE FROM "${lockTableName}" WHERE ordering_key = ?`).run(orderingKey);
204
+ });
188
205
  }
189
206
  continue;
190
207
  }
@@ -239,23 +256,30 @@ var SqliteMessageQueue = class SqliteMessageQueue {
239
256
  id TEXT PRIMARY KEY,
240
257
  message TEXT NOT NULL,
241
258
  created INTEGER NOT NULL,
242
- scheduled INTEGER NOT NULL
259
+ scheduled INTEGER NOT NULL,
260
+ ordering_key TEXT
243
261
  )
244
262
  `);
245
263
  this.#db.exec(`
246
264
  CREATE INDEX IF NOT EXISTS "idx_${this.#tableName}_scheduled"
247
265
  ON "${this.#tableName}" (scheduled)
266
+ `);
267
+ this.#db.exec(`
268
+ CREATE TABLE IF NOT EXISTS "${this.#tableName}_locks" (
269
+ ordering_key TEXT PRIMARY KEY
270
+ )
248
271
  `);
249
272
  });
250
273
  this.#initialized = true;
251
274
  logger.debug("Initialized the message queue table {tableName}.", { tableName: this.#tableName });
252
275
  }
253
276
  /**
254
- * Drops the table used by the message queue. Does nothing if the table
255
- * does not exist.
277
+ * Drops the tables used by the message queue. Does nothing if the tables
278
+ * do not exist.
256
279
  */
257
280
  drop() {
258
281
  this.#db.exec(`DROP TABLE IF EXISTS "${this.#tableName}"`);
282
+ this.#db.exec(`DROP TABLE IF EXISTS "${this.#tableName}_locks"`);
259
283
  this.#initialized = false;
260
284
  }
261
285
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/sqlite",
3
- "version": "2.0.0-dev.237+7f2bb1de",
3
+ "version": "2.0.0-dev.279+ce1bdc22",
4
4
  "description": "SQLite drivers for Fedify",
5
5
  "keywords": [
6
6
  "fedify",
@@ -72,19 +72,22 @@
72
72
  "es-toolkit": "^1.31.0"
73
73
  },
74
74
  "peerDependencies": {
75
- "@fedify/fedify": "^2.0.0-dev.237+7f2bb1de"
75
+ "@fedify/fedify": "^2.0.0-dev.279+ce1bdc22"
76
76
  },
77
77
  "devDependencies": {
78
78
  "@std/async": "npm:@jsr/std__async@^1.0.13",
79
79
  "tsdown": "^0.12.9",
80
80
  "typescript": "^5.9.3",
81
- "@fedify/testing": "^2.0.0-dev.237+7f2bb1de"
81
+ "@fedify/testing": "^2.0.0-dev.279+ce1bdc22"
82
82
  },
83
83
  "scripts": {
84
- "build": "tsdown",
85
- "prepublish": "tsdown",
86
- "test": "tsdown && node --experimental-transform-types --test",
87
- "test:bun": "tsdown && bun test --timeout=10000",
84
+ "build:self": "tsdown",
85
+ "build": "pnpm --filter @fedify/sqlite... run build:self",
86
+ "prepublish": "pnpm build",
87
+ "pretest": "pnpm build",
88
+ "test": "node --experimental-transform-types --test",
89
+ "pretest:bun": "pnpm build",
90
+ "test:bun": "bun test --timeout=10000",
88
91
  "test:deno": "deno task test"
89
92
  }
90
93
  }
package/src/mq.test.ts CHANGED
@@ -20,4 +20,5 @@ test("SqliteMessageQueue", () =>
20
20
  mq1[Symbol.dispose]();
21
21
  mq2[Symbol.dispose]();
22
22
  },
23
+ { testOrderingKey: true },
23
24
  ));
package/src/mq.ts CHANGED
@@ -181,15 +181,17 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
181
181
  logger.debug("Enqueuing a message...", { message });
182
182
  }
183
183
 
184
+ const orderingKey = options?.orderingKey ?? null;
185
+
184
186
  return this.#retryOnBusy(() => {
185
187
  this.#db
186
188
  .prepare(
187
- `INSERT INTO "${this.#tableName}" (id, message, created, scheduled)
188
- VALUES (?, ?, ?, ?)`,
189
+ `INSERT INTO "${this.#tableName}" (id, message, created, scheduled, ordering_key)
190
+ VALUES (?, ?, ?, ?, ?)`,
189
191
  )
190
- .run(id, encodedMessage, now, scheduled);
192
+ .run(id, encodedMessage, now, scheduled, orderingKey);
191
193
 
192
- logger.debug("Enqueued a message.", { message });
194
+ logger.debug("Enqueued a message.", { message, orderingKey });
193
195
 
194
196
  // Notify listeners that a message has been enqueued
195
197
  const delayMs = delay.total("millisecond");
@@ -224,16 +226,18 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
224
226
  logger.debug("Enqueuing messages...", { messages });
225
227
  }
226
228
 
229
+ const orderingKey = options?.orderingKey ?? null;
230
+
227
231
  return this.#withTransactionRetries(() => {
228
232
  const stmt = this.#db.prepare(
229
- `INSERT INTO "${this.#tableName}" (id, message, created, scheduled)
230
- VALUES (?, ?, ?, ?)`,
233
+ `INSERT INTO "${this.#tableName}" (id, message, created, scheduled, ordering_key)
234
+ VALUES (?, ?, ?, ?, ?)`,
231
235
  );
232
236
 
233
237
  for (const message of messages) {
234
238
  const id = crypto.randomUUID();
235
239
  const encodedMessage = this.#encodeMessage(message);
236
- stmt.run(id, encodedMessage, now, scheduled);
240
+ stmt.run(id, encodedMessage, now, scheduled, orderingKey);
237
241
  }
238
242
 
239
243
  logger.debug("Enqueued messages.", { messages });
@@ -274,6 +278,7 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
274
278
 
275
279
  const channel = SqliteMessageQueue.#getNotifyChannel(this.#tableName);
276
280
  const timeouts = new Set<ReturnType<typeof setTimeout>>();
281
+ const lockTableName = `${this.#tableName}_locks`;
277
282
 
278
283
  const poll = async () => {
279
284
  while (signal == null || !signal.aborted) {
@@ -283,6 +288,7 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
283
288
  // processed using DELETE ... RETURNING (SQLite >= 3.35.0)
284
289
  // Wrapped in BEGIN IMMEDIATE transaction to ensure proper locking
285
290
  // and prevent race conditions in multi-process scenarios
291
+ // Exclude messages with ordering keys currently being processed (DB-level lock)
286
292
  const result = await this.#withTransactionRetries(() => {
287
293
  return this.#db
288
294
  .prepare(
@@ -290,19 +296,37 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
290
296
  WHERE id = (
291
297
  SELECT id FROM "${this.#tableName}"
292
298
  WHERE scheduled <= ?
299
+ AND (ordering_key IS NULL
300
+ OR ordering_key NOT IN (SELECT ordering_key FROM "${lockTableName}"))
293
301
  ORDER BY scheduled
294
302
  LIMIT 1
295
303
  )
296
- RETURNING id, message`,
304
+ RETURNING id, message, ordering_key`,
297
305
  )
298
- .get(now) as { id: string; message: string } | undefined;
306
+ .get(now) as
307
+ | { id: string; message: string; ordering_key: string | null }
308
+ | undefined;
299
309
  });
300
310
 
301
311
  if (result) {
302
312
  const message = this.#decodeMessage(result.message);
313
+ const orderingKey = result.ordering_key;
314
+
315
+ // Acquire DB-level lock for this ordering key
316
+ if (orderingKey != null) {
317
+ await this.#retryOnBusy(() => {
318
+ this.#db
319
+ .prepare(
320
+ `INSERT OR IGNORE INTO "${lockTableName}" (ordering_key) VALUES (?)`,
321
+ )
322
+ .run(orderingKey);
323
+ });
324
+ }
325
+
303
326
  logger.debug("Processing message {id}...", {
304
327
  id: result.id,
305
328
  message,
329
+ orderingKey,
306
330
  });
307
331
  try {
308
332
  await handler(message);
@@ -317,6 +341,17 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
317
341
  error,
318
342
  },
319
343
  );
344
+ } finally {
345
+ // Release DB-level lock for this ordering key
346
+ if (orderingKey != null) {
347
+ await this.#retryOnBusy(() => {
348
+ this.#db
349
+ .prepare(
350
+ `DELETE FROM "${lockTableName}" WHERE ordering_key = ?`,
351
+ )
352
+ .run(orderingKey);
353
+ });
354
+ }
320
355
  }
321
356
 
322
357
  // Check for next message immediately
@@ -397,7 +432,8 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
397
432
  id TEXT PRIMARY KEY,
398
433
  message TEXT NOT NULL,
399
434
  created INTEGER NOT NULL,
400
- scheduled INTEGER NOT NULL
435
+ scheduled INTEGER NOT NULL,
436
+ ordering_key TEXT
401
437
  )
402
438
  `);
403
439
 
@@ -405,6 +441,13 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
405
441
  CREATE INDEX IF NOT EXISTS "idx_${this.#tableName}_scheduled"
406
442
  ON "${this.#tableName}" (scheduled)
407
443
  `);
444
+
445
+ // Create lock table for distributed ordering key locks
446
+ this.#db.exec(`
447
+ CREATE TABLE IF NOT EXISTS "${this.#tableName}_locks" (
448
+ ordering_key TEXT PRIMARY KEY
449
+ )
450
+ `);
408
451
  });
409
452
 
410
453
  this.#initialized = true;
@@ -414,11 +457,12 @@ export class SqliteMessageQueue implements MessageQueue, Disposable {
414
457
  }
415
458
 
416
459
  /**
417
- * Drops the table used by the message queue. Does nothing if the table
418
- * does not exist.
460
+ * Drops the tables used by the message queue. Does nothing if the tables
461
+ * do not exist.
419
462
  */
420
463
  drop(): void {
421
464
  this.#db.exec(`DROP TABLE IF EXISTS "${this.#tableName}"`);
465
+ this.#db.exec(`DROP TABLE IF EXISTS "${this.#tableName}_locks"`);
422
466
  this.#initialized = false;
423
467
  }
424
468