@fedify/sqlite 2.0.0-dev.1690 → 2.0.0-dev.170
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 +1 -1
- package/README.md +3 -3
- package/deno.json +2 -3
- package/dist/kv.cjs +39 -10
- package/dist/kv.d.cts +8 -3
- package/dist/kv.d.ts +8 -3
- package/dist/kv.js +35 -6
- package/package.json +4 -4
- package/src/kv.test.ts +96 -3
- package/src/kv.ts +58 -9
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
This package provides a SQLite-based [`KvStore`] implementation for [Fedify].
|
|
8
8
|
|
|
9
|
-
[JSR]: https://jsr.io/@fedify/sqlite
|
|
10
9
|
[JSR badge]: https://jsr.io/badges/@fedify/sqlite
|
|
11
|
-
[
|
|
10
|
+
[JSR]: https://jsr.io/@fedify/sqlite
|
|
12
11
|
[npm badge]: https://img.shields.io/npm/v/@fedify/sqlite?logo=npm
|
|
13
|
-
[
|
|
12
|
+
[npm]: https://www.npmjs.com/package/@fedify/sqlite
|
|
14
13
|
[`KvStore`]: https://jsr.io/@fedify/fedify/doc/federation/~/KvStore
|
|
14
|
+
[Fedify]: https://fedify.dev/
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
Usage
|
package/deno.json
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/sqlite",
|
|
3
|
-
"version": "2.0.0-dev.
|
|
3
|
+
"version": "2.0.0-dev.170+a03fedcd",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/mod.ts",
|
|
7
7
|
"./kv": "./src/kv.ts"
|
|
8
8
|
},
|
|
9
9
|
"imports": {
|
|
10
|
-
"@fedify/sqlite": "./src/mod.ts",
|
|
11
|
-
"@fedify/sqlite/": "./src/",
|
|
12
10
|
"#sqlite": "./src/sqlite.node.ts"
|
|
13
11
|
},
|
|
14
12
|
"exclude": [
|
|
@@ -18,6 +16,7 @@
|
|
|
18
16
|
],
|
|
19
17
|
"publish": {
|
|
20
18
|
"exclude": [
|
|
19
|
+
"**/*.test.ts",
|
|
21
20
|
"!dist/"
|
|
22
21
|
]
|
|
23
22
|
},
|
package/dist/kv.cjs
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
5
5
|
const __sqlite = require_rolldown_runtime.__toESM(require("#sqlite"));
|
|
6
|
-
const __js_temporal_polyfill = require_rolldown_runtime.__toESM(require("@js-temporal/polyfill"));
|
|
7
6
|
const __logtape_logtape = require_rolldown_runtime.__toESM(require("@logtape/logtape"));
|
|
8
7
|
const es_toolkit = require_rolldown_runtime.__toESM(require("es-toolkit"));
|
|
9
8
|
|
|
@@ -54,10 +53,10 @@ var SqliteKvStore = class SqliteKvStore {
|
|
|
54
53
|
async get(key) {
|
|
55
54
|
this.initialize();
|
|
56
55
|
const encodedKey = this.#encodeKey(key);
|
|
57
|
-
const now =
|
|
56
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
58
57
|
const result = this.#db.prepare(`
|
|
59
|
-
SELECT value
|
|
60
|
-
FROM "${this.#tableName}"
|
|
58
|
+
SELECT value
|
|
59
|
+
FROM "${this.#tableName}"
|
|
61
60
|
WHERE key = ? AND (expires IS NULL OR expires > ?)
|
|
62
61
|
`).get(encodedKey, now);
|
|
63
62
|
if (!result) return void 0;
|
|
@@ -71,7 +70,7 @@ var SqliteKvStore = class SqliteKvStore {
|
|
|
71
70
|
if (value === void 0) return;
|
|
72
71
|
const encodedKey = this.#encodeKey(key);
|
|
73
72
|
const encodedValue = this.#encodeValue(value);
|
|
74
|
-
const now =
|
|
73
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
75
74
|
const expiresAt = options?.ttl !== void 0 ? now + options.ttl.total({ unit: "milliseconds" }) : null;
|
|
76
75
|
this.#db.prepare(`INSERT INTO "${this.#tableName}" (key, value, created, expires)
|
|
77
76
|
VALUES (?, ?, ?, ?)
|
|
@@ -99,13 +98,13 @@ var SqliteKvStore = class SqliteKvStore {
|
|
|
99
98
|
async cas(key, expectedValue, newValue, options) {
|
|
100
99
|
this.initialize();
|
|
101
100
|
const encodedKey = this.#encodeKey(key);
|
|
102
|
-
const now =
|
|
101
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
103
102
|
const expiresAt = options?.ttl !== void 0 ? now + options.ttl.total({ unit: "milliseconds" }) : null;
|
|
104
103
|
try {
|
|
105
104
|
this.#db.exec("BEGIN IMMEDIATE");
|
|
106
105
|
const currentResult = this.#db.prepare(`
|
|
107
|
-
SELECT value
|
|
108
|
-
FROM "${this.#tableName}"
|
|
106
|
+
SELECT value
|
|
107
|
+
FROM "${this.#tableName}"
|
|
109
108
|
WHERE key = ? AND (expires IS NULL OR expires > ?)
|
|
110
109
|
`).get(encodedKey, now);
|
|
111
110
|
const currentValue = currentResult === void 0 ? void 0 : this.#decodeValue(currentResult.value);
|
|
@@ -135,6 +134,36 @@ var SqliteKvStore = class SqliteKvStore {
|
|
|
135
134
|
}
|
|
136
135
|
}
|
|
137
136
|
/**
|
|
137
|
+
* {@inheritDoc KvStore.list}
|
|
138
|
+
* @since 1.10.0
|
|
139
|
+
*/
|
|
140
|
+
async *list(prefix) {
|
|
141
|
+
this.initialize();
|
|
142
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
143
|
+
let results;
|
|
144
|
+
if (prefix == null || prefix.length === 0) results = this.#db.prepare(`
|
|
145
|
+
SELECT key, value
|
|
146
|
+
FROM "${this.#tableName}"
|
|
147
|
+
WHERE expires IS NULL OR expires > ?
|
|
148
|
+
ORDER BY key
|
|
149
|
+
`).all(now);
|
|
150
|
+
else {
|
|
151
|
+
const pattern = JSON.stringify(prefix).slice(0, -1) + ",%";
|
|
152
|
+
const exactKey = JSON.stringify(prefix);
|
|
153
|
+
results = this.#db.prepare(`
|
|
154
|
+
SELECT key, value
|
|
155
|
+
FROM "${this.#tableName}"
|
|
156
|
+
WHERE (key LIKE ? ESCAPE '\\' OR key = ?)
|
|
157
|
+
AND (expires IS NULL OR expires > ?)
|
|
158
|
+
ORDER BY key
|
|
159
|
+
`).all(pattern, exactKey, now);
|
|
160
|
+
}
|
|
161
|
+
for (const row of results) yield {
|
|
162
|
+
key: this.#decodeKey(row.key),
|
|
163
|
+
value: this.#decodeValue(row.value)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
138
167
|
* Creates the table used by the key–value store if it does not already exist.
|
|
139
168
|
* Does nothing if the table already exists.
|
|
140
169
|
*/
|
|
@@ -150,14 +179,14 @@ var SqliteKvStore = class SqliteKvStore {
|
|
|
150
179
|
)
|
|
151
180
|
`);
|
|
152
181
|
this.#db.exec(`
|
|
153
|
-
CREATE INDEX IF NOT EXISTS "idx_${this.#tableName}_expires"
|
|
182
|
+
CREATE INDEX IF NOT EXISTS "idx_${this.#tableName}_expires"
|
|
154
183
|
ON "${this.#tableName}" (expires)
|
|
155
184
|
`);
|
|
156
185
|
this.#initialized = true;
|
|
157
186
|
logger.debug("Initialized the key–value store table {tableName}.", { tableName: this.#tableName });
|
|
158
187
|
}
|
|
159
188
|
#expire() {
|
|
160
|
-
const now =
|
|
189
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
161
190
|
this.#db.prepare(`
|
|
162
191
|
DELETE FROM "${this.#tableName}"
|
|
163
192
|
WHERE expires IS NOT NULL AND expires <= ?
|
package/dist/kv.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync } from "./dist/sqlite.node.cjs";
|
|
2
|
-
import { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify";
|
|
2
|
+
import { KvKey, KvStore, KvStoreListEntry, KvStoreSetOptions } from "@fedify/fedify";
|
|
3
3
|
|
|
4
4
|
//#region src/kv.d.ts
|
|
5
5
|
/**
|
|
@@ -12,12 +12,12 @@ interface SqliteKvStoreOptions {
|
|
|
12
12
|
* `"fedify_kv"` by default.
|
|
13
13
|
* @default `"fedify_kv"`
|
|
14
14
|
*/
|
|
15
|
-
tableName?: string;
|
|
15
|
+
readonly tableName?: string;
|
|
16
16
|
/**
|
|
17
17
|
* Whether the table has been initialized. `false` by default.
|
|
18
18
|
* @default `false`
|
|
19
19
|
*/
|
|
20
|
-
initialized?: boolean;
|
|
20
|
+
readonly initialized?: boolean;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* A key–value store that uses SQLite as the underlying storage.
|
|
@@ -61,6 +61,11 @@ declare class SqliteKvStore implements KvStore {
|
|
|
61
61
|
* {@inheritDoc KvStore.cas}
|
|
62
62
|
*/
|
|
63
63
|
cas(key: KvKey, expectedValue: unknown, newValue: unknown, options?: KvStoreSetOptions): Promise<boolean>;
|
|
64
|
+
/**
|
|
65
|
+
* {@inheritDoc KvStore.list}
|
|
66
|
+
* @since 1.10.0
|
|
67
|
+
*/
|
|
68
|
+
list(prefix?: KvKey): AsyncIterable<KvStoreListEntry>;
|
|
64
69
|
/**
|
|
65
70
|
* Creates the table used by the key–value store if it does not already exist.
|
|
66
71
|
* Does nothing if the table already exists.
|
package/dist/kv.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
2
|
import { PlatformDatabase } from "#sqlite";
|
|
3
|
-
import { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify";
|
|
3
|
+
import { KvKey, KvStore, KvStoreListEntry, KvStoreSetOptions } from "@fedify/fedify";
|
|
4
4
|
|
|
5
5
|
//#region src/kv.d.ts
|
|
6
6
|
/**
|
|
@@ -13,12 +13,12 @@ interface SqliteKvStoreOptions {
|
|
|
13
13
|
* `"fedify_kv"` by default.
|
|
14
14
|
* @default `"fedify_kv"`
|
|
15
15
|
*/
|
|
16
|
-
tableName?: string;
|
|
16
|
+
readonly tableName?: string;
|
|
17
17
|
/**
|
|
18
18
|
* Whether the table has been initialized. `false` by default.
|
|
19
19
|
* @default `false`
|
|
20
20
|
*/
|
|
21
|
-
initialized?: boolean;
|
|
21
|
+
readonly initialized?: boolean;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* A key–value store that uses SQLite as the underlying storage.
|
|
@@ -62,6 +62,11 @@ declare class SqliteKvStore implements KvStore {
|
|
|
62
62
|
* {@inheritDoc KvStore.cas}
|
|
63
63
|
*/
|
|
64
64
|
cas(key: KvKey, expectedValue: unknown, newValue: unknown, options?: KvStoreSetOptions): Promise<boolean>;
|
|
65
|
+
/**
|
|
66
|
+
* {@inheritDoc KvStore.list}
|
|
67
|
+
* @since 1.10.0
|
|
68
|
+
*/
|
|
69
|
+
list(prefix?: KvKey): AsyncIterable<KvStoreListEntry>;
|
|
65
70
|
/**
|
|
66
71
|
* Creates the table used by the key–value store if it does not already exist.
|
|
67
72
|
* Does nothing if the table already exists.
|
package/dist/kv.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { Temporal } from "@js-temporal/polyfill";
|
|
3
3
|
|
|
4
4
|
import { SqliteDatabase } from "#sqlite";
|
|
5
|
-
import { Temporal } from "@js-temporal/polyfill";
|
|
6
5
|
import { getLogger } from "@logtape/logtape";
|
|
7
6
|
import { isEqual } from "es-toolkit";
|
|
8
7
|
|
|
@@ -55,8 +54,8 @@ var SqliteKvStore = class SqliteKvStore {
|
|
|
55
54
|
const encodedKey = this.#encodeKey(key);
|
|
56
55
|
const now = Temporal.Now.instant().epochMilliseconds;
|
|
57
56
|
const result = this.#db.prepare(`
|
|
58
|
-
SELECT value
|
|
59
|
-
FROM "${this.#tableName}"
|
|
57
|
+
SELECT value
|
|
58
|
+
FROM "${this.#tableName}"
|
|
60
59
|
WHERE key = ? AND (expires IS NULL OR expires > ?)
|
|
61
60
|
`).get(encodedKey, now);
|
|
62
61
|
if (!result) return void 0;
|
|
@@ -103,8 +102,8 @@ var SqliteKvStore = class SqliteKvStore {
|
|
|
103
102
|
try {
|
|
104
103
|
this.#db.exec("BEGIN IMMEDIATE");
|
|
105
104
|
const currentResult = this.#db.prepare(`
|
|
106
|
-
SELECT value
|
|
107
|
-
FROM "${this.#tableName}"
|
|
105
|
+
SELECT value
|
|
106
|
+
FROM "${this.#tableName}"
|
|
108
107
|
WHERE key = ? AND (expires IS NULL OR expires > ?)
|
|
109
108
|
`).get(encodedKey, now);
|
|
110
109
|
const currentValue = currentResult === void 0 ? void 0 : this.#decodeValue(currentResult.value);
|
|
@@ -134,6 +133,36 @@ var SqliteKvStore = class SqliteKvStore {
|
|
|
134
133
|
}
|
|
135
134
|
}
|
|
136
135
|
/**
|
|
136
|
+
* {@inheritDoc KvStore.list}
|
|
137
|
+
* @since 1.10.0
|
|
138
|
+
*/
|
|
139
|
+
async *list(prefix) {
|
|
140
|
+
this.initialize();
|
|
141
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
142
|
+
let results;
|
|
143
|
+
if (prefix == null || prefix.length === 0) results = this.#db.prepare(`
|
|
144
|
+
SELECT key, value
|
|
145
|
+
FROM "${this.#tableName}"
|
|
146
|
+
WHERE expires IS NULL OR expires > ?
|
|
147
|
+
ORDER BY key
|
|
148
|
+
`).all(now);
|
|
149
|
+
else {
|
|
150
|
+
const pattern = JSON.stringify(prefix).slice(0, -1) + ",%";
|
|
151
|
+
const exactKey = JSON.stringify(prefix);
|
|
152
|
+
results = this.#db.prepare(`
|
|
153
|
+
SELECT key, value
|
|
154
|
+
FROM "${this.#tableName}"
|
|
155
|
+
WHERE (key LIKE ? ESCAPE '\\' OR key = ?)
|
|
156
|
+
AND (expires IS NULL OR expires > ?)
|
|
157
|
+
ORDER BY key
|
|
158
|
+
`).all(pattern, exactKey, now);
|
|
159
|
+
}
|
|
160
|
+
for (const row of results) yield {
|
|
161
|
+
key: this.#decodeKey(row.key),
|
|
162
|
+
value: this.#decodeValue(row.value)
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
137
166
|
* Creates the table used by the key–value store if it does not already exist.
|
|
138
167
|
* Does nothing if the table already exists.
|
|
139
168
|
*/
|
|
@@ -149,7 +178,7 @@ var SqliteKvStore = class SqliteKvStore {
|
|
|
149
178
|
)
|
|
150
179
|
`);
|
|
151
180
|
this.#db.exec(`
|
|
152
|
-
CREATE INDEX IF NOT EXISTS "idx_${this.#tableName}_expires"
|
|
181
|
+
CREATE INDEX IF NOT EXISTS "idx_${this.#tableName}_expires"
|
|
153
182
|
ON "${this.#tableName}" (expires)
|
|
154
183
|
`);
|
|
155
184
|
this.#initialized = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/sqlite",
|
|
3
|
-
"version": "2.0.0-dev.
|
|
3
|
+
"version": "2.0.0-dev.170+a03fedcd",
|
|
4
4
|
"description": "SQLite drivers for Fedify",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fedify",
|
|
@@ -58,16 +58,16 @@
|
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@js-temporal/polyfill": "^0.5.1",
|
|
61
|
-
"@logtape/logtape": "^
|
|
61
|
+
"@logtape/logtape": "^2.0.0",
|
|
62
62
|
"es-toolkit": "^1.31.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@fedify/fedify": "^2.0.0-dev.
|
|
65
|
+
"@fedify/fedify": "^2.0.0-dev.170+a03fedcd"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@std/async": "npm:@jsr/std__async@^1.0.13",
|
|
69
69
|
"tsdown": "^0.12.9",
|
|
70
|
-
"typescript": "^5.9.
|
|
70
|
+
"typescript": "^5.9.3"
|
|
71
71
|
},
|
|
72
72
|
"scripts": {
|
|
73
73
|
"build": "tsdown",
|
package/src/kv.test.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { PlatformDatabase } from "#sqlite";
|
|
2
|
+
import { SqliteKvStore } from "@fedify/sqlite/kv";
|
|
2
3
|
import * as temporal from "@js-temporal/polyfill";
|
|
3
4
|
import { delay } from "@std/async/delay";
|
|
4
5
|
import assert from "node:assert/strict";
|
|
5
6
|
import { test } from "node:test";
|
|
6
|
-
import { SqliteKvStore } from "./kv.ts";
|
|
7
7
|
|
|
8
8
|
let Temporal: typeof temporal.Temporal;
|
|
9
9
|
if ("Temporal" in globalThis) {
|
|
@@ -31,7 +31,7 @@ test("SqliteKvStore.initialize()", async () => {
|
|
|
31
31
|
try {
|
|
32
32
|
await store.initialize();
|
|
33
33
|
const result = await db.prepare(`
|
|
34
|
-
SELECT name FROM sqlite_master
|
|
34
|
+
SELECT name FROM sqlite_master
|
|
35
35
|
WHERE type='table' AND name=?
|
|
36
36
|
`).get(tableName);
|
|
37
37
|
assert(result !== undefined);
|
|
@@ -180,7 +180,7 @@ test("SqliteKvStore.drop()", async () => {
|
|
|
180
180
|
try {
|
|
181
181
|
await store.drop();
|
|
182
182
|
const result = await db.prepare(`
|
|
183
|
-
SELECT name FROM sqlite_master
|
|
183
|
+
SELECT name FROM sqlite_master
|
|
184
184
|
WHERE type='table' AND name=?
|
|
185
185
|
`).get(tableName);
|
|
186
186
|
// Bun returns null, Node returns undefined
|
|
@@ -310,3 +310,96 @@ test("SqliteKvStore.set() - preserves created timestamp on update", async () =>
|
|
|
310
310
|
await db.close();
|
|
311
311
|
}
|
|
312
312
|
});
|
|
313
|
+
|
|
314
|
+
test("SqliteKvStore.list()", async () => {
|
|
315
|
+
const { db, store } = getStore();
|
|
316
|
+
try {
|
|
317
|
+
await store.set(["prefix", "a"], "value-a");
|
|
318
|
+
await store.set(["prefix", "b"], "value-b");
|
|
319
|
+
await store.set(["prefix", "nested", "c"], "value-c");
|
|
320
|
+
await store.set(["other", "x"], "value-x");
|
|
321
|
+
|
|
322
|
+
const entries: { key: readonly string[]; value: unknown }[] = [];
|
|
323
|
+
for await (const entry of store.list(["prefix"])) {
|
|
324
|
+
entries.push({ key: entry.key, value: entry.value });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
assert.strictEqual(entries.length, 3);
|
|
328
|
+
assert(
|
|
329
|
+
entries.some((e) => e.key[1] === "a" && e.value === "value-a"),
|
|
330
|
+
);
|
|
331
|
+
assert(entries.some((e) => e.key[1] === "b"));
|
|
332
|
+
assert(entries.some((e) => e.key[1] === "nested"));
|
|
333
|
+
} finally {
|
|
334
|
+
await store.drop();
|
|
335
|
+
await db.close();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("SqliteKvStore.list() - excludes expired", async () => {
|
|
340
|
+
const { db, tableName, store } = getStore();
|
|
341
|
+
try {
|
|
342
|
+
await store.initialize();
|
|
343
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
344
|
+
|
|
345
|
+
// Insert expired entry directly
|
|
346
|
+
db.prepare(`
|
|
347
|
+
INSERT INTO "${tableName}" (key, value, created, expires)
|
|
348
|
+
VALUES (?, ?, ?, ?)
|
|
349
|
+
`).run(
|
|
350
|
+
JSON.stringify(["list-test", "expired"]),
|
|
351
|
+
JSON.stringify("expired-value"),
|
|
352
|
+
now - 1000,
|
|
353
|
+
now - 500,
|
|
354
|
+
);
|
|
355
|
+
await store.set(["list-test", "valid"], "valid-value");
|
|
356
|
+
|
|
357
|
+
const entries: { key: readonly string[]; value: unknown }[] = [];
|
|
358
|
+
for await (const entry of store.list(["list-test"])) {
|
|
359
|
+
entries.push({ key: entry.key, value: entry.value });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
assert.strictEqual(entries.length, 1);
|
|
363
|
+
assert.deepStrictEqual(entries[0].key, ["list-test", "valid"]);
|
|
364
|
+
} finally {
|
|
365
|
+
await store.drop();
|
|
366
|
+
await db.close();
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("SqliteKvStore.list() - single element key", async () => {
|
|
371
|
+
const { db, store } = getStore();
|
|
372
|
+
try {
|
|
373
|
+
await store.set(["a"], "value-a");
|
|
374
|
+
await store.set(["b"], "value-b");
|
|
375
|
+
|
|
376
|
+
const entries: { key: readonly string[]; value: unknown }[] = [];
|
|
377
|
+
for await (const entry of store.list(["a"])) {
|
|
378
|
+
entries.push({ key: entry.key, value: entry.value });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
assert.strictEqual(entries.length, 1);
|
|
382
|
+
} finally {
|
|
383
|
+
await store.drop();
|
|
384
|
+
await db.close();
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("SqliteKvStore.list() - empty prefix", async () => {
|
|
389
|
+
const { db, store } = getStore();
|
|
390
|
+
try {
|
|
391
|
+
await store.set(["a"], "value-a");
|
|
392
|
+
await store.set(["b", "c"], "value-bc");
|
|
393
|
+
await store.set(["d", "e", "f"], "value-def");
|
|
394
|
+
|
|
395
|
+
const entries: { key: readonly string[]; value: unknown }[] = [];
|
|
396
|
+
for await (const entry of store.list()) {
|
|
397
|
+
entries.push({ key: entry.key, value: entry.value });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
assert.strictEqual(entries.length, 3);
|
|
401
|
+
} finally {
|
|
402
|
+
await store.drop();
|
|
403
|
+
await db.close();
|
|
404
|
+
}
|
|
405
|
+
});
|
package/src/kv.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type PlatformDatabase, SqliteDatabase } from "#sqlite";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
2
|
+
import type {
|
|
3
|
+
KvKey,
|
|
4
|
+
KvStore,
|
|
5
|
+
KvStoreListEntry,
|
|
6
|
+
KvStoreSetOptions,
|
|
7
|
+
} from "@fedify/fedify";
|
|
4
8
|
import { getLogger } from "@logtape/logtape";
|
|
5
9
|
import { isEqual } from "es-toolkit";
|
|
6
10
|
import type { SqliteDatabaseAdapter } from "./adapter.ts";
|
|
@@ -17,13 +21,13 @@ export interface SqliteKvStoreOptions {
|
|
|
17
21
|
* `"fedify_kv"` by default.
|
|
18
22
|
* @default `"fedify_kv"`
|
|
19
23
|
*/
|
|
20
|
-
tableName?: string;
|
|
24
|
+
readonly tableName?: string;
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* Whether the table has been initialized. `false` by default.
|
|
24
28
|
* @default `false`
|
|
25
29
|
*/
|
|
26
|
-
initialized?: boolean;
|
|
30
|
+
readonly initialized?: boolean;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
/**
|
|
@@ -81,8 +85,8 @@ export class SqliteKvStore implements KvStore {
|
|
|
81
85
|
|
|
82
86
|
const result = this.#db
|
|
83
87
|
.prepare(`
|
|
84
|
-
SELECT value
|
|
85
|
-
FROM "${this.#tableName}"
|
|
88
|
+
SELECT value
|
|
89
|
+
FROM "${this.#tableName}"
|
|
86
90
|
WHERE key = ? AND (expires IS NULL OR expires > ?)
|
|
87
91
|
`)
|
|
88
92
|
.get(encodedKey, now);
|
|
@@ -170,8 +174,8 @@ export class SqliteKvStore implements KvStore {
|
|
|
170
174
|
|
|
171
175
|
const currentResult = this.#db
|
|
172
176
|
.prepare(`
|
|
173
|
-
SELECT value
|
|
174
|
-
FROM "${this.#tableName}"
|
|
177
|
+
SELECT value
|
|
178
|
+
FROM "${this.#tableName}"
|
|
175
179
|
WHERE key = ? AND (expires IS NULL OR expires > ?)
|
|
176
180
|
`)
|
|
177
181
|
.get(encodedKey, now) as { value: string } | undefined;
|
|
@@ -213,6 +217,51 @@ export class SqliteKvStore implements KvStore {
|
|
|
213
217
|
}
|
|
214
218
|
}
|
|
215
219
|
|
|
220
|
+
/**
|
|
221
|
+
* {@inheritDoc KvStore.list}
|
|
222
|
+
* @since 1.10.0
|
|
223
|
+
*/
|
|
224
|
+
async *list(prefix?: KvKey): AsyncIterable<KvStoreListEntry> {
|
|
225
|
+
this.initialize();
|
|
226
|
+
|
|
227
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
228
|
+
|
|
229
|
+
let results: { key: string; value: string }[];
|
|
230
|
+
|
|
231
|
+
if (prefix == null || prefix.length === 0) {
|
|
232
|
+
// Empty prefix: return all entries
|
|
233
|
+
results = this.#db
|
|
234
|
+
.prepare(`
|
|
235
|
+
SELECT key, value
|
|
236
|
+
FROM "${this.#tableName}"
|
|
237
|
+
WHERE expires IS NULL OR expires > ?
|
|
238
|
+
ORDER BY key
|
|
239
|
+
`)
|
|
240
|
+
.all(now) as { key: string; value: string }[];
|
|
241
|
+
} else {
|
|
242
|
+
// JSON pattern: '["prefix","' matches keys starting with prefix
|
|
243
|
+
const pattern = JSON.stringify(prefix).slice(0, -1) + ",%";
|
|
244
|
+
const exactKey = JSON.stringify(prefix);
|
|
245
|
+
|
|
246
|
+
results = this.#db
|
|
247
|
+
.prepare(`
|
|
248
|
+
SELECT key, value
|
|
249
|
+
FROM "${this.#tableName}"
|
|
250
|
+
WHERE (key LIKE ? ESCAPE '\\' OR key = ?)
|
|
251
|
+
AND (expires IS NULL OR expires > ?)
|
|
252
|
+
ORDER BY key
|
|
253
|
+
`)
|
|
254
|
+
.all(pattern, exactKey, now) as { key: string; value: string }[];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const row of results) {
|
|
258
|
+
yield {
|
|
259
|
+
key: this.#decodeKey(row.key),
|
|
260
|
+
value: this.#decodeValue(row.value),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
216
265
|
/**
|
|
217
266
|
* Creates the table used by the key–value store if it does not already exist.
|
|
218
267
|
* Does nothing if the table already exists.
|
|
@@ -236,7 +285,7 @@ export class SqliteKvStore implements KvStore {
|
|
|
236
285
|
`);
|
|
237
286
|
|
|
238
287
|
this.#db.exec(`
|
|
239
|
-
CREATE INDEX IF NOT EXISTS "idx_${this.#tableName}_expires"
|
|
288
|
+
CREATE INDEX IF NOT EXISTS "idx_${this.#tableName}_expires"
|
|
240
289
|
ON "${this.#tableName}" (expires)
|
|
241
290
|
`);
|
|
242
291
|
|