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 +6 -3
- package/constant/via.ts +6 -2
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/server/akanApp.ts +2 -3
- package/server/routing/appWsData.ts +8 -1
- package/service/predefinedAdaptor/solidPubsub.adaptor.ts +10 -6
- package/service/predefinedAdaptor/websocket.adaptor.ts +6 -1
- package/service/serve.ts +9 -3
- package/signal/internalArg.ts +7 -2
- package/types/server/routing/appWsData.d.ts +7 -1
- package/types/service/serve.d.ts +1 -1
- package/types/signal/internalArg.d.ts +9 -2
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("
|
|
83
|
-
if (repoName === "unknown") throw new Error("
|
|
84
|
-
if (serveDomain === "unknown") throw new Error("
|
|
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
|
-
|
|
433
|
+
|
|
434
|
+
let defaultValue: DefaultOf<Model> | undefined;
|
|
434
435
|
Object.assign(model, {
|
|
435
436
|
purify: makePurify(model),
|
|
436
|
-
getDefault: () =>
|
|
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);
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
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
|
-
|
|
586
|
-
const upgraded = server.upgrade(req, { data: { childIdx: child.idx,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|
package/signal/internalArg.ts
CHANGED
|
@@ -21,15 +21,20 @@ export class Res implements InternalArg {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/**
|
|
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
|
-
|
|
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
|
}
|
package/types/service/serve.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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<
|
|
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;
|