@fedify/sqlite 1.8.1-pr.318.1225
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 +38 -0
- package/adapter.ts +44 -0
- package/deno.json +27 -0
- package/dist/_virtual/rolldown_runtime.js +30 -0
- package/dist/adapter.d.ts +48 -0
- package/dist/kv.d.ts +72 -0
- package/dist/kv.js +183 -0
- package/dist/mod.d.ts +3 -0
- package/dist/mod.js +6 -0
- package/dist/node_modules/.pnpm/@js-temporal_polyfill@0.5.1/node_modules/@js-temporal/polyfill/dist/index.esm.js +5795 -0
- package/dist/node_modules/.pnpm/jsbi@4.3.2/node_modules/jsbi/dist/jsbi-cjs.js +1139 -0
- package/dist/sqlite.bun.d.ts +24 -0
- package/dist/sqlite.bun.js +39 -0
- package/dist/sqlite.node.d.ts +24 -0
- package/dist/sqlite.node.js +41 -0
- package/kv.test.ts +312 -0
- package/kv.ts +278 -0
- package/mod.ts +5 -0
- package/package.json +69 -0
- package/sqlite.bun.ts +45 -0
- package/sqlite.node.ts +48 -0
- package/tsdown.config.ts +13 -0
package/kv.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { type PlatformDatabase, SqliteDatabase } from "#sqlite";
|
|
2
|
+
import type { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify";
|
|
3
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
4
|
+
import { getLogger } from "@logtape/logtape";
|
|
5
|
+
import { isEqual } from "es-toolkit";
|
|
6
|
+
import type { SqliteDatabaseAdapter } from "./adapter.ts";
|
|
7
|
+
|
|
8
|
+
const logger = getLogger(["fedify", "sqlite", "kv"]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Options for the SQLite key–value store.
|
|
12
|
+
*/
|
|
13
|
+
export interface SqliteKvStoreOptions {
|
|
14
|
+
/**
|
|
15
|
+
* The table name to use for the key–value store.
|
|
16
|
+
* Only letters, digits, and underscores are allowed.
|
|
17
|
+
* `"fedify_kv"` by default.
|
|
18
|
+
* @default `"fedify_kv"`
|
|
19
|
+
*/
|
|
20
|
+
tableName?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether the table has been initialized. `false` by default.
|
|
24
|
+
* @default `false`
|
|
25
|
+
*/
|
|
26
|
+
initialized?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A key–value store that uses SQLite as the underlying storage.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { createFederation } from "@fedify/fedify";
|
|
35
|
+
* import { SqliteKvStore } from "@fedify/sqlite";
|
|
36
|
+
* import { DatabaseSync } from "node:sqlite";
|
|
37
|
+
*
|
|
38
|
+
* const db = new DatabaseSync(":memory:");
|
|
39
|
+
* const federation = createFederation({
|
|
40
|
+
* // ...
|
|
41
|
+
* kv: new SqliteKvStore(db),
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export class SqliteKvStore implements KvStore {
|
|
46
|
+
static readonly #defaultTableName = "fedify_kv";
|
|
47
|
+
static readonly #tableNameRegex = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
48
|
+
readonly #db: SqliteDatabaseAdapter;
|
|
49
|
+
readonly #tableName: string;
|
|
50
|
+
#initialized: boolean;
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
readonly db: PlatformDatabase,
|
|
54
|
+
readonly options: SqliteKvStoreOptions = {},
|
|
55
|
+
) {
|
|
56
|
+
this.#db = new SqliteDatabase(db);
|
|
57
|
+
this.#initialized = options.initialized ?? false;
|
|
58
|
+
this.#tableName = options.tableName ?? SqliteKvStore.#defaultTableName;
|
|
59
|
+
|
|
60
|
+
if (!SqliteKvStore.#tableNameRegex.test(this.#tableName)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Invalid table name for the key–value store: ${this.#tableName}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* {@inheritDoc KvStore.get}
|
|
69
|
+
*/
|
|
70
|
+
// deno-lint-ignore require-await
|
|
71
|
+
async get<T = unknown>(key: KvKey): Promise<T | undefined> {
|
|
72
|
+
this.initialize();
|
|
73
|
+
|
|
74
|
+
const encodedKey = this.#encodeKey(key);
|
|
75
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
76
|
+
|
|
77
|
+
const result = this.#db
|
|
78
|
+
.prepare(`
|
|
79
|
+
SELECT value
|
|
80
|
+
FROM "${this.#tableName}"
|
|
81
|
+
WHERE key = ? AND (expires IS NULL OR expires > ?)
|
|
82
|
+
`)
|
|
83
|
+
.get(encodedKey, now);
|
|
84
|
+
|
|
85
|
+
if (!result) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
return this.#decodeValue((result as { value: string }).value) as T;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* {@inheritDoc KvStore.set}
|
|
93
|
+
*/
|
|
94
|
+
// deno-lint-ignore require-await
|
|
95
|
+
async set(
|
|
96
|
+
key: KvKey,
|
|
97
|
+
value: unknown,
|
|
98
|
+
options?: KvStoreSetOptions,
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
this.initialize();
|
|
101
|
+
|
|
102
|
+
if (value === undefined) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const encodedKey = this.#encodeKey(key);
|
|
107
|
+
const encodedValue = this.#encodeValue(value);
|
|
108
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
109
|
+
const expiresAt = options?.ttl !== undefined
|
|
110
|
+
? now + options.ttl.total({ unit: "milliseconds" })
|
|
111
|
+
: null;
|
|
112
|
+
|
|
113
|
+
this.#db
|
|
114
|
+
.prepare(
|
|
115
|
+
`INSERT INTO "${this.#tableName}" (key, value, created, expires)
|
|
116
|
+
VALUES (?, ?, ?, ?)
|
|
117
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
118
|
+
value = excluded.value,
|
|
119
|
+
expires = excluded.expires`,
|
|
120
|
+
)
|
|
121
|
+
.run(encodedKey, encodedValue, now, expiresAt);
|
|
122
|
+
|
|
123
|
+
this.#expire();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* {@inheritDoc KvStore.delete}
|
|
129
|
+
*/
|
|
130
|
+
// deno-lint-ignore require-await
|
|
131
|
+
async delete(key: KvKey): Promise<void> {
|
|
132
|
+
this.initialize();
|
|
133
|
+
|
|
134
|
+
const encodedKey = this.#encodeKey(key);
|
|
135
|
+
|
|
136
|
+
this.#db
|
|
137
|
+
.prepare(`
|
|
138
|
+
DELETE FROM "${this.#tableName}" WHERE key = ?
|
|
139
|
+
`)
|
|
140
|
+
.run(encodedKey);
|
|
141
|
+
this.#expire();
|
|
142
|
+
return Promise.resolve();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* {@inheritDoc KvStore.cas}
|
|
147
|
+
*/
|
|
148
|
+
// deno-lint-ignore require-await
|
|
149
|
+
async cas(
|
|
150
|
+
key: KvKey,
|
|
151
|
+
expectedValue: unknown,
|
|
152
|
+
newValue: unknown,
|
|
153
|
+
options?: KvStoreSetOptions,
|
|
154
|
+
): Promise<boolean> {
|
|
155
|
+
this.initialize();
|
|
156
|
+
|
|
157
|
+
const encodedKey = this.#encodeKey(key);
|
|
158
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
159
|
+
const expiresAt = options?.ttl !== undefined
|
|
160
|
+
? now + options.ttl.total({ unit: "milliseconds" })
|
|
161
|
+
: null;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
165
|
+
|
|
166
|
+
const currentResult = this.#db
|
|
167
|
+
.prepare(`
|
|
168
|
+
SELECT value
|
|
169
|
+
FROM "${this.#tableName}"
|
|
170
|
+
WHERE key = ? AND (expires IS NULL OR expires > ?)
|
|
171
|
+
`)
|
|
172
|
+
.get(encodedKey, now) as { value: string } | undefined;
|
|
173
|
+
const currentValue = currentResult === undefined
|
|
174
|
+
? undefined
|
|
175
|
+
: this.#decodeValue(currentResult.value);
|
|
176
|
+
|
|
177
|
+
if (!isEqual(currentValue, expectedValue)) {
|
|
178
|
+
this.#db.exec("ROLLBACK");
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (newValue === undefined) {
|
|
183
|
+
this.#db
|
|
184
|
+
.prepare(`
|
|
185
|
+
DELETE FROM "${this.#tableName}" WHERE key = ?
|
|
186
|
+
`)
|
|
187
|
+
.run(encodedKey);
|
|
188
|
+
} else {
|
|
189
|
+
const newValueJson = this.#encodeValue(newValue);
|
|
190
|
+
|
|
191
|
+
this.#db
|
|
192
|
+
.prepare(`
|
|
193
|
+
INSERT INTO "${this.#tableName}" (key, value, created, expires)
|
|
194
|
+
VALUES (?, ?, ?, ?)
|
|
195
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
196
|
+
value = excluded.value,
|
|
197
|
+
expires = excluded.expires
|
|
198
|
+
`)
|
|
199
|
+
.run(encodedKey, newValueJson, now, expiresAt);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.#db.exec("COMMIT");
|
|
203
|
+
this.#expire();
|
|
204
|
+
return true;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
this.#db.exec("ROLLBACK");
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Creates the table used by the key–value store if it does not already exist.
|
|
213
|
+
* Does nothing if the table already exists.
|
|
214
|
+
*/
|
|
215
|
+
initialize() {
|
|
216
|
+
if (this.#initialized) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
logger.debug("Initializing the key–value store table {tableName}...", {
|
|
221
|
+
tableName: this.#tableName,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
this.#db.exec(`
|
|
225
|
+
CREATE TABLE IF NOT EXISTS "${this.#tableName}" (
|
|
226
|
+
key TEXT PRIMARY KEY,
|
|
227
|
+
value TEXT NOT NULL,
|
|
228
|
+
created INTEGER NOT NULL,
|
|
229
|
+
expires INTEGER
|
|
230
|
+
)
|
|
231
|
+
`);
|
|
232
|
+
|
|
233
|
+
this.#db.exec(`
|
|
234
|
+
CREATE INDEX IF NOT EXISTS "idx_${this.#tableName}_expires"
|
|
235
|
+
ON "${this.#tableName}" (expires)
|
|
236
|
+
`);
|
|
237
|
+
|
|
238
|
+
this.#initialized = true;
|
|
239
|
+
logger.debug("Initialized the key–value store table {tableName}.", {
|
|
240
|
+
tableName: this.#tableName,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#expire() {
|
|
245
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
246
|
+
this.#db
|
|
247
|
+
.prepare(`
|
|
248
|
+
DELETE FROM "${this.#tableName}"
|
|
249
|
+
WHERE expires IS NOT NULL AND expires <= ?
|
|
250
|
+
`)
|
|
251
|
+
.run(now);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Drops the table used by the key–value store. Does nothing if the table
|
|
256
|
+
* does not exist.
|
|
257
|
+
*/
|
|
258
|
+
drop() {
|
|
259
|
+
this.#db.exec(`DROP TABLE IF EXISTS "${this.#tableName}"`);
|
|
260
|
+
this.#initialized = false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#encodeKey(key: KvKey): string {
|
|
264
|
+
return JSON.stringify(key);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#decodeKey(key: string): KvKey {
|
|
268
|
+
return JSON.parse(key);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#encodeValue(value: unknown): string {
|
|
272
|
+
return JSON.stringify(value);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#decodeValue(value: string): unknown {
|
|
276
|
+
return JSON.parse(value);
|
|
277
|
+
}
|
|
278
|
+
}
|
package/mod.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fedify/sqlite",
|
|
3
|
+
"version": "1.8.1-pr.318.1225+27a86736",
|
|
4
|
+
"description": "SQLite drivers for Fedify",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fedify",
|
|
7
|
+
"sqlite"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "An Nyeong <me@annyeong.me>",
|
|
11
|
+
"homepage": "https://fedify.dev/",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/fedify-dev/fedify.git",
|
|
15
|
+
"directory": "sqlite"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/fedify-dev/fedify/issues"
|
|
19
|
+
},
|
|
20
|
+
"funding": [
|
|
21
|
+
"https://opencollective.com/fedify",
|
|
22
|
+
"https://github.com/sponsors/dahlia"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./dist/mod.js",
|
|
26
|
+
"module": "./dist/mod.js",
|
|
27
|
+
"types": "./dist/mod.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/mod.d.ts",
|
|
31
|
+
"import": "./dist/mod.js",
|
|
32
|
+
"default": "./dist/mod.js"
|
|
33
|
+
},
|
|
34
|
+
"./kv": {
|
|
35
|
+
"types": "./dist/kv.d.ts",
|
|
36
|
+
"import": "./dist/kv.js",
|
|
37
|
+
"default": "./dist/kv.js"
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"imports": {
|
|
42
|
+
"#sqlite": {
|
|
43
|
+
"bun": "./dist/sqlite.bun.js",
|
|
44
|
+
"deno": "./dist/sqlite.node.js",
|
|
45
|
+
"import": "./dist/sqlite.node.js",
|
|
46
|
+
"require": "./dist/sqlite.node.cjs"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@logtape/logtape": "^1.0.0",
|
|
51
|
+
"es-toolkit": "^1.31.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@fedify/fedify": "1.8.1-pr.318.1225+27a86736"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@js-temporal/polyfill": "^0.5.1",
|
|
58
|
+
"@std/async": "npm:@jsr/std__async@^1.0.13",
|
|
59
|
+
"tsdown": "^0.12.9",
|
|
60
|
+
"typescript": "^5.8.3"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsdown",
|
|
64
|
+
"prepublish": "tsdown",
|
|
65
|
+
"test": "tsdown && node --experimental-transform-types --test kv.test.ts",
|
|
66
|
+
"test:bun": "tsdown && bun test --timeout=10000 kv.test.ts",
|
|
67
|
+
"test:deno": "tsdown && deno test kv.test.ts"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/sqlite.bun.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Database, type Statement } from "bun:sqlite";
|
|
2
|
+
import type {
|
|
3
|
+
SqliteDatabaseAdapter,
|
|
4
|
+
SqliteStatementAdapter,
|
|
5
|
+
} from "./adapter.ts";
|
|
6
|
+
|
|
7
|
+
export { Database as PlatformDatabase };
|
|
8
|
+
export type { Statement as PlatformStatement };
|
|
9
|
+
|
|
10
|
+
export class SqliteDatabase implements SqliteDatabaseAdapter {
|
|
11
|
+
constructor(private readonly db: Database) {}
|
|
12
|
+
|
|
13
|
+
prepare(sql: string): SqliteStatementAdapter {
|
|
14
|
+
return new SqliteStatement(this.db.query(sql));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
exec(sql: string): void {
|
|
18
|
+
this.db.exec(sql);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
close(): void {
|
|
22
|
+
this.db.close(false);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SqliteStatement implements SqliteStatementAdapter {
|
|
27
|
+
constructor(private readonly stmt: Statement) {}
|
|
28
|
+
|
|
29
|
+
run(...params: unknown[]): { changes: number; lastInsertRowid: number } {
|
|
30
|
+
return this.stmt.run(...params);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get(...params: unknown[]): unknown | undefined {
|
|
34
|
+
const result = this.stmt.get(...params);
|
|
35
|
+
// to make the return type compatible with the node version
|
|
36
|
+
if (result === null) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
all(...params: unknown[]): unknown[] {
|
|
43
|
+
return this.stmt.all(...params);
|
|
44
|
+
}
|
|
45
|
+
}
|
package/sqlite.node.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DatabaseSync,
|
|
3
|
+
type SQLInputValue,
|
|
4
|
+
type StatementSync,
|
|
5
|
+
} from "node:sqlite";
|
|
6
|
+
import type {
|
|
7
|
+
SqliteDatabaseAdapter,
|
|
8
|
+
SqliteStatementAdapter,
|
|
9
|
+
} from "./adapter.ts";
|
|
10
|
+
|
|
11
|
+
export { DatabaseSync as PlatformDatabase };
|
|
12
|
+
export type { StatementSync as PlatformStatement };
|
|
13
|
+
|
|
14
|
+
export class SqliteDatabase implements SqliteDatabaseAdapter {
|
|
15
|
+
constructor(private readonly db: DatabaseSync) {}
|
|
16
|
+
|
|
17
|
+
prepare(sql: string): SqliteStatementAdapter {
|
|
18
|
+
return new SqliteStatement(this.db.prepare(sql));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
exec(sql: string): void {
|
|
22
|
+
this.db.exec(sql);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
close(): void {
|
|
26
|
+
this.db.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class SqliteStatement implements SqliteStatementAdapter {
|
|
31
|
+
constructor(private readonly stmt: StatementSync) {}
|
|
32
|
+
|
|
33
|
+
run(...params: unknown[]): { changes: number; lastInsertRowid: number } {
|
|
34
|
+
const result = this.stmt.run(...params as SQLInputValue[]);
|
|
35
|
+
return {
|
|
36
|
+
changes: Number(result.changes),
|
|
37
|
+
lastInsertRowid: Number(result.lastInsertRowid),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get(...params: unknown[]): unknown | undefined {
|
|
42
|
+
return this.stmt.get(...params as SQLInputValue[]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
all(...params: unknown[]): unknown[] {
|
|
46
|
+
return this.stmt.all(...params as SQLInputValue[]);
|
|
47
|
+
}
|
|
48
|
+
}
|
package/tsdown.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from "tsdown";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ["mod.ts", "kv.ts", "sqlite.node.ts", "sqlite.bun.ts"],
|
|
5
|
+
dts: true,
|
|
6
|
+
unbundle: true,
|
|
7
|
+
platform: "node",
|
|
8
|
+
outputOptions: {
|
|
9
|
+
intro: `
|
|
10
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
11
|
+
`,
|
|
12
|
+
},
|
|
13
|
+
});
|