@fedify/postgres 2.4.0-pr.936.41 → 2.4.0

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/README.md CHANGED
@@ -25,6 +25,15 @@ const federation = createFederation({
25
25
  });
26
26
  ~~~~
27
27
 
28
+ `PostgresKvStore` uses a crash-safe logged table by default. For transient
29
+ data where losing the table contents after a PostgreSQL crash is acceptable,
30
+ pass `{ unlogged: true }` as its second constructor argument.
31
+
32
+ When a store created by an earlier version still uses an unlogged table, its
33
+ next initialization converts that table to logged storage. PostgreSQL rewrites
34
+ the table and takes an `ACCESS EXCLUSIVE` lock during this one-time migration,
35
+ so schedule the upgrade accordingly for a large or busy table.
36
+
28
37
  [JSR badge]: https://jsr.io/badges/@fedify/postgres
29
38
  [JSR]: https://jsr.io/@fedify/postgres
30
39
  [npm badge]: https://img.shields.io/npm/v/@fedify/postgres?logo=npm
package/dist/kv.cjs CHANGED
@@ -1,13 +1,18 @@
1
1
 
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- require("./_virtual/_rolldown/runtime.cjs");
3
+ const require_index = require("./node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/index.cjs");
4
4
  const require_utils = require("./utils.cjs");
5
+ let _logtape_logtape = require("@logtape/logtape");
5
6
  //#region src/kv.ts
