@fedify/redis 2.0.0-pr.412.1559 → 2.0.0-pr.412.1794
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/dist/_virtual/rolldown_runtime.cjs +33 -0
- package/dist/codec.cjs +66 -0
- package/dist/codec.d.cts +50 -0
- package/dist/codec.js +2 -2
- package/dist/kv.cjs +76 -0
- package/dist/kv.d.cts +61 -0
- package/dist/kv.js +2 -2
- package/dist/mod.cjs +13 -0
- package/dist/mod.d.cts +4 -0
- package/dist/mod.js +2 -2
- package/dist/mq.cjs +171 -0
- package/dist/mq.d.cts +86 -0
- package/dist/mq.js +2 -2
- package/package.json +28 -8
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
|
|
2
|
+
const { Temporal } = require("@js-temporal/polyfill");
|
|
3
|
+
|
|
4
|
+
//#region rolldown:runtime
|
|
5
|
+
var __create = Object.create;
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
8
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
9
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
10
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
13
|
+
key = keys[i];
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
22
|
+
value: mod,
|
|
23
|
+
enumerable: true
|
|
24
|
+
}) : target, mod));
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
|
|
28
|
+
Object.defineProperty(exports, '__toESM', {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
get: function () {
|
|
31
|
+
return __toESM;
|
|
32
|
+
}
|
|
33
|
+
});
|
package/dist/codec.cjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
|
|
2
|
+
const { Temporal } = require("@js-temporal/polyfill");
|
|
3
|
+
|
|
4
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
5
|
+
const node_buffer = require_rolldown_runtime.__toESM(require("node:buffer"));
|
|
6
|
+
|
|
7
|
+
//#region src/codec.ts
|
|
8
|
+
/**
|
|
9
|
+
* An error that occurs when encoding or decoding data.
|
|
10
|
+
*/
|
|
11
|
+
var CodecError = class extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "CodecError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* An error that occurs when encoding data.
|
|
19
|
+
*/
|
|
20
|
+
var EncodingError = class extends CodecError {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "EncodingError";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* An error that occurs when decoding data.
|
|
28
|
+
*/
|
|
29
|
+
var DecodingError = class extends CodecError {
|
|
30
|
+
constructor(message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "DecodingError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A codec that encodes and decodes JavaScript objects to and from JSON.
|
|
37
|
+
*/
|
|
38
|
+
var JsonCodec = class {
|
|
39
|
+
#textEncoder = new TextEncoder();
|
|
40
|
+
#textDecoder = new TextDecoder();
|
|
41
|
+
encode(value) {
|
|
42
|
+
let json;
|
|
43
|
+
try {
|
|
44
|
+
json = JSON.stringify(value);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
if (e instanceof TypeError) throw new EncodingError(e.message);
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
return node_buffer.Buffer.from(this.#textEncoder.encode(json));
|
|
50
|
+
}
|
|
51
|
+
decode(encoded) {
|
|
52
|
+
const json = this.#textDecoder.decode(encoded);
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(json);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
if (e instanceof SyntaxError) throw new DecodingError(e.message);
|
|
57
|
+
throw e;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
exports.CodecError = CodecError;
|
|
64
|
+
exports.DecodingError = DecodingError;
|
|
65
|
+
exports.EncodingError = EncodingError;
|
|
66
|
+
exports.JsonCodec = JsonCodec;
|
package/dist/codec.d.cts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
|
|
3
|
+
//#region src/codec.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Encode and decodes JavaScript objects to and from binary data.
|
|
6
|
+
*/
|
|
7
|
+
interface Codec {
|
|
8
|
+
/**
|
|
9
|
+
* Encodes a JavaScript object to binary data.
|
|
10
|
+
* @param value The JavaScript object to encode.
|
|
11
|
+
* @returns The encoded binary data.
|
|
12
|
+
* @throws {EncodingError} If the JavaScript object cannot be encoded.
|
|
13
|
+
*/
|
|
14
|
+
encode(value: unknown): Buffer;
|
|
15
|
+
/**
|
|
16
|
+
* Decodes a JavaScript object from binary data.
|
|
17
|
+
* @param encoded The binary data to decode.
|
|
18
|
+
* @returns The decoded JavaScript object.
|
|
19
|
+
* @throws {DecodingError} If the binary data is invalid.
|
|
20
|
+
*/
|
|
21
|
+
decode(encoded: Buffer): unknown;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* An error that occurs when encoding or decoding data.
|
|
25
|
+
*/
|
|
26
|
+
declare class CodecError extends Error {
|
|
27
|
+
constructor(message: string);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* An error that occurs when encoding data.
|
|
31
|
+
*/
|
|
32
|
+
declare class EncodingError extends CodecError {
|
|
33
|
+
constructor(message: string);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* An error that occurs when decoding data.
|
|
37
|
+
*/
|
|
38
|
+
declare class DecodingError extends CodecError {
|
|
39
|
+
constructor(message: string);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A codec that encodes and decodes JavaScript objects to and from JSON.
|
|
43
|
+
*/
|
|
44
|
+
declare class JsonCodec implements Codec {
|
|
45
|
+
#private;
|
|
46
|
+
encode(value: unknown): Buffer;
|
|
47
|
+
decode(encoded: Buffer): unknown;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { Codec, CodecError, DecodingError, EncodingError, JsonCodec };
|
package/dist/codec.js
CHANGED
package/dist/kv.cjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
|
|
2
|
+
const { Temporal } = require("@js-temporal/polyfill");
|
|
3
|
+
|
|
4
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
5
|
+
const require_codec = require('./codec.cjs');
|
|
6
|
+
const node_buffer = require_rolldown_runtime.__toESM(require("node:buffer"));
|
|
7
|
+
|
|
8
|
+
//#region src/kv.ts
|
|
9
|
+
/**
|
|
10
|
+
* A key–value store that uses Redis as the underlying storage.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts ignore
|
|
14
|
+
* import { createFederation } from "@fedify/fedify";
|
|
15
|
+
* import { RedisKvStore } from "@fedify/redis";
|
|
16
|
+
* import { Redis, Cluster } from "ioredis";
|
|
17
|
+
*
|
|
18
|
+
* // Using a standalone Redis instance:
|
|
19
|
+
* const federation = createFederation({
|
|
20
|
+
* // ...
|
|
21
|
+
* kv: new RedisKvStore(new Redis()),
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Using a Redis Cluster:
|
|
25
|
+
* const cluster = new Cluster([
|
|
26
|
+
* { host: "127.0.0.1", port: 7000 },
|
|
27
|
+
* { host: "127.0.0.1", port: 7001 },
|
|
28
|
+
* { host: "127.0.0.1", port: 7002 },
|
|
29
|
+
* ]);
|
|
30
|
+
* const federation = createFederation({
|
|
31
|
+
* // ...
|
|
32
|
+
* kv: new RedisKvStore(cluster),
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
var RedisKvStore = class {
|
|
37
|
+
#redis;
|
|
38
|
+
#keyPrefix;
|
|
39
|
+
#codec;
|
|
40
|
+
#textEncoder = new TextEncoder();
|
|
41
|
+
/**
|
|
42
|
+
* Creates a new Redis key–value store.
|
|
43
|
+
* @param redis The Redis client (standalone or cluster) to use.
|
|
44
|
+
* @param options The options for the key–value store.
|
|
45
|
+
*/
|
|
46
|
+
constructor(redis, options = {}) {
|
|
47
|
+
this.#redis = redis;
|
|
48
|
+
this.#keyPrefix = options.keyPrefix ?? "fedify::";
|
|
49
|
+
this.#codec = options.codec ?? new require_codec.JsonCodec();
|
|
50
|
+
}
|
|
51
|
+
#serializeKey(key) {
|
|
52
|
+
const suffix = key.map((part) => part.replaceAll(":", "_:")).join("::");
|
|
53
|
+
if (typeof this.#keyPrefix === "string") return `${this.#keyPrefix}${suffix}`;
|
|
54
|
+
const suffixBytes = this.#textEncoder.encode(suffix);
|
|
55
|
+
return node_buffer.Buffer.concat([new Uint8Array(this.#keyPrefix), suffixBytes]);
|
|
56
|
+
}
|
|
57
|
+
async get(key) {
|
|
58
|
+
const serializedKey = this.#serializeKey(key);
|
|
59
|
+
const encodedValue = await this.#redis.getBuffer(serializedKey);
|
|
60
|
+
if (encodedValue == null) return void 0;
|
|
61
|
+
return this.#codec.decode(encodedValue);
|
|
62
|
+
}
|
|
63
|
+
async set(key, value, options) {
|
|
64
|
+
const serializedKey = this.#serializeKey(key);
|
|
65
|
+
const encodedValue = this.#codec.encode(value);
|
|
66
|
+
if (options?.ttl != null) await this.#redis.setex(serializedKey, options.ttl.total("second"), encodedValue);
|
|
67
|
+
else await this.#redis.set(serializedKey, encodedValue);
|
|
68
|
+
}
|
|
69
|
+
async delete(key) {
|
|
70
|
+
const serializedKey = this.#serializeKey(key);
|
|
71
|
+
await this.#redis.del(serializedKey);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
exports.RedisKvStore = RedisKvStore;
|
package/dist/kv.d.cts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Codec } from "./codec.cjs";
|
|
2
|
+
import { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify";
|
|
3
|
+
import { Cluster, Redis, RedisKey } from "ioredis";
|
|
4
|
+
|
|
5
|
+
//#region src/kv.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Options for {@link RedisKvStore} class.
|
|
8
|
+
*/
|
|
9
|
+
interface RedisKvStoreOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The prefix to use for all keys in the key–value store in Redis.
|
|
12
|
+
* Defaults to `"fedify::"`.
|
|
13
|
+
*/
|
|
14
|
+
keyPrefix?: RedisKey;
|
|
15
|
+
/**
|
|
16
|
+
* The codec to use for encoding and decoding values in the key–value store.
|
|
17
|
+
* Defaults to {@link JsonCodec}.
|
|
18
|
+
*/
|
|
19
|
+
codec?: Codec;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A key–value store that uses Redis as the underlying storage.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts ignore
|
|
26
|
+
* import { createFederation } from "@fedify/fedify";
|
|
27
|
+
* import { RedisKvStore } from "@fedify/redis";
|
|
28
|
+
* import { Redis, Cluster } from "ioredis";
|
|
29
|
+
*
|
|
30
|
+
* // Using a standalone Redis instance:
|
|
31
|
+
* const federation = createFederation({
|
|
32
|
+
* // ...
|
|
33
|
+
* kv: new RedisKvStore(new Redis()),
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* // Using a Redis Cluster:
|
|
37
|
+
* const cluster = new Cluster([
|
|
38
|
+
* { host: "127.0.0.1", port: 7000 },
|
|
39
|
+
* { host: "127.0.0.1", port: 7001 },
|
|
40
|
+
* { host: "127.0.0.1", port: 7002 },
|
|
41
|
+
* ]);
|
|
42
|
+
* const federation = createFederation({
|
|
43
|
+
* // ...
|
|
44
|
+
* kv: new RedisKvStore(cluster),
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
declare class RedisKvStore implements KvStore {
|
|
49
|
+
#private;
|
|
50
|
+
/**
|
|
51
|
+
* Creates a new Redis key–value store.
|
|
52
|
+
* @param redis The Redis client (standalone or cluster) to use.
|
|
53
|
+
* @param options The options for the key–value store.
|
|
54
|
+
*/
|
|
55
|
+
constructor(redis: Redis | Cluster, options?: RedisKvStoreOptions);
|
|
56
|
+
get<T = unknown>(key: KvKey): Promise<T | undefined>;
|
|
57
|
+
set(key: KvKey, value: unknown, options?: KvStoreSetOptions | undefined): Promise<void>;
|
|
58
|
+
delete(key: KvKey): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { RedisKvStore, RedisKvStoreOptions };
|
package/dist/kv.js
CHANGED
package/dist/mod.cjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
const { Temporal } = require("@js-temporal/polyfill");
|
|
3
|
+
|
|
4
|
+
const require_codec = require('./codec.cjs');
|
|
5
|
+
const require_kv = require('./kv.cjs');
|
|
6
|
+
const require_mq = require('./mq.cjs');
|
|
7
|
+
|
|
8
|
+
exports.CodecError = require_codec.CodecError;
|
|
9
|
+
exports.DecodingError = require_codec.DecodingError;
|
|
10
|
+
exports.EncodingError = require_codec.EncodingError;
|
|
11
|
+
exports.JsonCodec = require_codec.JsonCodec;
|
|
12
|
+
exports.RedisKvStore = require_kv.RedisKvStore;
|
|
13
|
+
exports.RedisMessageQueue = require_mq.RedisMessageQueue;
|
package/dist/mod.d.cts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Codec, CodecError, DecodingError, EncodingError, JsonCodec } from "./codec.cjs";
|
|
2
|
+
import { RedisKvStore, RedisKvStoreOptions } from "./kv.cjs";
|
|
3
|
+
import { RedisMessageQueue, RedisMessageQueueOptions } from "./mq.cjs";
|
|
4
|
+
export { Codec, CodecError, DecodingError, EncodingError, JsonCodec, RedisKvStore, RedisKvStoreOptions, RedisMessageQueue, RedisMessageQueueOptions };
|
package/dist/mod.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
3
|
+
|
|
4
4
|
import { CodecError, DecodingError, EncodingError, JsonCodec } from "./codec.js";
|
|
5
5
|
import { RedisKvStore } from "./kv.js";
|
|
6
6
|
import { RedisMessageQueue } from "./mq.js";
|
package/dist/mq.cjs
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
|
|
2
|
+
const { Temporal } = require("@js-temporal/polyfill");
|
|
3
|
+
|
|
4
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
5
|
+
const require_codec = require('./codec.cjs');
|
|
6
|
+
const __logtape_logtape = require_rolldown_runtime.__toESM(require("@logtape/logtape"));
|
|
7
|
+
|
|
8
|
+
//#region src/mq.ts
|
|
9
|
+
const logger = (0, __logtape_logtape.getLogger)([
|
|
10
|
+
"fedify",
|
|
11
|
+
"redis",
|
|
12
|
+
"mq"
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* A message queue that uses Redis as the underlying storage.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts ignore
|
|
19
|
+
* import { createFederation } from "@fedify/fedify";
|
|
20
|
+
* import { RedisMessageQueue } from "@fedify/redis";
|
|
21
|
+
* import { Redis, Cluster } from "ioredis";
|
|
22
|
+
*
|
|
23
|
+
* // Using a standalone Redis instance:
|
|
24
|
+
* const federation = createFederation({
|
|
25
|
+
* // ...
|
|
26
|
+
* queue: new RedisMessageQueue(() => new Redis()),
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // Using a Redis Cluster:
|
|
30
|
+
* const federation = createFederation({
|
|
31
|
+
* // ...
|
|
32
|
+
* queue: new RedisMessageQueue(() => new Cluster([
|
|
33
|
+
* { host: "127.0.0.1", port: 7000 },
|
|
34
|
+
* { host: "127.0.0.1", port: 7001 },
|
|
35
|
+
* { host: "127.0.0.1", port: 7002 },
|
|
36
|
+
* ])),
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
var RedisMessageQueue = class {
|
|
41
|
+
#redis;
|
|
42
|
+
#subRedis;
|
|
43
|
+
#workerId;
|
|
44
|
+
#channelKey;
|
|
45
|
+
#queueKey;
|
|
46
|
+
#lockKey;
|
|
47
|
+
#codec;
|
|
48
|
+
#pollIntervalMs;
|
|
49
|
+
#loopHandle;
|
|
50
|
+
/**
|
|
51
|
+
* Creates a new Redis message queue.
|
|
52
|
+
* @param redis The Redis client factory.
|
|
53
|
+
* @param options The options for the message queue.
|
|
54
|
+
*/
|
|
55
|
+
constructor(redis, options = {}) {
|
|
56
|
+
this.#redis = redis();
|
|
57
|
+
this.#subRedis = redis();
|
|
58
|
+
this.#workerId = options.workerId ?? crypto.randomUUID();
|
|
59
|
+
this.#channelKey = options.channelKey ?? "fedify_channel";
|
|
60
|
+
this.#queueKey = options.queueKey ?? "fedify_queue";
|
|
61
|
+
this.#lockKey = options.lockKey ?? "fedify_lock";
|
|
62
|
+
this.#codec = options.codec ?? new require_codec.JsonCodec();
|
|
63
|
+
this.#pollIntervalMs = Temporal.Duration.from(options.pollInterval ?? { seconds: 5 }).total("millisecond");
|
|
64
|
+
}
|
|
65
|
+
async enqueue(message, options) {
|
|
66
|
+
const ts = options?.delay == null ? 0 : Temporal.Now.instant().add(options.delay).epochMilliseconds;
|
|
67
|
+
const encodedMessage = this.#codec.encode([crypto.randomUUID(), message]);
|
|
68
|
+
await this.#redis.zadd(this.#queueKey, ts, encodedMessage);
|
|
69
|
+
if (ts < 1) this.#redis.publish(this.#channelKey, "");
|
|
70
|
+
}
|
|
71
|
+
async enqueueMany(messages, options) {
|
|
72
|
+
if (messages.length === 0) return;
|
|
73
|
+
const ts = options?.delay == null ? 0 : Temporal.Now.instant().add(options.delay).epochMilliseconds;
|
|
74
|
+
const multi = this.#redis.multi();
|
|
75
|
+
for (const message of messages) {
|
|
76
|
+
const encodedMessage = this.#codec.encode([crypto.randomUUID(), message]);
|
|
77
|
+
multi.zadd(this.#queueKey, ts, encodedMessage);
|
|
78
|
+
}
|
|
79
|
+
await multi.exec();
|
|
80
|
+
if (ts < 1) this.#redis.publish(this.#channelKey, "");
|
|
81
|
+
}
|
|
82
|
+
async #poll() {
|
|
83
|
+
logger.debug("Polling for messages...");
|
|
84
|
+
const result = await this.#redis.set(this.#lockKey, this.#workerId, "EX", Math.floor(this.#pollIntervalMs / 1e3 * 2), "NX");
|
|
85
|
+
if (result == null) {
|
|
86
|
+
logger.debug("Another worker is already processing messages; skipping...");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
logger.debug("Acquired lock; processing messages...");
|
|
90
|
+
const messages = await this.#redis.zrangebyscoreBuffer(this.#queueKey, 0, Temporal.Now.instant().epochMilliseconds);
|
|
91
|
+
logger.debug("Found {messages} messages to process.", { messages: messages.length });
|
|
92
|
+
try {
|
|
93
|
+
if (messages.length < 1) return;
|
|
94
|
+
const encodedMessage = messages[0];
|
|
95
|
+
await this.#redis.zrem(this.#queueKey, encodedMessage);
|
|
96
|
+
const [_, message] = this.#codec.decode(encodedMessage);
|
|
97
|
+
return message;
|
|
98
|
+
} finally {
|
|
99
|
+
await this.#redis.del(this.#lockKey);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async listen(handler, options = {}) {
|
|
103
|
+
if (this.#loopHandle != null) throw new Error("Already listening");
|
|
104
|
+
const signal = options.signal;
|
|
105
|
+
const poll = async () => {
|
|
106
|
+
while (!signal?.aborted) {
|
|
107
|
+
let message;
|
|
108
|
+
try {
|
|
109
|
+
message = await this.#poll();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
logger.error("Error polling for messages: {error}", { error });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (message === void 0) return;
|
|
115
|
+
await handler(message);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const promise = this.#subRedis.subscribe(this.#channelKey, () => {
|
|
119
|
+
/**
|
|
120
|
+
* Cast to Redis for event methods. Both Redis and Cluster extend EventEmitter
|
|
121
|
+
* and get the same methods via applyMixin at runtime, but their TypeScript
|
|
122
|
+
* interfaces are incompatible:
|
|
123
|
+
* - Redis declares specific overloads: on(event: "message", cb: (channel, message) => void)
|
|
124
|
+
* - Cluster only has generic: on(event: string | symbol, listener: Function)
|
|
125
|
+
*
|
|
126
|
+
* This makes the union type Redis | Cluster incompatible for these method calls.
|
|
127
|
+
* The cast is safe because both classes use applyMixin(Class, EventEmitter) which
|
|
128
|
+
* copies all EventEmitter prototype methods, giving them identical pub/sub functionality.
|
|
129
|
+
*
|
|
130
|
+
* @see https://github.com/redis/ioredis/blob/main/lib/Redis.ts#L863 (has specific overloads)
|
|
131
|
+
* @see https://github.com/redis/ioredis/blob/main/lib/cluster/index.ts#L1110 (empty interface)
|
|
132
|
+
*/
|
|
133
|
+
const subRedis = this.#subRedis;
|
|
134
|
+
subRedis.on("message", poll);
|
|
135
|
+
signal?.addEventListener("abort", () => {
|
|
136
|
+
subRedis.off("message", poll);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
signal?.addEventListener("abort", () => {
|
|
140
|
+
for (const timeout of timeouts) clearTimeout(timeout);
|
|
141
|
+
});
|
|
142
|
+
const timeouts = /* @__PURE__ */ new Set();
|
|
143
|
+
while (!signal?.aborted) {
|
|
144
|
+
let timeout;
|
|
145
|
+
await new Promise((resolve) => {
|
|
146
|
+
signal?.addEventListener("abort", resolve);
|
|
147
|
+
timeout = setTimeout(() => {
|
|
148
|
+
signal?.removeEventListener("abort", resolve);
|
|
149
|
+
resolve(0);
|
|
150
|
+
}, this.#pollIntervalMs);
|
|
151
|
+
timeouts.add(timeout);
|
|
152
|
+
});
|
|
153
|
+
if (timeout != null) timeouts.delete(timeout);
|
|
154
|
+
await poll();
|
|
155
|
+
}
|
|
156
|
+
return await new Promise((resolve) => {
|
|
157
|
+
signal?.addEventListener("abort", () => {
|
|
158
|
+
promise.catch(() => resolve()).then(() => resolve());
|
|
159
|
+
});
|
|
160
|
+
promise.catch(() => resolve()).then(() => resolve());
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
[Symbol.dispose]() {
|
|
164
|
+
clearInterval(this.#loopHandle);
|
|
165
|
+
this.#redis.disconnect();
|
|
166
|
+
this.#subRedis.disconnect();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
exports.RedisMessageQueue = RedisMessageQueue;
|
package/dist/mq.d.cts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Codec } from "./codec.cjs";
|
|
2
|
+
import { MessageQueue, MessageQueueEnqueueOptions, MessageQueueListenOptions } from "@fedify/fedify";
|
|
3
|
+
import { Cluster, Redis, RedisKey } from "ioredis";
|
|
4
|
+
|
|
5
|
+
//#region src/mq.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Options for {@link RedisMessageQueue} class.
|
|
8
|
+
*/
|
|
9
|
+
interface RedisMessageQueueOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The unique identifier for the worker that is processing messages from the
|
|
12
|
+
* queue. If this is not specified, a random identifier will be generated.
|
|
13
|
+
* This is used to prevent multiple workers from processing the same message,
|
|
14
|
+
* so it should be unique for each worker.
|
|
15
|
+
*/
|
|
16
|
+
workerId?: string;
|
|
17
|
+
/**
|
|
18
|
+
* The Pub/Sub channel key to use for the message queue. `"fedify_channel"`
|
|
19
|
+
* by default.
|
|
20
|
+
* @default `"fedify_channel"`
|
|
21
|
+
*/
|
|
22
|
+
channelKey?: RedisKey;
|
|
23
|
+
/**
|
|
24
|
+
* The Sorted Set key to use for the delayed message queue. `"fedify_queue"`
|
|
25
|
+
* by default.
|
|
26
|
+
* @default `"fedify_queue"`
|
|
27
|
+
*/
|
|
28
|
+
queueKey?: RedisKey;
|
|
29
|
+
/**
|
|
30
|
+
* The key to use for locking the message queue. `"fedify_lock"` by default.
|
|
31
|
+
* @default `"fedify_lock"`
|
|
32
|
+
*/
|
|
33
|
+
lockKey?: RedisKey;
|
|
34
|
+
/**
|
|
35
|
+
* The codec to use for encoding and decoding messages in the key–value store.
|
|
36
|
+
* Defaults to {@link JsonCodec}.
|
|
37
|
+
* @default {@link JsonCodec}
|
|
38
|
+
*/
|
|
39
|
+
codec?: Codec;
|
|
40
|
+
/**
|
|
41
|
+
* The poll interval for the message queue. 5 seconds by default.
|
|
42
|
+
* @default `{ seconds: 5 }`
|
|
43
|
+
*/
|
|
44
|
+
pollInterval?: Temporal.Duration | Temporal.DurationLike;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A message queue that uses Redis as the underlying storage.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts ignore
|
|
51
|
+
* import { createFederation } from "@fedify/fedify";
|
|
52
|
+
* import { RedisMessageQueue } from "@fedify/redis";
|
|
53
|
+
* import { Redis, Cluster } from "ioredis";
|
|
54
|
+
*
|
|
55
|
+
* // Using a standalone Redis instance:
|
|
56
|
+
* const federation = createFederation({
|
|
57
|
+
* // ...
|
|
58
|
+
* queue: new RedisMessageQueue(() => new Redis()),
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* // Using a Redis Cluster:
|
|
62
|
+
* const federation = createFederation({
|
|
63
|
+
* // ...
|
|
64
|
+
* queue: new RedisMessageQueue(() => new Cluster([
|
|
65
|
+
* { host: "127.0.0.1", port: 7000 },
|
|
66
|
+
* { host: "127.0.0.1", port: 7001 },
|
|
67
|
+
* { host: "127.0.0.1", port: 7002 },
|
|
68
|
+
* ])),
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare class RedisMessageQueue implements MessageQueue, Disposable {
|
|
73
|
+
#private;
|
|
74
|
+
/**
|
|
75
|
+
* Creates a new Redis message queue.
|
|
76
|
+
* @param redis The Redis client factory.
|
|
77
|
+
* @param options The options for the message queue.
|
|
78
|
+
*/
|
|
79
|
+
constructor(redis: () => Redis | Cluster, options?: RedisMessageQueueOptions);
|
|
80
|
+
enqueue(message: any, options?: MessageQueueEnqueueOptions): Promise<void>;
|
|
81
|
+
enqueueMany(messages: any[], options?: MessageQueueEnqueueOptions): Promise<void>;
|
|
82
|
+
listen(handler: (message: any) => void | Promise<void>, options?: MessageQueueListenOptions): Promise<void>;
|
|
83
|
+
[Symbol.dispose](): void;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { RedisMessageQueue, RedisMessageQueueOptions };
|
package/dist/mq.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/redis",
|
|
3
|
-
"version": "2.0.0-pr.412.
|
|
3
|
+
"version": "2.0.0-pr.412.1794+5c393341",
|
|
4
4
|
"description": "Redis drivers for Fedify",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fedify",
|
|
@@ -26,28 +26,48 @@
|
|
|
26
26
|
"https://github.com/sponsors/dahlia"
|
|
27
27
|
],
|
|
28
28
|
"type": "module",
|
|
29
|
-
"main": "./dist/mod.
|
|
29
|
+
"main": "./dist/mod.cjs",
|
|
30
30
|
"module": "./dist/mod.js",
|
|
31
31
|
"types": "./dist/mod.d.ts",
|
|
32
32
|
"exports": {
|
|
33
33
|
".": {
|
|
34
|
-
"types":
|
|
34
|
+
"types": {
|
|
35
|
+
"import": "./dist/mod.d.ts",
|
|
36
|
+
"require": "./dist/mod.d.cts",
|
|
37
|
+
"default": "./dist/mod.d.ts"
|
|
38
|
+
},
|
|
35
39
|
"import": "./dist/mod.js",
|
|
40
|
+
"require": "./dist/mod.cjs",
|
|
36
41
|
"default": "./dist/mod.js"
|
|
37
42
|
},
|
|
38
43
|
"./codec": {
|
|
39
|
-
"types":
|
|
44
|
+
"types": {
|
|
45
|
+
"import": "./dist/codec.d.ts",
|
|
46
|
+
"require": "./dist/codec.d.cts",
|
|
47
|
+
"default": "./dist/codec.d.ts"
|
|
48
|
+
},
|
|
40
49
|
"import": "./dist/codec.js",
|
|
50
|
+
"require": "./dist/codec.cjs",
|
|
41
51
|
"default": "./dist/codec.js"
|
|
42
52
|
},
|
|
43
53
|
"./kv": {
|
|
44
|
-
"types":
|
|
54
|
+
"types": {
|
|
55
|
+
"import": "./dist/kv.d.ts",
|
|
56
|
+
"require": "./dist/kv.d.cts",
|
|
57
|
+
"default": "./dist/kv.d.ts"
|
|
58
|
+
},
|
|
45
59
|
"import": "./dist/kv.js",
|
|
60
|
+
"require": "./dist/kv.cjs",
|
|
46
61
|
"default": "./dist/kv.js"
|
|
47
62
|
},
|
|
48
63
|
"./mq": {
|
|
49
|
-
"types":
|
|
64
|
+
"types": {
|
|
65
|
+
"import": "./dist/mq.d.ts",
|
|
66
|
+
"require": "./dist/mq.d.cts",
|
|
67
|
+
"default": "./dist/mq.d.ts"
|
|
68
|
+
},
|
|
50
69
|
"import": "./dist/mq.js",
|
|
70
|
+
"require": "./dist/mq.cjs",
|
|
51
71
|
"default": "./dist/mq.js"
|
|
52
72
|
},
|
|
53
73
|
"./package.json": "./package.json"
|
|
@@ -58,11 +78,11 @@
|
|
|
58
78
|
],
|
|
59
79
|
"dependencies": {
|
|
60
80
|
"@js-temporal/polyfill": "^0.5.1",
|
|
61
|
-
"@logtape/logtape": "^1.
|
|
81
|
+
"@logtape/logtape": "^1.1.1"
|
|
62
82
|
},
|
|
63
83
|
"peerDependencies": {
|
|
64
84
|
"ioredis": "^5.6.1",
|
|
65
|
-
"@fedify/fedify": "2.0.0-pr.412.
|
|
85
|
+
"@fedify/fedify": "^2.0.0-pr.412.1794+5c393341"
|
|
66
86
|
},
|
|
67
87
|
"devDependencies": {
|
|
68
88
|
"@std/async": "npm:@jsr/std__async@^1.0.13",
|