@fedify/postgres 2.1.0-dev.421 → 2.1.0-dev.523

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/dist/mq.cjs CHANGED
@@ -17,6 +17,15 @@ const INITIALIZE_BACKOFF_MS = 10;
17
17
  function sleep(milliseconds) {
18
18
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
19
19
  }
20
+ function withTimeout(result, timeoutMs) {
21
+ const resolved = Promise.resolve(result);
22
+ if (timeoutMs <= 0) return resolved;
23
+ let timer;
24
+ const timeoutPromise = new Promise((_, reject) => {
25
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Message handler timed out after ${timeoutMs}ms`)), timeoutMs);
26
+ });
27
+ return Promise.race([resolved, timeoutPromise]).finally(() => clearTimeout(timer));
28
+ }
20
29
  function isInitializationRaceError(error) {
21
30
  return error instanceof postgres.default.PostgresError && (error.constraint_name === "pg_type_typname_nsp_index" || error.constraint_name === "pg_class_relname_nsp_index" || error.code === "42P07" || error.code === "42710");
22
31
  }
@@ -47,6 +56,7 @@ var PostgresMessageQueue = class {
47
56
  #tableName;
48
57
  #channelName;
49
58
  #pollIntervalMs;
59
+ #handlerTimeoutMs;
50
60
  #initialized;
51
61
  #initPromise;
52
62
  #driverSerializesJson = false;
@@ -55,6 +65,7 @@ var PostgresMessageQueue = class {
55
65
  this.#tableName = options?.tableName ?? "fedify_message_v2";
56
66
  this.#channelName = options?.channelName ?? "fedify_channel";
57
67
  this.#pollIntervalMs = Temporal.Duration.from(options?.pollInterval ?? { seconds: 5 }).total("millisecond");
68
+ this.#handlerTimeoutMs = Temporal.Duration.from(options?.handlerTimeout ?? { seconds: 60 }).total("millisecond");
58
69
  this.#initialized = options?.initialized ?? false;
59
70
  }
60
71
  async enqueue(message, options) {
@@ -143,7 +154,7 @@ var PostgresMessageQueue = class {
143
154
  RETURNING message, ordering_key;
144
155
  `) {
145
156
  if (signal?.aborted) return;
146
- await handler(row.message);
157
+ await withTimeout(handler(row.message), this.#handlerTimeoutMs);
147
158
  processed = true;
148
159
  }
149
160
  if (processed) continue;
@@ -180,7 +191,7 @@ var PostgresMessageQueue = class {
180
191
  `;
181
192
  for (const row of deleteResult) {
182
193
  if (signal?.aborted) return;
183
- await handler(row.message);
194
+ await withTimeout(handler(row.message), this.#handlerTimeoutMs);
184
195
  processed = true;
185
196
  }
186
197
  } finally {
@@ -219,15 +230,23 @@ var PostgresMessageQueue = class {
219
230
  };
220
231
  const timeouts = /* @__PURE__ */ new Set();
221
232
  const listen = await this.#sql.listen(this.#channelName, async (delay) => {
222
- const duration = Temporal.Duration.from(delay);
223
- const durationMs = duration.total("millisecond");
224
- if (durationMs < 1) await safeSerializedPoll("notify-immediate");
225
- else {
226
- const timeout = setTimeout(() => {
227
- timeouts.delete(timeout);
228
- safeSerializedPoll("notify-delayed");
229
- }, durationMs);
230
- timeouts.add(timeout);
233
+ try {
234
+ const duration = Temporal.Duration.from(delay);
235
+ const durationMs = duration.total("millisecond");
236
+ if (durationMs < 1) await safeSerializedPoll("notify-immediate");
237
+ else {
238
+ const timeout = setTimeout(() => {
239
+ timeouts.delete(timeout);
240
+ safeSerializedPoll("notify-delayed");
241
+ }, durationMs);
242
+ timeouts.add(timeout);
243
+ }
244
+ } catch (error) {
245
+ logger.error("Error parsing NOTIFY payload {delay}: {error}", {
246
+ delay,
247
+ error
248
+ });
249
+ await safeSerializedPoll("notify-fallback");
231
250
  }
232
251
  }, () => safeSerializedPoll("subscribe"));
233
252
  signal?.addEventListener("abort", () => {
package/dist/mq.d.cts CHANGED
@@ -28,6 +28,19 @@ interface PostgresMessageQueueOptions {
28
28
  * @default `{ seconds: 5 }`
29
29
  */
30
30
  readonly pollInterval?: Temporal.Duration | Temporal.DurationLike;
31
+ /**
32
+ * The maximum time to wait for a message handler to complete before
33
+ * considering it hung. When a handler exceeds this timeout, it is
34
+ * treated as an error and the poll loop moves on to the next message,
35
+ * preventing a single hung handler from permanently blocking the queue.
36
+ *
37
+ * Set to zero to disable the timeout (not recommended in production).
38
+ *
39
+ * 60 seconds by default.
40
+ * @default `{ seconds: 60 }`
41
+ * @since 2.0.3
42
+ */
43
+ readonly handlerTimeout?: Temporal.Duration | Temporal.DurationLike;
31
44
  }
32
45
  /**
33
46
  * A message queue that uses PostgreSQL as the underlying storage.
package/dist/mq.d.ts CHANGED
@@ -29,6 +29,19 @@ interface PostgresMessageQueueOptions {
29
29
  * @default `{ seconds: 5 }`
30
30
  */
31
31
  readonly pollInterval?: Temporal.Duration | Temporal.DurationLike;
32
+ /**
33
+ * The maximum time to wait for a message handler to complete before
34
+ * considering it hung. When a handler exceeds this timeout, it is
35
+ * treated as an error and the poll loop moves on to the next message,
36
+ * preventing a single hung handler from permanently blocking the queue.
37
+ *
38
+ * Set to zero to disable the timeout (not recommended in production).
39
+ *
40
+ * 60 seconds by default.
41
+ * @default `{ seconds: 60 }`
42
+ * @since 2.0.3
43
+ */
44
+ readonly handlerTimeout?: Temporal.Duration | Temporal.DurationLike;
32
45
  }
33
46
  /**
34
47
  * A message queue that uses PostgreSQL as the underlying storage.
package/dist/mq.js CHANGED
@@ -16,6 +16,15 @@ const INITIALIZE_BACKOFF_MS = 10;
16
16
  function sleep(milliseconds) {
17
17
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
18
18
  }
19
+ function withTimeout(result, timeoutMs) {
20
+ const resolved = Promise.resolve(result);
21
+ if (timeoutMs <= 0) return resolved;
22
+ let timer;
23
+ const timeoutPromise = new Promise((_, reject) => {
24
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Message handler timed out after ${timeoutMs}ms`)), timeoutMs);
25
+ });
26
+ return Promise.race([resolved, timeoutPromise]).finally(() => clearTimeout(timer));
27
+ }
19
28
  function isInitializationRaceError(error) {
20
29
  return error instanceof postgres.PostgresError && (error.constraint_name === "pg_type_typname_nsp_index" || error.constraint_name === "pg_class_relname_nsp_index" || error.code === "42P07" || error.code === "42710");
21
30
  }