6
- const logger = (0, require("@logtape/logtape").getLogger)([
7
+ const Temporal = require_index.Temporal;
8
+ const logger = (0, _logtape_logtape.getLogger)([
7
9
  "fedify",
8
10
  "postgres",
9
11
  "kv"
10
12
  ]);
13
+ function quoteIdentifier(identifier) {
14
+ return `"${identifier.replaceAll("\"", "\"\"").replaceAll(".", "\".\"")}"`;
15
+ }
11
16
  /**
12
17
  * A key–value store that uses PostgreSQL as the underlying storage.
13
18
  *
@@ -26,7 +31,10 @@ const logger = (0, require("@logtape/logtape").getLogger)([
26
31
  var PostgresKvStore = class {
27
32
  #sql;
28
33
  #tableName;
29
- #initialized;
34
+ #unlogged;
35
+ #skipDdl;
36
+ #initialized = false;
37
+ #initializing;
30
38
  #driverSerializesJson = false;
31
39
  /**
32
40
  * Creates a new PostgreSQL key–value store.
@@ -36,7 +44,8 @@ var PostgresKvStore = class {
36
44
  constructor(sql, options = {}) {
37
45
  this.#sql = sql;
38
46
  this.#tableName = options.tableName ?? "fedify_kv_v2";
39
- this.#initialized = options.initialized ?? false;
47
+ this.#unlogged = options.unlogged ?? false;
48
+ this.#skipDdl = options.initialized ?? false;
40
49
  }
41
50
  async #expire() {
42
51
  await this.#sql`
@@ -56,7 +65,7 @@ var PostgresKvStore = class {
56
65
  }
57
66
  async set(key, value, options) {
58
67
  await this.initialize();
59
- const ttl = options?.ttl == null ? null : options.ttl.toString();
68
+ const ttl = options?.ttl == null ? null : Temporal.Duration.from(options.ttl).toString();
60
69
  await this.#sql`
61
70
  INSERT INTO ${this.#sql(this.#tableName)} (key, value, ttl)
62
71
  VALUES (
@@ -65,7 +74,10 @@ var PostgresKvStore = class {
65
74
  ${ttl}
66
75
  )
67
76
  ON CONFLICT (key)
68
- DO UPDATE SET value = EXCLUDED.value, ttl = EXCLUDED.ttl;
77
+ DO UPDATE SET
78
+ value = EXCLUDED.value,
79
+ created = CURRENT_TIMESTAMP,
80
+ ttl = EXCLUDED.ttl;
69
81
  `;
70
82
  await this.#expire();
71
83
  }
@@ -78,6 +90,54 @@ var PostgresKvStore = class {
78
90
  await this.#expire();
79
91
  }
80
92
  /**
93
+ * {@inheritDoc KvStore.cas}
94
+ * @since 2.4.0
95
+ */
96
+ async cas(key, expectedValue, newValue, options) {
97
+ await this.initialize();
98
+ const ttl = options?.ttl == null ? null : Temporal.Duration.from(options.ttl).toString();
99
+ if (expectedValue === void 0 && newValue === void 0) return await this.get(key) === void 0;
100
+ let result;
101
+ if (expectedValue === void 0) result = await this.#sql`
102
+ INSERT INTO ${this.#sql(this.#tableName)} AS existing
103
+ (key, value, created, ttl)
104
+ VALUES (
105
+ ${key},
106
+ ${this.#json(newValue)},
107
+ CURRENT_TIMESTAMP,
108
+ ${ttl}
109
+ )
110
+ ON CONFLICT (key)
111
+ DO UPDATE SET
112
+ value = EXCLUDED.value,
113
+ created = EXCLUDED.created,
114
+ ttl = EXCLUDED.ttl
115
+ WHERE existing.ttl IS NOT NULL
116
+ AND existing.created + existing.ttl <= CURRENT_TIMESTAMP
117
+ RETURNING key;
118
+ `;
119
+ else if (newValue === void 0) result = await this.#sql`
120
+ DELETE FROM ${this.#sql(this.#tableName)}
121
+ WHERE key = ${key}
122
+ AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
123
+ AND value = ${this.#json(expectedValue)}
124
+ RETURNING key;
125
+ `;
126
+ else result = await this.#sql`
127
+ UPDATE ${this.#sql(this.#tableName)}
128
+ SET
129
+ value = ${this.#json(newValue)},
130
+ created = CURRENT_TIMESTAMP,
131
+ ttl = ${ttl}
132
+ WHERE key = ${key}
133
+ AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
134
+ AND value = ${this.#json(expectedValue)}
135
+ RETURNING key;
136
+ `;
137
+ await this.#expire();
138
+ return result.length > 0;
139
+ }
140
+ /**
81
141
  * {@inheritDoc KvStore.list}
82
142
  * @since 1.10.0
83
143
  */
@@ -112,18 +172,46 @@ var PostgresKvStore = class {
112
172
  */
113
173
  async initialize() {
114
174
  if (this.#initialized) return;
115
- logger.debug("Initializing the key–value store table {tableName}...", { tableName: this.#tableName });
116
- await this.#sql`
117
- CREATE UNLOGGED TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
118
- key text[] PRIMARY KEY,
119
- value jsonb NOT NULL,
120
- created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
121
- ttl interval
122
- );
123
- `;
124
- this.#driverSerializesJson = await require_utils.driverSerializesJson(this.#sql);
125
- this.#initialized = true;
126
- logger.debug("Initialized the key–value store table {tableName}.", { tableName: this.#tableName });
175
+ this.#initializing ??= (async () => {
176
+ logger.debug("Initializing the key–value store table {tableName}...", { tableName: this.#tableName });
177
+ if (!this.#skipDdl) await this.#initializeTable();
178
+ this.#driverSerializesJson = await require_utils.driverSerializesJson(this.#sql);
179
+ this.#initialized = true;
180
+ logger.debug("Initialized the key–value store table {tableName}.", { tableName: this.#tableName });
181
+ })();
182
+ try {
183
+ await this.#initializing;
184
+ } catch (error) {
185
+ this.#initializing = void 0;
186
+ throw error;
187
+ }
188
+ }
189
+ async #initializeTable() {
190
+ if (this.#unlogged) await this.#sql`
191
+ CREATE UNLOGGED TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
192
+ key text[] PRIMARY KEY,
193
+ value jsonb NOT NULL,
194
+ created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
195
+ ttl interval
196
+ );
197
+ `;
198
+ else {
199
+ await this.#sql`
200
+ CREATE TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
201
+ key text[] PRIMARY KEY,
202
+ value jsonb NOT NULL,
203
+ created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
204
+ ttl interval
205
+ );
206
+ `;
207
+ if ((await this.#sql`
208
+ SELECT relpersistence
209
+ FROM pg_class
210
+ WHERE oid = to_regclass(${quoteIdentifier(this.#tableName)});
211
+ `)[0]?.relpersistence === "u") await this.#sql`
212
+ ALTER TABLE ${this.#sql(this.#tableName)} SET LOGGED;
213
+ `;
214
+ }
127
215
  }
128
216
  /**
129
217
  * Drops the table used by the key–value store. Does nothing if the table
@@ -133,6 +221,7 @@ var PostgresKvStore = class {
133
221
  await this.#sql`DROP TABLE IF EXISTS ${this.#sql(this.#tableName)};`;
134
222
  }
135
223
  #json(value) {
224
+ if (value === null) return this.#sql`'null'::jsonb`;
136
225
  if (this.#driverSerializesJson) return this.#sql.json(value);
137
226
  return this.#sql.json(JSON.stringify(value));
138
227
  }
package/dist/kv.d.cts CHANGED
@@ -15,9 +15,20 @@ interface PostgresKvStoreOptions {
15
15
  readonly tableName?: string;
16
16
  /**
17
17
  * Whether the table has been initialized. `false` by default.
18
+ *
19
+ * This skips only the table's schema DDL. Driver-specific runtime setup,
20
+ * such as detecting whether the driver serializes JSON parameters on its
21
+ * own, still runs before the first key is read or written.
18
22
  * @default `false`
19
23
  */
20
24
  readonly initialized?: boolean;
25
+ /**
26
+ * Whether to use an unlogged table. Unlogged tables are faster, but are
27
+ * truncated after a PostgreSQL crash and are not replicated. `false` by
28
+ * default.
29
+ * @default `false`
30
+ */
31
+ readonly unlogged?: boolean;
21
32
  }
22
33
  /**
23
34
  * A key–value store that uses PostgreSQL as the underlying storage.
@@ -46,6 +57,11 @@ declare class PostgresKvStore implements KvStore {
46
57
  set(key: KvKey, value: unknown, options?: KvStoreSetOptions | undefined): Promise<void>;
47
58
  delete(key: KvKey): Promise<void>;
48
59
  /**
60
+ * {@inheritDoc KvStore.cas}
61
+ * @since 2.4.0
62
+ */
63
+ cas(key: KvKey, expectedValue: unknown, newValue: unknown, options?: KvStoreSetOptions): Promise<boolean>;
64
+ /**
49
65
  * {@inheritDoc KvStore.list}
50
66
  * @since 1.10.0
51
67
  */
package/dist/kv.d.ts CHANGED
@@ -15,9 +15,20 @@ interface PostgresKvStoreOptions {
15
15
  readonly tableName?: string;
16
16
  /**
17
17
  * Whether the table has been initialized. `false` by default.
18
+ *
19
+ * This skips only the table's schema DDL. Driver-specific runtime setup,
20
+ * such as detecting whether the driver serializes JSON parameters on its
21
+ * own, still runs before the first key is read or written.
18
22
  * @default `false`
19
23
  */
20
24
  readonly initialized?: boolean;
25
+ /**
26
+ * Whether to use an unlogged table. Unlogged tables are faster, but are
27
+ * truncated after a PostgreSQL crash and are not replicated. `false` by
28
+ * default.
29
+ * @default `false`
30
+ */
31
+ readonly unlogged?: boolean;
21
32
  }
22
33
  /**
23
34
  * A key–value store that uses PostgreSQL as the underlying storage.
@@ -46,6 +57,11 @@ declare class PostgresKvStore implements KvStore {
46
57
  set(key: KvKey, value: unknown, options?: KvStoreSetOptions | undefined): Promise<void>;
47
58
  delete(key: KvKey): Promise<void>;
48
59
  /**
60
+ * {@inheritDoc KvStore.cas}
61
+ * @since 2.4.0
62
+ */
63
+ cas(key: KvKey, expectedValue: unknown, newValue: unknown, options?: KvStoreSetOptions): Promise<boolean>;
64
+ /**
49
65
  * {@inheritDoc KvStore.list}
50
66
  * @since 1.10.0
51
67
  */
package/dist/kv.js CHANGED
@@ -7,6 +7,9 @@ const logger = getLogger([
7
7
  "postgres",
8
8
  "kv"
9
9
  ]);
10
+ function quoteIdentifier(identifier) {
11
+ return `"${identifier.replaceAll("\"", "\"\"").replaceAll(".", "\".\"")}"`;
12
+ }
10
13
  /**
11
14
  * A key–value store that uses PostgreSQL as the underlying storage.
12
15
  *
@@ -25,7 +28,10 @@ const logger = getLogger([
25
28
  var PostgresKvStore = class {
26
29
  #sql;
27
30
  #tableName;
28
- #initialized;
31
+ #unlogged;
32
+ #skipDdl;
33
+ #initialized = false;
34
+ #initializing;
29
35
  #driverSerializesJson = false;
30
36
  /**
31
37
  * Creates a new PostgreSQL key–value store.
@@ -35,7 +41,8 @@ var PostgresKvStore = class {
35
41
  constructor(sql, options = {}) {
36
42
  this.#sql = sql;
37
43
  this.#tableName = options.tableName ?? "fedify_kv_v2";
38
- this.#initialized = options.initialized ?? false;
44
+ this.#unlogged = options.unlogged ?? false;
45
+ this.#skipDdl = options.initialized ?? false;
39
46
  }
40
47
  async #expire() {
41
48
  await this.#sql`
@@ -55,7 +62,7 @@ var PostgresKvStore = class {
55
62
  }
56
63
  async set(key, value, options) {
57
64
  await this.initialize();
58
- const ttl = options?.ttl == null ? null : options.ttl.toString();
65
+ const ttl = options?.ttl == null ? null : Temporal.Duration.from(options.ttl).toString();
59
66
  await this.#sql`
60
67
  INSERT INTO ${this.#sql(this.#tableName)} (key, value, ttl)
61
68
  VALUES (
@@ -64,7 +71,10 @@ var PostgresKvStore = class {
64
71
  ${ttl}
65
72
  )
66
73
  ON CONFLICT (key)
67
- DO UPDATE SET value = EXCLUDED.value, ttl = EXCLUDED.ttl;
74
+ DO UPDATE SET
75
+ value = EXCLUDED.value,
76
+ created = CURRENT_TIMESTAMP,
77
+ ttl = EXCLUDED.ttl;
68
78
  `;
69
79
  await this.#expire();
70
80
  }
@@ -77,6 +87,54 @@ var PostgresKvStore = class {
77
87
  await this.#expire();
78
88
  }
79
89
  /**
90
+ * {@inheritDoc KvStore.cas}
91
+ * @since 2.4.0
92
+ */
93
+ async cas(key, expectedValue, newValue, options) {
94
+ await this.initialize();
95
+ const ttl = options?.ttl == null ? null : Temporal.Duration.from(options.ttl).toString();
96
+ if (expectedValue === void 0 && newValue === void 0) return await this.get(key) === void 0;
97
+ let result;
98
+ if (expectedValue === void 0) result = await this.#sql`
99
+ INSERT INTO ${this.#sql(this.#tableName)} AS existing
100
+ (key, value, created, ttl)
101
+ VALUES (
102
+ ${key},
103
+ ${this.#json(newValue)},
104
+ CURRENT_TIMESTAMP,
105
+ ${ttl}
106
+ )
107
+ ON CONFLICT (key)
108
+ DO UPDATE SET
109
+ value = EXCLUDED.value,
110
+ created = EXCLUDED.created,
111
+ ttl = EXCLUDED.ttl
112
+ WHERE existing.ttl IS NOT NULL
113
+ AND existing.created + existing.ttl <= CURRENT_TIMESTAMP
114
+ RETURNING key;
115
+ `;
116
+ else if (newValue === void 0) result = await this.#sql`
117
+ DELETE FROM ${this.#sql(this.#tableName)}
118
+ WHERE key = ${key}
119
+ AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
120
+ AND value = ${this.#json(expectedValue)}
121
+ RETURNING key;
122
+ `;
123
+ else result = await this.#sql`
124
+ UPDATE ${this.#sql(this.#tableName)}
125
+ SET
126
+ value = ${this.#json(newValue)},
127
+ created = CURRENT_TIMESTAMP,
128
+ ttl = ${ttl}
129
+ WHERE key = ${key}
130
+ AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
131
+ AND value = ${this.#json(expectedValue)}
132
+ RETURNING key;
133
+ `;
134
+ await this.#expire();
135
+ return result.length > 0;
136
+ }
137
+ /**
80
138
  * {@inheritDoc KvStore.list}
81
139
  * @since 1.10.0
82
140
  */
@@ -111,18 +169,46 @@ var PostgresKvStore = class {
111
169
  */
112
170
  async initialize() {
113
171
  if (this.#initialized) return;
114
- logger.debug("Initializing the key–value store table {tableName}...", { tableName: this.#tableName });
115
- await this.#sql`
116
- CREATE UNLOGGED TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
117
- key text[] PRIMARY KEY,
118
- value jsonb NOT NULL,
119
- created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
120
- ttl interval
121
- );
122
- `;
123
- this.#driverSerializesJson = await driverSerializesJson(this.#sql);
124
- this.#initialized = true;
125
- logger.debug("Initialized the key–value store table {tableName}.", { tableName: this.#tableName });
172
+ this.#initializing ??= (async () => {
173
+ logger.debug("Initializing the key–value store table {tableName}...", { tableName: this.#tableName });
174
+ if (!this.#skipDdl) await this.#initializeTable();
175
+ this.#driverSerializesJson = await driverSerializesJson(this.#sql);
176
+ this.#initialized = true;
177
+ logger.debug("Initialized the key–value store table {tableName}.", { tableName: this.#tableName });
178
+ })();
179
+ try {
180
+ await this.#initializing;
181
+ } catch (error) {
182
+ this.#initializing = void 0;
183
+ throw error;
184
+ }
185
+ }
186
+ async #initializeTable() {
187
+ if (this.#unlogged) await this.#sql`
188
+ CREATE UNLOGGED TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
189
+ key text[] PRIMARY KEY,
190
+ value jsonb NOT NULL,
191
+ created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
192
+ ttl interval
193
+ );
194
+ `;
195
+ else {
196
+ await this.#sql`
197
+ CREATE TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
198
+ key text[] PRIMARY KEY,
199
+ value jsonb NOT NULL,
200
+ created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
201
+ ttl interval
202
+ );
203
+ `;
204
+ if ((await this.#sql`
205
+ SELECT relpersistence
206
+ FROM pg_class
207
+ WHERE oid = to_regclass(${quoteIdentifier(this.#tableName)});
208
+ `)[0]?.relpersistence === "u") await this.#sql`
209
+ ALTER TABLE ${this.#sql(this.#tableName)} SET LOGGED;
210
+ `;
211
+ }
126
212
  }
127
213
  /**
128
214
  * Drops the table used by the key–value store. Does nothing if the table
@@ -132,6 +218,7 @@ var PostgresKvStore = class {
132
218
  await this.#sql`DROP TABLE IF EXISTS ${this.#sql(this.#tableName)};`;
133
219
  }
134
220
  #json(value) {
221
+ if (value === null) return this.#sql`'null'::jsonb`;
135
222
  if (this.#driverSerializesJson) return this.#sql.json(value);
136
223
  return this.#sql.json(JSON.stringify(value));
137
224
  }
package/dist/mq.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
4
- const require_utils = require("./utils.cjs");
5
4
  const require_index = require("./node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/index.cjs");
5
+ const require_utils = require("./utils.cjs");
6
6
  let _logtape_logtape = require("@logtape/logtape");
7
7
  let postgres = require("postgres");
8
8
  postgres = require_runtime.__toESM(postgres, 1);
@@ -57,12 +57,14 @@ function getCreatedIndexName(tableName) {
57
57
  * ```
58
58
  */
59
59
  var PostgresMessageQueue = class {
60
+ atomicEnqueueMany = false;
60
61
  #sql;
61
62
  #tableName;
62
63
  #channelName;
63
64
  #pollIntervalMs;
64
65
  #handlerTimeoutMs;
65
- #initialized;
66
+ #skipDdl;
67
+ #initialized = false;
66
68
  #initPromise;
67
69
  #driverSerializesJson = false;
68
70
  constructor(sql, options = {}) {
@@ -71,7 +73,7 @@ var PostgresMessageQueue = class {
71
73
  this.#channelName = options?.channelName ?? "fedify_channel";
72
74
  this.#pollIntervalMs = Temporal.Duration.from(options?.pollInterval ?? { seconds: 5 }).total("millisecond");
73
75
  this.#handlerTimeoutMs = Temporal.Duration.from(options?.handlerTimeout ?? { seconds: 60 }).total("millisecond");
74
- this.#initialized = options?.initialized ?? false;
76
+ this.#skipDdl = options?.initialized ?? false;
75
77
  }
76
78
  async enqueue(message, options) {
77
79
  await this.initialize();
@@ -159,6 +161,7 @@ var PostgresMessageQueue = class {
159
161
  async listen(handler, options = {}) {
160
162
  await this.initialize();
161
163
  const { signal } = options;
164
+ if (signal?.aborted) return;
162
165
  const poll = async () => {
163
166
  while (!signal?.aborted) {
164
167
  let processed = false;
@@ -252,6 +255,7 @@ var PostgresMessageQueue = class {
252
255
  };
253
256
  const timeouts = /* @__PURE__ */ new Set();
254
257
  const listen = await this.#sql.listen(this.#channelName, async (delay) => {
258
+ if (signal?.aborted) return;
255
259
  try {
256
260
  const durationMs = Temporal.Duration.from(delay).total("millisecond");
257
261
  if (durationMs < 1) await safeSerializedPoll("notify-immediate");
@@ -275,31 +279,35 @@ var PostgresMessageQueue = class {
275
279
  timeouts.clear();
276
280
  };
277
281
  signal?.addEventListener("abort", clearTimeouts, { once: true });
278
- let unlistenError;
279
282
  try {
280
283
  while (!signal?.aborted) {
281
- let timeout;
282
284
  await new Promise((resolve) => {
283
- signal?.addEventListener("abort", resolve, { once: true });
284
- timeout = setTimeout(() => {
285
- signal?.removeEventListener("abort", resolve);
286
- resolve(0);
285
+ const onAbort = () => {
286
+ clearTimeout(timeout);
287
+ signal?.removeEventListener("abort", onAbort);
288
+ resolve();
289
+ };
290
+ const timeout = setTimeout(() => {
291
+ signal?.removeEventListener("abort", onAbort);
292
+ resolve();
287
293
  }, this.#pollIntervalMs);
288
- timeouts.add(timeout);
294
+ signal?.addEventListener("abort", onAbort, { once: true });
289
295
  });
290
- if (timeout != null) timeouts.delete(timeout);
291
296
  await safeSerializedPoll("interval");
292
297
  }
293
298
  } finally {
294
299
  signal?.removeEventListener("abort", clearTimeouts);
295
300
  clearTimeouts();
301
+ await pollLock;
296
302
  try {
297
303
  await listen.unlisten();
298
304
  } catch (error) {
299
- if (!isConnectionDestroyedError(error)) unlistenError = error;
305
+ if (!isConnectionDestroyedError(error)) logger.error("Error while unlistening from channel {channelName}: {error}", {
306
+ channelName: this.#channelName,
307
+ error
308
+ });
300
309
  }
301
310
  }
302
- if (unlistenError != null) throw unlistenError;
303
311
  }
304
312
  /**
305
313
  * Initializes the message queue table if it does not already exist.
@@ -310,6 +318,12 @@ var PostgresMessageQueue = class {
310
318
  }
311
319
  async #doInitialize() {
312
320
  logger.debug("Initializing the message queue table {tableName}...", { tableName: this.#tableName });
321
+ if (!this.#skipDdl) await this.#initializeTable();
322
+ this.#driverSerializesJson = await require_utils.driverSerializesJson(this.#sql);
323
+ this.#initialized = true;
324
+ logger.debug("Initialized the message queue table {tableName}.", { tableName: this.#tableName });
325
+ }
326
+ async #initializeTable() {
313
327
  for (let attempt = 1; attempt <= INITIALIZE_MAX_ATTEMPTS; attempt++) try {
314
328
  await this.#sql`
315
329
  CREATE TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
@@ -344,9 +358,6 @@ var PostgresMessageQueue = class {
344
358
  });
345
359
  await sleep(backoffMs);
346
360
  }
347
- this.#driverSerializesJson = await require_utils.driverSerializesJson(this.#sql);
348
- this.#initialized = true;
349
- logger.debug("Initialized the message queue table {tableName}.", { tableName: this.#tableName });
350
361
  }
351
362
  /**
352
363
  * Drops the message queue table if it exists.
package/dist/mq.d.cts CHANGED
@@ -21,6 +21,10 @@ interface PostgresMessageQueueOptions {
21
21
  readonly channelName?: string;
22
22
  /**
23
23
  * Whether the table has been initialized. `false` by default.
24
+ *
25
+ * This skips only the table's schema DDL. Driver-specific runtime setup,
26
+ * such as detecting whether the driver serializes JSON parameters on its
27
+ * own, still runs before the first message is enqueued.
24
28
  * @default `false`
25
29
  */
26
30
  readonly initialized?: boolean;
@@ -62,6 +66,7 @@ interface PostgresMessageQueueOptions {
62
66
  */
63
67
  declare class PostgresMessageQueue implements MessageQueue {
64
68
  #private;
69
+ readonly atomicEnqueueMany = false;
65
70
  constructor(sql: Sql<{}>, options?: PostgresMessageQueueOptions);
66
71
  enqueue(message: any, options?: MessageQueueEnqueueOptions): Promise<void>;
67
72
  enqueueMany(messages: readonly any[], options?: MessageQueueEnqueueOptions): Promise<void>;
package/dist/mq.d.ts CHANGED
@@ -21,6 +21,10 @@ interface PostgresMessageQueueOptions {
21
21
  readonly channelName?: string;
22
22
  /**
23
23
  * Whether the table has been initialized. `false` by default.
24
+ *
25
+ * This skips only the table's schema DDL. Driver-specific runtime setup,
26
+ * such as detecting whether the driver serializes JSON parameters on its
27
+ * own, still runs before the first message is enqueued.
24
28
  * @default `false`
25
29
  */
26
30
  readonly initialized?: boolean;
@@ -62,6 +66,7 @@ interface PostgresMessageQueueOptions {
62
66
  */
63
67
  declare class PostgresMessageQueue implements MessageQueue {
64
68
  #private;
69
+ readonly atomicEnqueueMany = false;
65
70
  constructor(sql: Sql<{}>, options?: PostgresMessageQueueOptions);
66
71
  enqueue(message: any, options?: MessageQueueEnqueueOptions): Promise<void>;
67
72
  enqueueMany(messages: readonly any[], options?: MessageQueueEnqueueOptions): Promise<void>;