@fedify/pglite 2.4.0-dev.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/LICENSE +20 -0
- package/README.md +52 -0
- package/dist/kv.cjs +217 -0
- package/dist/kv.d.cts +74 -0
- package/dist/kv.d.ts +74 -0
- package/dist/kv.js +214 -0
- package/dist/mod.cjs +4 -0
- package/dist/mod.d.cts +4 -0
- package/dist/mod.d.ts +4 -0
- package/dist/mod.js +3 -0
- package/dist/node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/chunks/apiHelpers.cjs +143 -0
- package/dist/node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/chunks/classApi-basic.cjs +854 -0
- package/dist/node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/chunks/internal.cjs +2781 -0
- package/dist/node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/chunks/root.cjs +5 -0
- package/dist/node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/index.cjs +8 -0
- package/dist/node_modules/.pnpm/temporal-utils@1.0.1/node_modules/temporal-utils/dist/errorMessages.cjs +15 -0
- package/dist/node_modules/.pnpm/temporal-utils@1.0.1/node_modules/temporal-utils/dist/utils.cjs +52 -0
- package/package.json +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2024–2026 Hong Minhee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<!-- deno-fmt-ignore-file -->
|
|
2
|
+
|
|
3
|
+
@fedify/pglite: PGlite driver for Fedify
|
|
4
|
+
========================================
|
|
5
|
+
|
|
6
|
+
[![JSR][JSR badge]][JSR]
|
|
7
|
+
[![npm][npm badge]][npm]
|
|
8
|
+
|
|
9
|
+
This package provides [`PgliteKvStore`], a PGlite-backed implementation of
|
|
10
|
+
Fedify's [`KvStore`]:
|
|
11
|
+
|
|
12
|
+
~~~~ typescript
|
|
13
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
14
|
+
import { createFederation } from "@fedify/fedify";
|
|
15
|
+
import { PgliteKvStore } from "@fedify/pglite";
|
|
16
|
+
|
|
17
|
+
const pg = new PGlite("./data/fedify");
|
|
18
|
+
const federation = createFederation({
|
|
19
|
+
kv: new PgliteKvStore(pg),
|
|
20
|
+
});
|
|
21
|
+
~~~~
|
|
22
|
+
|
|
23
|
+
The caller owns the `PGlite` instance. Close it during application shutdown,
|
|
24
|
+
after every pending store operation has completed. `PgliteKvStore` neither
|
|
25
|
+
creates nor closes the instance.
|
|
26
|
+
|
|
27
|
+
Use one PGlite instance per data directory and runtime isolate. PGlite does
|
|
28
|
+
not make writes visible to other processes, so the same data directory cannot
|
|
29
|
+
back horizontally scaled workers. This package does not provide a message
|
|
30
|
+
queue because those constraints cannot preserve ordering or delivery during a
|
|
31
|
+
rolling restart. See [PGlite issue #489] and [PGlite pull request #892].
|
|
32
|
+
|
|
33
|
+
[JSR badge]: https://jsr.io/badges/@fedify/pglite
|
|
34
|
+
[JSR]: https://jsr.io/@fedify/pglite
|
|
35
|
+
[npm badge]: https://img.shields.io/npm/v/@fedify/pglite?logo=npm
|
|
36
|
+
[npm]: https://www.npmjs.com/package/@fedify/pglite
|
|
37
|
+
[`PgliteKvStore`]: https://jsr.io/@fedify/pglite/doc/~/PgliteKvStore
|
|
38
|
+
[`KvStore`]: https://jsr.io/@fedify/fedify/doc/federation/~/KvStore
|
|
39
|
+
[PGlite issue #489]: https://github.com/electric-sql/pglite/issues/489#issuecomment-2587892190
|
|
40
|
+
[PGlite pull request #892]: https://github.com/electric-sql/pglite/pull/892
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
Installation
|
|
44
|
+
------------
|
|
45
|
+
|
|
46
|
+
~~~~ sh
|
|
47
|
+
deno add jsr:@fedify/pglite npm:@electric-sql/pglite # Deno
|
|
48
|
+
npm add @fedify/pglite @electric-sql/pglite # npm
|
|
49
|
+
pnpm add @fedify/pglite @electric-sql/pglite # pnpm
|
|
50
|
+
yarn add @fedify/pglite @electric-sql/pglite # Yarn
|
|
51
|
+
bun add @fedify/pglite @electric-sql/pglite # Bun
|
|
52
|
+
~~~~
|
package/dist/kv.cjs
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const require_index = require("./node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/index.cjs");
|
|
4
|
+
let _logtape_logtape = require("@logtape/logtape");
|
|
5
|
+
//#region src/kv.ts
|
|
6
|
+
const Temporal = require_index.Temporal;
|
|
7
|
+
const logger = (0, _logtape_logtape.getLogger)([
|
|
8
|
+
"fedify",
|
|
9
|
+
"pglite",
|
|
10
|
+
"kv"
|
|
11
|
+
]);
|
|
12
|
+
function quoteIdentifier(identifier) {
|
|
13
|
+
const parts = identifier.split(".");
|
|
14
|
+
if (parts.includes("")) throw new TypeError(`Invalid table name for the key–value store: ${JSON.stringify(identifier)}`);
|
|
15
|
+
return parts.map((part) => `"${part.replaceAll("\"", "\"\"")}"`).join(".");
|
|
16
|
+
}
|
|
17
|
+
function serializeJson(value) {
|
|
18
|
+
const serialized = JSON.stringify(value);
|
|
19
|
+
if (serialized == null) throw new TypeError("PGlite cannot store a value that is not valid JSON");
|
|
20
|
+
return serialized;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A key–value store backed by an embedded PGlite database.
|
|
24
|
+
*
|
|
25
|
+
* The caller owns the PGlite instance and is responsible for closing it after
|
|
26
|
+
* every operation using the store has completed. Use only one PGlite instance
|
|
27
|
+
* for a data directory within a runtime isolate.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts ignore
|
|
31
|
+
* import { PGlite } from "@electric-sql/pglite";
|
|
32
|
+
* import { createFederation } from "@fedify/fedify";
|
|
33
|
+
* import { PgliteKvStore } from "@fedify/pglite";
|
|
34
|
+
*
|
|
35
|
+
* const pg = new PGlite("./data/fedify");
|
|
36
|
+
* const federation = createFederation({
|
|
37
|
+
* // ...
|
|
38
|
+
* kv: new PgliteKvStore(pg),
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* // Close pg during application shutdown, after pending operations finish.
|
|
42
|
+
* await pg.close();
|
|
43
|
+
* ```
|
|
44
|
+
* @since 2.4.0
|
|
45
|
+
*/
|
|
46
|
+
var PgliteKvStore = class {
|
|
47
|
+
#pg;
|
|
48
|
+
#tableName;
|
|
49
|
+
#quotedTableName;
|
|
50
|
+
#initialized;
|
|
51
|
+
#initializing;
|
|
52
|
+
/**
|
|
53
|
+
* Creates a new PGlite key–value store.
|
|
54
|
+
* @param pg The caller-owned PGlite instance to use.
|
|
55
|
+
* @param options The options for the key–value store.
|
|
56
|
+
*/
|
|
57
|
+
constructor(pg, options = {}) {
|
|
58
|
+
this.#pg = pg;
|
|
59
|
+
this.#tableName = options.tableName ?? "fedify_kv_v2";
|
|
60
|
+
this.#quotedTableName = quoteIdentifier(this.#tableName);
|
|
61
|
+
this.#initialized = options.initialized ?? false;
|
|
62
|
+
}
|
|
63
|
+
async #expire() {
|
|
64
|
+
await this.#pg.query(`
|
|
65
|
+
DELETE FROM ${this.#quotedTableName}
|
|
66
|
+
WHERE ttl IS NOT NULL AND created + ttl < CURRENT_TIMESTAMP;
|
|
67
|
+
`);
|
|
68
|
+
}
|
|
69
|
+
async get(key) {
|
|
70
|
+
await this.initialize();
|
|
71
|
+
return (await this.#pg.query(`
|
|
72
|
+
SELECT value
|
|
73
|
+
FROM ${this.#quotedTableName}
|
|
74
|
+
WHERE key = $1::text[]
|
|
75
|
+
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP);
|
|
76
|
+
`, [key])).rows[0]?.value;
|
|
77
|
+
}
|
|
78
|
+
async set(key, value, options) {
|
|
79
|
+
await this.initialize();
|
|
80
|
+
const ttl = options?.ttl == null ? null : Temporal.Duration.from(options.ttl).toString();
|
|
81
|
+
await this.#pg.query(`
|
|
82
|
+
INSERT INTO ${this.#quotedTableName} (key, value, ttl)
|
|
83
|
+
VALUES ($1::text[], $2::text::jsonb, $3::text::interval)
|
|
84
|
+
ON CONFLICT (key)
|
|
85
|
+
DO UPDATE SET
|
|
86
|
+
value = EXCLUDED.value,
|
|
87
|
+
created = CURRENT_TIMESTAMP,
|
|
88
|
+
ttl = EXCLUDED.ttl;
|
|
89
|
+
`, [
|
|
90
|
+
key,
|
|
91
|
+
serializeJson(value),
|
|
92
|
+
ttl
|
|
93
|
+
]);
|
|
94
|
+
await this.#expire();
|
|
95
|
+
}
|
|
96
|
+
async delete(key) {
|
|
97
|
+
await this.initialize();
|
|
98
|
+
await this.#pg.query(`
|
|
99
|
+
DELETE FROM ${this.#quotedTableName}
|
|
100
|
+
WHERE key = $1::text[];
|
|
101
|
+
`, [key]);
|
|
102
|
+
await this.#expire();
|
|
103
|
+
}
|
|
104
|
+
/** {@inheritDoc KvStore.cas} */
|
|
105
|
+
async cas(key, expectedValue, newValue, options) {
|
|
106
|
+
await this.initialize();
|
|
107
|
+
const ttl = options?.ttl == null ? null : Temporal.Duration.from(options.ttl).toString();
|
|
108
|
+
if (expectedValue === void 0 && newValue === void 0) {
|
|
109
|
+
const matched = await this.get(key) === void 0;
|
|
110
|
+
await this.#expire();
|
|
111
|
+
return matched;
|
|
112
|
+
}
|
|
113
|
+
let rowCount;
|
|
114
|
+
if (expectedValue === void 0) rowCount = (await this.#pg.query(`
|
|
115
|
+
INSERT INTO ${this.#quotedTableName} AS existing
|
|
116
|
+
(key, value, created, ttl)
|
|
117
|
+
VALUES (
|
|
118
|
+
$1::text[],
|
|
119
|
+
$2::text::jsonb,
|
|
120
|
+
CURRENT_TIMESTAMP,
|
|
121
|
+
$3::text::interval
|
|
122
|
+
)
|
|
123
|
+
ON CONFLICT (key)
|
|
124
|
+
DO UPDATE SET
|
|
125
|
+
value = EXCLUDED.value,
|
|
126
|
+
created = EXCLUDED.created,
|
|
127
|
+
ttl = EXCLUDED.ttl
|
|
128
|
+
WHERE existing.ttl IS NOT NULL
|
|
129
|
+
AND existing.created + existing.ttl <= CURRENT_TIMESTAMP
|
|
130
|
+
RETURNING key;
|
|
131
|
+
`, [
|
|
132
|
+
key,
|
|
133
|
+
serializeJson(newValue),
|
|
134
|
+
ttl
|
|
135
|
+
])).rows.length;
|
|
136
|
+
else if (newValue === void 0) rowCount = (await this.#pg.query(`
|
|
137
|
+
DELETE FROM ${this.#quotedTableName}
|
|
138
|
+
WHERE key = $1::text[]
|
|
139
|
+
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
|
|
140
|
+
AND value = $2::text::jsonb
|
|
141
|
+
RETURNING key;
|
|
142
|
+
`, [key, serializeJson(expectedValue)])).rows.length;
|
|
143
|
+
else rowCount = (await this.#pg.query(`
|
|
144
|
+
UPDATE ${this.#quotedTableName}
|
|
145
|
+
SET
|
|
146
|
+
value = $3::text::jsonb,
|
|
147
|
+
created = CURRENT_TIMESTAMP,
|
|
148
|
+
ttl = $4::text::interval
|
|
149
|
+
WHERE key = $1::text[]
|
|
150
|
+
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
|
|
151
|
+
AND value = $2::text::jsonb
|
|
152
|
+
RETURNING key;
|
|
153
|
+
`, [
|
|
154
|
+
key,
|
|
155
|
+
serializeJson(expectedValue),
|
|
156
|
+
serializeJson(newValue),
|
|
157
|
+
ttl
|
|
158
|
+
])).rows.length;
|
|
159
|
+
await this.#expire();
|
|
160
|
+
return rowCount > 0;
|
|
161
|
+
}
|
|
162
|
+
/** {@inheritDoc KvStore.list} */
|
|
163
|
+
async *list(prefix) {
|
|
164
|
+
await this.initialize();
|
|
165
|
+
const result = prefix == null || prefix.length === 0 ? await this.#pg.query(`
|
|
166
|
+
SELECT key, value
|
|
167
|
+
FROM ${this.#quotedTableName}
|
|
168
|
+
WHERE ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP
|
|
169
|
+
ORDER BY key;
|
|
170
|
+
`) : await this.#pg.query(`
|
|
171
|
+
SELECT key, value
|
|
172
|
+
FROM ${this.#quotedTableName}
|
|
173
|
+
WHERE array_length(key, 1) >= $1
|
|
174
|
+
AND key[1:$1] = $2::text[]
|
|
175
|
+
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
|
|
176
|
+
ORDER BY key;
|
|
177
|
+
`, [prefix.length, prefix]);
|
|
178
|
+
for (const row of result.rows) yield row;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Creates the table used by the store if it does not already exist.
|
|
182
|
+
*/
|
|
183
|
+
async initialize() {
|
|
184
|
+
await this.#pg.waitReady;
|
|
185
|
+
if (this.#initialized) return;
|
|
186
|
+
this.#initializing ??= (async () => {
|
|
187
|
+
logger.debug("Initializing the key–value store table {tableName}...", { tableName: this.#tableName });
|
|
188
|
+
await this.#pg.query(`
|
|
189
|
+
CREATE TABLE IF NOT EXISTS ${this.#quotedTableName} (
|
|
190
|
+
key text[] PRIMARY KEY,
|
|
191
|
+
value jsonb NOT NULL,
|
|
192
|
+
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
|
|
193
|
+
ttl interval
|
|
194
|
+
);
|
|
195
|
+
`);
|
|
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
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Drops the table used by the store. Does nothing if it does not exist.
|
|
208
|
+
*/
|
|
209
|
+
async drop() {
|
|
210
|
+
await this.#pg.waitReady;
|
|
211
|
+
await this.#pg.query(`DROP TABLE IF EXISTS ${this.#quotedTableName};`);
|
|
212
|
+
this.#initialized = false;
|
|
213
|
+
this.#initializing = void 0;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
//#endregion
|
|
217
|
+
exports.PgliteKvStore = PgliteKvStore;
|
package/dist/kv.d.cts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/// <reference types="emscripten" />
|
|
2
|
+
/// <reference lib="esnext.temporal" />
|
|
3
|
+
import { PGliteInterface } from "@electric-sql/pglite";
|
|
4
|
+
import { KvKey, KvStore, KvStoreListEntry, KvStoreSetOptions } from "@fedify/fedify";
|
|
5
|
+
|
|
6
|
+
//#region src/kv.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Options for the PGlite key–value store.
|
|
9
|
+
*/
|
|
10
|
+
interface PgliteKvStoreOptions {
|
|
11
|
+
/**
|
|
12
|
+
* The table name to use for the key–value store.
|
|
13
|
+
* A `.` separates the schema from the table name, so neither the whole
|
|
14
|
+
* name nor any segment between dots can be empty.
|
|
15
|
+
* `"fedify_kv_v2"` by default.
|
|
16
|
+
* @default `"fedify_kv_v2"`
|
|
17
|
+
*/
|
|
18
|
+
readonly tableName?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Whether the table has already been initialized. `false` by default.
|
|
21
|
+
* @default `false`
|
|
22
|
+
*/
|
|
23
|
+
readonly initialized?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A key–value store backed by an embedded PGlite database.
|
|
27
|
+
*
|
|
28
|
+
* The caller owns the PGlite instance and is responsible for closing it after
|
|
29
|
+
* every operation using the store has completed. Use only one PGlite instance
|
|
30
|
+
* for a data directory within a runtime isolate.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts ignore
|
|
34
|
+
* import { PGlite } from "@electric-sql/pglite";
|
|
35
|
+
* import { createFederation } from "@fedify/fedify";
|
|
36
|
+
* import { PgliteKvStore } from "@fedify/pglite";
|
|
37
|
+
*
|
|
38
|
+
* const pg = new PGlite("./data/fedify");
|
|
39
|
+
* const federation = createFederation({
|
|
40
|
+
* // ...
|
|
41
|
+
* kv: new PgliteKvStore(pg),
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Close pg during application shutdown, after pending operations finish.
|
|
45
|
+
* await pg.close();
|
|
46
|
+
* ```
|
|
47
|
+
* @since 2.4.0
|
|
48
|
+
*/
|
|
49
|
+
declare class PgliteKvStore implements KvStore {
|
|
50
|
+
#private;
|
|
51
|
+
/**
|
|
52
|
+
* Creates a new PGlite key–value store.
|
|
53
|
+
* @param pg The caller-owned PGlite instance to use.
|
|
54
|
+
* @param options The options for the key–value store.
|
|
55
|
+
*/
|
|
56
|
+
constructor(pg: PGliteInterface, options?: PgliteKvStoreOptions);
|
|
57
|
+
get<T = unknown>(key: KvKey): Promise<T | undefined>;
|
|
58
|
+
set(key: KvKey, value: unknown, options?: KvStoreSetOptions): Promise<void>;
|
|
59
|
+
delete(key: KvKey): Promise<void>;
|
|
60
|
+
/** {@inheritDoc KvStore.cas} */
|
|
61
|
+
cas(key: KvKey, expectedValue: unknown, newValue: unknown, options?: KvStoreSetOptions): Promise<boolean>;
|
|
62
|
+
/** {@inheritDoc KvStore.list} */
|
|
63
|
+
list(prefix?: KvKey): AsyncIterable<KvStoreListEntry>;
|
|
64
|
+
/**
|
|
65
|
+
* Creates the table used by the store if it does not already exist.
|
|
66
|
+
*/
|
|
67
|
+
initialize(): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Drops the table used by the store. Does nothing if it does not exist.
|
|
70
|
+
*/
|
|
71
|
+
drop(): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { PgliteKvStore, PgliteKvStoreOptions };
|
package/dist/kv.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/// <reference types="emscripten" />
|
|
2
|
+
/// <reference lib="esnext.temporal" />
|
|
3
|
+
import { PGliteInterface } from "@electric-sql/pglite";
|
|
4
|
+
import { KvKey, KvStore, KvStoreListEntry, KvStoreSetOptions } from "@fedify/fedify";
|
|
5
|
+
|
|
6
|
+
//#region src/kv.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Options for the PGlite key–value store.
|
|
9
|
+
*/
|
|
10
|
+
interface PgliteKvStoreOptions {
|
|
11
|
+
/**
|
|
12
|
+
* The table name to use for the key–value store.
|
|
13
|
+
* A `.` separates the schema from the table name, so neither the whole
|
|
14
|
+
* name nor any segment between dots can be empty.
|
|
15
|
+
* `"fedify_kv_v2"` by default.
|
|
16
|
+
* @default `"fedify_kv_v2"`
|
|
17
|
+
*/
|
|
18
|
+
readonly tableName?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Whether the table has already been initialized. `false` by default.
|
|
21
|
+
* @default `false`
|
|
22
|
+
*/
|
|
23
|
+
readonly initialized?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A key–value store backed by an embedded PGlite database.
|
|
27
|
+
*
|
|
28
|
+
* The caller owns the PGlite instance and is responsible for closing it after
|
|
29
|
+
* every operation using the store has completed. Use only one PGlite instance
|
|
30
|
+
* for a data directory within a runtime isolate.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts ignore
|
|
34
|
+
* import { PGlite } from "@electric-sql/pglite";
|
|
35
|
+
* import { createFederation } from "@fedify/fedify";
|
|
36
|
+
* import { PgliteKvStore } from "@fedify/pglite";
|
|
37
|
+
*
|
|
38
|
+
* const pg = new PGlite("./data/fedify");
|
|
39
|
+
* const federation = createFederation({
|
|
40
|
+
* // ...
|
|
41
|
+
* kv: new PgliteKvStore(pg),
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Close pg during application shutdown, after pending operations finish.
|
|
45
|
+
* await pg.close();
|
|
46
|
+
* ```
|
|
47
|
+
* @since 2.4.0
|
|
48
|
+
*/
|
|
49
|
+
declare class PgliteKvStore implements KvStore {
|
|
50
|
+
#private;
|
|
51
|
+
/**
|
|
52
|
+
* Creates a new PGlite key–value store.
|
|
53
|
+
* @param pg The caller-owned PGlite instance to use.
|
|
54
|
+
* @param options The options for the key–value store.
|
|
55
|
+
*/
|
|
56
|
+
constructor(pg: PGliteInterface, options?: PgliteKvStoreOptions);
|
|
57
|
+
get<T = unknown>(key: KvKey): Promise<T | undefined>;
|
|
58
|
+
set(key: KvKey, value: unknown, options?: KvStoreSetOptions): Promise<void>;
|
|
59
|
+
delete(key: KvKey): Promise<void>;
|
|
60
|
+
/** {@inheritDoc KvStore.cas} */
|
|
61
|
+
cas(key: KvKey, expectedValue: unknown, newValue: unknown, options?: KvStoreSetOptions): Promise<boolean>;
|
|
62
|
+
/** {@inheritDoc KvStore.list} */
|
|
63
|
+
list(prefix?: KvKey): AsyncIterable<KvStoreListEntry>;
|
|
64
|
+
/**
|
|
65
|
+
* Creates the table used by the store if it does not already exist.
|
|
66
|
+
*/
|
|
67
|
+
initialize(): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Drops the table used by the store. Does nothing if it does not exist.
|
|
70
|
+
*/
|
|
71
|
+
drop(): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { PgliteKvStore, PgliteKvStoreOptions };
|
package/dist/kv.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { Temporal } from "temporal-polyfill";
|
|
2
|
+
import { getLogger } from "@logtape/logtape";
|
|
3
|
+
//#region src/kv.ts
|
|
4
|
+
const logger = getLogger([
|
|
5
|
+
"fedify",
|
|
6
|
+
"pglite",
|
|
7
|
+
"kv"
|
|
8
|
+
]);
|
|
9
|
+
function quoteIdentifier(identifier) {
|
|
10
|
+
const parts = identifier.split(".");
|
|
11
|
+
if (parts.includes("")) throw new TypeError(`Invalid table name for the key–value store: ${JSON.stringify(identifier)}`);
|
|
12
|
+
return parts.map((part) => `"${part.replaceAll("\"", "\"\"")}"`).join(".");
|
|
13
|
+
}
|
|
14
|
+
function serializeJson(value) {
|
|
15
|
+
const serialized = JSON.stringify(value);
|
|
16
|
+
if (serialized == null) throw new TypeError("PGlite cannot store a value that is not valid JSON");
|
|
17
|
+
return serialized;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A key–value store backed by an embedded PGlite database.
|
|
21
|
+
*
|
|
22
|
+
* The caller owns the PGlite instance and is responsible for closing it after
|
|
23
|
+
* every operation using the store has completed. Use only one PGlite instance
|
|
24
|
+
* for a data directory within a runtime isolate.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts ignore
|
|
28
|
+
* import { PGlite } from "@electric-sql/pglite";
|
|
29
|
+
* import { createFederation } from "@fedify/fedify";
|
|
30
|
+
* import { PgliteKvStore } from "@fedify/pglite";
|
|
31
|
+
*
|
|
32
|
+
* const pg = new PGlite("./data/fedify");
|
|
33
|
+
* const federation = createFederation({
|
|
34
|
+
* // ...
|
|
35
|
+
* kv: new PgliteKvStore(pg),
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* // Close pg during application shutdown, after pending operations finish.
|
|
39
|
+
* await pg.close();
|
|
40
|
+
* ```
|
|
41
|
+
* @since 2.4.0
|
|
42
|
+
*/
|
|
43
|
+
var PgliteKvStore = class {
|
|
44
|
+
#pg;
|
|
45
|
+
#tableName;
|
|
46
|
+
#quotedTableName;
|
|
47
|
+
#initialized;
|
|
48
|
+
#initializing;
|
|
49
|
+
/**
|
|
50
|
+
* Creates a new PGlite key–value store.
|
|
51
|
+
* @param pg The caller-owned PGlite instance to use.
|
|
52
|
+
* @param options The options for the key–value store.
|
|
53
|
+
*/
|
|
54
|
+
constructor(pg, options = {}) {
|
|
55
|
+
this.#pg = pg;
|
|
56
|
+
this.#tableName = options.tableName ?? "fedify_kv_v2";
|
|
57
|
+
this.#quotedTableName = quoteIdentifier(this.#tableName);
|
|
58
|
+
this.#initialized = options.initialized ?? false;
|
|
59
|
+
}
|
|
60
|
+
async #expire() {
|
|
61
|
+
await this.#pg.query(`
|
|
62
|
+
DELETE FROM ${this.#quotedTableName}
|
|
63
|
+
WHERE ttl IS NOT NULL AND created + ttl < CURRENT_TIMESTAMP;
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
async get(key) {
|
|
67
|
+
await this.initialize();
|
|
68
|
+
return (await this.#pg.query(`
|
|
69
|
+
SELECT value
|
|
70
|
+
FROM ${this.#quotedTableName}
|
|
71
|
+
WHERE key = $1::text[]
|
|
72
|
+
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP);
|
|
73
|
+
`, [key])).rows[0]?.value;
|
|
74
|
+
}
|
|
75
|
+
async set(key, value, options) {
|
|
76
|
+
await this.initialize();
|
|
77
|
+
const ttl = options?.ttl == null ? null : Temporal.Duration.from(options.ttl).toString();
|
|
78
|
+
await this.#pg.query(`
|
|
79
|
+
INSERT INTO ${this.#quotedTableName} (key, value, ttl)
|
|
80
|
+
VALUES ($1::text[], $2::text::jsonb, $3::text::interval)
|
|
81
|
+
ON CONFLICT (key)
|
|
82
|
+
DO UPDATE SET
|
|
83
|
+
value = EXCLUDED.value,
|
|
84
|
+
created = CURRENT_TIMESTAMP,
|
|
85
|
+
ttl = EXCLUDED.ttl;
|
|
86
|
+
`, [
|
|
87
|
+
key,
|
|
88
|
+
serializeJson(value),
|
|
89
|
+
ttl
|
|
90
|
+
]);
|
|
91
|
+
await this.#expire();
|
|
92
|
+
}
|
|
93
|
+
async delete(key) {
|
|
94
|
+
await this.initialize();
|
|
95
|
+
await this.#pg.query(`
|
|
96
|
+
DELETE FROM ${this.#quotedTableName}
|
|
97
|
+
WHERE key = $1::text[];
|
|
98
|
+
`, [key]);
|
|
99
|
+
await this.#expire();
|
|
100
|
+
}
|
|
101
|
+
/** {@inheritDoc KvStore.cas} */
|
|
102
|
+
async cas(key, expectedValue, newValue, options) {
|
|
103
|
+
await this.initialize();
|
|
104
|
+
const ttl = options?.ttl == null ? null : Temporal.Duration.from(options.ttl).toString();
|
|
105
|
+
if (expectedValue === void 0 && newValue === void 0) {
|
|
106
|
+
const matched = await this.get(key) === void 0;
|
|
107
|
+
await this.#expire();
|
|
108
|
+
return matched;
|
|
109
|
+
}
|
|
110
|
+
let rowCount;
|
|
111
|
+
if (expectedValue === void 0) rowCount = (await this.#pg.query(`
|
|
112
|
+
INSERT INTO ${this.#quotedTableName} AS existing
|
|
113
|
+
(key, value, created, ttl)
|
|
114
|
+
VALUES (
|
|
115
|
+
$1::text[],
|
|
116
|
+
$2::text::jsonb,
|
|
117
|
+
CURRENT_TIMESTAMP,
|
|
118
|
+
$3::text::interval
|
|
119
|
+
)
|
|
120
|
+
ON CONFLICT (key)
|
|
121
|
+
DO UPDATE SET
|
|
122
|
+
value = EXCLUDED.value,
|
|
123
|
+
created = EXCLUDED.created,
|
|
124
|
+
ttl = EXCLUDED.ttl
|
|
125
|
+
WHERE existing.ttl IS NOT NULL
|
|
126
|
+
AND existing.created + existing.ttl <= CURRENT_TIMESTAMP
|
|
127
|
+
RETURNING key;
|
|
128
|
+
`, [
|
|
129
|
+
key,
|
|
130
|
+
serializeJson(newValue),
|
|
131
|
+
ttl
|
|
132
|
+
])).rows.length;
|
|
133
|
+
else if (newValue === void 0) rowCount = (await this.#pg.query(`
|
|
134
|
+
DELETE FROM ${this.#quotedTableName}
|
|
135
|
+
WHERE key = $1::text[]
|
|
136
|
+
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
|
|
137
|
+
AND value = $2::text::jsonb
|
|
138
|
+
RETURNING key;
|
|
139
|
+
`, [key, serializeJson(expectedValue)])).rows.length;
|
|
140
|
+
else rowCount = (await this.#pg.query(`
|
|
141
|
+
UPDATE ${this.#quotedTableName}
|
|
142
|
+
SET
|
|
143
|
+
value = $3::text::jsonb,
|
|
144
|
+
created = CURRENT_TIMESTAMP,
|
|
145
|
+
ttl = $4::text::interval
|
|
146
|
+
WHERE key = $1::text[]
|
|
147
|
+
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
|
|
148
|
+
AND value = $2::text::jsonb
|
|
149
|
+
RETURNING key;
|
|
150
|
+
`, [
|
|
151
|
+
key,
|
|
152
|
+
serializeJson(expectedValue),
|
|
153
|
+
serializeJson(newValue),
|
|
154
|
+
ttl
|
|
155
|
+
])).rows.length;
|
|
156
|
+
await this.#expire();
|
|
157
|
+
return rowCount > 0;
|
|
158
|
+
}
|
|
159
|
+
/** {@inheritDoc KvStore.list} */
|
|
160
|
+
async *list(prefix) {
|
|
161
|
+
await this.initialize();
|
|
162
|
+
const result = prefix == null || prefix.length === 0 ? await this.#pg.query(`
|
|
163
|
+
SELECT key, value
|
|
164
|
+
FROM ${this.#quotedTableName}
|
|
165
|
+
WHERE ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP
|
|
166
|
+
ORDER BY key;
|
|
167
|
+
`) : await this.#pg.query(`
|
|
168
|
+
SELECT key, value
|
|
169
|
+
FROM ${this.#quotedTableName}
|
|
170
|
+
WHERE array_length(key, 1) >= $1
|
|
171
|
+
AND key[1:$1] = $2::text[]
|
|
172
|
+
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
|
|
173
|
+
ORDER BY key;
|
|
174
|
+
`, [prefix.length, prefix]);
|
|
175
|
+
for (const row of result.rows) yield row;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Creates the table used by the store if it does not already exist.
|
|
179
|
+
*/
|
|
180
|
+
async initialize() {
|
|
181
|
+
await this.#pg.waitReady;
|
|
182
|
+
if (this.#initialized) return;
|
|
183
|
+
this.#initializing ??= (async () => {
|
|
184
|
+
logger.debug("Initializing the key–value store table {tableName}...", { tableName: this.#tableName });
|
|
185
|
+
await this.#pg.query(`
|
|
186
|
+
CREATE TABLE IF NOT EXISTS ${this.#quotedTableName} (
|
|
187
|
+
key text[] PRIMARY KEY,
|
|
188
|
+
value jsonb NOT NULL,
|
|
189
|
+
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
|
|
190
|
+
ttl interval
|
|
191
|
+
);
|
|
192
|
+
`);
|
|
193
|
+
this.#initialized = true;
|
|
194
|
+
logger.debug("Initialized the key–value store table {tableName}.", { tableName: this.#tableName });
|
|
195
|
+
})();
|
|
196
|
+
try {
|
|
197
|
+
await this.#initializing;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
this.#initializing = void 0;
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Drops the table used by the store. Does nothing if it does not exist.
|
|
205
|
+
*/
|
|
206
|
+
async drop() {
|
|
207
|
+
await this.#pg.waitReady;
|
|
208
|
+
await this.#pg.query(`DROP TABLE IF EXISTS ${this.#quotedTableName};`);
|
|
209
|
+
this.#initialized = false;
|
|
210
|
+
this.#initializing = void 0;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
//#endregion
|
|
214
|
+
export { PgliteKvStore };
|
package/dist/mod.cjs
ADDED
package/dist/mod.d.cts
ADDED
package/dist/mod.d.ts
ADDED
package/dist/mod.js
ADDED