@fedify/postgres 0.4.0-dev.31 → 1.8.0-dev.1004
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 +7 -76
- package/deno.json +7 -19
- package/dist/kv.d.ts +60 -0
- package/dist/kv.js +115 -0
- package/dist/mod.d.ts +4 -0
- package/dist/mod.js +7 -0
- package/dist/mq.d.ts +66 -0
- package/dist/mq.js +185 -0
- package/dist/utils.js +11 -0
- package/{src/kv.test.ts → kv.test.ts} +5 -0
- package/mod.ts +2 -1
- package/{src/mq.test.ts → mq.test.ts} +1 -0
- package/{src/mq.ts → mq.ts} +5 -5
- package/package.json +20 -13
- package/tsdown.config.ts +2 -1
- package/.github/FUNDING.yaml +0 -2
- package/.github/workflows/main.yaml +0 -101
- package/.vscode/extensions.json +0 -6
- package/.vscode/settings.json +0 -33
- package/.zed/settings.json +0 -30
- package/deno.lock +0 -767
- package/src/mod.ts +0 -2
- /package/{src/kv.ts → kv.ts} +0 -0
- /package/{src/utils.ts → utils.ts} +0 -0
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
[![JSR][JSR badge]][JSR]
|
|
7
7
|
[![npm][npm badge]][npm]
|
|
8
|
-
[![GitHub Actions][GitHub Actions badge]][GitHub Actions]
|
|
9
8
|
|
|
10
9
|
This package provides [Fedify]'s [`KvStore`] and [`MessageQueue`]
|
|
11
10
|
implementations for PostgreSQL:
|
|
@@ -30,88 +29,20 @@ const federation = createFederation({
|
|
|
30
29
|
[JSR badge]: https://jsr.io/badges/@fedify/postgres
|
|
31
30
|
[npm]: https://www.npmjs.com/package/@fedify/postgres
|
|
32
31
|
[npm badge]: https://img.shields.io/npm/v/@fedify/postgres?logo=npm
|
|
33
|
-
[GitHub Actions]: https://github.com/fedify-dev/postgres/actions/workflows/main.yaml
|
|
34
|
-
[GitHub Actions badge]: https://github.com/fedify-dev/postgres/actions/workflows/main.yaml/badge.svg
|
|
35
32
|
[Fedify]: https://fedify.dev/
|
|
36
33
|
[`KvStore`]: https://jsr.io/@fedify/fedify/doc/federation/~/KvStore
|
|
37
34
|
[`MessageQueue`]: https://jsr.io/@fedify/fedify/doc/federation/~/MessageQueue
|
|
38
|
-
[`PostgresKvStore`]: https://jsr.io/@fedify/postgres/doc
|
|
39
|
-
[`PostgresMessageQueue`]: https://jsr.io/@fedify/postgres/doc
|
|
35
|
+
[`PostgresKvStore`]: https://jsr.io/@fedify/postgres/doc/~/PostgresKvStore
|
|
36
|
+
[`PostgresMessageQueue`]: https://jsr.io/@fedify/postgres/doc/~/PostgresMessageQueue
|
|
40
37
|
|
|
41
38
|
|
|
42
39
|
Installation
|
|
43
40
|
------------
|
|
44
41
|
|
|
45
|
-
### Deno
|
|
46
|
-
|
|
47
|
-
~~~~ sh
|
|
48
|
-
deno add @fedify/postgres
|
|
49
|
-
~~~~
|
|
50
|
-
|
|
51
|
-
### Node.js
|
|
52
|
-
|
|
53
|
-
~~~~ sh
|
|
54
|
-
npm install @fedify/postgres
|
|
55
|
-
~~~~
|
|
56
|
-
|
|
57
|
-
### Bun
|
|
58
|
-
|
|
59
42
|
~~~~ sh
|
|
60
|
-
|
|
43
|
+
deno add jsr:@fedify/postgres # Deno
|
|
44
|
+
npm add @fedify/postgres # npm
|
|
45
|
+
pnpm add @fedify/postgres # pnpm
|
|
46
|
+
yarn add @fedify/postgres # Yarn
|
|
47
|
+
bun add @fedify/postgres # Bun
|
|
61
48
|
~~~~
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Changelog
|
|
65
|
-
---------
|
|
66
|
-
|
|
67
|
-
### Version 0.4.0
|
|
68
|
-
|
|
69
|
-
To be released.
|
|
70
|
-
|
|
71
|
-
### Version 0.3.0
|
|
72
|
-
|
|
73
|
-
Released on March 28, 2025.
|
|
74
|
-
|
|
75
|
-
- Added `PostgresMessageQueue.enqueueMany()` method for efficiently enqueuing
|
|
76
|
-
multiple messages at once.
|
|
77
|
-
|
|
78
|
-
- Updated *@js-temporal/polyfill* to 0.5.0 for Node.js and Bun. On Deno,
|
|
79
|
-
there is no change because the polyfill is not used.
|
|
80
|
-
|
|
81
|
-
- Added some logging using [LogTape] for the sake of debugging. The following
|
|
82
|
-
categories are used:
|
|
83
|
-
|
|
84
|
-
- `["fedify", "postgres", "kv"]`
|
|
85
|
-
- `["fedify", "postgres", "mq"]`
|
|
86
|
-
|
|
87
|
-
[LogTape]: https://logtape.org/
|
|
88
|
-
|
|
89
|
-
### Version 0.2.2
|
|
90
|
-
|
|
91
|
-
Released on November 18, 2024.
|
|
92
|
-
|
|
93
|
-
- Fixed a bug where binding parameters have not been properly escaped with
|
|
94
|
-
some settings of Postgres.js.
|
|
95
|
-
|
|
96
|
-
### Version 0.2.1
|
|
97
|
-
|
|
98
|
-
Released on November 3, 2024.
|
|
99
|
-
|
|
100
|
-
- Fixed a bug where some scalar values have failed to be stored in the
|
|
101
|
-
database.
|
|
102
|
-
|
|
103
|
-
### Version 0.2.0
|
|
104
|
-
|
|
105
|
-
Released on November 3, 2024.
|
|
106
|
-
|
|
107
|
-
- Fixed a bug where JSON values are double-quoted in the database. Since it's
|
|
108
|
-
a breaking change data-wise, the default values of the following options
|
|
109
|
-
are also changed:
|
|
110
|
-
|
|
111
|
-
- `PostgresKvStoreOptions.tableName` defaults to `"fedify_kv_v2"`.
|
|
112
|
-
- `PostgresMessageQueueOptions.tableName` defaults to
|
|
113
|
-
`"fedify_message_v2"`.
|
|
114
|
-
|
|
115
|
-
### Version 0.1.0
|
|
116
|
-
|
|
117
|
-
Initial release. Released on September 26, 2024.
|
package/deno.json
CHANGED
|
@@ -1,34 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/postgres",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.8.0-dev.1004+d5b05e27",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./mod.ts",
|
|
7
|
-
"./kv": "./
|
|
8
|
-
"./mq": "./
|
|
9
|
-
},
|
|
10
|
-
"imports": {
|
|
11
|
-
"@deno/dnt": "jsr:@deno/dnt@^0.41.3",
|
|
12
|
-
"@fedify/fedify": "jsr:@fedify/fedify@1.7.2",
|
|
13
|
-
"@logtape/logtape": "jsr:@logtape/logtape@^1.0.0",
|
|
14
|
-
"@std/assert": "jsr:@std/assert@^1.0.4",
|
|
15
|
-
"@std/async": "jsr:@std/async@^1.0.5",
|
|
16
|
-
"postgres": "npm:postgres@^3.4.7",
|
|
17
|
-
"tsdown": "npm:tsdown@^0.12.9"
|
|
7
|
+
"./kv": "./kv.ts",
|
|
8
|
+
"./mq": "./mq.ts"
|
|
18
9
|
},
|
|
19
10
|
"nodeModulesDir": "none",
|
|
20
11
|
"unstable": [
|
|
21
12
|
"temporal"
|
|
22
13
|
],
|
|
23
14
|
"exclude": [
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"npm",
|
|
27
|
-
"pnpm-lock.yaml"
|
|
15
|
+
"dist",
|
|
16
|
+
"node_modules"
|
|
28
17
|
],
|
|
29
18
|
"tasks": {
|
|
30
|
-
"check": "deno fmt --check && deno lint && deno check
|
|
31
|
-
"test": "deno test --allow-net --allow-env"
|
|
32
|
-
"dnt": "deno run -A dnt.ts"
|
|
19
|
+
"check": "deno fmt --check && deno lint && deno check *.ts",
|
|
20
|
+
"test": "deno test --allow-net --allow-env"
|
|
33
21
|
}
|
|
34
22
|
}
|
package/dist/kv.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
2
|
+
import { Sql } from "postgres";
|
|
3
|
+
import { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify";
|
|
4
|
+
|
|
5
|
+
//#region kv.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Options for the PostgreSQL key-value store.
|
|
8
|
+
*/
|
|
9
|
+
interface PostgresKvStoreOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The table name to use for the key-value store.
|
|
12
|
+
* `"fedify_kv_v2"` by default.
|
|
13
|
+
* @default `"fedify_kv_v2"`
|
|
14
|
+
*/
|
|
15
|
+
tableName?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the table has been initialized. `false` by default.
|
|
18
|
+
* @default `false`
|
|
19
|
+
*/
|
|
20
|
+
initialized?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A key-value store that uses PostgreSQL as the underlying storage.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { createFederation } from "@fedify/fedify";
|
|
28
|
+
* import { PostgresKvStore } from "@fedify/postgres";
|
|
29
|
+
* import postgres from "postgres";
|
|
30
|
+
*
|
|
31
|
+
* const federation = createFederation({
|
|
32
|
+
* // ...
|
|
33
|
+
* kv: new PostgresKvStore(postgres("postgres://user:pass@localhost/db")),
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
declare class PostgresKvStore implements KvStore {
|
|
38
|
+
#private;
|
|
39
|
+
/**
|
|
40
|
+
* Creates a new PostgreSQL key-value store.
|
|
41
|
+
* @param sql The PostgreSQL client to use.
|
|
42
|
+
* @param options The options for the key-value store.
|
|
43
|
+
*/
|
|
44
|
+
constructor(sql: Sql<{}>, options?: PostgresKvStoreOptions);
|
|
45
|
+
get<T = unknown>(key: KvKey): Promise<T | undefined>;
|
|
46
|
+
set(key: KvKey, value: unknown, options?: KvStoreSetOptions | undefined): Promise<void>;
|
|
47
|
+
delete(key: KvKey): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Creates the table used by the key-value store if it does not already exist.
|
|
50
|
+
* Does nothing if the table already exists.
|
|
51
|
+
*/
|
|
52
|
+
initialize(): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Drops the table used by the key-value store. Does nothing if the table
|
|
55
|
+
* does not exist.
|
|
56
|
+
*/
|
|
57
|
+
drop(): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { PostgresKvStore, PostgresKvStoreOptions };
|
package/dist/kv.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
|
|
2
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
3
|
+
|
|
4
|
+
import { driverSerializesJson } from "./utils.js";
|
|
5
|
+
import { getLogger } from "@logtape/logtape";
|
|
6
|
+
|
|
7
|
+
//#region kv.ts
|
|
8
|
+
const logger = getLogger([
|
|
9
|
+
"fedify",
|
|
10
|
+
"postgres",
|
|
11
|
+
"kv"
|
|
12
|
+
]);
|
|
13
|
+
/**
|
|
14
|
+
* A key-value store that uses PostgreSQL as the underlying storage.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { createFederation } from "@fedify/fedify";
|
|
19
|
+
* import { PostgresKvStore } from "@fedify/postgres";
|
|
20
|
+
* import postgres from "postgres";
|
|
21
|
+
*
|
|
22
|
+
* const federation = createFederation({
|
|
23
|
+
* // ...
|
|
24
|
+
* kv: new PostgresKvStore(postgres("postgres://user:pass@localhost/db")),
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
var PostgresKvStore = class {
|
|
29
|
+
#sql;
|
|
30
|
+
#tableName;
|
|
31
|
+
#initialized;
|
|
32
|
+
#driverSerializesJson = false;
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new PostgreSQL key-value store.
|
|
35
|
+
* @param sql The PostgreSQL client to use.
|
|
36
|
+
* @param options The options for the key-value store.
|
|
37
|
+
*/
|
|
38
|
+
constructor(sql, options = {}) {
|
|
39
|
+
this.#sql = sql;
|
|
40
|
+
this.#tableName = options.tableName ?? "fedify_kv_v2";
|
|
41
|
+
this.#initialized = options.initialized ?? false;
|
|
42
|
+
}
|
|
43
|
+
async #expire() {
|
|
44
|
+
await this.#sql`
|
|
45
|
+
DELETE FROM ${this.#sql(this.#tableName)}
|
|
46
|
+
WHERE ttl IS NOT NULL AND created + ttl < CURRENT_TIMESTAMP;
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
async get(key) {
|
|
50
|
+
await this.initialize();
|
|
51
|
+
const result = await this.#sql`
|
|
52
|
+
SELECT value
|
|
53
|
+
FROM ${this.#sql(this.#tableName)}
|
|
54
|
+
WHERE key = ${key} AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP);
|
|
55
|
+
`;
|
|
56
|
+
if (result.length < 1) return void 0;
|
|
57
|
+
return result[0].value;
|
|
58
|
+
}
|
|
59
|
+
async set(key, value, options) {
|
|
60
|
+
await this.initialize();
|
|
61
|
+
const ttl = options?.ttl == null ? null : options.ttl.toString();
|
|
62
|
+
await this.#sql`
|
|
63
|
+
INSERT INTO ${this.#sql(this.#tableName)} (key, value, ttl)
|
|
64
|
+
VALUES (
|
|
65
|
+
${key},
|
|
66
|
+
${this.#json(value)},
|
|
67
|
+
${ttl}
|
|
68
|
+
)
|
|
69
|
+
ON CONFLICT (key)
|
|
70
|
+
DO UPDATE SET value = EXCLUDED.value, ttl = EXCLUDED.ttl;
|
|
71
|
+
`;
|
|
72
|
+
await this.#expire();
|
|
73
|
+
}
|
|
74
|
+
async delete(key) {
|
|
75
|
+
await this.initialize();
|
|
76
|
+
await this.#sql`
|
|
77
|
+
DELETE FROM ${this.#sql(this.#tableName)}
|
|
78
|
+
WHERE key = ${key};
|
|
79
|
+
`;
|
|
80
|
+
await this.#expire();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Creates the table used by the key-value store if it does not already exist.
|
|
84
|
+
* Does nothing if the table already exists.
|
|
85
|
+
*/
|
|
86
|
+
async initialize() {
|
|
87
|
+
if (this.#initialized) return;
|
|
88
|
+
logger.debug("Initializing the key-value store table {tableName}...", { tableName: this.#tableName });
|
|
89
|
+
await this.#sql`
|
|
90
|
+
CREATE UNLOGGED TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
|
|
91
|
+
key text[] PRIMARY KEY,
|
|
92
|
+
value jsonb NOT NULL,
|
|
93
|
+
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
|
|
94
|
+
ttl interval
|
|
95
|
+
);
|
|
96
|
+
`;
|
|
97
|
+
this.#driverSerializesJson = await driverSerializesJson(this.#sql);
|
|
98
|
+
this.#initialized = true;
|
|
99
|
+
logger.debug("Initialized the key-value store table {tableName}.", { tableName: this.#tableName });
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Drops the table used by the key-value store. Does nothing if the table
|
|
103
|
+
* does not exist.
|
|
104
|
+
*/
|
|
105
|
+
async drop() {
|
|
106
|
+
await this.#sql`DROP TABLE IF EXISTS ${this.#sql(this.#tableName)};`;
|
|
107
|
+
}
|
|
108
|
+
#json(value) {
|
|
109
|
+
if (this.#driverSerializesJson) return this.#sql.json(value);
|
|
110
|
+
return this.#sql.json(JSON.stringify(value));
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
export { PostgresKvStore };
|
package/dist/mod.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
2
|
+
import { PostgresKvStore, PostgresKvStoreOptions } from "./kv.js";
|
|
3
|
+
import { PostgresMessageQueue, PostgresMessageQueueOptions } from "./mq.js";
|
|
4
|
+
export { PostgresKvStore, PostgresKvStoreOptions, PostgresMessageQueue, PostgresMessageQueueOptions };
|
package/dist/mod.js
ADDED
package/dist/mq.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
2
|
+
import { Sql } from "postgres";
|
|
3
|
+
import { MessageQueue, MessageQueueEnqueueOptions, MessageQueueListenOptions } from "@fedify/fedify";
|
|
4
|
+
|
|
5
|
+
//#region mq.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Options for the PostgreSQL message queue.
|
|
8
|
+
*/
|
|
9
|
+
interface PostgresMessageQueueOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The table name to use for the message queue.
|
|
12
|
+
* `"fedify_message_v2"` by default.
|
|
13
|
+
* @default `"fedify_message_v2"`
|
|
14
|
+
*/
|
|
15
|
+
tableName?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The channel name to use for the message queue.
|
|
18
|
+
* `"fedify_channel"` by default.
|
|
19
|
+
* @default `"fedify_channel"`
|
|
20
|
+
*/
|
|
21
|
+
channelName?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the table has been initialized. `false` by default.
|
|
24
|
+
* @default `false`
|
|
25
|
+
*/
|
|
26
|
+
initialized?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The poll interval for the message queue. 5 seconds by default.
|
|
29
|
+
* @default `{ seconds: 5 }`
|
|
30
|
+
*/
|
|
31
|
+
pollInterval?: Temporal.Duration | Temporal.DurationLike;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A message queue that uses PostgreSQL as the underlying storage.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { createFederation } from "@fedify/fedify";
|
|
39
|
+
* import { PostgresKvStore, PostgresMessageQueue } from "@fedify/postgres";
|
|
40
|
+
* import postgres from "postgres";
|
|
41
|
+
*
|
|
42
|
+
* const sql = postgres("postgres://user:pass@localhost/db");
|
|
43
|
+
*
|
|
44
|
+
* const federation = createFederation({
|
|
45
|
+
* kv: new PostgresKvStore(sql),
|
|
46
|
+
* queue: new PostgresMessageQueue(sql),
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
declare class PostgresMessageQueue implements MessageQueue {
|
|
51
|
+
#private;
|
|
52
|
+
constructor(sql: Sql<{}>, options?: PostgresMessageQueueOptions);
|
|
53
|
+
enqueue(message: any, options?: MessageQueueEnqueueOptions): Promise<void>;
|
|
54
|
+
enqueueMany(messages: any[], options?: MessageQueueEnqueueOptions): Promise<void>;
|
|
55
|
+
listen(handler: (message: any) => void | Promise<void>, options?: MessageQueueListenOptions): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Initializes the message queue table if it does not already exist.
|
|
58
|
+
*/
|
|
59
|
+
initialize(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Drops the message queue table if it exists.
|
|
62
|
+
*/
|
|
63
|
+
drop(): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { PostgresMessageQueue, PostgresMessageQueueOptions };
|
package/dist/mq.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
|
|
2
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
3
|
+
|
|
4
|
+
import { driverSerializesJson } from "./utils.js";
|
|
5
|
+
import { getLogger } from "@logtape/logtape";
|
|
6
|
+
import postgres from "postgres";
|
|
7
|
+
|
|
8
|
+
//#region mq.ts
|
|
9
|
+
const logger = getLogger([
|
|
10
|
+
"fedify",
|
|
11
|
+
"postgres",
|
|
12
|
+
"mq"
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* A message queue that uses PostgreSQL as the underlying storage.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { createFederation } from "@fedify/fedify";
|
|
20
|
+
* import { PostgresKvStore, PostgresMessageQueue } from "@fedify/postgres";
|
|
21
|
+
* import postgres from "postgres";
|
|
22
|
+
*
|
|
23
|
+
* const sql = postgres("postgres://user:pass@localhost/db");
|
|
24
|
+
*
|
|
25
|
+
* const federation = createFederation({
|
|
26
|
+
* kv: new PostgresKvStore(sql),
|
|
27
|
+
* queue: new PostgresMessageQueue(sql),
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
var PostgresMessageQueue = class {
|
|
32
|
+
#sql;
|
|
33
|
+
#tableName;
|
|
34
|
+
#channelName;
|
|
35
|
+
#pollIntervalMs;
|
|
36
|
+
#initialized;
|
|
37
|
+
#driverSerializesJson = false;
|
|
38
|
+
constructor(sql, options = {}) {
|
|
39
|
+
this.#sql = sql;
|
|
40
|
+
this.#tableName = options?.tableName ?? "fedify_message_v2";
|
|
41
|
+
this.#channelName = options?.channelName ?? "fedify_channel";
|
|
42
|
+
this.#pollIntervalMs = Temporal.Duration.from(options?.pollInterval ?? { seconds: 5 }).total("millisecond");
|
|
43
|
+
this.#initialized = options?.initialized ?? false;
|
|
44
|
+
}
|
|
45
|
+
async enqueue(message, options) {
|
|
46
|
+
await this.initialize();
|
|
47
|
+
const delay = options?.delay ?? Temporal.Duration.from({ seconds: 0 });
|
|
48
|
+
if (options?.delay) logger.debug("Enqueuing a message with a delay of {delay}...", {
|
|
49
|
+
delay,
|
|
50
|
+
message
|
|
51
|
+
});
|
|
52
|
+
else logger.debug("Enqueuing a message...", { message });
|
|
53
|
+
await this.#sql`
|
|
54
|
+
INSERT INTO ${this.#sql(this.#tableName)} (message, delay)
|
|
55
|
+
VALUES (
|
|
56
|
+
${this.#json(message)},
|
|
57
|
+
${delay.toString()}
|
|
58
|
+
);
|
|
59
|
+
`;
|
|
60
|
+
logger.debug("Enqueued a message.", { message });
|
|
61
|
+
await this.#sql.notify(this.#channelName, delay.toString());
|
|
62
|
+
logger.debug("Notified the message queue channel {channelName}.", {
|
|
63
|
+
channelName: this.#channelName,
|
|
64
|
+
message
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async enqueueMany(messages, options) {
|
|
68
|
+
if (messages.length === 0) return;
|
|
69
|
+
await this.initialize();
|
|
70
|
+
const delay = options?.delay ?? Temporal.Duration.from({ seconds: 0 });
|
|
71
|
+
if (options?.delay) logger.debug("Enqueuing messages with a delay of {delay}...", {
|
|
72
|
+
delay,
|
|
73
|
+
messages
|
|
74
|
+
});
|
|
75
|
+
else logger.debug("Enqueuing messages...", { messages });
|
|
76
|
+
for (const message of messages) await this.#sql`
|
|
77
|
+
INSERT INTO ${this.#sql(this.#tableName)} (message, delay)
|
|
78
|
+
VALUES (
|
|
79
|
+
${this.#json(message)},
|
|
80
|
+
${delay.toString()}
|
|
81
|
+
);
|
|
82
|
+
`;
|
|
83
|
+
logger.debug("Enqueued messages.", { messages });
|
|
84
|
+
await this.#sql.notify(this.#channelName, delay.toString());
|
|
85
|
+
logger.debug("Notified the message queue channel {channelName}.", {
|
|
86
|
+
channelName: this.#channelName,
|
|
87
|
+
messages
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async listen(handler, options = {}) {
|
|
91
|
+
await this.initialize();
|
|
92
|
+
const { signal } = options;
|
|
93
|
+
const poll = async () => {
|
|
94
|
+
while (!signal?.aborted) {
|
|
95
|
+
const query = this.#sql`
|
|
96
|
+
DELETE FROM ${this.#sql(this.#tableName)}
|
|
97
|
+
WHERE id = (
|
|
98
|
+
SELECT id
|
|
99
|
+
FROM ${this.#sql(this.#tableName)}
|
|
100
|
+
WHERE created + delay < CURRENT_TIMESTAMP
|
|
101
|
+
ORDER BY created
|
|
102
|
+
LIMIT 1
|
|
103
|
+
)
|
|
104
|
+
RETURNING message;
|
|
105
|
+
`.execute();
|
|
106
|
+
const cancel = query.cancel.bind(query);
|
|
107
|
+
signal?.addEventListener("abort", cancel);
|
|
108
|
+
let i = 0;
|
|
109
|
+
for (const message of await query) {
|
|
110
|
+
if (signal?.aborted) return;
|
|
111
|
+
await handler(message.message);
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
signal?.removeEventListener("abort", cancel);
|
|
115
|
+
if (i < 1) break;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const timeouts = /* @__PURE__ */ new Set();
|
|
119
|
+
const listen = await this.#sql.listen(this.#channelName, async (delay) => {
|
|
120
|
+
const duration = Temporal.Duration.from(delay);
|
|
121
|
+
const durationMs = duration.total("millisecond");
|
|
122
|
+
if (durationMs < 1) await poll();
|
|
123
|
+
else timeouts.add(setTimeout(poll, durationMs));
|
|
124
|
+
}, poll);
|
|
125
|
+
signal?.addEventListener("abort", () => {
|
|
126
|
+
listen.unlisten();
|
|
127
|
+
for (const timeout of timeouts) clearTimeout(timeout);
|
|
128
|
+
});
|
|
129
|
+
while (!signal?.aborted) {
|
|
130
|
+
let timeout;
|
|
131
|
+
await new Promise((resolve) => {
|
|
132
|
+
signal?.addEventListener("abort", resolve);
|
|
133
|
+
timeout = setTimeout(() => {
|
|
134
|
+
signal?.removeEventListener("abort", resolve);
|
|
135
|
+
resolve(0);
|
|
136
|
+
}, this.#pollIntervalMs);
|
|
137
|
+
timeouts.add(timeout);
|
|
138
|
+
});
|
|
139
|
+
if (timeout != null) timeouts.delete(timeout);
|
|
140
|
+
await poll();
|
|
141
|
+
}
|
|
142
|
+
await new Promise((resolve) => {
|
|
143
|
+
signal?.addEventListener("abort", () => resolve());
|
|
144
|
+
if (signal?.aborted) return resolve();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Initializes the message queue table if it does not already exist.
|
|
149
|
+
*/
|
|
150
|
+
async initialize() {
|
|
151
|
+
if (this.#initialized) return;
|
|
152
|
+
logger.debug("Initializing the message queue table {tableName}...", { tableName: this.#tableName });
|
|
153
|
+
try {
|
|
154
|
+
await this.#sql`
|
|
155
|
+
CREATE TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} (
|
|
156
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
157
|
+
message jsonb NOT NULL,
|
|
158
|
+
delay interval DEFAULT '0 seconds',
|
|
159
|
+
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP
|
|
160
|
+
);
|
|
161
|
+
`;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (!(error instanceof postgres.PostgresError && error.constraint_name === "pg_type_typname_nsp_index")) {
|
|
164
|
+
logger.error("Failed to initialize the message queue table: {error}", { error });
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
this.#driverSerializesJson = await driverSerializesJson(this.#sql);
|
|
169
|
+
this.#initialized = true;
|
|
170
|
+
logger.debug("Initialized the message queue table {tableName}.", { tableName: this.#tableName });
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Drops the message queue table if it exists.
|
|
174
|
+
*/
|
|
175
|
+
async drop() {
|
|
176
|
+
await this.#sql`DROP TABLE IF EXISTS ${this.#sql(this.#tableName)};`;
|
|
177
|
+
}
|
|
178
|
+
#json(value) {
|
|
179
|
+
if (this.#driverSerializesJson) return this.#sql.json(value);
|
|
180
|
+
return this.#sql.json(JSON.stringify(value));
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
//#endregion
|
|
185
|
+
export { PostgresMessageQueue };
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
3
|
+
|
|
4
|
+
//#region utils.ts
|
|
5
|
+
async function driverSerializesJson(sql) {
|
|
6
|
+
const result = await sql`SELECT ${sql.json("{\"foo\":1}")}::jsonb AS test;`;
|
|
7
|
+
return result[0].test === "{\"foo\":1}";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
export { driverSerializesJson };
|
|
@@ -31,6 +31,7 @@ function getStore(): {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
test("PostgresKvStore.initialize()", { skip: dbUrl == null }, async () => {
|
|
34
|
+
if (dbUrl == null) return; // Bun does not support skip option
|
|
34
35
|
const { sql, tableName, store } = getStore();
|
|
35
36
|
try {
|
|
36
37
|
await store.initialize();
|
|
@@ -45,6 +46,7 @@ test("PostgresKvStore.initialize()", { skip: dbUrl == null }, async () => {
|
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
test("PostgresKvStore.get()", { skip: dbUrl == null }, async () => {
|
|
49
|
+
if (dbUrl == null) return; // Bun does not support skip option
|
|
48
50
|
const { sql, tableName, store } = getStore();
|
|
49
51
|
try {
|
|
50
52
|
await store.initialize();
|
|
@@ -67,6 +69,7 @@ test("PostgresKvStore.get()", { skip: dbUrl == null }, async () => {
|
|
|
67
69
|
});
|
|
68
70
|
|
|
69
71
|
test("PostgresKvStore.set()", { skip: dbUrl == null }, async () => {
|
|
72
|
+
if (dbUrl == null) return; // Bun does not support skip option
|
|
70
73
|
const { sql, tableName, store } = getStore();
|
|
71
74
|
try {
|
|
72
75
|
await store.set(["foo", "baz"], "baz");
|
|
@@ -107,6 +110,7 @@ test("PostgresKvStore.set()", { skip: dbUrl == null }, async () => {
|
|
|
107
110
|
});
|
|
108
111
|
|
|
109
112
|
test("PostgresKvStore.delete()", { skip: dbUrl == null }, async () => {
|
|
113
|
+
if (dbUrl == null) return; // Bun does not support skip option
|
|
110
114
|
const { sql, tableName, store } = getStore();
|
|
111
115
|
try {
|
|
112
116
|
await store.delete(["foo", "bar"]);
|
|
@@ -122,6 +126,7 @@ test("PostgresKvStore.delete()", { skip: dbUrl == null }, async () => {
|
|
|
122
126
|
});
|
|
123
127
|
|
|
124
128
|
test("PostgresKvStore.drop()", { skip: dbUrl == null }, async () => {
|
|
129
|
+
if (dbUrl == null) return; // Bun does not support skip option
|
|
125
130
|
const { sql, tableName, store } = getStore();
|
|
126
131
|
try {
|
|
127
132
|
await store.drop();
|
package/mod.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from "./
|
|
1
|
+
export * from "./kv.ts";
|
|
2
|
+
export * from "./mq.ts";
|
|
@@ -16,6 +16,7 @@ if ("Temporal" in globalThis) {
|
|
|
16
16
|
const dbUrl = process.env.DATABASE_URL;
|
|
17
17
|
|
|
18
18
|
test("PostgresMessageQueue", { skip: dbUrl == null }, async () => {
|
|
19
|
+
if (dbUrl == null) return; // Bun does not support skip option
|
|
19
20
|
const sql = postgres(dbUrl!);
|
|
20
21
|
const sql2 = postgres(dbUrl!);
|
|
21
22
|
const tableName = `fedify_message_test_${
|
package/{src/mq.ts → mq.ts}
RENAMED
|
@@ -47,14 +47,14 @@ export interface PostgresMessageQueueOptions {
|
|
|
47
47
|
* @example
|
|
48
48
|
* ```ts
|
|
49
49
|
* import { createFederation } from "@fedify/fedify";
|
|
50
|
-
* import { PostgresMessageQueue } from "@fedify/postgres";
|
|
50
|
+
* import { PostgresKvStore, PostgresMessageQueue } from "@fedify/postgres";
|
|
51
51
|
* import postgres from "postgres";
|
|
52
52
|
*
|
|
53
|
+
* const sql = postgres("postgres://user:pass@localhost/db");
|
|
54
|
+
*
|
|
53
55
|
* const federation = createFederation({
|
|
54
|
-
*
|
|
55
|
-
* queue: new PostgresMessageQueue(
|
|
56
|
-
* postgres("postgres://user:pass@localhost/db")
|
|
57
|
-
* ),
|
|
56
|
+
* kv: new PostgresKvStore(sql),
|
|
57
|
+
* queue: new PostgresMessageQueue(sql),
|
|
58
58
|
* });
|
|
59
59
|
* ```
|
|
60
60
|
*/
|