@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.
- package/dist/mq.cjs +75 -39
- package/dist/mq.js +75 -39
- 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
|
|
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
|
-
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
194
|
-
else
|
|
195
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
193
|
-
else
|
|
194
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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=
|
|
93
|
+
"test:bun": "bun test --timeout=60000"
|
|
94
94
|
}
|
|
95
95
|
}
|