keryx 0.3.4 → 0.4.0
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/classes/Connection.ts +1 -1
- package/config/server/web.ts +5 -0
- package/initializers/channels.ts +1 -1
- package/initializers/connections.ts +18 -8
- package/initializers/pubsub.ts +1 -1
- package/package.json +5 -5
- package/servers/web.ts +52 -7
- package/util/scaffold.ts +8 -1
package/classes/Connection.ts
CHANGED
|
@@ -32,7 +32,7 @@ export class Connection<T extends Record<string, any> = Record<string, any>> {
|
|
|
32
32
|
this.subscriptions = new Set();
|
|
33
33
|
this.rawConnection = rawConnection;
|
|
34
34
|
|
|
35
|
-
api.connections.connections.
|
|
35
|
+
api.connections.connections.set(this.id, this);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
package/config/server/web.ts
CHANGED
|
@@ -27,6 +27,11 @@ export const configServerWeb = {
|
|
|
27
27
|
"assets",
|
|
28
28
|
),
|
|
29
29
|
staticFilesRoute: await loadFromEnvIfSet("WEB_SERVER_STATIC_ROUTE", "/"),
|
|
30
|
+
staticFilesCacheControl: await loadFromEnvIfSet(
|
|
31
|
+
"WEB_SERVER_STATIC_CACHE_CONTROL",
|
|
32
|
+
"public, max-age=3600",
|
|
33
|
+
),
|
|
34
|
+
staticFilesEtag: await loadFromEnvIfSet("WEB_SERVER_STATIC_ETAG", true),
|
|
30
35
|
websocketMaxPayloadSize: await loadFromEnvIfSet(
|
|
31
36
|
"WS_MAX_PAYLOAD_SIZE",
|
|
32
37
|
65_536,
|
package/initializers/channels.ts
CHANGED
|
@@ -175,7 +175,7 @@ export class Channels extends Initializer {
|
|
|
175
175
|
refreshPresence = async (): Promise<void> => {
|
|
176
176
|
const keysToRefresh = new Set<string>();
|
|
177
177
|
|
|
178
|
-
for (const connection of api.connections.connections) {
|
|
178
|
+
for (const connection of api.connections.connections.values()) {
|
|
179
179
|
for (const channelName of connection.subscriptions) {
|
|
180
180
|
const channel = this.findChannel(channelName);
|
|
181
181
|
const key = channel
|
|
@@ -17,21 +17,31 @@ export class Connections extends Initializer {
|
|
|
17
17
|
|
|
18
18
|
async initialize() {
|
|
19
19
|
function find(type: string, identifier: string, id: string) {
|
|
20
|
-
const
|
|
21
|
-
(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
for (const connection of api.connections.connections.values()) {
|
|
21
|
+
if (
|
|
22
|
+
connection.type === type &&
|
|
23
|
+
connection.id === id &&
|
|
24
|
+
connection.identifier === identifier
|
|
25
|
+
) {
|
|
26
|
+
return { connection };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { connection: undefined };
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
function destroy(type: string, identifier: string, id: string) {
|
|
28
|
-
const { connection
|
|
33
|
+
const { connection } = find(type, identifier, id);
|
|
29
34
|
if (connection) {
|
|
30
|
-
|
|
35
|
+
api.connections.connections.delete(connection.id);
|
|
36
|
+
return [connection];
|
|
31
37
|
}
|
|
32
38
|
return [];
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
return {
|
|
41
|
+
return {
|
|
42
|
+
connections: new Map<string, Connection>(),
|
|
43
|
+
find,
|
|
44
|
+
destroy,
|
|
45
|
+
};
|
|
36
46
|
}
|
|
37
47
|
}
|
package/initializers/pubsub.ts
CHANGED
|
@@ -72,7 +72,7 @@ export class PubSub extends Initializer {
|
|
|
72
72
|
incomingMessage: string | Buffer,
|
|
73
73
|
) {
|
|
74
74
|
const payload = JSON.parse(incomingMessage.toString()) as PubSubMessage;
|
|
75
|
-
for (const connection of api.connections.connections) {
|
|
75
|
+
for (const connection of api.connections.connections.values()) {
|
|
76
76
|
if (connection.subscriptions.has(payload.channel)) {
|
|
77
77
|
connection.onBroadcastMessageReceived(payload);
|
|
78
78
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keryx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -69,22 +69,22 @@
|
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
71
71
|
"colors": "^1.4.0",
|
|
72
|
-
"commander": "^12.1.0",
|
|
73
72
|
"cookie": "^0.6.0",
|
|
74
|
-
"drizzle-orm": "^0.45.1",
|
|
75
73
|
"ioredis": "^5.9.2",
|
|
76
74
|
"mustache": "^4.2.0",
|
|
77
75
|
"node-resque": "^9.5.0",
|
|
78
76
|
"pg": "^8.18.0",
|
|
79
77
|
"ts-morph": "^27.0.2",
|
|
80
78
|
"typescript": "^5.9.3",
|
|
81
|
-
"zod": "^4.3.6",
|
|
82
79
|
"@types/cookie": "^0.6.0",
|
|
83
80
|
"@types/mustache": "^4.2.6",
|
|
84
81
|
"@types/pg": "^8.16.0"
|
|
85
82
|
},
|
|
86
83
|
"peerDependencies": {
|
|
87
|
-
"
|
|
84
|
+
"commander": "^12.1.0",
|
|
85
|
+
"drizzle-orm": "^0.45.1",
|
|
86
|
+
"drizzle-zod": "^0.8.3",
|
|
87
|
+
"zod": "^4.3.6"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
package/servers/web.ts
CHANGED
|
@@ -220,6 +220,8 @@ export class WebServer extends Server<ReturnType<typeof Bun.serve>> {
|
|
|
220
220
|
//@ts-expect-error
|
|
221
221
|
ws.data.id,
|
|
222
222
|
);
|
|
223
|
+
if (!connection) return;
|
|
224
|
+
|
|
223
225
|
this.wsRateMap.delete(connection.id);
|
|
224
226
|
|
|
225
227
|
try {
|
|
@@ -547,7 +549,7 @@ export class WebServer extends Server<ReturnType<typeof Bun.serve>> {
|
|
|
547
549
|
}
|
|
548
550
|
|
|
549
551
|
async handleStaticFile(
|
|
550
|
-
|
|
552
|
+
req: Request,
|
|
551
553
|
url: ReturnType<typeof parse>,
|
|
552
554
|
): Promise<Response | null> {
|
|
553
555
|
const staticRoute = config.server.web.staticFilesRoute;
|
|
@@ -592,23 +594,66 @@ export class WebServer extends Server<ReturnType<typeof Bun.serve>> {
|
|
|
592
594
|
const indexFile = Bun.file(indexPath);
|
|
593
595
|
const indexExists = await indexFile.exists();
|
|
594
596
|
if (indexExists) {
|
|
595
|
-
return
|
|
596
|
-
|
|
597
|
-
|
|
597
|
+
return this.buildStaticFileResponse(
|
|
598
|
+
req,
|
|
599
|
+
indexFile,
|
|
600
|
+
finalPath + "/index.html",
|
|
601
|
+
);
|
|
598
602
|
}
|
|
599
603
|
}
|
|
600
604
|
return null; // File not found, let other handlers deal with it
|
|
601
605
|
}
|
|
602
606
|
|
|
603
|
-
return
|
|
604
|
-
headers: this.getStaticFileHeaders(finalPath),
|
|
605
|
-
});
|
|
607
|
+
return this.buildStaticFileResponse(req, file, finalPath);
|
|
606
608
|
} catch (error) {
|
|
607
609
|
logger.error(`Error serving static file ${finalPath}: ${error}`);
|
|
608
610
|
return null;
|
|
609
611
|
}
|
|
610
612
|
}
|
|
611
613
|
|
|
614
|
+
private async buildStaticFileResponse(
|
|
615
|
+
req: Request,
|
|
616
|
+
file: ReturnType<typeof Bun.file>,
|
|
617
|
+
filePath: string,
|
|
618
|
+
): Promise<Response> {
|
|
619
|
+
const headers = this.getStaticFileHeaders(filePath);
|
|
620
|
+
|
|
621
|
+
// Generate ETag from mtime + size (fast, no hashing needed)
|
|
622
|
+
if (config.server.web.staticFilesEtag) {
|
|
623
|
+
const mtime = file.lastModified;
|
|
624
|
+
const size = file.size;
|
|
625
|
+
const etag = `"${mtime.toString(36)}-${size.toString(36)}"`;
|
|
626
|
+
headers["ETag"] = etag;
|
|
627
|
+
headers["Last-Modified"] = new Date(mtime).toUTCString();
|
|
628
|
+
|
|
629
|
+
// Check If-None-Match (takes precedence over If-Modified-Since per HTTP spec)
|
|
630
|
+
const ifNoneMatch = req.headers.get("if-none-match");
|
|
631
|
+
if (ifNoneMatch && ifNoneMatch === etag) {
|
|
632
|
+
return new Response(null, { status: 304, headers });
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Check If-Modified-Since
|
|
636
|
+
const ifModifiedSince = req.headers.get("if-modified-since");
|
|
637
|
+
if (ifModifiedSince) {
|
|
638
|
+
const ifModifiedSinceDate = new Date(ifModifiedSince).getTime();
|
|
639
|
+
// File mtime is in ms; compare at second precision (HTTP dates are second-precision)
|
|
640
|
+
if (
|
|
641
|
+
!isNaN(ifModifiedSinceDate) &&
|
|
642
|
+
Math.floor(mtime / 1000) <= Math.floor(ifModifiedSinceDate / 1000)
|
|
643
|
+
) {
|
|
644
|
+
return new Response(null, { status: 304, headers });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Add Cache-Control
|
|
650
|
+
if (config.server.web.staticFilesCacheControl) {
|
|
651
|
+
headers["Cache-Control"] = config.server.web.staticFilesCacheControl;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return new Response(file, { headers });
|
|
655
|
+
}
|
|
656
|
+
|
|
612
657
|
private getStaticFileHeaders(filePath: string): Record<string, string> {
|
|
613
658
|
const headers: Record<string, string> = {
|
|
614
659
|
"X-SERVER-NAME": config.process.name,
|
package/util/scaffold.ts
CHANGED
|
@@ -216,7 +216,14 @@ export async function scaffoldProject(
|
|
|
216
216
|
},
|
|
217
217
|
dependencies: {
|
|
218
218
|
keryx: `^${keryxVersion}`,
|
|
219
|
-
|
|
219
|
+
commander: "^12.1.0",
|
|
220
|
+
zod: "^4.3.6",
|
|
221
|
+
...(options.includeDb
|
|
222
|
+
? {
|
|
223
|
+
"drizzle-orm": "^0.45.1",
|
|
224
|
+
"drizzle-zod": "^0.8.3",
|
|
225
|
+
}
|
|
226
|
+
: {}),
|
|
220
227
|
},
|
|
221
228
|
devDependencies: {
|
|
222
229
|
"@types/bun": "latest",
|