@fedify/postgres 2.0.0-pr.490.2 → 2.0.0-pr.559.5
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 +2 -2
- package/dist/kv.cjs +29 -0
- package/dist/kv.d.cts +8 -3
- package/dist/kv.d.ts +8 -3
- package/dist/kv.js +29 -0
- package/dist/mq.cjs +97 -22
- package/dist/mq.d.cts +5 -5
- package/dist/mq.d.ts +5 -5
- package/dist/mq.js +97 -22
- package/package.json +13 -8
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -25,10 +25,10 @@ const federation = createFederation({
|
|
|
25
25
|
});
|
|
26
26
|
~~~~
|
|
27
27
|
|
|
28
|
-
[JSR]: https://jsr.io/@fedify/postgres
|
|
29
28
|
[JSR badge]: https://jsr.io/badges/@fedify/postgres
|
|
30
|
-
[
|
|
29
|
+
[JSR]: https://jsr.io/@fedify/postgres
|
|
31
30
|
[npm badge]: https://img.shields.io/npm/v/@fedify/postgres?logo=npm
|
|
31
|
+
[npm]: https://www.npmjs.com/package/@fedify/postgres
|
|
32
32
|
[Fedify]: https://fedify.dev/
|
|
33
33
|
[`KvStore`]: https://jsr.io/@fedify/fedify/doc/federation/~/KvStore
|
|
34
34
|
[`MessageQueue`]: https://jsr.io/@fedify/fedify/doc/federation/~/MessageQueue
|
package/dist/kv.cjs
CHANGED
|
@@ -81,6 +81,35 @@ var PostgresKvStore = class {
|
|
|
81
81
|
await this.#expire();
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
+
* {@inheritDoc KvStore.list}
|
|
85
|
+
* @since 1.10.0
|
|
86
|
+
*/
|
|
87
|
+
async *list(prefix) {
|
|
88
|
+
await this.initialize();
|
|
89
|
+
let results;
|
|
90
|
+
if (prefix == null || prefix.length === 0) results = await this.#sql`
|
|
91
|
+
SELECT key, value
|
|
92
|
+
FROM ${this.#sql(this.#tableName)}
|
|
93
|
+
WHERE ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP
|
|
94
|
+
ORDER BY key
|
|
95
|
+
`;
|
|
96
|
+
else {
|
|
97
|
+
const prefixLength = prefix.length;
|
|
98
|
+
results = await this.#sql`
|
|
99
|
+
SELECT key, value
|
|
100
|
+
FROM ${this.#sql(this.#tableName)}
|
|
101
|
+
WHERE array_length(key, 1) >= ${prefixLength}
|
|
102
|
+
AND key[1:${prefixLength}] = ${prefix}::text[]
|
|
103
|
+
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
|
|
104
|
+
ORDER BY key
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
for (const row of results) yield {
|
|
108
|
+
key: row.key,
|
|
109
|
+
value: row.value
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
84
113
|
* Creates the table used by the key–value store if it does not already exist.
|
|
85
114
|
* Does nothing if the table already exists.
|
|
86
115
|
*/
|
package/dist/kv.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify";
|
|
1
|
+
import { KvKey, KvStore, KvStoreListEntry, KvStoreSetOptions } from "@fedify/fedify";
|
|
2
2
|
import { Sql } from "postgres";
|
|
3
3
|
|
|
4
4
|
//#region src/kv.d.ts
|
|
@@ -11,12 +11,12 @@ interface PostgresKvStoreOptions {
|
|
|
11
11
|
* `"fedify_kv_v2"` by default.
|
|
12
12
|
* @default `"fedify_kv_v2"`
|
|
13
13
|
*/
|
|
14
|
-
tableName?: string;
|
|
14
|
+
readonly tableName?: string;
|
|
15
15
|
/**
|
|
16
16
|
* Whether the table has been initialized. `false` by default.
|
|
17
17
|
* @default `false`
|
|
18
18
|
*/
|
|
19
|
-
initialized?: boolean;
|
|
19
|
+
readonly initialized?: boolean;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
22
|
* A key–value store that uses PostgreSQL as the underlying storage.
|
|
@@ -44,6 +44,11 @@ declare class PostgresKvStore implements KvStore {
|
|
|
44
44
|
get<T = unknown>(key: KvKey): Promise<T | undefined>;
|
|
45
45
|
set(key: KvKey, value: unknown, options?: KvStoreSetOptions | undefined): Promise<void>;
|
|
46
46
|
delete(key: KvKey): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* {@inheritDoc KvStore.list}
|
|
49
|
+
* @since 1.10.0
|
|
50
|
+
*/
|
|
51
|
+
list(prefix?: KvKey): AsyncIterable<KvStoreListEntry>;
|
|
47
52
|
/**
|
|
48
53
|
* Creates the table used by the key–value store if it does not already exist.
|
|
49
54
|
* 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 { Sql } from "postgres";
|
|
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
|
/**
|
|
@@ -12,12 +12,12 @@ interface PostgresKvStoreOptions {
|
|
|
12
12
|
* `"fedify_kv_v2"` by default.
|
|
13
13
|
* @default `"fedify_kv_v2"`
|
|
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 PostgreSQL as the underlying storage.
|
|
@@ -45,6 +45,11 @@ declare class PostgresKvStore implements KvStore {
|
|
|
45
45
|
get<T = unknown>(key: KvKey): Promise<T | undefined>;
|
|
46
46
|
set(key: KvKey, value: unknown, options?: KvStoreSetOptions | undefined): Promise<void>;
|
|
47
47
|
delete(key: KvKey): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* {@inheritDoc KvStore.list}
|
|
50
|
+
* @since 1.10.0
|
|
51
|
+
*/
|
|
52
|
+
list(prefix?: KvKey): AsyncIterable<KvStoreListEntry>;
|
|
48
53
|
/**
|
|
49
54
|
* Creates the table used by the key–value store if it does not already exist.
|
|
50
55
|
* Does nothing if the table already exists.
|
package/dist/kv.js
CHANGED
|
@@ -80,6 +80,35 @@ var PostgresKvStore = class {
|
|
|
80
80
|
await this.#expire();
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
|
+
* {@inheritDoc KvStore.list}
|
|
84
|
+
* @since 1.10.0
|
|
85
|
+
*/
|
|
86
|
+
async *list(prefix) {
|
|
87
|
+
await this.initialize();
|
|
88
|
+
let results;
|
|
89
|
+
if (prefix == null || prefix.length === 0) results = await this.#sql`
|
|
90
|
+
SELECT key, value
|
|
91
|
+
FROM ${this.#sql(this.#tableName)}
|
|
92
|
+
WHERE ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP
|
|
93
|
+
ORDER BY key
|
|
94
|
+
`;
|
|
95
|
+
else {
|
|
96
|
+
const prefixLength = prefix.length;
|
|
97
|
+
results = await this.#sql`
|
|
98
|
+
SELECT key, value
|
|
99
|
+
FROM ${this.#sql(this.#tableName)}
|
|
100
|
+
WHERE array_length(key, 1) >= ${prefixLength}
|
|
101
|
+
AND key[1:${prefixLength}] = ${prefix}::text[]
|
|
102
|
+
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
|
|
103
|
+
ORDER BY key
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
for (const row of results) yield {
|
|
107
|
+
key: row.key,
|
|
108
|
+
value: row.value
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
83
112
|
* Creates the table used by the key–value store if it does not already exist.
|
|
84
113
|
* Does nothing if the table already exists.
|
|
85
114
|
*/
|
package/dist/mq.cjs
CHANGED
|
@@ -46,46 +46,66 @@ var PostgresMessageQueue = class {
|
|
|
46
46
|
async enqueue(message, options) {
|
|
47
47
|
await this.initialize();
|
|
48
48
|
const delay = options?.delay ?? Temporal.Duration.from({ seconds: 0 });
|
|
49
|
+
const orderingKey = options?.orderingKey ?? null;
|
|
49
50
|
if (options?.delay) logger.debug("Enqueuing a message with a delay of {delay}...", {
|
|
50
51
|
delay,
|
|
51
|
-
message
|
|
52
|
+
message,
|
|
53
|
+
orderingKey
|
|
54
|
+
});
|
|
55
|
+
else logger.debug("Enqueuing a message...", {
|
|
56
|
+
message,
|
|
57
|
+
orderingKey
|
|
52
58
|
});
|
|
53
|
-
else logger.debug("Enqueuing a message...", { message });
|
|
54
59
|
await this.#sql`
|
|
55
|
-
INSERT INTO ${this.#sql(this.#tableName)} (message, delay)
|
|
60
|
+
INSERT INTO ${this.#sql(this.#tableName)} (message, delay, ordering_key)
|
|
56
61
|
VALUES (
|
|
57
62
|
${this.#json(message)},
|
|
58
|
-
${delay.toString()}
|
|
63
|
+
${delay.toString()},
|
|
64
|
+
${orderingKey}
|
|
59
65
|
);
|
|
60
66
|
`;
|
|
61
|
-
logger.debug("Enqueued a message.", {
|
|
67
|
+
logger.debug("Enqueued a message.", {
|
|
68
|
+
message,
|
|
69
|
+
orderingKey
|
|
70
|
+
});
|
|
62
71
|
await this.#sql.notify(this.#channelName, delay.toString());
|
|
63
72
|
logger.debug("Notified the message queue channel {channelName}.", {
|
|
64
73
|
channelName: this.#channelName,
|
|
65
|
-
message
|
|
74
|
+
message,
|
|
75
|
+
orderingKey
|
|
66
76
|
});
|
|
67
77
|
}
|
|
68
78
|
async enqueueMany(messages, options) {
|
|
69
79
|
if (messages.length === 0) return;
|
|
70
80
|
await this.initialize();
|
|
71
81
|
const delay = options?.delay ?? Temporal.Duration.from({ seconds: 0 });
|
|
82
|
+
const orderingKey = options?.orderingKey ?? null;
|
|
72
83
|
if (options?.delay) logger.debug("Enqueuing messages with a delay of {delay}...", {
|
|
73
84
|
delay,
|
|
74
|
-
messages
|
|
85
|
+
messages,
|
|
86
|
+
orderingKey
|
|
87
|
+
});
|
|
88
|
+
else logger.debug("Enqueuing messages...", {
|
|
89
|
+
messages,
|
|
90
|
+
orderingKey
|
|
75
91
|
});
|
|
76
|
-
else logger.debug("Enqueuing messages...", { messages });
|
|
77
92
|
for (const message of messages) await this.#sql`
|
|
78
|
-
INSERT INTO ${this.#sql(this.#tableName)} (message, delay)
|
|
93
|
+
INSERT INTO ${this.#sql(this.#tableName)} (message, delay, ordering_key)
|
|
79
94
|
VALUES (
|
|
80
95
|
${this.#json(message)},
|
|
81
|
-
${delay.toString()}
|
|
96
|
+
${delay.toString()},
|
|
97
|
+
${orderingKey}
|
|
82
98
|
);
|
|
83
99
|
`;
|
|
84
|
-
logger.debug("Enqueued messages.", {
|
|
100
|
+
logger.debug("Enqueued messages.", {
|
|
101
|
+
messages,
|
|
102
|
+
orderingKey
|
|
103
|
+
});
|
|
85
104
|
await this.#sql.notify(this.#channelName, delay.toString());
|
|
86
105
|
logger.debug("Notified the message queue channel {channelName}.", {
|
|
87
106
|
channelName: this.#channelName,
|
|
88
|
-
messages
|
|
107
|
+
messages,
|
|
108
|
+
orderingKey
|
|
89
109
|
});
|
|
90
110
|
}
|
|
91
111
|
async listen(handler, options = {}) {
|
|
@@ -93,27 +113,77 @@ var PostgresMessageQueue = class {
|
|
|
93
113
|
const { signal } = options;
|
|
94
114
|
const poll = async () => {
|
|
95
115
|
while (!signal?.aborted) {
|
|
116
|
+
let processed = false;
|
|
96
117
|
const query = this.#sql`
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
SELECT id
|
|
118
|
+
WITH candidate AS (
|
|
119
|
+
SELECT id, ordering_key
|
|
100
120
|
FROM ${this.#sql(this.#tableName)}
|
|
101
121
|
WHERE created + delay < CURRENT_TIMESTAMP
|
|
122
|
+
AND ordering_key IS NULL
|
|
102
123
|
ORDER BY created
|
|
103
124
|
LIMIT 1
|
|
125
|
+
FOR UPDATE SKIP LOCKED
|
|
104
126
|
)
|
|
105
|
-
|
|
127
|
+
DELETE FROM ${this.#sql(this.#tableName)}
|
|
128
|
+
WHERE id IN (SELECT id FROM candidate)
|
|
129
|
+
RETURNING message, ordering_key;
|
|
106
130
|
`.execute();
|
|
107
131
|
const cancel = query.cancel.bind(query);
|
|
108
132
|
signal?.addEventListener("abort", cancel);
|
|
109
|
-
|
|
110
|
-
for (const message of await query) {
|
|
133
|
+
for (const row of await query) {
|
|
111
134
|
if (signal?.aborted) return;
|
|
112
|
-
await handler(
|
|
113
|
-
|
|
135
|
+
await handler(row.message);
|
|
136
|
+
processed = true;
|
|
114
137
|
}
|
|
115
138
|
signal?.removeEventListener("abort", cancel);
|
|
116
|
-
if (
|
|
139
|
+
if (processed) continue;
|
|
140
|
+
const attemptedOrderingKeys = /* @__PURE__ */ new Set();
|
|
141
|
+
while (!signal?.aborted) {
|
|
142
|
+
const candidateResult = await this.#sql`
|
|
143
|
+
SELECT id, ordering_key
|
|
144
|
+
FROM ${this.#sql(this.#tableName)}
|
|
145
|
+
WHERE created + delay < CURRENT_TIMESTAMP
|
|
146
|
+
AND ordering_key IS NOT NULL
|
|
147
|
+
${attemptedOrderingKeys.size > 0 ? this.#sql`AND ordering_key NOT IN ${this.#sql([...attemptedOrderingKeys])}` : this.#sql``}
|
|
148
|
+
ORDER BY created
|
|
149
|
+
LIMIT 1
|
|
150
|
+
`;
|
|
151
|
+
if (candidateResult.length === 0) break;
|
|
152
|
+
const candidate = candidateResult[0];
|
|
153
|
+
const candidateId = candidate.id;
|
|
154
|
+
const orderingKey = candidate.ordering_key;
|
|
155
|
+
attemptedOrderingKeys.add(orderingKey);
|
|
156
|
+
const lockResult = await this.#sql`
|
|
157
|
+
SELECT pg_try_advisory_lock(
|
|
158
|
+
hashtext(${this.#tableName}),
|
|
159
|
+
hashtext(${orderingKey})
|
|
160
|
+
) AS acquired
|
|
161
|
+
`;
|
|
162
|
+
if (lockResult[0].acquired) {
|
|
163
|
+
try {
|
|
164
|
+
const deleteResult = await this.#sql`
|
|
165
|
+
DELETE FROM ${this.#sql(this.#tableName)}
|
|
166
|
+
WHERE id = ${candidateId}
|
|
167
|
+
RETURNING message, ordering_key
|
|
168
|
+
`;
|
|
169
|
+
for (const row of deleteResult) {
|
|
170
|
+
if (signal?.aborted) return;
|
|
171
|
+
await handler(row.message);
|
|
172
|
+
processed = true;
|
|
173
|
+
}
|
|
174
|
+
} finally {
|
|
175
|
+
await this.#sql`
|
|
176
|
+
SELECT pg_advisory_unlock(
|
|
177
|
+
hashtext(${this.#tableName}),
|
|
178
|
+
hashtext(${orderingKey})
|
|
179
|
+
)
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
182
|
+
if (processed) break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (processed) continue;
|
|
186
|
+
break;
|
|
117
187
|
}
|
|
118
188
|
};
|
|
119
189
|
const timeouts = /* @__PURE__ */ new Set();
|
|
@@ -157,8 +227,13 @@ var PostgresMessageQueue = class {
|
|
|
157
227
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
158
228
|
message jsonb NOT NULL,
|
|
159
229
|
delay interval DEFAULT '0 seconds',
|
|
160
|
-
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP
|
|
230
|
+
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
|
|
231
|
+
ordering_key text
|
|
161
232
|
);
|
|
233
|
+
`;
|
|
234
|
+
await this.#sql`
|
|
235
|
+
ALTER TABLE ${this.#sql(this.#tableName)}
|
|
236
|
+
ADD COLUMN IF NOT EXISTS ordering_key text;
|
|
162
237
|
`;
|
|
163
238
|
} catch (error) {
|
|
164
239
|
if (!(error instanceof postgres.default.PostgresError && error.constraint_name === "pg_type_typname_nsp_index")) {
|
package/dist/mq.d.cts
CHANGED
|
@@ -11,23 +11,23 @@ interface PostgresMessageQueueOptions {
|
|
|
11
11
|
* `"fedify_message_v2"` by default.
|
|
12
12
|
* @default `"fedify_message_v2"`
|
|
13
13
|
*/
|
|
14
|
-
tableName?: string;
|
|
14
|
+
readonly tableName?: string;
|
|
15
15
|
/**
|
|
16
16
|
* The channel name to use for the message queue.
|
|
17
17
|
* `"fedify_channel"` by default.
|
|
18
18
|
* @default `"fedify_channel"`
|
|
19
19
|
*/
|
|
20
|
-
channelName?: string;
|
|
20
|
+
readonly channelName?: string;
|
|
21
21
|
/**
|
|
22
22
|
* Whether the table has been initialized. `false` by default.
|
|
23
23
|
* @default `false`
|
|
24
24
|
*/
|
|
25
|
-
initialized?: boolean;
|
|
25
|
+
readonly initialized?: boolean;
|
|
26
26
|
/**
|
|
27
27
|
* The poll interval for the message queue. 5 seconds by default.
|
|
28
28
|
* @default `{ seconds: 5 }`
|
|
29
29
|
*/
|
|
30
|
-
pollInterval?: Temporal.Duration | Temporal.DurationLike;
|
|
30
|
+
readonly pollInterval?: Temporal.Duration | Temporal.DurationLike;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* A message queue that uses PostgreSQL as the underlying storage.
|
|
@@ -50,7 +50,7 @@ declare class PostgresMessageQueue implements MessageQueue {
|
|
|
50
50
|
#private;
|
|
51
51
|
constructor(sql: Sql<{}>, options?: PostgresMessageQueueOptions);
|
|
52
52
|
enqueue(message: any, options?: MessageQueueEnqueueOptions): Promise<void>;
|
|
53
|
-
enqueueMany(messages: any[], options?: MessageQueueEnqueueOptions): Promise<void>;
|
|
53
|
+
enqueueMany(messages: readonly any[], options?: MessageQueueEnqueueOptions): Promise<void>;
|
|
54
54
|
listen(handler: (message: any) => void | Promise<void>, options?: MessageQueueListenOptions): Promise<void>;
|
|
55
55
|
/**
|
|
56
56
|
* Initializes the message queue table if it does not already exist.
|
package/dist/mq.d.ts
CHANGED
|
@@ -12,23 +12,23 @@ interface PostgresMessageQueueOptions {
|
|
|
12
12
|
* `"fedify_message_v2"` by default.
|
|
13
13
|
* @default `"fedify_message_v2"`
|
|
14
14
|
*/
|
|
15
|
-
tableName?: string;
|
|
15
|
+
readonly tableName?: string;
|
|
16
16
|
/**
|
|
17
17
|
* The channel name to use for the message queue.
|
|
18
18
|
* `"fedify_channel"` by default.
|
|
19
19
|
* @default `"fedify_channel"`
|
|
20
20
|
*/
|
|
21
|
-
channelName?: string;
|
|
21
|
+
readonly channelName?: string;
|
|
22
22
|
/**
|
|
23
23
|
* Whether the table has been initialized. `false` by default.
|
|
24
24
|
* @default `false`
|
|
25
25
|
*/
|
|
26
|
-
initialized?: boolean;
|
|
26
|
+
readonly initialized?: boolean;
|
|
27
27
|
/**
|
|
28
28
|
* The poll interval for the message queue. 5 seconds by default.
|
|
29
29
|
* @default `{ seconds: 5 }`
|
|
30
30
|
*/
|
|
31
|
-
pollInterval?: Temporal.Duration | Temporal.DurationLike;
|
|
31
|
+
readonly pollInterval?: Temporal.Duration | Temporal.DurationLike;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* A message queue that uses PostgreSQL as the underlying storage.
|
|
@@ -51,7 +51,7 @@ declare class PostgresMessageQueue implements MessageQueue {
|
|
|
51
51
|
#private;
|
|
52
52
|
constructor(sql: Sql<{}>, options?: PostgresMessageQueueOptions);
|
|
53
53
|
enqueue(message: any, options?: MessageQueueEnqueueOptions): Promise<void>;
|
|
54
|
-
enqueueMany(messages: any[], options?: MessageQueueEnqueueOptions): Promise<void>;
|
|
54
|
+
enqueueMany(messages: readonly any[], options?: MessageQueueEnqueueOptions): Promise<void>;
|
|
55
55
|
listen(handler: (message: any) => void | Promise<void>, options?: MessageQueueListenOptions): Promise<void>;
|
|
56
56
|
/**
|
|
57
57
|
* Initializes the message queue table if it does not already exist.
|
package/dist/mq.js
CHANGED
|
@@ -45,46 +45,66 @@ var PostgresMessageQueue = class {
|
|
|
45
45
|
async enqueue(message, options) {
|
|
46
46
|
await this.initialize();
|
|
47
47
|
const delay = options?.delay ?? Temporal.Duration.from({ seconds: 0 });
|
|
48
|
+
const orderingKey = options?.orderingKey ?? null;
|
|
48
49
|
if (options?.delay) logger.debug("Enqueuing a message with a delay of {delay}...", {
|
|
49
50
|
delay,
|
|
50
|
-
message
|
|
51
|
+
message,
|
|
52
|
+
orderingKey
|
|
53
|
+
});
|
|
54
|
+
else logger.debug("Enqueuing a message...", {
|
|
55
|
+
message,
|
|
56
|
+
orderingKey
|
|
51
57
|
});
|
|
52
|
-
else logger.debug("Enqueuing a message...", { message });
|
|
53
58
|
await this.#sql`
|
|
54
|
-
INSERT INTO ${this.#sql(this.#tableName)} (message, delay)
|
|
59
|
+
INSERT INTO ${this.#sql(this.#tableName)} (message, delay, ordering_key)
|
|
55
60
|
VALUES (
|
|
56
61
|
${this.#json(message)},
|
|
57
|
-
${delay.toString()}
|
|
62
|
+
${delay.toString()},
|
|
63
|
+
${orderingKey}
|
|
58
64
|
);
|
|
59
65
|
`;
|
|
60
|
-
logger.debug("Enqueued a message.", {
|
|
66
|
+
logger.debug("Enqueued a message.", {
|
|
67
|
+
message,
|
|
68
|
+
orderingKey
|
|
69
|
+
});
|
|
61
70
|
await this.#sql.notify(this.#channelName, delay.toString());
|
|
62
71
|
logger.debug("Notified the message queue channel {channelName}.", {
|
|
63
72
|
channelName: this.#channelName,
|
|
64
|
-
message
|
|
73
|
+
message,
|
|
74
|
+
orderingKey
|
|
65
75
|
});
|
|
66
76
|
}
|
|
67
77
|
async enqueueMany(messages, options) {
|
|
68
78
|
if (messages.length === 0) return;
|
|
69
79
|
await this.initialize();
|
|
70
80
|
const delay = options?.delay ?? Temporal.Duration.from({ seconds: 0 });
|
|
81
|
+
const orderingKey = options?.orderingKey ?? null;
|
|
71
82
|
if (options?.delay) logger.debug("Enqueuing messages with a delay of {delay}...", {
|
|
72
83
|
delay,
|
|
73
|
-
messages
|
|
84
|
+
messages,
|
|
85
|
+
orderingKey
|
|
86
|
+
});
|
|
87
|
+
else logger.debug("Enqueuing messages...", {
|
|
88
|
+
messages,
|
|
89
|
+
orderingKey
|
|
74
90
|
});
|
|
75
|
-
else logger.debug("Enqueuing messages...", { messages });
|
|
76
91
|
for (const message of messages) await this.#sql`
|
|
77
|
-
INSERT INTO ${this.#sql(this.#tableName)} (message, delay)
|
|
92
|
+
INSERT INTO ${this.#sql(this.#tableName)} (message, delay, ordering_key)
|
|
78
93
|
VALUES (
|
|
79
94
|
${this.#json(message)},
|
|
80
|
-
${delay.toString()}
|
|
95
|
+
${delay.toString()},
|
|
96
|
+
${orderingKey}
|
|
81
97
|
);
|
|
82
98
|
`;
|
|
83
|
-
logger.debug("Enqueued messages.", {
|
|
99
|
+
logger.debug("Enqueued messages.", {
|
|
100
|
+
messages,
|
|
101
|
+
orderingKey
|
|
102
|
+
});
|
|
84
103
|
await this.#sql.notify(this.#channelName, delay.toString());
|
|
85
104
|
logger.debug("Notified the message queue channel {channelName}.", {
|
|
86
105
|
channelName: this.#channelName,
|
|
87
|
-
messages
|
|
106
|
+
messages,
|
|
107
|
+
orderingKey
|
|
88
108
|
});
|
|
89
109
|
}
|
|
90
110
|
async listen(handler, options = {}) {
|
|
@@ -92,27 +112,77 @@ var PostgresMessageQueue = class {
|
|
|
92
112
|
const { signal } = options;
|
|
93
113
|
const poll = async () => {
|
|
94
114
|
while (!signal?.aborted) {
|
|
115
|
+
let processed = false;
|
|
95
116
|
const query = this.#sql`
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
SELECT id
|
|
117
|
+
WITH candidate AS (
|
|
118
|
+
SELECT id, ordering_key
|
|
99
119
|
FROM ${this.#sql(this.#tableName)}
|
|
100
120
|
WHERE created + delay < CURRENT_TIMESTAMP
|
|
121
|
+
AND ordering_key IS NULL
|
|
101
122
|
ORDER BY created
|
|
102
123
|
LIMIT 1
|
|
124
|
+
FOR UPDATE SKIP LOCKED
|
|
103
125
|
)
|
|
104
|
-
|
|
126
|
+
DELETE FROM ${this.#sql(this.#tableName)}
|
|
127
|
+
WHERE id IN (SELECT id FROM candidate)
|
|
128
|
+
RETURNING message, ordering_key;
|
|
105
129
|
`.execute();
|
|
106
130
|
const cancel = query.cancel.bind(query);
|
|
107
131
|
signal?.addEventListener("abort", cancel);
|
|
108
|
-
|
|
109
|
-
for (const message of await query) {
|
|
132
|
+
for (const row of await query) {
|
|
110
133
|
if (signal?.aborted) return;
|
|
111
|
-
await handler(
|
|
112
|
-
|
|
134
|
+
await handler(row.message);
|
|
135
|
+
processed = true;
|
|
113
136
|
}
|
|
114
137
|
signal?.removeEventListener("abort", cancel);
|
|
115
|
-
if (
|
|
138
|
+
if (processed) continue;
|
|
139
|
+
const attemptedOrderingKeys = /* @__PURE__ */ new Set();
|
|
140
|
+
while (!signal?.aborted) {
|
|
141
|
+
const candidateResult = await this.#sql`
|
|
142
|
+
SELECT id, ordering_key
|
|
143
|
+
FROM ${this.#sql(this.#tableName)}
|
|
144
|
+
WHERE created + delay < CURRENT_TIMESTAMP
|
|
145
|
+
AND ordering_key IS NOT NULL
|
|
146
|
+
${attemptedOrderingKeys.size > 0 ? this.#sql`AND ordering_key NOT IN ${this.#sql([...attemptedOrderingKeys])}` : this.#sql``}
|
|
147
|
+
ORDER BY created
|
|
148
|
+
LIMIT 1
|
|
149
|
+
`;
|
|
150
|
+
if (candidateResult.length === 0) break;
|
|
151
|
+
const candidate = candidateResult[0];
|
|
152
|
+
const candidateId = candidate.id;
|
|
153
|
+
const orderingKey = candidate.ordering_key;
|
|
154
|
+
attemptedOrderingKeys.add(orderingKey);
|
|
155
|
+
const lockResult = await this.#sql`
|
|
156
|
+
SELECT pg_try_advisory_lock(
|
|
157
|
+
hashtext(${this.#tableName}),
|
|
158
|
+
hashtext(${orderingKey})
|
|
159
|
+
) AS acquired
|
|
160
|
+
`;
|
|
161
|
+
if (lockResult[0].acquired) {
|
|
162
|
+
try {
|
|
163
|
+
const deleteResult = await this.#sql`
|
|
164
|
+
DELETE FROM ${this.#sql(this.#tableName)}
|
|
165
|
+
WHERE id = ${candidateId}
|
|
166
|
+
RETURNING message, ordering_key
|
|
167
|
+
`;
|
|
168
|
+
for (const row of deleteResult) {
|
|
169
|
+
if (signal?.aborted) return;
|
|
170
|
+
await handler(row.message);
|
|
171
|
+
processed = true;
|
|
172
|
+
}
|
|
173
|
+
} finally {
|
|
174
|
+
await this.#sql`
|
|
175
|
+
SELECT pg_advisory_unlock(
|
|
176
|
+
hashtext(${this.#tableName}),
|
|
177
|
+
hashtext(${orderingKey})
|
|
178
|
+
)
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
if (processed) break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (processed) continue;
|
|
185
|
+
break;
|
|
116
186
|
}
|
|
117
187
|
};
|
|
118
188
|
const timeouts = /* @__PURE__ */ new Set();
|
|
@@ -156,8 +226,13 @@ var PostgresMessageQueue = class {
|
|
|
156
226
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
157
227
|
message jsonb NOT NULL,
|
|
158
228
|
delay interval DEFAULT '0 seconds',
|
|
159
|
-
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP
|
|
229
|
+
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
|
|
230
|
+
ordering_key text
|
|
160
231
|
);
|
|
232
|
+
`;
|
|
233
|
+
await this.#sql`
|
|
234
|
+
ALTER TABLE ${this.#sql(this.#tableName)}
|
|
235
|
+
ADD COLUMN IF NOT EXISTS ordering_key text;
|
|
161
236
|
`;
|
|
162
237
|
} catch (error) {
|
|
163
238
|
if (!(error instanceof postgres.PostgresError && error.constraint_name === "pg_type_typname_nsp_index")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/postgres",
|
|
3
|
-
"version": "2.0.0-pr.
|
|
3
|
+
"version": "2.0.0-pr.559.5+39b87b25",
|
|
4
4
|
"description": "PostgreSQL drivers for Fedify",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fedify",
|
|
@@ -70,21 +70,26 @@
|
|
|
70
70
|
],
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@js-temporal/polyfill": "^0.5.1",
|
|
73
|
-
"@logtape/logtape": "^
|
|
73
|
+
"@logtape/logtape": "^2.0.0"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"postgres": "^3.4.7",
|
|
77
|
-
"@fedify/fedify": "^2.0.0-pr.
|
|
77
|
+
"@fedify/fedify": "^2.0.0-pr.559.5+39b87b25"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@std/async": "npm:@jsr/std__async@^1.0.13",
|
|
81
81
|
"tsdown": "^0.12.9",
|
|
82
|
-
"typescript": "^5.9.3"
|
|
82
|
+
"typescript": "^5.9.3",
|
|
83
|
+
"@fedify/fixture": "^2.0.0",
|
|
84
|
+
"@fedify/testing": "^2.0.0-pr.559.5+39b87b25"
|
|
83
85
|
},
|
|
84
86
|
"scripts": {
|
|
85
|
-
"build": "tsdown",
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
87
|
+
"build:self": "tsdown",
|
|
88
|
+
"build": "pnpm --filter @fedify/postgres... run build:self",
|
|
89
|
+
"prepublish": "pnpm build",
|
|
90
|
+
"pretest": "pnpm build",
|
|
91
|
+
"test": "node --experimental-transform-types --test",
|
|
92
|
+
"pretest:bun": "pnpm build",
|
|
93
|
+
"test:bun": "bun test --timeout=10000"
|
|
89
94
|
}
|
|
90
95
|
}
|