@fedify/postgres 2.4.0-pr.936.41 → 2.4.1
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 +9 -0
- package/dist/kv.cjs +107 -18
- package/dist/kv.d.cts +16 -0
- package/dist/kv.d.ts +16 -0
- package/dist/kv.js +103 -16
- package/dist/mq.cjs +27 -16
- package/dist/mq.d.cts +5 -0
- package/dist/mq.d.ts +5 -0
- package/dist/mq.js +26 -15
- package/dist/node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/chunks/apiHelpers.cjs +3 -3
- package/dist/node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/chunks/classApi-basic.cjs +23 -23
- package/dist/node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/chunks/internal.cjs +154 -84
- package/dist/node_modules/.pnpm/temporal-utils@1.0.1/node_modules/temporal-utils/dist/utils.cjs +1 -0
- package/package.json +3 -3
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("./
|
|
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
|
|
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
|
-
#
|
|
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.#
|
|
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
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
#
|
|
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.#
|
|
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
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
#
|
|
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.#
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
signal?.removeEventListener("abort",
|
|
286
|
-
resolve(
|
|
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
|
-
|
|
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))
|
|
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>;
|