@@ -46,6 +55,7 @@ var PostgresMessageQueue = class {
46
55
  #tableName;
47
56
  #channelName;
48
57
  #pollIntervalMs;
58
+ #handlerTimeoutMs;
49
59
  #initialized;
50
60
  #initPromise;
51
61
  #driverSerializesJson = false;
@@ -54,6 +64,7 @@ var PostgresMessageQueue = class {
54
64
  this.#tableName = options?.tableName ?? "fedify_message_v2";
55
65
  this.#channelName = options?.channelName ?? "fedify_channel";
56
66
  this.#pollIntervalMs = Temporal.Duration.from(options?.pollInterval ?? { seconds: 5 }).total("millisecond");
67
+ this.#handlerTimeoutMs = Temporal.Duration.from(options?.handlerTimeout ?? { seconds: 60 }).total("millisecond");
57
68
  this.#initialized = options?.initialized ?? false;
58
69
  }
59
70
  async enqueue(message, options) {
@@ -142,7 +153,7 @@ var PostgresMessageQueue = class {
142
153
  RETURNING message, ordering_key;
143
154
  `) {
144
155
  if (signal?.aborted) return;
145
- await handler(row.message);
156
+ await withTimeout(handler(row.message), this.#handlerTimeoutMs);
146
157
  processed = true;
147
158
  }
148
159
  if (processed) continue;
@@ -179,7 +190,7 @@ var PostgresMessageQueue = class {
179
190
  `;
180
191
  for (const row of deleteResult) {
181
192
  if (signal?.aborted) return;
182
- await handler(row.message);
193
+ await withTimeout(handler(row.message), this.#handlerTimeoutMs);
183
194
  processed = true;
184
195
  }
185
196
  } finally {
@@ -218,15 +229,23 @@ var PostgresMessageQueue = class {
218
229
  };
219
230
  const timeouts = /* @__PURE__ */ new Set();
220
231
  const listen = await this.#sql.listen(this.#channelName, async (delay) => {
221
- const duration = Temporal.Duration.from(delay);
222
- const durationMs = duration.total("millisecond");
223
- if (durationMs < 1) await safeSerializedPoll("notify-immediate");
224
- else {
225
- const timeout = setTimeout(() => {
226
- timeouts.delete(timeout);
227
- safeSerializedPoll("notify-delayed");
228
- }, durationMs);
229
- timeouts.add(timeout);
232
+ try {
233
+ const duration = Temporal.Duration.from(delay);
234
+ const durationMs = duration.total("millisecond");
235
+ if (durationMs < 1) await safeSerializedPoll("notify-immediate");
236
+ else {
237
+ const timeout = setTimeout(() => {
238
+ timeouts.delete(timeout);
239
+ safeSerializedPoll("notify-delayed");
240
+ }, durationMs);
241
+ timeouts.add(timeout);
242
+ }
243
+ } catch (error) {
244
+ logger.error("Error parsing NOTIFY payload {delay}: {error}", {
245
+ delay,
246
+ error
247
+ });
248
+ await safeSerializedPoll("notify-fallback");
230
249
  }
231
250
  }, () => safeSerializedPoll("subscribe"));
232
251
  signal?.addEventListener("abort", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/postgres",
3
- "version": "2.1.0-dev.421+8225ebc2",
3
+ "version": "2.1.0-dev.523+150998a5",
4
4
  "description": "PostgreSQL drivers for Fedify",
5
5
  "keywords": [
6
6
  "fedify",
@@ -74,13 +74,13 @@
74
74
  },
75
75
  "peerDependencies": {
76
76
  "postgres": "^3.4.7",
77
- "@fedify/fedify": "^2.1.0-dev.421+8225ebc2"
77
+ "@fedify/fedify": "^2.1.0-dev.523+150998a5"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@std/async": "npm:@jsr/std__async@^1.0.13",
81
81
  "tsdown": "^0.12.9",
82
82
  "typescript": "^5.9.3",
83
- "@fedify/testing": "^2.1.0-dev.421+8225ebc2",
83
+ "@fedify/testing": "^2.1.0-dev.523+150998a5",
84
84
  "@fedify/fixture": "^2.0.0"
85
85
  },
86
86
  "scripts": {