akanjs 3.0.0-alpha.46 → 3.0.0-alpha.47

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/base/baseEnv.ts CHANGED
@@ -73,15 +73,18 @@ export type ClientEnv = BaseEnv & {
73
73
 
74
74
  let cachedEnv: ClientEnv | undefined;
75
75
 
76
+ const missingPublicEnv = (key: string) =>
77
+ `getEnv() cannot run at build time: akan build does not inject ${key}. Call it from a runtime function instead of at module scope (e.g. env(() => getEnv()) in adapt(), a method body, or a default thunk).`;
78
+
76
79
  /** Reads and caches Akan runtime environment values from process/browser environment settings. */
77
80
  export const getEnv = (): ClientEnv => {
78
81
  if (cachedEnv) return cachedEnv;
79
82
  const appName = process.env.AKAN_PUBLIC_APP_NAME ?? "unknown";
80
83
  const repoName = process.env.AKAN_PUBLIC_REPO_NAME ?? "unknown";
81
84
  const serveDomain = process.env.AKAN_PUBLIC_SERVE_DOMAIN ?? "unknown";
82
- if (appName === "unknown") throw new Error("environment variable AKAN_PUBLIC_APP_NAME is required");
83
- if (repoName === "unknown") throw new Error("environment variable AKAN_PUBLIC_REPO_NAME is required");
84
- if (serveDomain === "unknown") throw new Error("environment variable AKAN_PUBLIC_SERVE_DOMAIN is required");
85
+ if (appName === "unknown") throw new Error(missingPublicEnv("AKAN_PUBLIC_APP_NAME"));
86
+ if (repoName === "unknown") throw new Error(missingPublicEnv("AKAN_PUBLIC_REPO_NAME"));
87
+ if (serveDomain === "unknown") throw new Error(missingPublicEnv("AKAN_PUBLIC_SERVE_DOMAIN"));
85
88
  const environment = (process.env.AKAN_PUBLIC_ENV ?? "debug") as BaseEnv["environment"];
86
89
  const operationMode = (process.env.AKAN_PUBLIC_OPERATION_MODE ??
87
90
  (environment === "local" ? "local" : "cloud")) as BaseEnv["operationMode"];
package/constant/via.ts CHANGED
@@ -430,10 +430,14 @@ declare global {
430
430
  }
431
431
 
432
432
  const applyConstantStatics = <Model>(model: ConstantCls<Model>, fieldMap: FieldObject): ConstantCls<Model> => {
433
- const defaultValue = getDefault(model[FIELD_META]);
433
+
434
+ let defaultValue: DefaultOf<Model> | undefined;
434
435
  Object.assign(model, {
435
436
  purify: makePurify(model),
436
- getDefault: () => ({ ...defaultValue }),
437
+ getDefault: () => {
438
+ defaultValue ??= getDefault<Model>(model[FIELD_META]);
439
+ return { ...defaultValue };
440
+ },
437
441
  });
438
442
  Object.entries(fieldMap).forEach(([, field]) => {
439
443
  if (field.enum) model.enums.add(field.enum);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.46",
3
+ "version": "3.0.0-alpha.47",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/server/akanApp.ts CHANGED
@@ -37,7 +37,6 @@ interface ChildState {
37
37
 
38
38
  interface GatewayWsData {
39
39
  childIdx: number;
40
- socketId: string;
41
40
  upstream: WebSocket;
42
41
  }
43
42
 
@@ -582,8 +581,8 @@ export class AkanApp {
582
581
  const upstreamWs = new WebSocket(`ws://${upstream.host}:${upstream.port}${url.pathname}${url.search}`, {
583
582
  headers: this.#makeProxyHeaders(req, child.idx),
584
583
  } as unknown as string[]);
585
- const socketId = crypto.randomUUID();
586
- const upgraded = server.upgrade(req, { data: { childIdx: child.idx, socketId, upstream: upstreamWs } });
584
+
585
+ const upgraded = server.upgrade(req, { data: { childIdx: child.idx, upstream: upstreamWs } });
587
586
  if (!upgraded) {
588
587
  upstreamWs.close();
589
588
  return new Response("WebSocket upgrade failed", { status: 500 });
@@ -41,10 +41,17 @@ export class AppWsData {
41
41
  account?: unknown;
42
42
  /** The `authorization` value `account` was resolved from, so each frame need not re-verify it. */
43
43
  resolvedAuthorization?: string;
44
- socketId?: string;
44
+ /**
45
+ * Identity of this connection, minted here so every app socket carries one from its first frame and
46
+ * adaptors and endpoints only ever read it. Per-connection and process-local — a reconnect gets a new
47
+ * one, and the federation gateway's own socket is a different one — so it is never a caller identity.
48
+ * It outlives a credential swap on purpose: the socket is still the same socket.
49
+ */
50
+ socketId: string;
45
51
  constructor(headers: Headers) {
46
52
  this.createdAt = Date.now();
47
53
  this.headers = headers;
48
54
  this.cookies = new Bun.CookieMap(headers.get("cookie") ?? "");
55
+ this.socketId = Bun.randomUUIDv7();
49
56
  }
50
57
  }
@@ -3,9 +3,13 @@ import { adapt } from "../adapt";
3
3
  import { sendAkanIpc } from "../ipcTypes";
4
4
  import type { WebsocketAdaptor, WsRedisEventHandler, WsSocketData } from "./websocket.adaptor";
5
5
 
6
- const getSocketId = (ws: Bun.ServerWebSocket<unknown>, serverId: string) => {
6
+ /**
7
+ * `AppWsData` mints the id at the handshake, so this reads it; the fallback only covers a socket that
8
+ * was upgraded outside the app router, where nothing else would have given it one.
9
+ */
10
+ const getSocketId = (ws: Bun.ServerWebSocket<unknown>) => {
7
11
  const data = ws.data as WsSocketData;
8
- if (!data.socketId) data.socketId = `${serverId}-${Bun.randomUUIDv7()}`;
12
+ data.socketId ??= Bun.randomUUIDv7();
9
13
  return data.socketId;
10
14
  };
11
15
 
@@ -64,7 +68,7 @@ export class SolidPubSub
64
68
  }
65
69
 
66
70
  async joinRoom(ws: Bun.ServerWebSocket<unknown>, room: string): Promise<void> {
67
- const socketId = getSocketId(ws, this.serverId);
71
+ const socketId = getSocketId(ws);
68
72
  const rooms = this.#socketRooms.get(socketId) ?? new Set<string>();
69
73
  rooms.add(room);
70
74
  this.#socketRooms.set(socketId, rooms);
@@ -72,7 +76,7 @@ export class SolidPubSub
72
76
  }
73
77
 
74
78
  async leaveRoom(ws: Bun.ServerWebSocket<unknown>, room: string): Promise<void> {
75
- const socketId = getSocketId(ws, this.serverId);
79
+ const socketId = getSocketId(ws);
76
80
  const rooms = this.#socketRooms.get(socketId);
77
81
  rooms?.delete(room);
78
82
  if (!rooms || rooms.size === 0) this.#socketRooms.delete(socketId);
@@ -80,7 +84,7 @@ export class SolidPubSub
80
84
  }
81
85
 
82
86
  async leaveAllRooms(ws: Bun.ServerWebSocket<unknown>): Promise<void> {
83
- const socketId = getSocketId(ws, this.serverId);
87
+ const socketId = getSocketId(ws);
84
88
  const rooms = this.#socketRooms.get(socketId);
85
89
  if (rooms) {
86
90
  for (const room of rooms) sendAkanIpc({ type: "pubsub.unsubscribe", roomId: room, socketId, pid: process.pid });
@@ -89,7 +93,7 @@ export class SolidPubSub
89
93
  }
90
94
 
91
95
  async registerSocket(ws: Bun.ServerWebSocket<unknown>): Promise<void> {
92
- getSocketId(ws, this.serverId);
96
+ getSocketId(ws);
93
97
  }
94
98
 
95
99
  async unregisterSocket(ws: Bun.ServerWebSocket<unknown>): Promise<void> {
@@ -209,9 +209,14 @@ export class WebSocketRedisAdaptor
209
209
  await pipeline.exec();
210
210
  }
211
211
 
212
+ /**
213
+ * `AppWsData` mints the id at the handshake, so this reads it; the fallback only covers a socket that
214
+ * was upgraded outside the app router. The owning server is recorded in the socket hash below, so the
215
+ * id itself carries no prefix.
216
+ */
212
217
  #getSocketId(ws: Bun.ServerWebSocket<unknown>): string {
213
218
  const data = ws.data as WsSocketData;
214
- if (!data.socketId) data.socketId = `${this.serverId}-${Bun.randomUUIDv7()}`;
219
+ data.socketId ??= Bun.randomUUIDv7();
215
220
  return data.socketId;
216
221
  }
217
222
 
package/service/serve.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  import type { DatabaseService, DatabaseServiceForModel } from "./types";
13
13
 
14
14
  interface ServiceOptions {
15
- enabled?: boolean;
15
+ enabled?: boolean | (() => boolean);
16
16
  serverMode?: "batch" | "federation";
17
17
  }
18
18
  export type ServiceType = "database" | "plain";
@@ -102,9 +102,10 @@ export function serve(
102
102
  ...(typeof optionOrInjectBuilder === "function" && injectBuilderOrExtendSrv ? [injectBuilderOrExtendSrv] : []),
103
103
  ...extendSrvs,
104
104
  ] as ServiceCls[];
105
- const isEnabled =
105
+ const enabledOption =
106
106
  option.enabled ??
107
107
  (!option.serverMode || process.env.SERVER_MODE === option.serverMode || process.env.SERVER_MODE === "all");
108
+ let enabledCache: boolean | undefined;
108
109
  const serviceType = typeof refNameOrDb === "string" ? "plain" : "database";
109
110
  const injectInfoMap = injectBuilder(injectionBuilder(refName));
110
111
  if (serviceType === "database")
@@ -115,7 +116,12 @@ export function serve(
115
116
  const srvRef = class Service {
116
117
  static readonly type = serviceType;
117
118
  static readonly refName = refName;
118
- static enabled = isEnabled;
119
+ static get enabled() {
120
+
121
+ if (enabledCache === undefined)
122
+ enabledCache = typeof enabledOption === "function" ? enabledOption() : enabledOption;
123
+ return enabledCache;
124
+ }
119
125
  static get name() {
120
126
  return `${capitalize(refName)}Service`;
121
127
  }
@@ -21,15 +21,20 @@ export class Res implements InternalArg {
21
21
  }
22
22
  }
23
23
 
24
- /** Injects websocket state and subscription hooks into message/pubsub handlers. */
24
+ /**
25
+ * Injects websocket state, this connection's id, and subscription hooks into message/pubsub handlers.
26
+ * `socketId` is the one `AppWsData` minted at the handshake, so a handler never reads `ws.data` to
27
+ * tell two callers apart — and never mints an id of its own, which would not match the room bookkeeping.
28
+ */
25
29
  export class Ws implements InternalArg {
26
30
  onDisconnect?: () => void;
27
31
  onUnsubscribe?: () => void;
28
32
  getArg(context: SignalContext) {
29
- const webSocketContext = context.getWebSocketContext();
33
+ const webSocketContext = context.getWebSocketContext<{ socketId: string }>();
30
34
  const ws = webSocketContext.ws;
31
35
  return {
32
36
  ws,
37
+ socketId: ws.data.socketId,
33
38
  subscribe: webSocketContext.eventType === "subscribe",
34
39
  on: webSocketContext.on,
35
40
  off: webSocketContext.off,
@@ -19,6 +19,12 @@ export declare class AppWsData {
19
19
  account?: unknown;
20
20
  /** The `authorization` value `account` was resolved from, so each frame need not re-verify it. */
21
21
  resolvedAuthorization?: string;
22
- socketId?: string;
22
+ /**
23
+ * Identity of this connection, minted here so every app socket carries one from its first frame and
24
+ * adaptors and endpoints only ever read it. Per-connection and process-local — a reconnect gets a new
25
+ * one, and the federation gateway's own socket is a different one — so it is never a caller identity.
26
+ * It outlives a credential swap on purpose: the socket is still the same socket.
27
+ */
28
+ socketId: string;
23
29
  constructor(headers: Headers);
24
30
  }
@@ -4,7 +4,7 @@ import type { DatabaseModel } from "akanjs/document";
4
4
  import { type ExtractInjectInfoObject, type InjectBuilder, InjectInfo } from "./injectInfo.d.ts";
5
5
  import type { DatabaseServiceForModel } from "./types.d.ts";
6
6
  interface ServiceOptions {
7
- enabled?: boolean;
7
+ enabled?: boolean | (() => boolean);
8
8
  serverMode?: "batch" | "federation";
9
9
  }
10
10
  export type ServiceType = "database" | "plain";
@@ -18,12 +18,19 @@ export declare class Res implements InternalArg {
18
18
  redirect(url: string | URL, status?: number): Response;
19
19
  };
20
20
  }
21
- /** Injects websocket state and subscription hooks into message/pubsub handlers. */
21
+ /**
22
+ * Injects websocket state, this connection's id, and subscription hooks into message/pubsub handlers.
23
+ * `socketId` is the one `AppWsData` minted at the handshake, so a handler never reads `ws.data` to
24
+ * tell two callers apart — and never mints an id of its own, which would not match the room bookkeeping.
25
+ */
22
26
  export declare class Ws implements InternalArg {
23
27
  onDisconnect?: () => void;
24
28
  onUnsubscribe?: () => void;
25
29
  getArg(context: SignalContext): {
26
- ws: Bun.ServerWebSocket<unknown>;
30
+ ws: Bun.ServerWebSocket<{
31
+ socketId: string;
32
+ }>;
33
+ socketId: string;
27
34
  subscribe: boolean;
28
35
  on: (event: "disconnect" | "unsubscribe", handler: () => void) => void;
29
36
  off: (event: "disconnect" | "unsubscribe", handler: () => void) => void;