@fedify/postgres 2.0.0-pr.559.6 → 2.0.1-dev.400

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.
Files changed (3) hide show
  1. package/dist/mq.cjs +75 -39
  2. package/dist/mq.js +75 -39
  3. package/package.json +4 -4
package/dist/mq.cjs CHANGED
@@ -12,6 +12,14 @@ const logger = (0, __logtape_logtape.getLogger)([
12
12
  "postgres",
13
13
  "mq"
14
14
  ]);
15
+ const INITIALIZE_MAX_ATTEMPTS = 5;
16
+ const INITIALIZE_BACKOFF_MS = 10;
17
+ function sleep(milliseconds) {
18
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
19
+ }
20
+ function isInitializationRaceError(error) {
21
+ return error instanceof postgres.default.PostgresError && (error.constraint_name === "pg_type_typname_nsp_index" || error.code === "42P07" || error.code === "42710");
22
+ }
15
23
  /**
16
24
  * A message queue that uses PostgreSQL as the underlying storage.
17
25
  *
@@ -35,6 +43,7 @@ var PostgresMessageQueue = class {
35
43
  #channelName;
36
44
  #pollIntervalMs;
37
45
  #initialized;
46
+ #initPromise;
38
47
  #driverSerializesJson = false;
39
48
  constructor(sql, options = {}) {
40
49
  this.#sql = sql;
@@ -114,7 +123,7 @@ var PostgresMessageQueue = class {
114
123
  const poll = async () => {
115
124
  while (!signal?.aborted) {
116
125
  let processed = false;
117
- const query = this.#sql`
126
+ for (const row of await this.#sql`
118
127
  WITH candidate AS (
119
128
  SELECT id, ordering_key
120
129
  FROM ${this.#sql(this.#tableName)}
@@ -127,15 +136,11 @@ var PostgresMessageQueue = class {
127
136
  DELETE FROM ${this.#sql(this.#tableName)}
128
137
  WHERE id IN (SELECT id FROM candidate)
129
138
  RETURNING message, ordering_key;
130
- `.execute();
131
- const cancel = query.cancel.bind(query);
132
- signal?.addEventListener("abort", cancel);
133
- for (const row of await query) {
139
+ `) {
134
140
  if (signal?.aborted) return;
135
141
  await handler(row.message);
136
142
  processed = true;
137
143
  }
138
- signal?.removeEventListener("abort", cancel);
139
144
  if (processed) continue;
140
145
  const attemptedOrderingKeys = /* @__PURE__ */ new Set();
