keryx 0.0.1

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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/actions/status.ts +25 -0
  3. package/actions/swagger.ts +170 -0
  4. package/api.ts +45 -0
  5. package/classes/API.ts +168 -0
  6. package/classes/Action.ts +128 -0
  7. package/classes/Channel.ts +81 -0
  8. package/classes/Connection.ts +282 -0
  9. package/classes/ExitCode.ts +4 -0
  10. package/classes/Initializer.ts +45 -0
  11. package/classes/Logger.ts +132 -0
  12. package/classes/Server.ts +16 -0
  13. package/classes/TypedError.ts +91 -0
  14. package/config/channels.ts +9 -0
  15. package/config/database.ts +6 -0
  16. package/config/index.ts +23 -0
  17. package/config/logger.ts +8 -0
  18. package/config/process.ts +9 -0
  19. package/config/rateLimit.ts +22 -0
  20. package/config/redis.ts +8 -0
  21. package/config/server/cli.ts +9 -0
  22. package/config/server/mcp.ts +11 -0
  23. package/config/server/web.ts +68 -0
  24. package/config/session.ts +18 -0
  25. package/config/tasks.ts +26 -0
  26. package/index.ts +29 -0
  27. package/initializers/actionts.ts +669 -0
  28. package/initializers/channels.ts +284 -0
  29. package/initializers/connections.ts +37 -0
  30. package/initializers/db.ts +158 -0
  31. package/initializers/mcp.ts +477 -0
  32. package/initializers/oauth.ts +610 -0
  33. package/initializers/process.ts +25 -0
  34. package/initializers/pubsub.ts +86 -0
  35. package/initializers/redis.ts +77 -0
  36. package/initializers/resque.ts +354 -0
  37. package/initializers/servers.ts +66 -0
  38. package/initializers/session.ts +84 -0
  39. package/initializers/signals.ts +60 -0
  40. package/initializers/swagger.ts +317 -0
  41. package/keryx.ts +61 -0
  42. package/lua/add-presence.lua +13 -0
  43. package/lua/refresh-presence.lua +8 -0
  44. package/lua/remove-presence.lua +16 -0
  45. package/middleware/rateLimit.ts +92 -0
  46. package/migrations.ts +5 -0
  47. package/package.json +97 -0
  48. package/servers/web.ts +721 -0
  49. package/templates/lion.svg +102 -0
  50. package/templates/oauth-authorize.html +75 -0
  51. package/templates/oauth-common.css +140 -0
  52. package/templates/oauth-success.html +38 -0
  53. package/tsconfig.json +24 -0
  54. package/util/cli.ts +135 -0
  55. package/util/config.ts +24 -0
  56. package/util/connectionString.ts +5 -0
  57. package/util/glob.ts +41 -0
  58. package/util/http.ts +86 -0
  59. package/util/oauth.ts +69 -0
  60. package/util/zodMixins.ts +88 -0
