@veryfront/ext-redis 0.1.1185
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 +202 -0
- package/NOTICE +2 -0
- package/README.md +59 -0
- package/esm/connection-config.d.ts +3 -0
- package/esm/connection-config.d.ts.map +1 -0
- package/esm/connection-config.js +24 -0
- package/esm/event-publisher.d.ts +23 -0
- package/esm/event-publisher.d.ts.map +1 -0
- package/esm/event-publisher.js +356 -0
- package/esm/index.d.ts +11 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +51 -0
- package/esm/package.json +3 -0
- package/esm/redis-client-manager.d.ts +33 -0
- package/esm/redis-client-manager.d.ts.map +1 -0
- package/esm/redis-client-manager.js +535 -0
- package/esm/redis-runtime-provider.d.ts +9 -0
- package/esm/redis-runtime-provider.d.ts.map +1 -0
- package/esm/redis-runtime-provider.js +326 -0
- package/package.json +76 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import * as NodeRedis from "redis";
|
|
2
|
+
import { createRedisEventPublisher } from "./event-publisher.js";
|
|
3
|
+
import { createRedisClientManager, openRedisClient, takeRedisClientSetupCleanupClient, } from "./redis-client-manager.js";
|
|
4
|
+
const PROVIDER_ID = "redis@5.11.0";
|
|
5
|
+
const NODE_CLIENT_METHODS = [
|
|
6
|
+
"connect",
|
|
7
|
+
"hSet",
|
|
8
|
+
"hGetAll",
|
|
9
|
+
"hDel",
|
|
10
|
+
"del",
|
|
11
|
+
"sAdd",
|
|
12
|
+
"sRem",
|
|
13
|
+
"sMembers",
|
|
14
|
+
"rPush",
|
|
15
|
+
"lRange",
|
|
16
|
+
"lIndex",
|
|
17
|
+
"lSet",
|
|
18
|
+
"lLen",
|
|
19
|
+
"xAdd",
|
|
20
|
+
"xGroupCreate",
|
|
21
|
+
"xReadGroup",
|
|
22
|
+
"xAck",
|
|
23
|
+
"keys",
|
|
24
|
+
"exists",
|
|
25
|
+
"expire",
|
|
26
|
+
"set",
|
|
27
|
+
"get",
|
|
28
|
+
"publish",
|
|
29
|
+
"subscribe",
|
|
30
|
+
"unsubscribe",
|
|
31
|
+
"eval",
|
|
32
|
+
"close",
|
|
33
|
+
"destroy",
|
|
34
|
+
"on",
|
|
35
|
+
];
|
|
36
|
+
const CLIENT_METHODS = [
|
|
37
|
+
"connect",
|
|
38
|
+
"disconnect",
|
|
39
|
+
"get",
|
|
40
|
+
"mGet",
|
|
41
|
+
"set",
|
|
42
|
+
"del",
|
|
43
|
+
"scan",
|
|
44
|
+
"expire",
|
|
45
|
+
"eval",
|
|
46
|
+
"incr",
|
|
47
|
+
"pExpire",
|
|
48
|
+
"pTTL",
|
|
49
|
+
];
|
|
50
|
+
const OPTIONAL_CLIENT_METHODS = ["ttl", "on"];
|
|
51
|
+
function readOptionalClientMethod(target, name) {
|
|
52
|
+
let owner = target;
|
|
53
|
+
const visited = new Set();
|
|
54
|
+
while (owner) {
|
|
55
|
+
if (visited.has(owner))
|
|
56
|
+
return undefined;
|
|
57
|
+
visited.add(owner);
|
|
58
|
+
let descriptor;
|
|
59
|
+
try {
|
|
60
|
+
descriptor = Object.getOwnPropertyDescriptor(owner, name);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
if (descriptor) {
|
|
66
|
+
return "value" in descriptor && typeof descriptor.value === "function"
|
|
67
|
+
? descriptor.value
|
|
68
|
+
: undefined;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
owner = Object.getPrototypeOf(owner);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
function bindClientMethods(target, methods, options = {}) {
|
|
80
|
+
const adapter = Object.fromEntries(methods.map((name) => [
|
|
81
|
+
name,
|
|
82
|
+
(...args) => {
|
|
83
|
+
const method = Reflect.get(target, name);
|
|
84
|
+
if (typeof method !== "function") {
|
|
85
|
+
throw new TypeError(`Redis client must expose ${name}`);
|
|
86
|
+
}
|
|
87
|
+
return Reflect.apply(method, target, args);
|
|
88
|
+
},
|
|
89
|
+
]));
|
|
90
|
+
for (const name of options.optionalMethods ?? []) {
|
|
91
|
+
const method = readOptionalClientMethod(target, name);
|
|
92
|
+
if (!method)
|
|
93
|
+
continue;
|
|
94
|
+
Object.defineProperty(adapter, name, {
|
|
95
|
+
enumerable: true,
|
|
96
|
+
value: (...args) => Reflect.apply(method, target, args),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
for (const property of options.properties ?? []) {
|
|
100
|
+
Object.defineProperty(adapter, property, {
|
|
101
|
+
enumerable: true,
|
|
102
|
+
get() {
|
|
103
|
+
return Reflect.get(target, property, target);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return Object.freeze(adapter);
|
|
108
|
+
}
|
|
109
|
+
/** Construct an isolated Redis runtime provider. */
|
|
110
|
+
export function createRedisRuntimeProvider(dependencies = {}) {
|
|
111
|
+
const clientManagerDependencies = dependencies.clientManagerDependencies ?? {};
|
|
112
|
+
const clientManager = createRedisClientManager(clientManagerDependencies);
|
|
113
|
+
const publishers = new Set();
|
|
114
|
+
const clientHandles = new Set();
|
|
115
|
+
const failedSetupHandles = new Set();
|
|
116
|
+
const forcedHandleClosers = new WeakMap();
|
|
117
|
+
const openingAbortControllers = new Set();
|
|
118
|
+
const exposedClients = new WeakMap();
|
|
119
|
+
const connectClient = dependencies.openClient ??
|
|
120
|
+
((options, lifecycle) => openRedisClient(options, clientManagerDependencies, lifecycle));
|
|
121
|
+
const moduleAdapter = Object.freeze({
|
|
122
|
+
createClient(options) {
|
|
123
|
+
requireOpen();
|
|
124
|
+
const client = NodeRedis.createClient(options);
|
|
125
|
+
return bindClientMethods(client, NODE_CLIENT_METHODS);
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
let state = "open";
|
|
129
|
+
let closePromise = null;
|
|
130
|
+
function requireOpen() {
|
|
131
|
+
if (state !== "open")
|
|
132
|
+
throw new Error(`Redis runtime provider is ${state}`);
|
|
133
|
+
}
|
|
134
|
+
function createPublisher(config) {
|
|
135
|
+
requireOpen();
|
|
136
|
+
const implementation = createRedisEventPublisher(config, {
|
|
137
|
+
createClient(url) {
|
|
138
|
+
return NodeRedis.createClient({ url });
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
const publisher = {
|
|
142
|
+
publish(event) {
|
|
143
|
+
return implementation.publish(event);
|
|
144
|
+
},
|
|
145
|
+
subscribe(runId, handler) {
|
|
146
|
+
return implementation.subscribe(runId, handler);
|
|
147
|
+
},
|
|
148
|
+
async close() {
|
|
149
|
+
await implementation.close();
|
|
150
|
+
publishers.delete(publisher);
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
publishers.add(publisher);
|
|
154
|
+
return publisher;
|
|
155
|
+
}
|
|
156
|
+
function createClientHandle(client) {
|
|
157
|
+
let closed = false;
|
|
158
|
+
let closePromise = null;
|
|
159
|
+
let exposedClient = exposedClients.get(client);
|
|
160
|
+
if (!exposedClient) {
|
|
161
|
+
exposedClient = bindClientMethods(client, CLIENT_METHODS, {
|
|
162
|
+
optionalMethods: OPTIONAL_CLIENT_METHODS,
|
|
163
|
+
properties: ["isOpen"],
|
|
164
|
+
});
|
|
165
|
+
exposedClients.set(client, exposedClient);
|
|
166
|
+
}
|
|
167
|
+
const closeClient = (force) => {
|
|
168
|
+
if (force) {
|
|
169
|
+
closed = false;
|
|
170
|
+
clientHandles.add(handle);
|
|
171
|
+
}
|
|
172
|
+
if (closed) {
|
|
173
|
+
closed = true;
|
|
174
|
+
clientHandles.delete(handle);
|
|
175
|
+
failedSetupHandles.delete(handle);
|
|
176
|
+
return Promise.resolve();
|
|
177
|
+
}
|
|
178
|
+
if (closePromise)
|
|
179
|
+
return closePromise;
|
|
180
|
+
const closing = Promise.resolve()
|
|
181
|
+
.then(() => client.isOpen === false ? undefined : client.disconnect())
|
|
182
|
+
.then(() => {
|
|
183
|
+
closed = true;
|
|
184
|
+
clientHandles.delete(handle);
|
|
185
|
+
failedSetupHandles.delete(handle);
|
|
186
|
+
});
|
|
187
|
+
const tracked = closing.finally(() => {
|
|
188
|
+
if (closePromise === tracked)
|
|
189
|
+
closePromise = null;
|
|
190
|
+
});
|
|
191
|
+
closePromise = tracked;
|
|
192
|
+
return tracked;
|
|
193
|
+
};
|
|
194
|
+
const handle = {
|
|
195
|
+
client: exposedClient,
|
|
196
|
+
close() {
|
|
197
|
+
return closeClient(false);
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
forcedHandleClosers.set(handle, () => closeClient(true));
|
|
201
|
+
clientHandles.add(handle);
|
|
202
|
+
return handle;
|
|
203
|
+
}
|
|
204
|
+
async function drainFailedSetups() {
|
|
205
|
+
const results = await Promise.allSettled([...failedSetupHandles].map((handle) => handle.close()));
|
|
206
|
+
const failures = results
|
|
207
|
+
.filter((result) => result.status === "rejected")
|
|
208
|
+
.map((result) => result.reason);
|
|
209
|
+
if (failures.length === 1)
|
|
210
|
+
throw failures[0];
|
|
211
|
+
if (failures.length > 1) {
|
|
212
|
+
throw new AggregateError(failures, "Redis failed-client cleanup failed");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function openOwnedClient(options = {}, signal) {
|
|
216
|
+
requireOpen();
|
|
217
|
+
const abortController = new AbortController();
|
|
218
|
+
const abortFromCaller = () => abortController.abort(signal?.reason);
|
|
219
|
+
if (signal?.aborted)
|
|
220
|
+
abortFromCaller();
|
|
221
|
+
else
|
|
222
|
+
signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
223
|
+
openingAbortControllers.add(abortController);
|
|
224
|
+
let provisionalHandle;
|
|
225
|
+
const opening = (async () => {
|
|
226
|
+
await drainFailedSetups();
|
|
227
|
+
requireOpen();
|
|
228
|
+
let client;
|
|
229
|
+
try {
|
|
230
|
+
client = await connectClient(options, {
|
|
231
|
+
signal: abortController.signal,
|
|
232
|
+
onClientCreated(createdClient) {
|
|
233
|
+
provisionalHandle = createClientHandle(createdClient);
|
|
234
|
+
return forcedHandleClosers.get(provisionalHandle);
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (provisionalHandle && clientHandles.has(provisionalHandle)) {
|
|
240
|
+
failedSetupHandles.add(provisionalHandle);
|
|
241
|
+
}
|
|
242
|
+
const failedClient = takeRedisClientSetupCleanupClient(error);
|
|
243
|
+
if (failedClient && !provisionalHandle) {
|
|
244
|
+
const failedHandle = createClientHandle(failedClient);
|
|
245
|
+
failedSetupHandles.add(failedHandle);
|
|
246
|
+
}
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
const handle = provisionalHandle ?? createClientHandle(client);
|
|
250
|
+
if (state === "open" && !abortController.signal.aborted)
|
|
251
|
+
return handle;
|
|
252
|
+
await handle.close();
|
|
253
|
+
throw abortController.signal.reason ?? new Error(`Redis runtime provider is ${state}`);
|
|
254
|
+
})();
|
|
255
|
+
const tracked = opening.finally(() => {
|
|
256
|
+
openingAbortControllers.delete(abortController);
|
|
257
|
+
signal?.removeEventListener("abort", abortFromCaller);
|
|
258
|
+
});
|
|
259
|
+
return tracked;
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
id: PROVIDER_ID,
|
|
263
|
+
loadModule() {
|
|
264
|
+
requireOpen();
|
|
265
|
+
return Promise.resolve(moduleAdapter);
|
|
266
|
+
},
|
|
267
|
+
async getClient(options = {}) {
|
|
268
|
+
requireOpen();
|
|
269
|
+
const client = await clientManager.getClient(options);
|
|
270
|
+
let exposedClient = exposedClients.get(client);
|
|
271
|
+
if (!exposedClient) {
|
|
272
|
+
exposedClient = bindClientMethods(client, CLIENT_METHODS, {
|
|
273
|
+
optionalMethods: OPTIONAL_CLIENT_METHODS,
|
|
274
|
+
properties: ["isOpen"],
|
|
275
|
+
});
|
|
276
|
+
exposedClients.set(client, exposedClient);
|
|
277
|
+
}
|
|
278
|
+
return exposedClient;
|
|
279
|
+
},
|
|
280
|
+
disconnectClient() {
|
|
281
|
+
// Shared-client owners may outlive registry replacement or extension
|
|
282
|
+
// teardown. The manager cleanup is therefore independently idempotent.
|
|
283
|
+
return clientManager.disconnect();
|
|
284
|
+
},
|
|
285
|
+
openClient(options = {}, signal) {
|
|
286
|
+
return openOwnedClient(options, signal);
|
|
287
|
+
},
|
|
288
|
+
async createEventPublisher(config) {
|
|
289
|
+
return createPublisher(config);
|
|
290
|
+
},
|
|
291
|
+
close() {
|
|
292
|
+
if (closePromise)
|
|
293
|
+
return closePromise;
|
|
294
|
+
if (state === "closed")
|
|
295
|
+
return Promise.resolve();
|
|
296
|
+
state = "closing";
|
|
297
|
+
for (const controller of openingAbortControllers) {
|
|
298
|
+
controller.abort(new Error("Redis runtime provider is closing"));
|
|
299
|
+
}
|
|
300
|
+
const closing = Promise.allSettled([
|
|
301
|
+
...[...publishers].map((publisher) => publisher.close()),
|
|
302
|
+
...[...clientHandles].map((handle) => handle.close()),
|
|
303
|
+
clientManager.disconnect(),
|
|
304
|
+
]).then((results) => {
|
|
305
|
+
const failures = results
|
|
306
|
+
.filter((result) => result.status === "rejected")
|
|
307
|
+
.map((result) => result.reason);
|
|
308
|
+
if (failures.length === 0) {
|
|
309
|
+
state = "closed";
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
state = "close-failed";
|
|
313
|
+
if (failures.length === 1)
|
|
314
|
+
throw failures[0];
|
|
315
|
+
if (failures.length > 1) {
|
|
316
|
+
throw new AggregateError(failures, "Redis runtime provider close failed");
|
|
317
|
+
}
|
|
318
|
+
}).finally(() => {
|
|
319
|
+
if (closePromise === closing)
|
|
320
|
+
closePromise = null;
|
|
321
|
+
});
|
|
322
|
+
closePromise = closing;
|
|
323
|
+
return closing;
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@veryfront/ext-redis",
|
|
3
|
+
"version": "0.1.1185",
|
|
4
|
+
"description": "Veryfront first-party extension package for ext-redis",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"veryfront",
|
|
7
|
+
"extension",
|
|
8
|
+
"ext-redis"
|
|
9
|
+
],
|
|
10
|
+
"author": "Veryfront",
|
|
11
|
+
"homepage": "https://github.com/veryfront/veryfront-code/tree/main/extensions/ext-redis",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/veryfront/veryfront-code.git",
|
|
15
|
+
"directory": "extensions/ext-redis"
|
|
16
|
+
},
|
|
17
|
+
"license": "Apache-2.0",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/veryfront/veryfront-code/issues"
|
|
20
|
+
},
|
|
21
|
+
"module": "./esm/index.js",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"import": "./esm/index.js",
|
|
25
|
+
"types": "./esm/index.d.ts"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"scripts": {},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"veryfront": {
|
|
36
|
+
"extension": true,
|
|
37
|
+
"activation": "explicit",
|
|
38
|
+
"contracts": {
|
|
39
|
+
"provides": [
|
|
40
|
+
"RedisRuntimeProvider"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"capabilities": [
|
|
44
|
+
{
|
|
45
|
+
"type": "net:outbound",
|
|
46
|
+
"hosts": [
|
|
47
|
+
"*"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"type": "env:read",
|
|
52
|
+
"keys": [
|
|
53
|
+
"NODE_ENV",
|
|
54
|
+
"REDIS_PASSWORD",
|
|
55
|
+
"REDIS_URL",
|
|
56
|
+
"REDIS_USERNAME"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@redis/client": "1.5.8",
|
|
63
|
+
"redis": "5.11.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"veryfront": "^0.1.1185"
|
|
67
|
+
},
|
|
68
|
+
"type": "module",
|
|
69
|
+
"types": "./esm/index.d.ts",
|
|
70
|
+
"files": [
|
|
71
|
+
"esm",
|
|
72
|
+
"LICENSE",
|
|
73
|
+
"NOTICE",
|
|
74
|
+
"README.md"
|
|
75
|
+
]
|
|
76
|
+
}
|