141
146
  while (!signal?.aborted) {
@@ -153,49 +158,67 @@ var PostgresMessageQueue = class {
153
158
  const candidateId = candidate.id;
154
159
  const orderingKey = candidate.ordering_key;
155
160
  attemptedOrderingKeys.add(orderingKey);
156
- const lockResult = await this.#sql`
157
- SELECT pg_try_advisory_lock(
158
- hashtext(${this.#tableName}),
159
- hashtext(${orderingKey})
160
- ) AS acquired
161
- `;
162
- if (lockResult[0].acquired) {
163
- try {
164
- const deleteResult = await this.#sql`
165
- DELETE FROM ${this.#sql(this.#tableName)}
166
- WHERE id = ${candidateId}
167
- RETURNING message, ordering_key
168
- `;
169
- for (const row of deleteResult) {
170
- if (signal?.aborted) return;
171
- await handler(row.message);
172
- processed = true;
161
+ const reserved = await this.#sql.reserve();
162
+ try {
163
+ const lockResult = await reserved`
164
+ SELECT pg_try_advisory_lock(
165
+ hashtext(${this.#tableName}),
166
+ hashtext(${orderingKey})
167
+ ) AS acquired
168
+ `;
169
+ if (lockResult[0].acquired) {
170
+ try {
171
+ const deleteResult = await reserved`
172
+ DELETE FROM ${reserved(this.#tableName)}
173
+ WHERE id = ${candidateId}
174
+ RETURNING message, ordering_key
175
+ `;
176
+ for (const row of deleteResult) {
177
+ if (signal?.aborted) return;
178
+ await handler(row.message);
179
+ processed = true;
180
+ }
181
+ } finally {
182
+ await reserved`
183
+ SELECT pg_advisory_unlock(
184
+ hashtext(${this.#tableName}),
185
+ hashtext(${orderingKey})
186
+ )
187
+ `;
173
188
  }
174
- } finally {
175
- await this.#sql`
176
- SELECT pg_advisory_unlock(
177
- hashtext(${this.#tableName}),
178
- hashtext(${orderingKey})
179
- )
180
- `;
189
+ if (processed) break;
181
190
  }
182
- if (processed) break;
191
+ } finally {
192
+ reserved.release();
183
193
  }
184
194
  }
185
195
  if (processed) continue;
186
196
  break;
187
197
  }
188
198
  };
199
+ let pollLock = Promise.resolve();
200
+ const serializedPoll = () => {
201
+ const next = pollLock.then(poll);
202
+ pollLock = next.catch(() => {});
203
+ return next;
204
+ };
189
205
  const timeouts = /* @__PURE__ */ new Set();
190
206
  const listen = await this.#sql.listen(this.#channelName, async (delay) => {
191
207
  const duration = Temporal.Duration.from(delay);
192
208
  const durationMs = duration.total("millisecond");
193
- if (durationMs < 1) await poll();
194
- else timeouts.add(setTimeout(poll, durationMs));
195
- }, poll);
209
+ if (durationMs < 1) await serializedPoll();
210
+ else {
211
+ const timeout = setTimeout(() => {
212
+ timeouts.delete(timeout);
213
+ serializedPoll();
214
+ }, durationMs);
215
+ timeouts.add(timeout);
216
+ }
217
+ }, serializedPoll);
196
218
  signal?.addEventListener("abort", () => {
197
219
  listen.unlisten();
198
220
  for (const timeout of timeouts) clearTimeout(timeout);
221
+ timeouts.clear();
199
222
  });
200
223
  while (!signal?.aborted) {
201
224
  let timeout;
@@ -208,7 +231,7 @@ var PostgresMessageQueue = class {
208
231
  timeouts.add(timeout);
209
232
  });
210
233
  if (timeout != null) timeouts.delete(timeout);
211
- await poll();
234
+ await serializedPoll();
212
235
  }
213
236
  await new Promise((resolve) => {
214
237
  signal?.addEventListener("abort", () => resolve());
@@ -218,10 +241,13 @@ var PostgresMessageQueue = class {
218
241
  /**
219
242
  * Initializes the message queue table if it does not already exist.
220
243
  */
221
- async initialize() {
222
- if (this.#initialized) return;
244
+ initialize() {
245
+ if (this.#initialized) return Promise.resolve();
246
+ return this.#initPromise ??= this.#doInitialize();
247
+ }
248
+ async #doInitialize() {
223
249
  logger.debug("Initializing the message queue table {tableName}...", { tableName: this.#tableName });
224
- try {
250
+ for (let attempt = 1; attempt <= INITIALIZE_MAX_ATTEMPTS; attempt++) try {
225
251
  await this.#sql`
226
252
  CREATE TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
227
253
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -235,11 +261,21 @@ var PostgresMessageQueue = class {
235
261
  ALTER TABLE ${this.#sql(this.#tableName)}
236
262
  ADD COLUMN IF NOT EXISTS ordering_key text;
237
263
  `;
264
+ break;
238
265
  } catch (error) {
239
- if (!(error instanceof postgres.default.PostgresError && error.constraint_name === "pg_type_typname_nsp_index")) {
266
+ if (!isInitializationRaceError(error) || attempt >= INITIALIZE_MAX_ATTEMPTS) {
240
267
  logger.error("Failed to initialize the message queue table: {error}", { error });
241
268
  throw error;
242
269
  }
270
+ const backoffMs = INITIALIZE_BACKOFF_MS * 2 ** (attempt - 1);
271
+ logger.debug("Initialization raced for table {tableName}; retrying in {backoffMs}ms (attempt {attempt}/{maxAttempts}).", {
272
+ tableName: this.#tableName,
273
+ backoffMs,
274
+ attempt,
275
+ maxAttempts: INITIALIZE_MAX_ATTEMPTS,
276
+ error
277
+ });
278
+ await sleep(backoffMs);
243
279
  }
244
280
  this.#driverSerializesJson = await require_utils.driverSerializesJson(this.#sql);
245
281
  this.#initialized = true;
package/dist/mq.js CHANGED
@@ -11,6 +11,14 @@ const logger = getLogger([
11
11
  "postgres",
12
12
  "mq"
13
13
  ]);
14
+ const INITIALIZE_MAX_ATTEMPTS = 5;
15
+ const INITIALIZE_BACKOFF_MS = 10;
16
+ function sleep(milliseconds) {
17
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
18
+ }
19
+ function isInitializationRaceError(error) {
20
+ return error instanceof postgres.PostgresError && (error.constraint_name === "pg_type_typname_nsp_index" || error.code === "42P07" || error.code === "42710");
21
+ }
14
22
  /**
15
23
  * A message queue that uses PostgreSQL as the underlying storage.
16
24
  *
@@ -34,6 +42,7 @@ var PostgresMessageQueue = class {
34
42
  #channelName;
35
43
  #pollIntervalMs;
36
44
  #initialized;
45
+ #initPromise;
37
46
  #driverSerializesJson = false;
38
47
  constructor(sql, options = {}) {
39
48
  this.#sql = sql;
@@ -113,7 +122,7 @@ var PostgresMessageQueue = class {
113
122
  const poll = async () => {
114
123
  while (!signal?.aborted) {
115
124
  let processed = false;
116
- const query = this.#sql`
125
+ for (const row of await this.#sql`
117
126
  WITH candidate AS (
118
127
  SELECT id, ordering_key
119
128
  FROM ${this.#sql(this.#tableName)}
@@ -126,15 +135,11 @@ var PostgresMessageQueue = class {
126
135
  DELETE FROM ${this.#sql(this.#tableName)}
127
136
  WHERE id IN (SELECT id FROM candidate)
128
137
  RETURNING message, ordering_key;
129
- `.execute();
130
- const cancel = query.cancel.bind(query);
131
- signal?.addEventListener("abort", cancel);
132
- for (const row of await query) {
138
+ `) {
133
139
  if (signal?.aborted) return;
134
140
  await handler(row.message);
135
141
  processed = true;
136
142
  }
137
- signal?.removeEventListener("abort", cancel);
138
143
  if (processed) continue;
139
144
  const attemptedOrderingKeys = /* @__PURE__ */ new Set();
140
145
  while (!signal?.aborted) {
@@ -152,49 +157,67 @@ var PostgresMessageQueue = class {
152
157
  const candidateId = candidate.id;
153
158
  const orderingKey = candidate.ordering_key;
154
159
  attemptedOrderingKeys.add(orderingKey);
155
- const lockResult = await this.#sql`
156
- SELECT pg_try_advisory_lock(
157
- hashtext(${this.#tableName}),
158
- hashtext(${orderingKey})
159
- ) AS acquired
160
- `;
161
- if (lockResult[0].acquired) {
162
- try {
163
- const deleteResult = await this.#sql`
164
- DELETE FROM ${this.#sql(this.#tableName)}
165
- WHERE id = ${candidateId}
166
- RETURNING message, ordering_key
167
- `;
168
- for (const row of deleteResult) {
169
- if (signal?.aborted) return;
170
- await handler(row.message);
171
- processed = true;
160
+ const reserved = await this.#sql.reserve();
161
+ try {
162
+ const lockResult = await reserved`
163
+ SELECT pg_try_advisory_lock(
164
+ hashtext(${this.#tableName}),
165
+ hashtext(${orderingKey})
166
+ ) AS acquired
167
+ `;
168
+ if (lockResult[0].acquired) {
169
+ try {
170
+ const deleteResult = await reserved`
171
+ DELETE FROM ${reserved(this.#tableName)}
172
+ WHERE id = ${candidateId}
173
+ RETURNING message, ordering_key
174
+ `;
175
+ for (const row of deleteResult) {
176
+ if (signal?.aborted) return;
177
+ await handler(row.message);
178
+ processed = true;
179
+ }
180
+ } finally {
181
+ await reserved`
182
+ SELECT pg_advisory_unlock(
183
+ hashtext(${this.#tableName}),
184
+ hashtext(${orderingKey})
185
+ )
186
+ `;
172
187
  }
173
- } finally {
174
- await this.#sql`
175
- SELECT pg_advisory_unlock(
176
- hashtext(${this.#tableName}),
177
- hashtext(${orderingKey})
178
- )
179
- `;
188
+ if (processed) break;
180
189
  }
181
- if (processed) break;
190
+ } finally {
191
+ reserved.release();
182
192
  }
183
193
  }
184
194
  if (processed) continue;
185
195
  break;
186
196
  }
187
197
  };
198
+ let pollLock = Promise.resolve();
199
+ const serializedPoll = () => {
200
+ const next = pollLock.then(poll);
201
+ pollLock = next.catch(() => {});
202
+ return next;
203
+ };
188
204
  const timeouts = /* @__PURE__ */ new Set();
189
205
  const listen = await this.#sql.listen(this.#channelName, async (delay) => {
190
206
  const duration = Temporal.Duration.from(delay);
191
207
  const durationMs = duration.total("millisecond");
192
- if (durationMs < 1) await poll();
193
- else timeouts.add(setTimeout(poll, durationMs));
194
- }, poll);
208
+ if (durationMs < 1) await serializedPoll();
209
+ else {
210
+ const timeout = setTimeout(() => {
211
+ timeouts.delete(timeout);
212
+ serializedPoll();
213
+ }, durationMs);
214
+ timeouts.add(timeout);
215
+ }
216
+ }, serializedPoll);
195
217
  signal?.addEventListener("abort", () => {
196
218
  listen.unlisten();
197
219
  for (const timeout of timeouts) clearTimeout(timeout);
220
+ timeouts.clear();
198
221
  });
199
222
  while (!signal?.aborted) {
200
223
  let timeout;
@@ -207,7 +230,7 @@ var PostgresMessageQueue = class {
207
230
  timeouts.add(timeout);
208
231
  });
209
232
  if (timeout != null) timeouts.delete(timeout);
210
- await poll();
233
+ await serializedPoll();
211
234
  }
212
235
  await new Promise((resolve) => {
213
236
  signal?.addEventListener("abort", () => resolve());
@@ -217,10 +240,13 @@ var PostgresMessageQueue = class {
217
240
  /**
218
241
  * Initializes the message queue table if it does not already exist.
219
242
  */
220
- async initialize() {
221
- if (this.#initialized) return;
243
+ initialize() {
244
+ if (this.#initialized) return Promise.resolve();
245
+ return this.#initPromise ??= this.#doInitialize();
246
+ }
247
+ async #doInitialize() {
222
248
  logger.debug("Initializing the message queue table {tableName}...", { tableName: this.#tableName });
223
- try {
249
+ for (let attempt = 1; attempt <= INITIALIZE_MAX_ATTEMPTS; attempt++) try {
224
250
  await this.#sql`
225
251
  CREATE TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
226
252
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -234,11 +260,21 @@ var PostgresMessageQueue = class {
234
260
  ALTER TABLE ${this.#sql(this.#tableName)}
235
261
  ADD COLUMN IF NOT EXISTS ordering_key text;
236
262
  `;
263
+ break;
237
264
  } catch (error) {
238
- if (!(error instanceof postgres.PostgresError && error.constraint_name === "pg_type_typname_nsp_index")) {
265
+ if (!isInitializationRaceError(error) || attempt >= INITIALIZE_MAX_ATTEMPTS) {
239
266
  logger.error("Failed to initialize the message queue table: {error}", { error });
240
267
  throw error;
241
268
  }
269
+ const backoffMs = INITIALIZE_BACKOFF_MS * 2 ** (attempt - 1);
270
+ logger.debug("Initialization raced for table {tableName}; retrying in {backoffMs}ms (attempt {attempt}/{maxAttempts}).", {
271
+ tableName: this.#tableName,
272
+ backoffMs,
273
+ attempt,
274
+ maxAttempts: INITIALIZE_MAX_ATTEMPTS,
275
+ error
276
+ });
277
+ await sleep(backoffMs);
242
278
  }
243
279
  this.#driverSerializesJson = await driverSerializesJson(this.#sql);
244
280
  this.#initialized = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/postgres",
3
- "version": "2.0.0-pr.559.6+b50404e9",
3
+ "version": "2.0.1-dev.400+51b2d013",
4
4
  "description": "PostgreSQL drivers for Fedify",
5
5
  "keywords": [
6
6
  "fedify",
@@ -74,14 +74,14 @@
74
74
  },
75
75
  "peerDependencies": {
76
76
  "postgres": "^3.4.7",
77
- "@fedify/fedify": "^2.0.0-pr.559.6+b50404e9"
77
+ "@fedify/fedify": "^2.0.1-dev.400+51b2d013"
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
83
  "@fedify/fixture": "^2.0.0",
84
- "@fedify/testing": "^2.0.0-pr.559.6+b50404e9"
84
+ "@fedify/testing": "^2.0.1-dev.400+51b2d013"
85
85
  },
86
86
  "scripts": {
87
87
  "build:self": "tsdown",
@@ -90,6 +90,6 @@
90
90
  "pretest": "pnpm build",
91
91
  "test": "node --experimental-transform-types --test",
92
92
  "pretest:bun": "pnpm build",
93
- "test:bun": "bun test --timeout=10000"
93
+ "test:bun": "bun test --timeout=60000"
94
94
  }
95
95
  }