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.
@@ -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.push(this);
35
+ api.connections.connections.set(this.id, this);
36
36
  }
37
37
 
38
38
  /**
@@ -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,
@@ -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 index = api.connections.connections.findIndex(
21
- (c) => c.type === type && c.id === id && c.identifier === identifier,
22
- );
23
-
24
- return { connection: api.connections.connections[index], index };
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, index } = find(type, identifier, id);
33
+ const { connection } = find(type, identifier, id);
29
34
  if (connection) {
30
- return api.connections.connections.splice(index, 1);
35
+ api.connections.connections.delete(connection.id);
36
+ return [connection];
31
37
  }
32
38
  return [];
33
39
  }
34
40
 
35
- return { connections: [] as Connection[], find, destroy };
41
+ return {
42
+ connections: new Map<string, Connection>(),
43
+ find,
44
+ destroy,
45
+ };
36
46
  }
37
47
  }
@@ -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.4",
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
- "drizzle-zod": "^0.8.3"
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
- _req: Request,
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 new Response(indexFile, {
596
- headers: this.getStaticFileHeaders(finalPath + "/index.html"),
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 new Response(file, {
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
- ...(options.includeDb ? { "drizzle-zod": "^0.8.3" } : {}),
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",