@fedify/redis 2.0.0-dev.237 → 2.0.0-dev.279
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/mq.cjs +67 -14
- package/dist/mq.js +67 -14
- package/package.json +11 -8
package/dist/mq.cjs
CHANGED
|
@@ -47,6 +47,8 @@ var RedisMessageQueue = class {
|
|
|
47
47
|
#codec;
|
|
48
48
|
#pollIntervalMs;
|
|
49
49
|
#loopHandle;
|
|
50
|
+
#lastTimestamp = 0;
|
|
51
|
+
#sequenceInMs = 0;
|
|
50
52
|
/**
|
|
51
53
|
* Creates a new Redis message queue.
|
|
52
54
|
* @param redis The Redis client factory.
|
|
@@ -62,22 +64,53 @@ var RedisMessageQueue = class {
|
|
|
62
64
|
this.#codec = options.codec ?? new require_codec.JsonCodec();
|
|
63
65
|
this.#pollIntervalMs = Temporal.Duration.from(options.pollInterval ?? { seconds: 5 }).total("millisecond");
|
|
64
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Returns a monotonically increasing timestamp to ensure message ordering.
|
|
69
|
+
* When multiple messages are enqueued in the same millisecond, a fractional
|
|
70
|
+
* sequence number is added to preserve insertion order.
|
|
71
|
+
*/
|
|
72
|
+
#getMonotonicTimestamp(baseTimestamp) {
|
|
73
|
+
if (baseTimestamp === this.#lastTimestamp) this.#sequenceInMs++;
|
|
74
|
+
else {
|
|
75
|
+
this.#lastTimestamp = baseTimestamp;
|
|
76
|
+
this.#sequenceInMs = 0;
|
|
77
|
+
}
|
|
78
|
+
return baseTimestamp + this.#sequenceInMs * .001;
|
|
79
|
+
}
|
|
65
80
|
async enqueue(message, options) {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
81
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
82
|
+
const baseTs = options?.delay == null ? now : now + options.delay.total("millisecond");
|
|
83
|
+
const ts = this.#getMonotonicTimestamp(baseTs);
|
|
84
|
+
const encodedMessage = this.#codec.encode([
|
|
85
|
+
crypto.randomUUID(),
|
|
86
|
+
message,
|
|
87
|
+
options?.orderingKey
|
|
88
|
+
]);
|
|
68
89
|
await this.#redis.zadd(this.#queueKey, ts, encodedMessage);
|
|
69
|
-
if (
|
|
90
|
+
if (baseTs <= now) this.#redis.publish(this.#channelKey, "");
|
|
70
91
|
}
|
|
71
92
|
async enqueueMany(messages, options) {
|
|
72
93
|
if (messages.length === 0) return;
|
|
73
|
-
const
|
|
94
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
95
|
+
const baseTs = options?.delay == null ? now : now + options.delay.total("millisecond");
|
|
74
96
|
const multi = this.#redis.multi();
|
|
75
97
|
for (const message of messages) {
|
|
76
|
-
const
|
|
98
|
+
const ts = this.#getMonotonicTimestamp(baseTs);
|
|
99
|
+
const encodedMessage = this.#codec.encode([
|
|
100
|
+
crypto.randomUUID(),
|
|
101
|
+
message,
|
|
102
|
+
options?.orderingKey
|
|
103
|
+
]);
|
|
77
104
|
multi.zadd(this.#queueKey, ts, encodedMessage);
|
|
78
105
|
}
|
|
79
106
|
await multi.exec();
|
|
80
|
-
if (
|
|
107
|
+
if (baseTs <= now) this.#redis.publish(this.#channelKey, "");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Returns the Redis key used to lock a specific ordering key.
|
|
111
|
+
*/
|
|
112
|
+
#getOrderingLockKey(orderingKey) {
|
|
113
|
+
return `${this.#lockKey}:ordering:${orderingKey}`;
|
|
81
114
|
}
|
|
82
115
|
async #poll() {
|
|
83
116
|
logger.debug("Polling for messages...");
|
|
@@ -91,10 +124,25 @@ var RedisMessageQueue = class {
|
|
|
91
124
|
logger.debug("Found {messages} messages to process.", { messages: messages.length });
|
|
92
125
|
try {
|
|
93
126
|
if (messages.length < 1) return;
|
|
94
|
-
const encodedMessage
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
127
|
+
for (const encodedMessage of messages) {
|
|
128
|
+
const decoded = this.#codec.decode(encodedMessage);
|
|
129
|
+
const orderingKey = decoded[2];
|
|
130
|
+
if (orderingKey != null) {
|
|
131
|
+
const orderingLockKey = this.#getOrderingLockKey(orderingKey);
|
|
132
|
+
const lockResult = await this.#redis.set(orderingLockKey, this.#workerId, "EX", 60, "NX");
|
|
133
|
+
if (lockResult == null) continue;
|
|
134
|
+
}
|
|
135
|
+
const removed = await this.#redis.zrem(this.#queueKey, encodedMessage);
|
|
136
|
+
if (removed === 0) {
|
|
137
|
+
if (orderingKey != null) await this.#redis.del(this.#getOrderingLockKey(orderingKey));
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
message: decoded[1],
|
|
142
|
+
orderingKey
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
98
146
|
} finally {
|
|
99
147
|
await this.#redis.del(this.#lockKey);
|
|
100
148
|
}
|
|
@@ -104,15 +152,20 @@ var RedisMessageQueue = class {
|
|
|
104
152
|
const signal = options.signal;
|
|
105
153
|
const poll = async () => {
|
|
106
154
|
while (!signal?.aborted) {
|
|
107
|
-
let
|
|
155
|
+
let result;
|
|
108
156
|
try {
|
|
109
|
-
|
|
157
|
+
result = await this.#poll();
|
|
110
158
|
} catch (error) {
|
|
111
159
|
logger.error("Error polling for messages: {error}", { error });
|
|
112
160
|
return;
|
|
113
161
|
}
|
|
114
|
-
if (
|
|
115
|
-
|
|
162
|
+
if (result === void 0) return;
|
|
163
|
+
const { message, orderingKey } = result;
|
|
164
|
+
try {
|
|
165
|
+
await handler(message);
|
|
166
|
+
} finally {
|
|
167
|
+
if (orderingKey != null) await this.#redis.del(this.#getOrderingLockKey(orderingKey));
|
|
168
|
+
}
|
|
116
169
|
}
|
|
117
170
|
};
|
|
118
171
|
await this.#subRedis.subscribe(this.#channelKey);
|
package/dist/mq.js
CHANGED
|
@@ -46,6 +46,8 @@ var RedisMessageQueue = class {
|
|
|
46
46
|
#codec;
|
|
47
47
|
#pollIntervalMs;
|
|
48
48
|
#loopHandle;
|
|
49
|
+
#lastTimestamp = 0;
|
|
50
|
+
#sequenceInMs = 0;
|
|
49
51
|
/**
|
|
50
52
|
* Creates a new Redis message queue.
|
|
51
53
|
* @param redis The Redis client factory.
|
|
@@ -61,22 +63,53 @@ var RedisMessageQueue = class {
|
|
|
61
63
|
this.#codec = options.codec ?? new JsonCodec();
|
|
62
64
|
this.#pollIntervalMs = Temporal.Duration.from(options.pollInterval ?? { seconds: 5 }).total("millisecond");
|
|
63
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns a monotonically increasing timestamp to ensure message ordering.
|
|
68
|
+
* When multiple messages are enqueued in the same millisecond, a fractional
|
|
69
|
+
* sequence number is added to preserve insertion order.
|
|
70
|
+
*/
|
|
71
|
+
#getMonotonicTimestamp(baseTimestamp) {
|
|
72
|
+
if (baseTimestamp === this.#lastTimestamp) this.#sequenceInMs++;
|
|
73
|
+
else {
|
|
74
|
+
this.#lastTimestamp = baseTimestamp;
|
|
75
|
+
this.#sequenceInMs = 0;
|
|
76
|
+
}
|
|
77
|
+
return baseTimestamp + this.#sequenceInMs * .001;
|
|
78
|
+
}
|
|
64
79
|
async enqueue(message, options) {
|
|
65
|
-
const
|
|
66
|
-
const
|
|
80
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
81
|
+
const baseTs = options?.delay == null ? now : now + options.delay.total("millisecond");
|
|
82
|
+
const ts = this.#getMonotonicTimestamp(baseTs);
|
|
83
|
+
const encodedMessage = this.#codec.encode([
|
|
84
|
+
crypto.randomUUID(),
|
|
85
|
+
message,
|
|
86
|
+
options?.orderingKey
|
|
87
|
+
]);
|
|
67
88
|
await this.#redis.zadd(this.#queueKey, ts, encodedMessage);
|
|
68
|
-
if (
|
|
89
|
+
if (baseTs <= now) this.#redis.publish(this.#channelKey, "");
|
|
69
90
|
}
|
|
70
91
|
async enqueueMany(messages, options) {
|
|
71
92
|
if (messages.length === 0) return;
|
|
72
|
-
const
|
|
93
|
+
const now = Temporal.Now.instant().epochMilliseconds;
|
|
94
|
+
const baseTs = options?.delay == null ? now : now + options.delay.total("millisecond");
|
|
73
95
|
const multi = this.#redis.multi();
|
|
74
96
|
for (const message of messages) {
|
|
75
|
-
const
|
|
97
|
+
const ts = this.#getMonotonicTimestamp(baseTs);
|
|
98
|
+
const encodedMessage = this.#codec.encode([
|
|
99
|
+
crypto.randomUUID(),
|
|
100
|
+
message,
|
|
101
|
+
options?.orderingKey
|
|
102
|
+
]);
|
|
76
103
|
multi.zadd(this.#queueKey, ts, encodedMessage);
|
|
77
104
|
}
|
|
78
105
|
await multi.exec();
|
|
79
|
-
if (
|
|
106
|
+
if (baseTs <= now) this.#redis.publish(this.#channelKey, "");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Returns the Redis key used to lock a specific ordering key.
|
|
110
|
+
*/
|
|
111
|
+
#getOrderingLockKey(orderingKey) {
|
|
112
|
+
return `${this.#lockKey}:ordering:${orderingKey}`;
|
|
80
113
|
}
|
|
81
114
|
async #poll() {
|
|
82
115
|
logger.debug("Polling for messages...");
|
|
@@ -90,10 +123,25 @@ var RedisMessageQueue = class {
|
|
|
90
123
|
logger.debug("Found {messages} messages to process.", { messages: messages.length });
|
|
91
124
|
try {
|
|
92
125
|
if (messages.length < 1) return;
|
|
93
|
-
const encodedMessage
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
126
|
+
for (const encodedMessage of messages) {
|
|
127
|
+
const decoded = this.#codec.decode(encodedMessage);
|
|
128
|
+
const orderingKey = decoded[2];
|
|
129
|
+
if (orderingKey != null) {
|
|
130
|
+
const orderingLockKey = this.#getOrderingLockKey(orderingKey);
|
|
131
|
+
const lockResult = await this.#redis.set(orderingLockKey, this.#workerId, "EX", 60, "NX");
|
|
132
|
+
if (lockResult == null) continue;
|
|
133
|
+
}
|
|
134
|
+
const removed = await this.#redis.zrem(this.#queueKey, encodedMessage);
|
|
135
|
+
if (removed === 0) {
|
|
136
|
+
if (orderingKey != null) await this.#redis.del(this.#getOrderingLockKey(orderingKey));
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
message: decoded[1],
|
|
141
|
+
orderingKey
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
97
145
|
} finally {
|
|
98
146
|
await this.#redis.del(this.#lockKey);
|
|
99
147
|
}
|
|
@@ -103,15 +151,20 @@ var RedisMessageQueue = class {
|
|
|
103
151
|
const signal = options.signal;
|
|
104
152
|
const poll = async () => {
|
|
105
153
|
while (!signal?.aborted) {
|
|
106
|
-
let
|
|
154
|
+
let result;
|
|
107
155
|
try {
|
|
108
|
-
|
|
156
|
+
result = await this.#poll();
|
|
109
157
|
} catch (error) {
|
|
110
158
|
logger.error("Error polling for messages: {error}", { error });
|
|
111
159
|
return;
|
|
112
160
|
}
|
|
113
|
-
if (
|
|
114
|
-
|
|
161
|
+
if (result === void 0) return;
|
|
162
|
+
const { message, orderingKey } = result;
|
|
163
|
+
try {
|
|
164
|
+
await handler(message);
|
|
165
|
+
} finally {
|
|
166
|
+
if (orderingKey != null) await this.#redis.del(this.#getOrderingLockKey(orderingKey));
|
|
167
|
+
}
|
|
115
168
|
}
|
|
116
169
|
};
|
|
117
170
|
await this.#subRedis.subscribe(this.#channelKey);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/redis",
|
|
3
|
-
"version": "2.0.0-dev.
|
|
3
|
+
"version": "2.0.0-dev.279+ce1bdc22",
|
|
4
4
|
"description": "Redis drivers for Fedify",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fedify",
|
|
@@ -82,20 +82,23 @@
|
|
|
82
82
|
},
|
|
83
83
|
"peerDependencies": {
|
|
84
84
|
"ioredis": "^5.8.2",
|
|
85
|
-
"@fedify/fedify": "^2.0.0-dev.
|
|
85
|
+
"@fedify/fedify": "^2.0.0-dev.279+ce1bdc22"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"@std/async": "npm:@jsr/std__async@^1.0.13",
|
|
89
89
|
"@types/node": "^22.17.0",
|
|
90
90
|
"tsdown": "^0.12.9",
|
|
91
91
|
"typescript": "^5.9.3",
|
|
92
|
-
"@fedify/
|
|
93
|
-
"@fedify/
|
|
92
|
+
"@fedify/testing": "^2.0.0-dev.279+ce1bdc22",
|
|
93
|
+
"@fedify/fixture": "^2.0.0"
|
|
94
94
|
},
|
|
95
95
|
"scripts": {
|
|
96
|
-
"build": "tsdown",
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
96
|
+
"build:self": "tsdown",
|
|
97
|
+
"build": "pnpm --filter @fedify/redis... run build:self",
|
|
98
|
+
"prepublish": "pnpm build",
|
|
99
|
+
"pretest": "pnpm build",
|
|
100
|
+
"test": "node --experimental-transform-types --test",
|
|
101
|
+
"pretest:bun": "pnpm build",
|
|
102
|
+
"test:bun": "bun test --timeout=10000"
|
|
100
103
|
}
|
|
101
104
|
}
|