@fedify/postgres 2.0.0-dev.279 → 2.0.0-dev.335

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 +78 -32
  2. package/dist/mq.js +78 -32
  3. package/package.json +5 -5
package/dist/mq.cjs CHANGED
@@ -35,6 +35,7 @@ var PostgresMessageQueue = class {
35
35
  #channelName;
36
36
  #pollIntervalMs;
37
37
  #initialized;
38
+ #initPromise;
38
39
  #driverSerializesJson = false;
39
40
  constructor(sql, options = {}) {
40
41
  this.#sql = sql;
@@ -113,51 +114,93 @@ var PostgresMessageQueue = class {
113
114
  const { signal } = options;
114
115
  const poll = async () => {
115
116
  while (!signal?.aborted) {
116
- const query = this.#sql`
117
- DELETE FROM ${this.#sql(this.#tableName)}
118
- WHERE id = (
119
- SELECT id
117
+ let processed = false;
118
+ for (const row of await this.#sql`
119
+ WITH candidate AS (
120
+ SELECT id, ordering_key
120
121
  FROM ${this.#sql(this.#tableName)}
121
122
  WHERE created + delay < CURRENT_TIMESTAMP
122
- AND (ordering_key IS NULL
123
- OR pg_try_advisory_lock(
124
- hashtext(${this.#tableName}),
125
- hashtext(ordering_key)
126
- ))
123
+ AND ordering_key IS NULL
127
124
  ORDER BY created
128
125
  LIMIT 1
126
+ FOR UPDATE SKIP LOCKED
129
127
  )
128
+ DELETE FROM ${this.#sql(this.#tableName)}
129
+ WHERE id IN (SELECT id FROM candidate)
130
130
  RETURNING message, ordering_key;
131
- `.execute();
132
- const cancel = query.cancel.bind(query);
133
- signal?.addEventListener("abort", cancel);
134
- let i = 0;
135
- for (const row of await query) {
131
+ `) {
136
132
  if (signal?.aborted) return;
137
- const orderingKey = row.ordering_key;
133
+ await handler(row.message);
134
+ processed = true;
135
+ }
136
+ if (processed) continue;
137
+ const attemptedOrderingKeys = /* @__PURE__ */ new Set();
138
+ while (!signal?.aborted) {
139
+ const candidateResult = await this.#sql`
140
+ SELECT id, ordering_key
141
+ FROM ${this.#sql(this.#tableName)}
142
+ WHERE created + delay < CURRENT_TIMESTAMP
143
+ AND ordering_key IS NOT NULL
144
+ ${attemptedOrderingKeys.size > 0 ? this.#sql`AND ordering_key NOT IN ${this.#sql([...attemptedOrderingKeys])}` : this.#sql``}
145
+ ORDER BY created
146
+ LIMIT 1
147
+ `;
148
+ if (candidateResult.length === 0) break;
149
+ const candidate = candidateResult[0];
150
+ const candidateId = candidate.id;
151
+ const orderingKey = candidate.ordering_key;
152
+ attemptedOrderingKeys.add(orderingKey);
153
+ const reserved = await this.#sql.reserve();
138
154
  try {
139
- await handler(row.message);
155
+ const lockResult = await reserved`
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 reserved`
164
+ DELETE FROM ${reserved(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;
172
+ }
173
+ } finally {
174
+ await reserved`
175
+ SELECT pg_advisory_unlock(
176
+ hashtext(${this.#tableName}),
177
+ hashtext(${orderingKey})
178
+ )
179
+ `;
180
+ }
181
+ if (processed) break;
182
+ }
140
183
  } finally {
141
- if (orderingKey != null) await this.#sql`
142
- SELECT pg_advisory_unlock(
143
- hashtext(${this.#tableName}),
144
- hashtext(${orderingKey})
145
- );
146
- `;
184
+ reserved.release();
147
185
  }
148
- i++;
149
186
  }
150
- signal?.removeEventListener("abort", cancel);
151
- if (i < 1) break;
187
+ if (processed) continue;
188
+ break;
152
189
  }
153
190
  };
191
+ let pollLock = Promise.resolve();
192
+ const serializedPoll = () => {
193
+ const next = pollLock.then(poll);
194
+ pollLock = next.catch(() => {});
195
+ return next;
196
+ };
154
197
  const timeouts = /* @__PURE__ */ new Set();
155
198
  const listen = await this.#sql.listen(this.#channelName, async (delay) => {
156
199
  const duration = Temporal.Duration.from(delay);
157
200
  const durationMs = duration.total("millisecond");
158
- if (durationMs < 1) await poll();
159
- else timeouts.add(setTimeout(poll, durationMs));
160
- }, poll);
201
+ if (durationMs < 1) await serializedPoll();
202
+ else timeouts.add(setTimeout(serializedPoll, durationMs));
203
+ }, serializedPoll);
161
204
  signal?.addEventListener("abort", () => {
162
205
  listen.unlisten();
163
206
  for (const timeout of timeouts) clearTimeout(timeout);
@@ -173,7 +216,7 @@ var PostgresMessageQueue = class {
173
216
  timeouts.add(timeout);
174
217
  });
175
218
  if (timeout != null) timeouts.delete(timeout);
176
- await poll();
219
+ await serializedPoll();
177
220
  }
178
221
  await new Promise((resolve) => {
179
222
  signal?.addEventListener("abort", () => resolve());
@@ -183,8 +226,11 @@ var PostgresMessageQueue = class {
183
226
  /**
184
227
  * Initializes the message queue table if it does not already exist.
185
228
  */
186
- async initialize() {
187
- if (this.#initialized) return;
229
+ initialize() {
230
+ if (this.#initialized) return Promise.resolve();
231
+ return this.#initPromise ??= this.#doInitialize();
232
+ }
233
+ async #doInitialize() {
188
234
  logger.debug("Initializing the message queue table {tableName}...", { tableName: this.#tableName });
189
235
  try {
190
236
  await this.#sql`
@@ -201,7 +247,7 @@ var PostgresMessageQueue = class {
201
247
  ADD COLUMN IF NOT EXISTS ordering_key text;
202
248
  `;
203
249
  } catch (error) {
204
- if (!(error instanceof postgres.default.PostgresError && error.constraint_name === "pg_type_typname_nsp_index")) {
250
+ if (!(error instanceof postgres.default.PostgresError && (error.constraint_name === "pg_type_typname_nsp_index" || error.code === "42P07"))) {
205
251
  logger.error("Failed to initialize the message queue table: {error}", { error });
206
252
  throw error;
207
253
  }
package/dist/mq.js CHANGED
@@ -34,6 +34,7 @@ var PostgresMessageQueue = class {
34
34
  #channelName;
35
35
  #pollIntervalMs;
36
36
  #initialized;
37
+ #initPromise;
37
38
  #driverSerializesJson = false;
38
39
  constructor(sql, options = {}) {
39
40
  this.#sql = sql;
@@ -112,51 +113,93 @@ var PostgresMessageQueue = class {
112
113
  const { signal } = options;
113
114
  const poll = async () => {
114
115
  while (!signal?.aborted) {
115
- const query = this.#sql`
116
- DELETE FROM ${this.#sql(this.#tableName)}
117
- WHERE id = (
118
- SELECT id
116
+ let processed = false;
117
+ for (const row of await this.#sql`
118
+ WITH candidate AS (
119
+ SELECT id, ordering_key
119
120
  FROM ${this.#sql(this.#tableName)}
120
121
  WHERE created + delay < CURRENT_TIMESTAMP
121
- AND (ordering_key IS NULL
122
- OR pg_try_advisory_lock(
123
- hashtext(${this.#tableName}),
124
- hashtext(ordering_key)
125
- ))
122
+ AND ordering_key IS NULL
126
123
  ORDER BY created
127
124
  LIMIT 1
125
+ FOR UPDATE SKIP LOCKED
128
126
  )
127
+ DELETE FROM ${this.#sql(this.#tableName)}
128
+ WHERE id IN (SELECT id FROM candidate)
129
129
  RETURNING message, ordering_key;
130
- `.execute();
131
- const cancel = query.cancel.bind(query);
132
- signal?.addEventListener("abort", cancel);
133
- let i = 0;
134
- for (const row of await query) {
130
+ `) {
135
131
  if (signal?.aborted) return;
136
- const orderingKey = row.ordering_key;
132
+ await handler(row.message);
133
+ processed = true;
134
+ }
135
+ if (processed) continue;
136
+ const attemptedOrderingKeys = /* @__PURE__ */ new Set();
137
+ while (!signal?.aborted) {
138
+ const candidateResult = await this.#sql`
139
+ SELECT id, ordering_key
140
+ FROM ${this.#sql(this.#tableName)}
141
+ WHERE created + delay < CURRENT_TIMESTAMP
142
+ AND ordering_key IS NOT NULL
143
+ ${attemptedOrderingKeys.size > 0 ? this.#sql`AND ordering_key NOT IN ${this.#sql([...attemptedOrderingKeys])}` : this.#sql``}
144
+ ORDER BY created
145
+ LIMIT 1
146
+ `;
147
+ if (candidateResult.length === 0) break;
148
+ const candidate = candidateResult[0];
149
+ const candidateId = candidate.id;
150
+ const orderingKey = candidate.ordering_key;
151
+ attemptedOrderingKeys.add(orderingKey);
152
+ const reserved = await this.#sql.reserve();
137
153
  try {
138
- await handler(row.message);
154
+ const lockResult = await reserved`
155
+ SELECT pg_try_advisory_lock(
156
+ hashtext(${this.#tableName}),
157
+ hashtext(${orderingKey})
158
+ ) AS acquired
159
+ `;
160
+ if (lockResult[0].acquired) {
161
+ try {
162
+ const deleteResult = await reserved`
163
+ DELETE FROM ${reserved(this.#tableName)}
164
+ WHERE id = ${candidateId}
165
+ RETURNING message, ordering_key
166
+ `;
167
+ for (const row of deleteResult) {
168
+ if (signal?.aborted) return;
169
+ await handler(row.message);
170
+ processed = true;
171
+ }
172
+ } finally {
173
+ await reserved`
174
+ SELECT pg_advisory_unlock(
175
+ hashtext(${this.#tableName}),
176
+ hashtext(${orderingKey})
177
+ )
178
+ `;
179
+ }
180
+ if (processed) break;
181
+ }
139
182
  } finally {
140
- if (orderingKey != null) await this.#sql`
141
- SELECT pg_advisory_unlock(
142
- hashtext(${this.#tableName}),
143
- hashtext(${orderingKey})
144
- );
145
- `;
183
+ reserved.release();
146
184
  }
147
- i++;
148
185
  }
149
- signal?.removeEventListener("abort", cancel);
150
- if (i < 1) break;
186
+ if (processed) continue;
187
+ break;
151
188
  }
152
189
  };
190
+ let pollLock = Promise.resolve();
191
+ const serializedPoll = () => {
192
+ const next = pollLock.then(poll);
193
+ pollLock = next.catch(() => {});
194
+ return next;
195
+ };
153
196
  const timeouts = /* @__PURE__ */ new Set();
154
197
  const listen = await this.#sql.listen(this.#channelName, async (delay) => {
155
198
  const duration = Temporal.Duration.from(delay);
156
199
  const durationMs = duration.total("millisecond");
157
- if (durationMs < 1) await poll();
158
- else timeouts.add(setTimeout(poll, durationMs));
159
- }, poll);
200
+ if (durationMs < 1) await serializedPoll();
201
+ else timeouts.add(setTimeout(serializedPoll, durationMs));
202
+ }, serializedPoll);
160
203
  signal?.addEventListener("abort", () => {
161
204
  listen.unlisten();
162
205
  for (const timeout of timeouts) clearTimeout(timeout);
@@ -172,7 +215,7 @@ var PostgresMessageQueue = class {
172
215
  timeouts.add(timeout);
173
216
  });
174
217
  if (timeout != null) timeouts.delete(timeout);
175
- await poll();
218
+ await serializedPoll();
176
219
  }
177
220
  await new Promise((resolve) => {
178
221
  signal?.addEventListener("abort", () => resolve());
@@ -182,8 +225,11 @@ var PostgresMessageQueue = class {
182
225
  /**
183
226
  * Initializes the message queue table if it does not already exist.
184
227
  */
185
- async initialize() {
186
- if (this.#initialized) return;
228
+ initialize() {
229
+ if (this.#initialized) return Promise.resolve();
230
+ return this.#initPromise ??= this.#doInitialize();
231
+ }
232
+ async #doInitialize() {
187
233
  logger.debug("Initializing the message queue table {tableName}...", { tableName: this.#tableName });
188
234
  try {
189
235
  await this.#sql`
@@ -200,7 +246,7 @@ var PostgresMessageQueue = class {
200
246
  ADD COLUMN IF NOT EXISTS ordering_key text;
201
247
  `;
202
248
  } catch (error) {
203
- if (!(error instanceof postgres.PostgresError && error.constraint_name === "pg_type_typname_nsp_index")) {
249
+ if (!(error instanceof postgres.PostgresError && (error.constraint_name === "pg_type_typname_nsp_index" || error.code === "42P07"))) {
204
250
  logger.error("Failed to initialize the message queue table: {error}", { error });
205
251
  throw error;
206
252
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/postgres",
3
- "version": "2.0.0-dev.279+ce1bdc22",
3
+ "version": "2.0.0-dev.335+6fe86704",
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-dev.279+ce1bdc22"
77
+ "@fedify/fedify": "^2.0.0-dev.335+6fe86704"
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/fixture": "^2.0.0",
84
- "@fedify/testing": "^2.0.0-dev.279+ce1bdc22"
83
+ "@fedify/testing": "^2.0.0-dev.335+6fe86704",
84
+ "@fedify/fixture": "^2.0.0"
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
  }