@fedify/postgres 2.4.5-dev.2462 → 2.4.6-dev.2628
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/kv.cjs +61 -14
- package/dist/kv.js +61 -14
- package/package.json +3 -3
package/dist/kv.cjs
CHANGED
|
@@ -13,6 +13,9 @@ const logger = (0, _logtape_logtape.getLogger)([
|
|
|
13
13
|
function quoteIdentifier(identifier) {
|
|
14
14
|
return `"${identifier.replaceAll("\"", "\"\"").replaceAll(".", "\".\"")}"`;
|
|
15
15
|
}
|
|
16
|
+
const expiryBatchSize = 1e3;
|
|
17
|
+
const expiryInterval = 6e4;
|
|
18
|
+
const expiryCatchUpInterval = 1e3;
|
|
16
19
|
/**
|
|
17
20
|
* A key–value store that uses PostgreSQL as the underlying storage.
|
|
18
21
|
*
|
|
@@ -36,6 +39,8 @@ var PostgresKvStore = class {
|
|
|
36
39
|
#initialized = false;
|
|
37
40
|
#initializing;
|
|
38
41
|
#driverSerializesJson = false;
|
|
42
|
+
#expiring;
|
|
43
|
+
#nextExpiry = 0;
|
|
39
44
|
/**
|
|
40
45
|
* Creates a new PostgreSQL key–value store.
|
|
41
46
|
* @param sql The PostgreSQL client to use.
|
|
@@ -47,18 +52,58 @@ var PostgresKvStore = class {
|
|
|
47
52
|
this.#unlogged = options.unlogged ?? false;
|
|
48
53
|
this.#skipDdl = options.initialized ?? false;
|
|
49
54
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
#expire() {
|
|
56
|
+
const callerOwned = "savepoint" in this.#sql && typeof this.#sql.savepoint === "function" || "release" in this.#sql && typeof this.#sql.release === "function";
|
|
57
|
+
if (this.#expiring) return callerOwned ? this.#expiring : void 0;
|
|
58
|
+
if (performance.now() < this.#nextExpiry) return;
|
|
59
|
+
this.#expiring = (async () => {
|
|
60
|
+
let interval = expiryInterval;
|
|
61
|
+
try {
|
|
62
|
+
if (await this.#sweep() === expiryBatchSize) interval = expiryCatchUpInterval;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (callerOwned) throw error;
|
|
65
|
+
logger.warn("Failed to expire keys in {tableName}: {error}", {
|
|
66
|
+
tableName: this.#tableName,
|
|
67
|
+
error
|
|
68
|
+
});
|
|
69
|
+
} finally {
|
|
70
|
+
this.#nextExpiry = performance.now() + interval * (1 + Math.random());
|
|
71
|
+
this.#expiring = void 0;
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
return callerOwned ? this.#expiring : void 0;
|
|
75
|
+
}
|
|
76
|
+
async #sweep() {
|
|
77
|
+
const [result] = await this.#sql`
|
|
78
|
+
WITH sweep_lock AS MATERIALIZED (
|
|
79
|
+
SELECT pg_try_advisory_xact_lock(
|
|
80
|
+
(hashtext('fedify.postgres.kv.expire')::bigint << 32) |
|
|
81
|
+
(SELECT typrelid::bigint FROM pg_catalog.pg_type
|
|
82
|
+
WHERE oid = pg_typeof(NULL::${this.#sql(this.#tableName)})::oid)
|
|
83
|
+
) AS acquired
|
|
84
|
+
), candidates AS MATERIALIZED (
|
|
85
|
+
SELECT tableoid AS relation, ctid AS tid
|
|
86
|
+
FROM ${this.#sql(this.#tableName)}
|
|
87
|
+
WHERE (SELECT acquired FROM sweep_lock)
|
|
88
|
+
AND ttl IS NOT NULL AND created + ttl < statement_timestamp()
|
|
89
|
+
LIMIT ${expiryBatchSize} FOR UPDATE SKIP LOCKED
|
|
90
|
+
), deleted AS (
|
|
91
|
+
DELETE FROM ${this.#sql(this.#tableName)}
|
|
92
|
+
WHERE ctid = ANY(ARRAY(SELECT tid FROM candidates))
|
|
93
|
+
AND (tableoid, ctid) IN (SELECT relation, tid FROM candidates)
|
|
94
|
+
AND ttl IS NOT NULL AND created + ttl < statement_timestamp()
|
|
95
|
+
RETURNING 1
|
|
96
|
+
)
|
|
97
|
+
SELECT count(*)::int AS count FROM deleted;
|
|
54
98
|
`;
|
|
99
|
+
return result.count;
|
|
55
100
|
}
|
|
56
101
|
async get(key) {
|
|
57
102
|
await this.initialize();
|
|
58
103
|
const result = await this.#sql`
|
|
59
104
|
SELECT value
|
|
60
105
|
FROM ${this.#sql(this.#tableName)}
|
|
61
|
-
WHERE key = ${key} AND (ttl IS NULL OR created + ttl >
|
|
106
|
+
WHERE key = ${key} AND (ttl IS NULL OR created + ttl > statement_timestamp());
|
|
62
107
|
`;
|
|
63
108
|
if (result.length < 1) return void 0;
|
|
64
109
|
return result[0].value;
|
|
@@ -67,16 +112,17 @@ var PostgresKvStore = class {
|
|
|
67
112
|
await this.initialize();
|
|
68
113
|
const ttl = options?.ttl == null ? null : Temporal.Duration.from(options.ttl).toString();
|
|
69
114
|
await this.#sql`
|
|
70
|
-
INSERT INTO ${this.#sql(this.#tableName)} (key, value, ttl)
|
|
115
|
+
INSERT INTO ${this.#sql(this.#tableName)} (key, value, created, ttl)
|
|
71
116
|
VALUES (
|
|
72
117
|
${key},
|
|
73
118
|
${this.#json(value)},
|
|
119
|
+
statement_timestamp(),
|
|
74
120
|
${ttl}
|
|
75
121
|
)
|
|
76
122
|
ON CONFLICT (key)
|
|
77
123
|
DO UPDATE SET
|
|
78
124
|
value = EXCLUDED.value,
|
|
79
|
-
created =
|
|
125
|
+
created = statement_timestamp(),
|
|
80
126
|
ttl = EXCLUDED.ttl;
|
|
81
127
|
`;
|
|
82
128
|
await this.#expire();
|
|
@@ -104,7 +150,7 @@ var PostgresKvStore = class {
|
|
|
104
150
|
VALUES (
|
|
105
151
|
${key},
|
|
106
152
|
${this.#json(newValue)},
|
|
107
|
-
|
|
153
|
+
statement_timestamp(),
|
|
108
154
|
${ttl}
|
|
109
155
|
)
|
|
110
156
|
ON CONFLICT (key)
|
|
@@ -113,13 +159,13 @@ var PostgresKvStore = class {
|
|
|
113
159
|
created = EXCLUDED.created,
|
|
114
160
|
ttl = EXCLUDED.ttl
|
|
115
161
|
WHERE existing.ttl IS NOT NULL
|
|
116
|
-
AND existing.created + existing.ttl <=
|
|
162
|
+
AND existing.created + existing.ttl <= statement_timestamp()
|
|
117
163
|
RETURNING key;
|
|
118
164
|
`;
|
|
119
165
|
else if (newValue === void 0) result = await this.#sql`
|
|
120
166
|
DELETE FROM ${this.#sql(this.#tableName)}
|
|
121
167
|
WHERE key = ${key}
|
|
122
|
-
AND (ttl IS NULL OR created + ttl >
|
|
168
|
+
AND (ttl IS NULL OR created + ttl > statement_timestamp())
|
|
123
169
|
AND value = ${this.#json(expectedValue)}
|
|
124
170
|
RETURNING key;
|
|
125
171
|
`;
|
|
@@ -127,10 +173,10 @@ var PostgresKvStore = class {
|
|
|
127
173
|
UPDATE ${this.#sql(this.#tableName)}
|
|
128
174
|
SET
|
|
129
175
|
value = ${this.#json(newValue)},
|
|
130
|
-
created =
|
|
176
|
+
created = statement_timestamp(),
|
|
131
177
|
ttl = ${ttl}
|
|
132
178
|
WHERE key = ${key}
|
|
133
|
-
AND (ttl IS NULL OR created + ttl >
|
|
179
|
+
AND (ttl IS NULL OR created + ttl > statement_timestamp())
|
|
134
180
|
AND value = ${this.#json(expectedValue)}
|
|
135
181
|
RETURNING key;
|
|
136
182
|
`;
|
|
@@ -147,7 +193,7 @@ var PostgresKvStore = class {
|
|
|
147
193
|
if (prefix == null || prefix.length === 0) results = await this.#sql`
|
|
148
194
|
SELECT key, value
|
|
149
195
|
FROM ${this.#sql(this.#tableName)}
|
|
150
|
-
WHERE ttl IS NULL OR created + ttl >
|
|
196
|
+
WHERE ttl IS NULL OR created + ttl > statement_timestamp()
|
|
151
197
|
ORDER BY key
|
|
152
198
|
`;
|
|
153
199
|
else {
|
|
@@ -157,7 +203,7 @@ var PostgresKvStore = class {
|
|
|
157
203
|
FROM ${this.#sql(this.#tableName)}
|
|
158
204
|
WHERE array_length(key, 1) >= ${prefixLength}
|
|
159
205
|
AND key[1:${prefixLength}] = ${prefix}::text[]
|
|
160
|
-
AND (ttl IS NULL OR created + ttl >
|
|
206
|
+
AND (ttl IS NULL OR created + ttl > statement_timestamp())
|
|
161
207
|
ORDER BY key
|
|
162
208
|
`;
|
|
163
209
|
}
|
|
@@ -218,6 +264,7 @@ var PostgresKvStore = class {
|
|
|
218
264
|
* does not exist.
|
|
219
265
|
*/
|
|
220
266
|
async drop() {
|
|
267
|
+
await this.#expiring;
|
|
221
268
|
await this.#sql`DROP TABLE IF EXISTS ${this.#sql(this.#tableName)};`;
|
|
222
269
|
}
|
|
223
270
|
#json(value) {
|
package/dist/kv.js
CHANGED
|
@@ -10,6 +10,9 @@ const logger = getLogger([
|
|
|
10
10
|
function quoteIdentifier(identifier) {
|
|
11
11
|
return `"${identifier.replaceAll("\"", "\"\"").replaceAll(".", "\".\"")}"`;
|
|
12
12
|
}
|
|
13
|
+
const expiryBatchSize = 1e3;
|
|
14
|
+
const expiryInterval = 6e4;
|
|
15
|
+
const expiryCatchUpInterval = 1e3;
|
|
13
16
|
/**
|
|
14
17
|
* A key–value store that uses PostgreSQL as the underlying storage.
|
|
15
18
|
*
|
|
@@ -33,6 +36,8 @@ var PostgresKvStore = class {
|
|
|
33
36
|
#initialized = false;
|
|
34
37
|
#initializing;
|
|
35
38
|
#driverSerializesJson = false;
|
|
39
|
+
#expiring;
|
|
40
|
+
#nextExpiry = 0;
|
|
36
41
|
/**
|
|
37
42
|
* Creates a new PostgreSQL key–value store.
|
|
38
43
|
* @param sql The PostgreSQL client to use.
|
|
@@ -44,18 +49,58 @@ var PostgresKvStore = class {
|
|
|
44
49
|
this.#unlogged = options.unlogged ?? false;
|
|
45
50
|
this.#skipDdl = options.initialized ?? false;
|
|
46
51
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
#expire() {
|
|
53
|
+
const callerOwned = "savepoint" in this.#sql && typeof this.#sql.savepoint === "function" || "release" in this.#sql && typeof this.#sql.release === "function";
|
|
54
|
+
if (this.#expiring) return callerOwned ? this.#expiring : void 0;
|
|
55
|
+
if (performance.now() < this.#nextExpiry) return;
|
|
56
|
+
this.#expiring = (async () => {
|
|
57
|
+
let interval = expiryInterval;
|
|
58
|
+
try {
|
|
59
|
+
if (await this.#sweep() === expiryBatchSize) interval = expiryCatchUpInterval;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (callerOwned) throw error;
|
|
62
|
+
logger.warn("Failed to expire keys in {tableName}: {error}", {
|
|
63
|
+
tableName: this.#tableName,
|
|
64
|
+
error
|
|
65
|
+
});
|
|
66
|
+
} finally {
|
|
67
|
+
this.#nextExpiry = performance.now() + interval * (1 + Math.random());
|
|
68
|
+
this.#expiring = void 0;
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
return callerOwned ? this.#expiring : void 0;
|
|
72
|
+
}
|
|
73
|
+
async #sweep() {
|
|
74
|
+
const [result] = await this.#sql`
|
|
75
|
+
WITH sweep_lock AS MATERIALIZED (
|
|
76
|
+
SELECT pg_try_advisory_xact_lock(
|
|
77
|
+
(hashtext('fedify.postgres.kv.expire')::bigint << 32) |
|
|
78
|
+
(SELECT typrelid::bigint FROM pg_catalog.pg_type
|
|
79
|
+
WHERE oid = pg_typeof(NULL::${this.#sql(this.#tableName)})::oid)
|
|
80
|
+
) AS acquired
|
|
81
|
+
), candidates AS MATERIALIZED (
|
|
82
|
+
SELECT tableoid AS relation, ctid AS tid
|
|
83
|
+
FROM ${this.#sql(this.#tableName)}
|
|
84
|
+
WHERE (SELECT acquired FROM sweep_lock)
|
|
85
|
+
AND ttl IS NOT NULL AND created + ttl < statement_timestamp()
|
|
86
|
+
LIMIT ${expiryBatchSize} FOR UPDATE SKIP LOCKED
|
|
87
|
+
), deleted AS (
|
|
88
|
+
DELETE FROM ${this.#sql(this.#tableName)}
|
|
89
|
+
WHERE ctid = ANY(ARRAY(SELECT tid FROM candidates))
|
|
90
|
+
AND (tableoid, ctid) IN (SELECT relation, tid FROM candidates)
|
|
91
|
+
AND ttl IS NOT NULL AND created + ttl < statement_timestamp()
|
|
92
|
+
RETURNING 1
|
|
93
|
+
)
|
|
94
|
+
SELECT count(*)::int AS count FROM deleted;
|
|
51
95
|
`;
|
|
96
|
+
return result.count;
|
|
52
97
|
}
|
|
53
98
|
async get(key) {
|
|
54
99
|
await this.initialize();
|
|
55
100
|
const result = await this.#sql`
|
|
56
101
|
SELECT value
|
|
57
102
|
FROM ${this.#sql(this.#tableName)}
|
|
58
|
-
WHERE key = ${key} AND (ttl IS NULL OR created + ttl >
|
|
103
|
+
WHERE key = ${key} AND (ttl IS NULL OR created + ttl > statement_timestamp());
|
|
59
104
|
`;
|
|
60
105
|
if (result.length < 1) return void 0;
|
|
61
106
|
return result[0].value;
|
|
@@ -64,16 +109,17 @@ var PostgresKvStore = class {
|
|
|
64
109
|
await this.initialize();
|
|
65
110
|
const ttl = options?.ttl == null ? null : Temporal.Duration.from(options.ttl).toString();
|
|
66
111
|
await this.#sql`
|
|
67
|
-
INSERT INTO ${this.#sql(this.#tableName)} (key, value, ttl)
|
|
112
|
+
INSERT INTO ${this.#sql(this.#tableName)} (key, value, created, ttl)
|
|
68
113
|
VALUES (
|
|
69
114
|
${key},
|
|
70
115
|
${this.#json(value)},
|
|
116
|
+
statement_timestamp(),
|
|
71
117
|
${ttl}
|
|
72
118
|
)
|
|
73
119
|
ON CONFLICT (key)
|
|
74
120
|
DO UPDATE SET
|
|
75
121
|
value = EXCLUDED.value,
|
|
76
|
-
created =
|
|
122
|
+
created = statement_timestamp(),
|
|
77
123
|
ttl = EXCLUDED.ttl;
|
|
78
124
|
`;
|
|
79
125
|
await this.#expire();
|
|
@@ -101,7 +147,7 @@ var PostgresKvStore = class {
|
|
|
101
147
|
VALUES (
|
|
102
148
|
${key},
|
|
103
149
|
${this.#json(newValue)},
|
|
104
|
-
|
|
150
|
+
statement_timestamp(),
|
|
105
151
|
${ttl}
|
|
106
152
|
)
|
|
107
153
|
ON CONFLICT (key)
|
|
@@ -110,13 +156,13 @@ var PostgresKvStore = class {
|
|
|
110
156
|
created = EXCLUDED.created,
|
|
111
157
|
ttl = EXCLUDED.ttl
|
|
112
158
|
WHERE existing.ttl IS NOT NULL
|
|
113
|
-
AND existing.created + existing.ttl <=
|
|
159
|
+
AND existing.created + existing.ttl <= statement_timestamp()
|
|
114
160
|
RETURNING key;
|
|
115
161
|
`;
|
|
116
162
|
else if (newValue === void 0) result = await this.#sql`
|
|
117
163
|
DELETE FROM ${this.#sql(this.#tableName)}
|
|
118
164
|
WHERE key = ${key}
|
|
119
|
-
AND (ttl IS NULL OR created + ttl >
|
|
165
|
+
AND (ttl IS NULL OR created + ttl > statement_timestamp())
|
|
120
166
|
AND value = ${this.#json(expectedValue)}
|
|
121
167
|
RETURNING key;
|
|
122
168
|
`;
|
|
@@ -124,10 +170,10 @@ var PostgresKvStore = class {
|
|
|
124
170
|
UPDATE ${this.#sql(this.#tableName)}
|
|
125
171
|
SET
|
|
126
172
|
value = ${this.#json(newValue)},
|
|
127
|
-
created =
|
|
173
|
+
created = statement_timestamp(),
|
|
128
174
|
ttl = ${ttl}
|
|
129
175
|
WHERE key = ${key}
|
|
130
|
-
AND (ttl IS NULL OR created + ttl >
|
|
176
|
+
AND (ttl IS NULL OR created + ttl > statement_timestamp())
|
|
131
177
|
AND value = ${this.#json(expectedValue)}
|
|
132
178
|
RETURNING key;
|
|
133
179
|
`;
|
|
@@ -144,7 +190,7 @@ var PostgresKvStore = class {
|
|
|
144
190
|
if (prefix == null || prefix.length === 0) results = await this.#sql`
|
|
145
191
|
SELECT key, value
|
|
146
192
|
FROM ${this.#sql(this.#tableName)}
|
|
147
|
-
WHERE ttl IS NULL OR created + ttl >
|
|
193
|
+
WHERE ttl IS NULL OR created + ttl > statement_timestamp()
|
|
148
194
|
ORDER BY key
|
|
149
195
|
`;
|
|
150
196
|
else {
|
|
@@ -154,7 +200,7 @@ var PostgresKvStore = class {
|
|
|
154
200
|
FROM ${this.#sql(this.#tableName)}
|
|
155
201
|
WHERE array_length(key, 1) >= ${prefixLength}
|
|
156
202
|
AND key[1:${prefixLength}] = ${prefix}::text[]
|
|
157
|
-
AND (ttl IS NULL OR created + ttl >
|
|
203
|
+
AND (ttl IS NULL OR created + ttl > statement_timestamp())
|
|
158
204
|
ORDER BY key
|
|
159
205
|
`;
|
|
160
206
|
}
|
|
@@ -215,6 +261,7 @@ var PostgresKvStore = class {
|
|
|
215
261
|
* does not exist.
|
|
216
262
|
*/
|
|
217
263
|
async drop() {
|
|
264
|
+
await this.#expiring;
|
|
218
265
|
await this.#sql`DROP TABLE IF EXISTS ${this.#sql(this.#tableName)};`;
|
|
219
266
|
}
|
|
220
267
|
#json(value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/postgres",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.6-dev.2628+2a78382f",
|
|
4
4
|
"description": "PostgreSQL drivers for Fedify",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fedify",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"postgres": "^3.4.7",
|
|
74
|
-
"@fedify/fedify": "^2.4.
|
|
74
|
+
"@fedify/fedify": "^2.4.6-dev.2628+2a78382f"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@js-temporal/polyfill": "^0.5.1",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"tsdown": "^0.22.0",
|
|
80
80
|
"typescript": "^6.0.0",
|
|
81
81
|
"@fedify/fixture": "^2.0.0",
|
|
82
|
-
"@fedify/testing": "^2.4.
|
|
82
|
+
"@fedify/testing": "^2.4.6-dev.2628+2a78382f"
|
|
83
83
|
},
|
|
84
84
|
"scripts": {
|
|
85
85
|
"build:self": "tsdown",
|