@@ -0,0 +1,86 @@
1
+ import { api } from "../api";
2
+ import { Initializer } from "../classes/Initializer";
3
+ import { config } from "../config";
4
+ import pkg from "../package.json";
5
+
6
+ const namespace = "pubsub";
7
+ const redisPubSubChannel = `keryx:pubsub:${pkg.name}`;
8
+
9
+ export type PubSubMessage = {
10
+ channel: string;
11
+ message: string;
12
+ sender: string;
13
+ };
14
+
15
+ export type ClientSubscribeMessage = {
16
+ messageType: "subscribe";
17
+ messageId: string | number;
18
+ channel: string;
19
+ };
20
+
21
+ export type ClientUnsubscribeMessage = {
22
+ messageType: "unsubscribe";
23
+ messageId: string | number;
24
+ channel: string;
25
+ };
26
+
27
+ declare module "../classes/API" {
28
+ export interface API {
29
+ [namespace]: Awaited<ReturnType<PubSub["initialize"]>>;
30
+ }
31
+ }
32
+
33
+ export class PubSub extends Initializer {
34
+ constructor() {
35
+ super(namespace);
36
+ this.startPriority = 150;
37
+ this.stopPriority = 950;
38
+ }
39
+
40
+ async initialize() {
41
+ async function broadcast(
42
+ channel: string,
43
+ message: any,
44
+ sender = "unknown-sender",
45
+ ) {
46
+ const payload: PubSubMessage = { channel, message, sender };
47
+ return api.redis.redis.publish(
48
+ redisPubSubChannel,
49
+ JSON.stringify(payload),
50
+ );
51
+ }
52
+
53
+ return { broadcast };
54
+ }
55
+
56
+ async start() {
57
+ if (api.redis.subscription) {
58
+ await api.redis.subscription.subscribe(redisPubSubChannel);
59
+ api.redis.subscription.on("message", this.handleMessage.bind(this));
60
+ }
61
+ }
62
+
63
+ async stop() {
64
+ if (api.redis.subscription) {
65
+ api.redis.subscription.removeAllListeners("message");
66
+ await api.redis.subscription.unsubscribe(redisPubSubChannel);
67
+ }
68
+ }
69
+
70
+ async handleMessage(
71
+ _pubSubChannel: string,
72
+ incomingMessage: string | Buffer,
73
+ ) {
74
+ const payload = JSON.parse(incomingMessage.toString()) as PubSubMessage;
75
+ for (const connection of api.connections.connections) {
76
+ if (connection.subscriptions.has(payload.channel)) {
77
+ connection.onBroadcastMessageReceived(payload);
78
+ }
79
+ }
80
+
81
+ // Forward to MCP as notifications
82
+ if (config.server.mcp.enabled && api.mcp?.sendNotification) {
83
+ api.mcp.sendNotification(payload);
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,77 @@
1
+ import { Redis as RedisClient } from "ioredis";
2
+ import { api, logger } from "../api";
3
+ import { Initializer } from "../classes/Initializer";
4
+ import { ErrorType, TypedError } from "../classes/TypedError";
5
+ import { config } from "../config";
6
+ import { formatConnectionStringForLogging } from "../util/connectionString";
7
+
8
+ const namespace = "redis";
9
+ const testKey = `__keryx_test_key:${config.process.name}`;
10
+
11
+ declare module "../classes/API" {
12
+ export interface API {
13
+ [namespace]: Awaited<ReturnType<Redis["initialize"]>>;
14
+ }
15
+ }
16
+
17
+ export class Redis extends Initializer {
18
+ constructor() {
19
+ super(namespace);
20
+ this.loadPriority = 200;
21
+ this.startPriority = 110;
22
+ this.stopPriority = 990;
23
+ }
24
+
25
+ async initialize() {
26
+ const redisContainer = {} as {
27
+ redis: RedisClient;
28
+ subscription: RedisClient;
29
+ };
30
+ return redisContainer;
31
+ }
32
+
33
+ async start() {
34
+ api.redis.redis = new RedisClient(config.redis.connectionString);
35
+ api.redis.subscription = new RedisClient(config.redis.connectionString);
36
+
37
+ try {
38
+ await api.redis.redis.set(testKey, Date.now());
39
+ await api.redis.redis.del(testKey);
40
+ await api.redis.subscription.set(testKey, Date.now());
41
+ await api.redis.subscription.del(testKey);
42
+ } catch (e) {
43
+ throw new TypedError({
44
+ type: ErrorType.SERVER_INITIALIZATION,
45
+ message: `Cannot connect to redis (${formatConnectionStringForLogging(config.redis.connectionString)}): ${e}`,
46
+ });
47
+ }
48
+
49
+ logger.info(
50
+ `redis connections established (${formatConnectionStringForLogging(config.redis.connectionString)})`,
51
+ );
52
+ }
53
+
54
+ async stop() {
55
+ let acted = false;
56
+
57
+ if (api.redis.redis) {
58
+ try {
59
+ await api.redis.redis.quit();
60
+ acted = true;
61
+ } catch (e) {
62
+ logger.error(`error closing redis connection: ${e}`);
63
+ }
64
+ }
65
+
66
+ if (api.redis.subscription) {
67
+ try {
68
+ await api.redis.subscription.quit();
69
+ acted = true;
70
+ } catch (e) {
71
+ logger.error(`error closing redis subscription connection: ${e}`);
72
+ }
73
+ }
74
+
75
+ if (acted) logger.info("redis connections closed");
76
+ }
77
+ }
@@ -0,0 +1,354 @@
1
+ import {
2
+ Queue,
3
+ Scheduler,
4
+ Worker,
5
+ type Job,
6
+ type ParsedJob,
7
+ } from "node-resque";
8
+ import {
9
+ Action,
10
+ api,
11
+ config,
12
+ Connection,
13
+ logger,
14
+ RUN_MODE,
15
+ type ActionParams,
16
+ } from "../api";
17
+ import { Initializer } from "../classes/Initializer";
18
+ import { TypedError } from "../classes/TypedError";
19
+
20
+ const namespace = "resque";
21
+
22
+ declare module "../classes/API" {
23
+ export interface API {
24
+ [namespace]: Awaited<ReturnType<Resque["initialize"]>>;
25
+ }
26
+ }
27
+
28
+ let SERVER_JOB_COUNTER = 1;
29
+
30
+ export class Resque extends Initializer {
31
+ constructor() {
32
+ super(namespace);
33
+
34
+ this.loadPriority = 250;
35
+ this.startPriority = 10000;
36
+ this.stopPriority = 900;
37
+ }
38
+
39
+ startQueue = async () => {
40
+ api.resque.queue = new Queue(
41
+ { connection: { redis: api.redis.redis } },
42
+ api.resque.jobs,
43
+ );
44
+
45
+ api.resque.queue.on("error", (error) => {
46
+ logger.error(`[resque:queue] ${error}`);
47
+ });
48
+
49
+ await api.resque.queue.connect();
50
+ };
51
+
52
+ stopQueue = async () => {
53
+ if (api.resque.queue) {
54
+ return api.resque.queue.end();
55
+ }
56
+ };
57
+
58
+ startScheduler = async () => {
59
+ if (config.tasks.enabled === true) {
60
+ api.resque.scheduler = new Scheduler({
61
+ connection: { redis: api.redis.redis },
62
+ timeout: config.tasks.timeout,
63
+ stuckWorkerTimeout: config.tasks.stuckWorkerTimeout,
64
+ retryStuckJobs: config.tasks.retryStuckJobs,
65
+ });
66
+
67
+ api.resque.scheduler.on("error", (error) => {
68
+ logger.error(`[resque:scheduler] ${error}`);
69
+ });
70
+
71
+ await api.resque.scheduler.connect();
72
+
73
+ api.resque.scheduler.on("start", () => {
74
+ logger.info(`[resque:scheduler] started`);
75
+ });
76
+ api.resque.scheduler.on("end", () => {
77
+ logger.info(`[resque:scheduler] ended`);
78
+ });
79
+ api.resque.scheduler.on("poll", () => {
80
+ logger.debug(`[resque:scheduler] polling`);
81
+ });
82
+ api.resque.scheduler.on("leader", () => {
83
+ logger.info(`[resque:scheduler] leader elected`);
84
+ });
85
+ api.resque.scheduler.on(
86
+ "cleanStuckWorker",
87
+ (workerName, errorPayload, delta) => {
88
+ logger.warn(
89
+ `[resque:scheduler] cleaning stuck worker: ${workerName}, ${errorPayload}, ${delta}`,
90
+ );
91
+ },
92
+ );
93
+
94
+ api.resque.scheduler.start();
95
+ await api.actions.enqueueAllRecurrent();
96
+ }
97
+ };
98
+
99
+ stopScheduler = async () => {
100
+ if (api.resque.scheduler && api.resque.scheduler.connection.connected) {
101
+ await api.resque.scheduler.end();
102
+ }
103
+ };
104
+
105
+ startWorkers = async () => {
106
+ let id = 0;
107
+
108
+ while (id < config.tasks.taskProcessors) {
109
+ const worker = new Worker(
110
+ {
111
+ connection: { redis: api.redis.redis },
112
+ queues: Array.isArray(config.tasks.queues)
113
+ ? config.tasks.queues
114
+ : await config.tasks.queues(),
115
+ timeout: config.tasks.timeout,
116
+ name: `worker:${id}`,
117
+ },
118
+ api.resque.jobs,
119
+ );
120
+
121
+ // normal worker emitters
122
+ worker.on("start", () => {
123
+ logger.info(`[resque:${worker.name}] started`);
124
+ });
125
+ worker.on("end", () => {
126
+ logger.info(`[resque:${worker.name}] ended`);
127
+ });
128
+ worker.on("cleaning_worker", () => {
129
+ logger.debug(`[resque:${worker.name}] cleaning worker`);
130
+ });
131
+ worker.on("poll", (queue) => {
132
+ logger.debug(`[resque:${worker.name}] polling, ${queue}`);
133
+ });
134
+ worker.on("job", (queue, job: ParsedJob) => {
135
+ logger.debug(
136
+ `[resque:${worker.name}] job acquired, ${queue}, ${job.class}, ${JSON.stringify(job.args[0])}`,
137
+ );
138
+ });
139
+ worker.on("reEnqueue", (queue, job: ParsedJob, _plugin) => {
140
+ logger.debug(
141
+ `[resque:${worker.name}] job reEnqueue, ${queue}, ${job.class}, ${JSON.stringify(job.args[0])}`,
142
+ );
143
+ });
144
+ worker.on("pause", () => {
145
+ logger.debug(`[resque:${worker.name}] paused`);
146
+ });
147
+
148
+ worker.on("failure", (queue, job, failure, duration) => {
149
+ logger.warn(
150
+ `[resque:${worker.name}] job failed, ${queue}, ${job.class}, ${JSON.stringify(job?.args[0] ?? {})}: ${failure} (${duration}ms)`,
151
+ );
152
+ });
153
+ worker.on("error", (error, queue, job) => {
154
+ logger.warn(
155
+ `[resque:${worker.name}] job error, ${queue}, ${job?.class}, ${JSON.stringify(job?.args[0] ?? {})}: ${error}`,
156
+ );
157
+ });
158
+
159
+ worker.on("success", (queue, job: ParsedJob, result, duration) => {
160
+ logger.info(
161
+ `[resque:${worker.name}] job success ${queue}, ${job.class}, ${JSON.stringify(job.args[0])} | ${JSON.stringify(result)} (${duration}ms)`,
162
+ );
163
+ });
164
+
165
+ api.resque.workers.push(worker);
166
+ id++;
167
+ }
168
+
169
+ for (const worker of api.resque.workers) {
170
+ try {
171
+ await worker.connect();
172
+ await worker.start();
173
+ } catch (error) {
174
+ logger.fatal(`[resque:${worker.name}] ${error}`);
175
+ throw error;
176
+ }
177
+ }
178
+ };
179
+
180
+ stopWorkers = async () => {
181
+ // Signal all workers to stop polling/pinging before closing connections.
182
+ // worker.end() clears timers and closes the Redis connection, but if a
183
+ // poll() or ping() callback already fired and has an in-flight Redis
184
+ // command, it will reject with "Connection is closed." Setting running=false
185
+ // first ensures no NEW operations start, then we drain any in-flight ones.
186
+ for (const worker of api.resque.workers) {
187
+ worker.running = false;
188
+ }
189
+ await Bun.sleep(250);
190
+
191
+ while (true) {
192
+ const worker = api.resque.workers.pop();
193
+ if (!worker) break;
194
+ await worker.end();
195
+ }
196
+ api.resque.workers = [];
197
+ };
198
+
199
+ /** Load all actions as tasks and wrap them for node-resque jobs */
200
+ loadJobs = async () => {
201
+ const jobs: Record<string, Job<any>> = {};
202
+
203
+ for (const action of api.actions.actions) {
204
+ const job = this.wrapActionAsJob(action);
205
+ jobs[action.name] = job;
206
+ }
207
+
208
+ return jobs;
209
+ };
210
+
211
+ wrapActionAsJob = (
212
+ action: Action,
213
+ ): Job<Awaited<ReturnType<(typeof action)["run"]>>> => {
214
+ const job: Job<ReturnType<Action["run"]>> = {
215
+ plugins: [],
216
+ pluginOptions: {},
217
+
218
+ perform: async function (params: ActionParams<typeof action>) {
219
+ const connection = new Connection(
220
+ "resque",
221
+ `job:${api.process.name}:${SERVER_JOB_COUNTER++}}`,
222
+ );
223
+ const paramsAsFormData = new FormData();
224
+
225
+ if (typeof params.entries === "function") {
226
+ for (const [key, value] of params.entries()) {
227
+ paramsAsFormData.append(key, value);
228
+ }
229
+ } else if (typeof params === "object" && params !== null) {
230
+ for (const [key, value] of Object.entries(params)) {
231
+ if (value !== undefined && value !== null) {
232
+ paramsAsFormData.append(key, String(value));
233
+ }
234
+ }
235
+ }
236
+
237
+ const fanOutId = params._fanOutId as string | undefined;
238
+
239
+ let response: Awaited<ReturnType<(typeof action)["run"]>>;
240
+ let error: TypedError | undefined;
241
+ try {
242
+ const payload = await connection.act(action.name, paramsAsFormData);
243
+ response = payload.response;
244
+ error = payload.error;
245
+
246
+ if (error) throw error;
247
+ } catch (e) {
248
+ // Collect fan-out error before re-throwing
249
+ if (fanOutId) {
250
+ const metaKey = `fanout:${fanOutId}`;
251
+ const errorsKey = `fanout:${fanOutId}:errors`;
252
+ const errorMessage = e instanceof Error ? e.message : String(e);
253
+ await api.redis.redis.rpush(
254
+ errorsKey,
255
+ JSON.stringify({ params, error: errorMessage }),
256
+ );
257
+ await api.redis.redis.hincrby(metaKey, "failed", 1);
258
+ // Refresh TTL on all fan-out keys
259
+ const ttl = await api.redis.redis.ttl(metaKey);
260
+ if (ttl > 0) {
261
+ await api.redis.redis.expire(metaKey, ttl);
262
+ await api.redis.redis.expire(`fanout:${fanOutId}:results`, ttl);
263
+ await api.redis.redis.expire(errorsKey, ttl);
264
+ }
265
+ }
266
+ throw e;
267
+ } finally {
268
+ if (
269
+ action.task &&
270
+ action.task.frequency &&
271
+ action.task.frequency > 0
272
+ ) {
273
+ await api.actions.enqueueRecurrent(action);
274
+ }
275
+ }
276
+
277
+ // Collect fan-out result on success
278
+ if (fanOutId) {
279
+ const metaKey = `fanout:${fanOutId}`;
280
+ const resultsKey = `fanout:${fanOutId}:results`;
281
+ await api.redis.redis.rpush(
282
+ resultsKey,
283
+ JSON.stringify({ params, result: response }),
284
+ );
285
+ await api.redis.redis.hincrby(metaKey, "completed", 1);
286
+ // Refresh TTL on all fan-out keys
287
+ const ttl = await api.redis.redis.ttl(metaKey);
288
+ if (ttl > 0) {
289
+ await api.redis.redis.expire(metaKey, ttl);
290
+ await api.redis.redis.expire(resultsKey, ttl);
291
+ await api.redis.redis.expire(`fanout:${fanOutId}:errors`, ttl);
292
+ }
293
+ }
294
+
295
+ return response;
296
+ },
297
+ };
298
+
299
+ if (action.task && action.task.frequency && action.task.frequency > 0) {
300
+ job.plugins!.push("JobLock");
301
+ job.pluginOptions!.JobLock = { reEnqueue: false };
302
+ job.plugins!.push("QueueLock");
303
+ job.plugins!.push("DelayQueueLock");
304
+ }
305
+
306
+ return job;
307
+ };
308
+
309
+ async initialize() {
310
+ const resqueContainer = {
311
+ jobs: await this.loadJobs(),
312
+ workers: [] as Worker[],
313
+ startQueue: this.startQueue,
314
+ stopQueue: this.stopQueue,
315
+ startScheduler: this.startScheduler,
316
+ stopScheduler: this.stopScheduler,
317
+ startWorkers: this.startWorkers,
318
+ stopWorkers: this.stopWorkers,
319
+ wrapActionAsJob: this.wrapActionAsJob,
320
+ } as {
321
+ queue: Queue;
322
+ scheduler: Scheduler;
323
+ workers: Worker[];
324
+ jobs: Awaited<ReturnType<Resque["loadJobs"]>>;
325
+ startQueue: () => Promise<void>;
326
+ stopQueue: () => Promise<void>;
327
+ startScheduler: () => Promise<void>;
328
+ stopScheduler: () => Promise<void>;
329
+ startWorkers: () => Promise<void>;
330
+ stopWorkers: () => Promise<void>;
331
+ wrapActionAsJob: (action: Action) => Job<any>;
332
+ };
333
+
334
+ return resqueContainer;
335
+ }
336
+
337
+ async start() {
338
+ await this.startQueue();
339
+
340
+ if (api.runMode === RUN_MODE.SERVER) {
341
+ await this.startScheduler();
342
+ await this.startWorkers();
343
+ }
344
+ }
345
+
346
+ async stop() {
347
+ if (api.runMode === RUN_MODE.SERVER) {
348
+ await this.stopWorkers();
349
+ await this.stopScheduler();
350
+ }
351
+
352
+ await this.stopQueue();
353
+ }
354
+ }
@@ -0,0 +1,66 @@
1
+ import path from "path";
2
+ import { api, RUN_MODE } from "../api";
3
+ import { Initializer } from "../classes/Initializer";
4
+ import type { Server } from "../classes/Server";
5
+ import { globLoader } from "../util/glob";
6
+
7
+ const namespace = "servers";
8
+
9
+ declare module "../classes/API" {
10
+ export interface API {
11
+ [namespace]: Awaited<ReturnType<Servers["initialize"]>>;
12
+ }
13
+ }
14
+
15
+ export class Servers extends Initializer {
16
+ constructor() {
17
+ super(namespace);
18
+ this.loadPriority = 800;
19
+ this.startPriority = 550;
20
+ this.stopPriority = 100;
21
+ this.runModes = [RUN_MODE.SERVER];
22
+ }
23
+
24
+ async initialize() {
25
+ // Load framework servers from the package directory
26
+ const frameworkServers = await globLoader<Server<any>>(
27
+ path.join(api.packageDir, "servers"),
28
+ );
29
+
30
+ // Load user project servers (if rootDir differs from packageDir)
31
+ let userServers: Server<any>[] = [];
32
+ if (api.rootDir !== api.packageDir) {
33
+ try {
34
+ userServers = await globLoader<Server<any>>(
35
+ path.join(api.rootDir, "servers"),
36
+ );
37
+ } catch {
38
+ // user project may not have servers, that's fine
39
+ }
40
+ }
41
+
42
+ const servers = [...frameworkServers, ...userServers];
43
+
44
+ for (const server of servers) {
45
+ await server.initialize();
46
+ }
47
+
48
+ return { servers };
49
+ }
50
+
51
+ async start() {
52
+ const { servers } = api[namespace];
53
+
54
+ for (const server of servers) {
55
+ await server.start();
56
+ }
57
+ }
58
+
59
+ async stop() {
60
+ const { servers } = api[namespace];
61
+
62
+ for (const server of servers) {
63
+ await server.stop();
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,84 @@
1
+ import { api, Connection } from "../api";
2
+ import { Initializer } from "../classes/Initializer";
3
+ import { config } from "../config";
4
+
5
+ const namespace = "session";
6
+ const prefix = `${namespace}` as const;
7
+
8
+ export interface SessionData<
9
+ T extends Record<string, any> = Record<string, any>,
10
+ > {
11
+ id: string;
12
+ cookieName: typeof config.session.cookieName;
13
+ createdAt: number;
14
+ data: T;
15
+ }
16
+
17
+ function getKey(connectionId: Connection["id"]) {
18
+ return `${prefix}:${connectionId}`;
19
+ }
20
+
21
+ async function load<T extends Record<string, any>>(connection: Connection) {
22
+ const key = getKey(connection.id);
23
+ const data = await api.redis.redis.get(key);
24
+ if (!data) return null;
25
+ await api.redis.redis.expire(key, config.session.ttl);
26
+ return JSON.parse(data) as SessionData<T>;
27
+ }
28
+
29
+ async function create<T extends Record<string, any>>(
30
+ connection: Connection,
31
+ data = {} as T,
32
+ ) {
33
+ const key = getKey(connection.id);
34
+
35
+ const sessionData: SessionData<T> = {
36
+ id: connection.id,
37
+ cookieName: config.session.cookieName,
38
+ createdAt: new Date().getTime(),
39
+ data,
40
+ };
41
+
42
+ await api.redis.redis.set(key, JSON.stringify(sessionData));
43
+ await api.redis.redis.expire(key, config.session.ttl);
44
+ return sessionData;
45
+ }
46
+
47
+ async function update<T extends Record<string, any>>(
48
+ session: SessionData<T>,
49
+ data: Record<string, any>,
50
+ ) {
51
+ const key = getKey(session.id);
52
+ session.data = { ...session.data, ...data };
53
+ await api.redis.redis.set(key, JSON.stringify(session));
54
+ await api.redis.redis.expire(key, config.session.ttl);
55
+ return session.data;
56
+ }
57
+
58
+ async function destroy(connection: Connection) {
59
+ const key = getKey(connection.id);
60
+ const response = await api.redis.redis.del(key);
61
+ return response > 0;
62
+ }
63
+
64
+ declare module "../classes/API" {
65
+ export interface API {
66
+ [namespace]: Awaited<ReturnType<Session["initialize"]>>;
67
+ }
68
+ }
69
+
70
+ export class Session extends Initializer {
71
+ constructor() {
72
+ super(namespace);
73
+ this.startPriority = 600;
74
+ }
75
+
76
+ async initialize() {
77
+ return {
78
+ load,
79
+ create,
80
+ update,
81
+ destroy,
82
+ };
83
+ }
84
+ }