akanjs 3.0.0-alpha.45 → 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/base/primitiveRegistry.ts +14 -10
- package/common/index.ts +1 -0
- package/common/routeConvention.ts +22 -12
- 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/adapt.ts +4 -4
- package/service/predefinedAdaptor/solidPubsub.adaptor.ts +10 -6
- package/service/predefinedAdaptor/websocket.adaptor.ts +6 -1
- package/service/serve.ts +16 -9
- package/signal/intercept.ts +4 -4
- package/signal/internalArg.ts +7 -2
- package/types/base/primitiveRegistry.d.ts +6 -6
- package/types/common/index.d.ts +1 -1
- package/types/common/routeConvention.d.ts +6 -0
- package/types/server/routing/appWsData.d.ts +7 -1
- package/types/service/adapt.d.ts +2 -2
- package/types/service/serve.d.ts +3 -3
- package/types/signal/intercept.d.ts +2 -2
- 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"];
|
|
@@ -209,12 +209,12 @@ declare global {
|
|
|
209
209
|
[DEFAULT_VALUE]: boolean;
|
|
210
210
|
[PURIFIED_VALUE]: boolean;
|
|
211
211
|
[EXAMPLE_VALUE]: boolean;
|
|
212
|
-
validate(value: boolean | number): boolean;
|
|
213
|
-
parseValue(input: boolean | number): boolean | number;
|
|
214
|
-
serializeValue(value: boolean | number): boolean | number;
|
|
215
|
-
_parse(input: boolean | number): boolean;
|
|
216
|
-
_serialize(value: boolean | number): boolean;
|
|
217
|
-
_checkValue(value: boolean | number): void;
|
|
212
|
+
validate(value: boolean | number | string): boolean;
|
|
213
|
+
parseValue(input: boolean | number | string): boolean | number | string;
|
|
214
|
+
serializeValue(value: boolean | number | string): boolean | number | string;
|
|
215
|
+
_parse(input: boolean | number | string): boolean;
|
|
216
|
+
_serialize(value: boolean | number | string): boolean;
|
|
217
|
+
_checkValue(value: boolean | number | string): void;
|
|
218
218
|
}
|
|
219
219
|
interface DateConstructor {
|
|
220
220
|
refName: "Date";
|
|
@@ -305,10 +305,14 @@ Object.assign(String, scalarPrimitiveStatics, {
|
|
|
305
305
|
});
|
|
306
306
|
PrimitiveRegistry.register(String);
|
|
307
307
|
|
|
308
|
-
const normalizeBooleanPrimitiveValue = (value: boolean | number): boolean | null => {
|
|
308
|
+
const normalizeBooleanPrimitiveValue = (value: boolean | number | string): boolean | null => {
|
|
309
309
|
if (typeof value === "boolean") return value;
|
|
310
310
|
if (value === 1) return true;
|
|
311
311
|
if (value === 0) return false;
|
|
312
|
+
if (typeof value !== "string") return null;
|
|
313
|
+
const text = value.trim().toLowerCase();
|
|
314
|
+
if (text === "true" || text === "1") return true;
|
|
315
|
+
if (text === "false" || text === "0") return false;
|
|
312
316
|
return null;
|
|
313
317
|
};
|
|
314
318
|
|
|
@@ -317,13 +321,13 @@ Object.assign(Boolean, {
|
|
|
317
321
|
refName: "Boolean",
|
|
318
322
|
[DEFAULT_VALUE]: false,
|
|
319
323
|
[EXAMPLE_VALUE]: true,
|
|
320
|
-
validate(value: boolean | number) {
|
|
324
|
+
validate(value: boolean | number | string) {
|
|
321
325
|
return normalizeBooleanPrimitiveValue(value) !== null;
|
|
322
326
|
},
|
|
323
|
-
parseValue(input: boolean | number) {
|
|
327
|
+
parseValue(input: boolean | number | string) {
|
|
324
328
|
return normalizeBooleanPrimitiveValue(input) ?? input;
|
|
325
329
|
},
|
|
326
|
-
serializeValue(value: boolean | number) {
|
|
330
|
+
serializeValue(value: boolean | number | string) {
|
|
327
331
|
return normalizeBooleanPrimitiveValue(value) ?? value;
|
|
328
332
|
},
|
|
329
333
|
});
|
package/common/index.ts
CHANGED
|
@@ -67,27 +67,37 @@ export function isRouteSourceFile(filePath: string): boolean {
|
|
|
67
67
|
return tryParseRouteModuleKey(key) !== null;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Why a `page/` file breaks the route convention, or null when it is fine. Null also covers a non-source
|
|
72
|
+
* asset, which `page/` tolerates. `akan sync <lib>` reports these alongside its other layout violations,
|
|
73
|
+
* so the rule stays in one place instead of being restated where it cannot afford to throw.
|
|
74
|
+
*/
|
|
75
|
+
export function getPageSourceFileViolation(filePath: string): string | null {
|
|
76
|
+
if (!SOURCE_EXT_RE.test(filePath)) return null;
|
|
72
77
|
|
|
73
78
|
const key = filePath.startsWith("./") ? filePath : `./${filePath.split(/[\\/]/).join("/")}`;
|
|
74
79
|
const match = ROUTE_SOURCE_RE.exec(key);
|
|
75
|
-
|
|
76
|
-
if (!match) throw new Error(`[route-convention] invalid page source file: ${displayPath}`);
|
|
80
|
+
if (!match) return "invalid page source file";
|
|
77
81
|
|
|
78
82
|
const file = match[1] as string;
|
|
79
83
|
const ext = match[2] as string;
|
|
80
84
|
const leaf = file.split("/").filter(Boolean).at(-1);
|
|
81
|
-
if (!leaf)
|
|
85
|
+
if (!leaf) return "invalid page source file";
|
|
82
86
|
|
|
83
|
-
if (ext !== "tsx")
|
|
87
|
+
if (ext !== "tsx") return "route source files under page/ must use .tsx";
|
|
84
88
|
if (leaf.startsWith("_") && !RESERVED_ROUTE_FILES.has(leaf) && leaf !== INTERNAL_ROOT_LAYOUT_LEAF)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
return "only _index.tsx, _layout.tsx and _overrides.tsx are allowed as reserved route files under page/";
|
|
90
|
+
if (/^[A-Z]/.test(leaf)) return "route page filenames must not start with an uppercase letter";
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function validatePageSourceFile(filePath: string, options: ValidatePageSourceFileOptions = {}): boolean {
|
|
95
|
+
if (!SOURCE_EXT_RE.test(filePath)) return false;
|
|
96
|
+
|
|
97
|
+
const violation = getPageSourceFileViolation(filePath);
|
|
98
|
+
if (!violation) return true;
|
|
99
|
+
const key = filePath.startsWith("./") ? filePath : `./${filePath.split(/[\\/]/).join("/")}`;
|
|
100
|
+
throw new Error(`[route-convention] ${violation}: ${options.filePath ?? key}`);
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
export function validateSubRoutePageKey(
|
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
|
}
|
package/service/adapt.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { type ExtractInjectInfoObject, type InjectBuilder, type InjectInfo, inje
|
|
|
4
4
|
|
|
5
5
|
export interface Adaptor {
|
|
6
6
|
readonly logger: Logger;
|
|
7
|
-
onInit(): Promise<void
|
|
8
|
-
onDestroy(): Promise<void
|
|
7
|
+
onInit(): Promise<void> | void;
|
|
8
|
+
onDestroy(): Promise<void> | void;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export type AdaptorCls<
|
|
@@ -30,9 +30,9 @@ export function adapt(name: string, injectBuilder?: InjectBuilder) {
|
|
|
30
30
|
readonly logger = new Logger(name);
|
|
31
31
|
static readonly [INJECT_META] = injectInfoMap;
|
|
32
32
|
static readonly refName = name;
|
|
33
|
-
|
|
33
|
+
onInit(): Promise<void> | void {
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
onDestroy(): Promise<void> | void {
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
return Adaptor;
|
|
@@ -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";
|
|
@@ -43,9 +43,9 @@ const avoidKeys = new Set([
|
|
|
43
43
|
export interface Service {
|
|
44
44
|
readonly logger: Logger;
|
|
45
45
|
|
|
46
|
-
onInit(): Promise<void
|
|
46
|
+
onInit(): Promise<void> | void;
|
|
47
47
|
_libsOnInit(): Promise<void>;
|
|
48
|
-
onDestroy(): Promise<void
|
|
48
|
+
onDestroy(): Promise<void> | void;
|
|
49
49
|
_libsOnDestroy(): Promise<void>;
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -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,15 +116,20 @@ 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
|
}
|
|
122
128
|
static [INJECT_META] = {};
|
|
123
129
|
readonly logger = new Logger(this.constructor.name);
|
|
124
|
-
|
|
130
|
+
onInit(): Promise<void> | void {
|
|
125
131
|
}
|
|
126
|
-
|
|
132
|
+
onDestroy(): Promise<void> | void {
|
|
127
133
|
}
|
|
128
134
|
};
|
|
129
135
|
applyMixins(srvRef, extSrvs, avoidKeys);
|
|
@@ -132,10 +138,11 @@ export function serve(
|
|
|
132
138
|
const onDestroyFns = extSrvs.map((srv) => srv.prototype.onDestroy);
|
|
133
139
|
Object.assign(srvRef.prototype, {
|
|
134
140
|
async _libsOnInit(this: Service) {
|
|
135
|
-
|
|
141
|
+
|
|
142
|
+
await Promise.all([...onInitFns, this.onInit].map(async (onInit) => await onInit?.call(this)));
|
|
136
143
|
},
|
|
137
144
|
async _libsOnDestroy(this: Service) {
|
|
138
|
-
await Promise.all([...onDestroyFns.map((onDestroy) => onDestroy?.call(this))
|
|
145
|
+
await Promise.all([...onDestroyFns, this.onDestroy].map(async (onDestroy) => await onDestroy?.call(this)));
|
|
139
146
|
},
|
|
140
147
|
});
|
|
141
148
|
|
package/signal/intercept.ts
CHANGED
|
@@ -7,8 +7,8 @@ export type InterceptorCls<Methods = {}, InjectMap extends { [key: string]: Inje
|
|
|
7
7
|
Methods &
|
|
8
8
|
ExtractInjectInfoObject<InjectMap> & {
|
|
9
9
|
readonly logger: Logger;
|
|
10
|
-
onInit(): Promise<void
|
|
11
|
-
onDestroy(): Promise<void
|
|
10
|
+
onInit(): Promise<void> | void;
|
|
11
|
+
onDestroy(): Promise<void> | void;
|
|
12
12
|
intercept(context: SignalContext): AsyncGenerator<unknown> | Promise<unknown>;
|
|
13
13
|
},
|
|
14
14
|
{ readonly [INJECT_META]: InjectMap; readonly refName: string }
|
|
@@ -31,9 +31,9 @@ export function intercept(refName: string, injectBuilder?: InjectBuilder) {
|
|
|
31
31
|
intercept(context: SignalContext): AsyncGenerator | Promise<(res: Response) => Promise<Response>> {
|
|
32
32
|
return Promise.resolve((res: Response) => Promise.resolve(res));
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
onInit(): Promise<void> | void {
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
onDestroy(): Promise<void> | void {
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
39
|
}
|
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,
|
|
@@ -114,12 +114,12 @@ declare global {
|
|
|
114
114
|
[DEFAULT_VALUE]: boolean;
|
|
115
115
|
[PURIFIED_VALUE]: boolean;
|
|
116
116
|
[EXAMPLE_VALUE]: boolean;
|
|
117
|
-
validate(value: boolean | number): boolean;
|
|
118
|
-
parseValue(input: boolean | number): boolean | number;
|
|
119
|
-
serializeValue(value: boolean | number): boolean | number;
|
|
120
|
-
_parse(input: boolean | number): boolean;
|
|
121
|
-
_serialize(value: boolean | number): boolean;
|
|
122
|
-
_checkValue(value: boolean | number): void;
|
|
117
|
+
validate(value: boolean | number | string): boolean;
|
|
118
|
+
parseValue(input: boolean | number | string): boolean | number | string;
|
|
119
|
+
serializeValue(value: boolean | number | string): boolean | number | string;
|
|
120
|
+
_parse(input: boolean | number | string): boolean;
|
|
121
|
+
_serialize(value: boolean | number | string): boolean;
|
|
122
|
+
_checkValue(value: boolean | number | string): void;
|
|
123
123
|
}
|
|
124
124
|
interface DateConstructor {
|
|
125
125
|
refName: "Date";
|
package/types/common/index.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export { pathGet } from "./pathGet.d.ts";
|
|
|
24
24
|
export { pathSet } from "./pathSet.d.ts";
|
|
25
25
|
export { randomPick } from "./randomPick.d.ts";
|
|
26
26
|
export { randomPicks } from "./randomPicks.d.ts";
|
|
27
|
-
export { assertUniqueRoutePatterns, compareRouteSpecificity, getRouteExports, isRouteSourceFile, isSpecialRouteLeaf, LAYOUT_ROUTE_EXPORTS, matchRoutePattern, normalizeRoutePattern, PAGE_ROUTE_EXPORTS, type ParsedRouteModuleKey, parseRouteModuleKey, RESERVED_ROUTE_CONFIG_EXPORTS, ROOT_LAYOUT_ROUTE_EXPORTS, type RouteModuleKind, routeSegmentToPatternPart, routeSegmentToTreePath, tryParseRouteModuleKey, type ValidatePageSourceFileOptions, type ValidateSubRoutePageKeyOptions, validatePageSourceFile, validateSubRoutePageKey, } from "./routeConvention.d.ts";
|
|
27
|
+
export { assertUniqueRoutePatterns, compareRouteSpecificity, getPageSourceFileViolation, getRouteExports, isRouteSourceFile, isSpecialRouteLeaf, LAYOUT_ROUTE_EXPORTS, matchRoutePattern, normalizeRoutePattern, PAGE_ROUTE_EXPORTS, type ParsedRouteModuleKey, parseRouteModuleKey, RESERVED_ROUTE_CONFIG_EXPORTS, ROOT_LAYOUT_ROUTE_EXPORTS, type RouteModuleKind, routeSegmentToPatternPart, routeSegmentToTreePath, tryParseRouteModuleKey, type ValidatePageSourceFileOptions, type ValidateSubRoutePageKeyOptions, validatePageSourceFile, validateSubRoutePageKey, } from "./routeConvention.d.ts";
|
|
28
28
|
export { sleep } from "./sleep.d.ts";
|
|
29
29
|
export { splitVersion } from "./splitVersion.d.ts";
|
|
30
30
|
export { getBasePathFromPathname, parseBasePaths, parseSubRouteHosts, resolveSubRouteHosts } from "./subRoute.d.ts";
|
|
@@ -27,6 +27,12 @@ export interface ValidatePageSourceFileOptions {
|
|
|
27
27
|
filePath?: string;
|
|
28
28
|
}
|
|
29
29
|
export declare function isRouteSourceFile(filePath: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Why a `page/` file breaks the route convention, or null when it is fine. Null also covers a non-source
|
|
32
|
+
* asset, which `page/` tolerates. `akan sync <lib>` reports these alongside its other layout violations,
|
|
33
|
+
* so the rule stays in one place instead of being restated where it cannot afford to throw.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getPageSourceFileViolation(filePath: string): string | null;
|
|
30
36
|
export declare function validatePageSourceFile(filePath: string, options?: ValidatePageSourceFileOptions): boolean;
|
|
31
37
|
export declare function validateSubRoutePageKey(key: string, basePaths: Iterable<string>, options?: ValidateSubRoutePageKeyOptions): void;
|
|
32
38
|
export declare function parseRouteModuleKey(key: string): ParsedRouteModuleKey;
|
|
@@ -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/adapt.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { Logger } from "akanjs/common";
|
|
|
3
3
|
import { type ExtractInjectInfoObject, type InjectBuilder, type InjectInfo } from "./injectInfo.d.ts";
|
|
4
4
|
export interface Adaptor {
|
|
5
5
|
readonly logger: Logger;
|
|
6
|
-
onInit(): Promise<void
|
|
7
|
-
onDestroy(): Promise<void
|
|
6
|
+
onInit(): Promise<void> | void;
|
|
7
|
+
onDestroy(): Promise<void> | void;
|
|
8
8
|
}
|
|
9
9
|
export type AdaptorCls<Methods = any, InjectMap extends Record<string, InjectInfo> = {}> = Cls<Methods & ExtractInjectInfoObject<InjectMap> & Adaptor, {
|
|
10
10
|
readonly [INJECT_META]: InjectMap;
|
package/types/service/serve.d.ts
CHANGED
|
@@ -4,15 +4,15 @@ 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";
|
|
11
11
|
export interface Service {
|
|
12
12
|
readonly logger: Logger;
|
|
13
|
-
onInit(): Promise<void
|
|
13
|
+
onInit(): Promise<void> | void;
|
|
14
14
|
_libsOnInit(): Promise<void>;
|
|
15
|
-
onDestroy(): Promise<void
|
|
15
|
+
onDestroy(): Promise<void> | void;
|
|
16
16
|
_libsOnDestroy(): Promise<void>;
|
|
17
17
|
}
|
|
18
18
|
export type ServiceCls<RefName extends string = string, Methods = {}, InjectMap extends {
|
|
@@ -6,8 +6,8 @@ export type InterceptorCls<Methods = {}, InjectMap extends {
|
|
|
6
6
|
[key: string]: InjectInfo;
|
|
7
7
|
} = {}> = Cls<Methods & ExtractInjectInfoObject<InjectMap> & {
|
|
8
8
|
readonly logger: Logger;
|
|
9
|
-
onInit(): Promise<void
|
|
10
|
-
onDestroy(): Promise<void
|
|
9
|
+
onInit(): Promise<void> | void;
|
|
10
|
+
onDestroy(): Promise<void> | void;
|
|
11
11
|
intercept(context: SignalContext): AsyncGenerator<unknown> | Promise<unknown>;
|
|
12
12
|
}, {
|
|
13
13
|
readonly [INJECT_META]: InjectMap;
|
|
@@ -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;
|