@fedify/postgres 2.4.0-dev.1655 → 2.4.0-pr.934.40
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 +91 -12
- package/dist/kv.d.cts +12 -0
- package/dist/kv.d.ts +12 -0
- package/dist/kv.js +91 -12
- package/dist/mq.cjs +1 -0
- package/dist/mq.d.cts +1 -0
- package/dist/mq.d.ts +1 -0
- package/dist/mq.js +1 -0
- package/package.json +4 -4
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
|
@@ -8,6 +8,9 @@ const logger = (0, require("@logtape/logtape").getLogger)([
|
|
|
8
8
|
"postgres",
|
|
9
9
|
"kv"
|
|
10
10
|
]);
|
|
11
|
+
function quoteIdentifier(identifier) {
|
|
12
|
+
return `"${identifier.replaceAll("\"", "\"\"").replaceAll(".", "\".\"")}"`;
|
|
13
|
+
}
|
|
11
14
|
/**
|
|
12
15
|
* A key–value store that uses PostgreSQL as the underlying storage.
|
|
13
16
|
*
|
|
@@ -26,7 +29,9 @@ const logger = (0, require("@logtape/logtape").getLogger)([
|
|
|
26
29
|
var PostgresKvStore = class {
|
|
27
30
|
#sql;
|
|
28
31
|
#tableName;
|
|
32
|
+
#unlogged;
|
|
29
33
|
#initialized;
|
|
34
|
+
#initializing;
|
|
30
35
|
#driverSerializesJson = false;
|
|
31
36
|
/**
|
|
32
37
|
* Creates a new PostgreSQL key–value store.
|
|
@@ -36,6 +41,7 @@ var PostgresKvStore = class {
|
|
|
36
41
|
constructor(sql, options = {}) {
|
|
37
42
|
this.#sql = sql;
|
|
38
43
|
this.#tableName = options.tableName ?? "fedify_kv_v2";
|
|
44
|
+
this.#unlogged = options.unlogged ?? false;
|
|
39
45
|
this.#initialized = options.initialized ?? false;
|
|
40
46
|
}
|
|
41
47
|
async #expire() {
|
|
@@ -78,6 +84,54 @@ var PostgresKvStore = class {
|
|
|
78
84
|
await this.#expire();
|
|
79
85
|
}
|
|
80
86
|
/**
|
|
87
|
+
* {@inheritDoc KvStore.cas}
|
|
88
|
+
* @since 2.4.0
|
|
89
|
+
*/
|
|
90
|
+
async cas(key, expectedValue, newValue, options) {
|
|
91
|
+
await this.initialize();
|
|
92
|
+
const ttl = options?.ttl == null ? null : options.ttl.toString();
|
|
93
|
+
if (expectedValue === void 0 && newValue === void 0) return await this.get(key) === void 0;
|
|
94
|
+
let result;
|
|
95
|
+
if (expectedValue === void 0) result = await this.#sql`
|
|
96
|
+
INSERT INTO ${this.#sql(this.#tableName)} AS existing
|
|
97
|
+
(key, value, created, ttl)
|
|
98
|
+
VALUES (
|
|
99
|
+
${key},
|
|
100
|
+
${this.#json(newValue)},
|
|
101
|
+
CURRENT_TIMESTAMP,
|
|
102
|
+
${ttl}
|
|
103
|
+
)
|
|
104
|
+
ON CONFLICT (key)
|
|
105
|
+
DO UPDATE SET
|
|
106
|
+
value = EXCLUDED.value,
|
|
107
|
+
created = EXCLUDED.created,
|
|
108
|
+
ttl = EXCLUDED.ttl
|
|
109
|
+
WHERE existing.ttl IS NOT NULL
|
|
110
|
+
AND existing.created + existing.ttl <= CURRENT_TIMESTAMP
|
|
111
|
+
RETURNING key;
|
|
112
|
+
`;
|
|
113
|
+
else if (newValue === void 0) result = await this.#sql`
|
|
114
|
+
DELETE FROM ${this.#sql(this.#tableName)}
|
|
115
|
+
WHERE key = ${key}
|
|
116
|
+
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
|
|
117
|
+
AND value = ${this.#json(expectedValue)}
|
|
118
|
+
RETURNING key;
|
|
119
|
+
`;
|
|
120
|
+
else result = await this.#sql`
|
|
121
|
+
UPDATE ${this.#sql(this.#tableName)}
|
|
122
|
+
SET
|
|
123
|
+
value = ${this.#json(newValue)},
|
|
124
|
+
created = CURRENT_TIMESTAMP,
|
|
125
|
+
ttl = ${ttl}
|
|
126
|
+
WHERE key = ${key}
|
|
127
|
+
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
|
|
128
|
+
AND value = ${this.#json(expectedValue)}
|
|
129
|
+
RETURNING key;
|
|
130
|
+
`;
|
|
131
|
+
await this.#expire();
|
|
132
|
+
return result.length > 0;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
81
135
|
* {@inheritDoc KvStore.list}
|
|
82
136
|
* @since 1.10.0
|
|
83
137
|
*/
|
|
@@ -112,18 +166,43 @@ var PostgresKvStore = class {
|
|
|
112
166
|
*/
|
|
113
167
|
async initialize() {
|
|
114
168
|
if (this.#initialized) return;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
169
|
+
this.#initializing ??= (async () => {
|
|
170
|
+
logger.debug("Initializing the key–value store table {tableName}...", { tableName: this.#tableName });
|
|
171
|
+
if (this.#unlogged) await this.#sql`
|
|
172
|
+
CREATE UNLOGGED TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
|
|
173
|
+
key text[] PRIMARY KEY,
|
|
174
|
+
value jsonb NOT NULL,
|
|
175
|
+
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
|
|
176
|
+
ttl interval
|
|
177
|
+
);
|
|
178
|
+
`;
|
|
179
|
+
else {
|
|
180
|
+
await this.#sql`
|
|
181
|
+
CREATE TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
|
|
182
|
+
key text[] PRIMARY KEY,
|
|
183
|
+
value jsonb NOT NULL,
|
|
184
|
+
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
|
|
185
|
+
ttl interval
|
|
186
|
+
);
|
|
187
|
+
`;
|
|
188
|
+
if ((await this.#sql`
|
|
189
|
+
SELECT relpersistence
|
|
190
|
+
FROM pg_class
|
|
191
|
+
WHERE oid = to_regclass(${quoteIdentifier(this.#tableName)});
|
|
192
|
+
`)[0]?.relpersistence === "u") await this.#sql`
|
|
193
|
+
ALTER TABLE ${this.#sql(this.#tableName)} SET LOGGED;
|
|
194
|
+
`;
|
|
195
|
+
}
|
|
196
|
+
this.#driverSerializesJson = await require_utils.driverSerializesJson(this.#sql);
|
|
197
|
+
this.#initialized = true;
|
|
198
|
+
logger.debug("Initialized the key–value store table {tableName}.", { tableName: this.#tableName });
|
|
199
|
+
})();
|
|
200
|
+
try {
|
|
201
|
+
await this.#initializing;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
this.#initializing = void 0;
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
127
206
|
}
|
|
128
207
|
/**
|
|
129
208
|
* Drops the table used by the key–value store. Does nothing if the table
|
package/dist/kv.d.cts
CHANGED
|
@@ -18,6 +18,13 @@ interface PostgresKvStoreOptions {
|
|
|
18
18
|
* @default `false`
|
|
19
19
|
*/
|
|
20
20
|
readonly initialized?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether to use an unlogged table. Unlogged tables are faster, but are
|
|
23
|
+
* truncated after a PostgreSQL crash and are not replicated. `false` by
|
|
24
|
+
* default.
|
|
25
|
+
* @default `false`
|
|
26
|
+
*/
|
|
27
|
+
readonly unlogged?: boolean;
|
|
21
28
|
}
|
|
22
29
|
/**
|
|
23
30
|
* A key–value store that uses PostgreSQL as the underlying storage.
|
|
@@ -46,6 +53,11 @@ declare class PostgresKvStore implements KvStore {
|
|
|
46
53
|
set(key: KvKey, value: unknown, options?: KvStoreSetOptions | undefined): Promise<void>;
|
|
47
54
|
delete(key: KvKey): Promise<void>;
|
|
48
55
|
/**
|
|
56
|
+
* {@inheritDoc KvStore.cas}
|
|
57
|
+
* @since 2.4.0
|
|
58
|
+
*/
|
|
59
|
+
cas(key: KvKey, expectedValue: unknown, newValue: unknown, options?: KvStoreSetOptions): Promise<boolean>;
|
|
60
|
+
/**
|
|
49
61
|
* {@inheritDoc KvStore.list}
|
|
50
62
|
* @since 1.10.0
|
|
51
63
|
*/
|
package/dist/kv.d.ts
CHANGED
|
@@ -18,6 +18,13 @@ interface PostgresKvStoreOptions {
|
|
|
18
18
|
* @default `false`
|
|
19
19
|
*/
|
|
20
20
|
readonly initialized?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether to use an unlogged table. Unlogged tables are faster, but are
|
|
23
|
+
* truncated after a PostgreSQL crash and are not replicated. `false` by
|
|
24
|
+
* default.
|
|
25
|
+
* @default `false`
|
|
26
|
+
*/
|
|
27
|
+
readonly unlogged?: boolean;
|
|
21
28
|
}
|
|
22
29
|
/**
|
|
23
30
|
* A key–value store that uses PostgreSQL as the underlying storage.
|
|
@@ -46,6 +53,11 @@ declare class PostgresKvStore implements KvStore {
|
|
|
46
53
|
set(key: KvKey, value: unknown, options?: KvStoreSetOptions | undefined): Promise<void>;
|
|
47
54
|
delete(key: KvKey): Promise<void>;
|
|
48
55
|
/**
|
|
56
|
+
* {@inheritDoc KvStore.cas}
|
|
57
|
+
* @since 2.4.0
|
|
58
|
+
*/
|
|
59
|
+
cas(key: KvKey, expectedValue: unknown, newValue: unknown, options?: KvStoreSetOptions): Promise<boolean>;
|
|
60
|
+
/**
|
|
49
61
|
* {@inheritDoc KvStore.list}
|
|
50
62
|
* @since 1.10.0
|
|
51
63
|
*/
|
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,9 @@ const logger = getLogger([
|
|
|
25
28
|
var PostgresKvStore = class {
|
|
26
29
|
#sql;
|
|
27
30
|
#tableName;
|
|
31
|
+
#unlogged;
|
|
28
32
|
#initialized;
|
|
33
|
+
#initializing;
|
|
29
34
|
#driverSerializesJson = false;
|
|
30
35
|
/**
|
|
31
36
|
* Creates a new PostgreSQL key–value store.
|
|
@@ -35,6 +40,7 @@ var PostgresKvStore = class {
|
|
|
35
40
|
constructor(sql, options = {}) {
|
|
36
41
|
this.#sql = sql;
|
|
37
42
|
this.#tableName = options.tableName ?? "fedify_kv_v2";
|
|
43
|
+
this.#unlogged = options.unlogged ?? false;
|
|
38
44
|
this.#initialized = options.initialized ?? false;
|
|
39
45
|
}
|
|
40
46
|
async #expire() {
|
|
@@ -77,6 +83,54 @@ var PostgresKvStore = class {
|
|
|
77
83
|
await this.#expire();
|
|
78
84
|
}
|
|
79
85
|
/**
|
|
86
|
+
* {@inheritDoc KvStore.cas}
|
|
87
|
+
* @since 2.4.0
|
|
88
|
+
*/
|
|
89
|
+
async cas(key, expectedValue, newValue, options) {
|
|
90
|
+
await this.initialize();
|
|
91
|
+
const ttl = options?.ttl == null ? null : options.ttl.toString();
|
|
92
|
+
if (expectedValue === void 0 && newValue === void 0) return await this.get(key) === void 0;
|
|
93
|
+
let result;
|
|
94
|
+
if (expectedValue === void 0) result = await this.#sql`
|
|
95
|
+
INSERT INTO ${this.#sql(this.#tableName)} AS existing
|
|
96
|
+
(key, value, created, ttl)
|
|
97
|
+
VALUES (
|
|
98
|
+
${key},
|
|
99
|
+
${this.#json(newValue)},
|
|
100
|
+
CURRENT_TIMESTAMP,
|
|
101
|
+
${ttl}
|
|
102
|
+
)
|
|
103
|
+
ON CONFLICT (key)
|
|
104
|
+
DO UPDATE SET
|
|
105
|
+
value = EXCLUDED.value,
|
|
106
|
+
created = EXCLUDED.created,
|
|
107
|
+
ttl = EXCLUDED.ttl
|
|
108
|
+
WHERE existing.ttl IS NOT NULL
|
|
109
|
+
AND existing.created + existing.ttl <= CURRENT_TIMESTAMP
|
|
110
|
+
RETURNING key;
|
|
111
|
+
`;
|
|
112
|
+
else if (newValue === void 0) result = await this.#sql`
|
|
113
|
+
DELETE FROM ${this.#sql(this.#tableName)}
|
|
114
|
+
WHERE key = ${key}
|
|
115
|
+
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
|
|
116
|
+
AND value = ${this.#json(expectedValue)}
|
|
117
|
+
RETURNING key;
|
|
118
|
+
`;
|
|
119
|
+
else result = await this.#sql`
|
|
120
|
+
UPDATE ${this.#sql(this.#tableName)}
|
|
121
|
+
SET
|
|
122
|
+
value = ${this.#json(newValue)},
|
|
123
|
+
created = CURRENT_TIMESTAMP,
|
|
124
|
+
ttl = ${ttl}
|
|
125
|
+
WHERE key = ${key}
|
|
126
|
+
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
|
|
127
|
+
AND value = ${this.#json(expectedValue)}
|
|
128
|
+
RETURNING key;
|
|
129
|
+
`;
|
|
130
|
+
await this.#expire();
|
|
131
|
+
return result.length > 0;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
80
134
|
* {@inheritDoc KvStore.list}
|
|
81
135
|
* @since 1.10.0
|
|
82
136
|
*/
|
|
@@ -111,18 +165,43 @@ var PostgresKvStore = class {
|
|
|
111
165
|
*/
|
|
112
166
|
async initialize() {
|
|
113
167
|
if (this.#initialized) return;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
168
|
+
this.#initializing ??= (async () => {
|
|
169
|
+
logger.debug("Initializing the key–value store table {tableName}...", { tableName: this.#tableName });
|
|
170
|
+
if (this.#unlogged) await this.#sql`
|
|
171
|
+
CREATE UNLOGGED TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
|
|
172
|
+
key text[] PRIMARY KEY,
|
|
173
|
+
value jsonb NOT NULL,
|
|
174
|
+
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
|
|
175
|
+
ttl interval
|
|
176
|
+
);
|
|
177
|
+
`;
|
|
178
|
+
else {
|
|
179
|
+
await this.#sql`
|
|
180
|
+
CREATE TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
|
|
181
|
+
key text[] PRIMARY KEY,
|
|
182
|
+
value jsonb NOT NULL,
|
|
183
|
+
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
|
|
184
|
+
ttl interval
|
|
185
|
+
);
|
|
186
|
+
`;
|
|
187
|
+
if ((await this.#sql`
|
|
188
|
+
SELECT relpersistence
|
|
189
|
+
FROM pg_class
|
|
190
|
+
WHERE oid = to_regclass(${quoteIdentifier(this.#tableName)});
|
|
191
|
+
`)[0]?.relpersistence === "u") await this.#sql`
|
|
192
|
+
ALTER TABLE ${this.#sql(this.#tableName)} SET LOGGED;
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
this.#driverSerializesJson = await driverSerializesJson(this.#sql);
|
|
196
|
+
this.#initialized = true;
|
|
197
|
+
logger.debug("Initialized the key–value store table {tableName}.", { tableName: this.#tableName });
|
|
198
|
+
})();
|
|
199
|
+
try {
|
|
200
|
+
await this.#initializing;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
this.#initializing = void 0;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
126
205
|
}
|
|
127
206
|
/**
|
|
128
207
|
* Drops the table used by the key–value store. Does nothing if the table
|
package/dist/mq.cjs
CHANGED
package/dist/mq.d.cts
CHANGED
|
@@ -62,6 +62,7 @@ interface PostgresMessageQueueOptions {
|
|
|
62
62
|
*/
|
|
63
63
|
declare class PostgresMessageQueue implements MessageQueue {
|
|
64
64
|
#private;
|
|
65
|
+
readonly atomicEnqueueMany = false;
|
|
65
66
|
constructor(sql: Sql<{}>, options?: PostgresMessageQueueOptions);
|
|
66
67
|
enqueue(message: any, options?: MessageQueueEnqueueOptions): Promise<void>;
|
|
67
68
|
enqueueMany(messages: readonly any[], options?: MessageQueueEnqueueOptions): Promise<void>;
|
package/dist/mq.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ interface PostgresMessageQueueOptions {
|
|
|
62
62
|
*/
|
|
63
63
|
declare class PostgresMessageQueue implements MessageQueue {
|
|
64
64
|
#private;
|
|
65
|
+
readonly atomicEnqueueMany = false;
|
|
65
66
|
constructor(sql: Sql<{}>, options?: PostgresMessageQueueOptions);
|
|
66
67
|
enqueue(message: any, options?: MessageQueueEnqueueOptions): Promise<void>;
|
|
67
68
|
enqueueMany(messages: readonly any[], options?: MessageQueueEnqueueOptions): Promise<void>;
|
package/dist/mq.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/postgres",
|
|
3
|
-
"version": "2.4.0-
|
|
3
|
+
"version": "2.4.0-pr.934.40+3331d302",
|
|
4
4
|
"description": "PostgreSQL drivers for Fedify",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fedify",
|
|
@@ -71,15 +71,15 @@
|
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"postgres": "^3.4.7",
|
|
74
|
-
"@fedify/fedify": "^2.4.0-
|
|
74
|
+
"@fedify/fedify": "^2.4.0-pr.934.40+3331d302"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@js-temporal/polyfill": "^0.5.1",
|
|
78
78
|
"@std/async": "npm:@jsr/std__async@^1.0.13",
|
|
79
79
|
"tsdown": "^0.22.0",
|
|
80
80
|
"typescript": "^6.0.0",
|
|
81
|
-
"@fedify/
|
|
82
|
-
"@fedify/
|
|
81
|
+
"@fedify/testing": "^2.4.0-pr.934.40+3331d302",
|
|
82
|
+
"@fedify/fixture": "^2.0.0"
|
|
83
83
|
},
|
|
84
84
|
"scripts": {
|
|
85
85
|
"build:self": "tsdown",
|