akanjs 2.3.5 → 2.3.6-rc.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.
@@ -26,6 +26,7 @@ export interface ApiRouteInputs {
26
26
  prefix: string;
27
27
  websocketPrefix: string;
28
28
  routes: HttpRoutes;
29
+ builtinRoutes?: HttpRoutes;
29
30
  routeOptions?: Record<string, SignalRouteOptions>;
30
31
  renderEnvRoutes: HttpRoutes;
31
32
  /** Upgrades the incoming request into an app-signal WebSocket. */
@@ -71,6 +72,7 @@ export class ApiRouter {
71
72
  prefix,
72
73
  websocketPrefix,
73
74
  routes,
75
+ builtinRoutes,
74
76
  routeOptions,
75
77
  renderEnvRoutes,
76
78
  upgradeAppWs,
@@ -79,7 +81,8 @@ export class ApiRouter {
79
81
  const endpointEntries = Object.entries(routes ?? {}).map(
80
82
  ([p, handler]) => [ApiRouter.#applyGlobalPrefix(prefix, p, routeOptions?.[p]), handler] as const,
81
83
  );
82
- const endpointPaths = new Set(endpointEntries.map(([path]) => path));
84
+ const builtinEntries = Object.entries(builtinRoutes ?? {});
85
+ const endpointPaths = new Set([...endpointEntries.map(([path]) => path), ...builtinEntries.map(([path]) => path)]);
83
86
  const routeTable = {
84
87
  [`${prefix}${websocketPrefix}` as "/api/ws"]: (req) => {
85
88
  const upgraded = upgradeAppWs(req, { createdAt: Date.now() });
@@ -87,6 +90,7 @@ export class ApiRouter {
87
90
  return new Response("Failed to upgrade to WebSocket", { status: 500 });
88
91
  },
89
92
  ...Object.fromEntries(endpointEntries),
93
+ ...Object.fromEntries(builtinEntries),
90
94
  ...(renderEnvRoutes ?? {}),
91
95
  } as NonNullHttpRoutes;
92
96
  return webProxyRunner
package/signal/index.ts CHANGED
@@ -9,6 +9,7 @@ export * from "./internal";
9
9
  export * from "./internalArg";
10
10
  export * from "./internalInfo";
11
11
  export * from "./middleware";
12
+ export * from "./openapi";
12
13
  export * from "./serializer";
13
14
  export * from "./serverSignal";
14
15
  export * from "./signalContext";
@@ -0,0 +1 @@
1
+ export * from "./openapi";
@@ -0,0 +1,359 @@
1
+ import { type Cls, FIELD_META, getNonArrayModel, PrimitiveRegistry, type PrimitiveScalar } from "akanjs/base";
2
+ import { type ConstantCls, type ConstantField, ConstantRegistry, type ConstantType } from "akanjs/constant";
3
+ import { FetchClient } from "akanjs/fetch";
4
+ import type { SerializedArg, SerializedEndpoint, SerializedReturns, SerializedSignal } from "../types";
5
+
6
+ type OpenApiSchema = Record<string, unknown>;
7
+ type OpenApiParameter = Record<string, unknown>;
8
+ type OpenApiOperation = Record<string, unknown>;
9
+ type OpenApiPathItem = Record<string, OpenApiOperation>;
10
+
11
+ export interface OpenApiDocumentOptions {
12
+ title?: string;
13
+ version?: string;
14
+ servers?: { url: string; description?: string }[];
15
+ description?: string;
16
+ resolveDescription?: (key: string) => string | undefined;
17
+ excludeSignals?: string[];
18
+ includeNonStandardPaths?: boolean;
19
+ }
20
+
21
+ export interface OpenApiDocument {
22
+ openapi: "3.1.0";
23
+ info: {
24
+ title: string;
25
+ version: string;
26
+ description?: string;
27
+ };
28
+ servers?: { url: string; description?: string }[];
29
+ paths: Record<string, OpenApiPathItem>;
30
+ components: {
31
+ schemas: Record<string, OpenApiSchema>;
32
+ securitySchemes?: Record<string, OpenApiSchema>;
33
+ };
34
+ }
35
+
36
+ const httpMethods = {
37
+ query: "get",
38
+ mutation: "post",
39
+ } as const;
40
+
41
+ export const createOpenApiDocument = (
42
+ serializedSignal: Record<string, SerializedSignal>,
43
+ options: OpenApiDocumentOptions = {},
44
+ ): OpenApiDocument => {
45
+ const paths: Record<string, OpenApiPathItem> = {};
46
+ const excludeSignals = new Set(options.excludeSignals ?? ["base"]);
47
+ let hasProtectedOperation = false;
48
+
49
+ for (const [refName, signal] of Object.entries(serializedSignal)) {
50
+ if (excludeSignals.has(refName)) continue;
51
+ for (const [endpointKey, endpoint] of collectRestEndpoints(refName, signal)) {
52
+ const method = httpMethods[endpoint.type as keyof typeof httpMethods];
53
+ if (!method) continue;
54
+
55
+ const path = toOpenApiPath(FetchClient.makeHttpUrl(endpointKey, endpoint, signal.prefix, new Map()));
56
+ if (!options.includeNonStandardPaths && isNonStandardOpenApiPath(path)) continue;
57
+ const operation = createOperation(refName, endpointKey, endpoint, options);
58
+ if (operation.security) hasProtectedOperation = true;
59
+ paths[path] ??= {};
60
+ paths[path][method] = operation;
61
+ }
62
+ }
63
+
64
+ return {
65
+ openapi: "3.1.0",
66
+ info: {
67
+ title: options.title ?? "Akan API",
68
+ version: options.version ?? "0.0.0",
69
+ ...(options.description ? { description: options.description } : {}),
70
+ },
71
+ ...(options.servers?.length ? { servers: options.servers } : {}),
72
+ paths,
73
+ components: {
74
+ schemas: createReferencedComponentSchemas(paths),
75
+ ...(hasProtectedOperation
76
+ ? {
77
+ securitySchemes: {
78
+ bearerAuth: {
79
+ type: "http",
80
+ scheme: "bearer",
81
+ bearerFormat: "JWT",
82
+ },
83
+ },
84
+ }
85
+ : {}),
86
+ },
87
+ };
88
+ };
89
+
90
+ const collectRestEndpoints = (refName: string, signal: SerializedSignal): [string, SerializedEndpoint][] => {
91
+ const baseEndpointEntries = Object.entries(FetchClient.getBaseEndpoint(refName, signal));
92
+ const sliceEndpointEntries = signal.slice
93
+ ? Object.entries(signal.slice).flatMap(([suffix, slice]) =>
94
+ Object.entries(FetchClient.getEndpointFromSlice(refName, suffix, slice)),
95
+ )
96
+ : [];
97
+ return [...baseEndpointEntries, ...sliceEndpointEntries, ...Object.entries(signal.endpoint)].filter(
98
+ ([, endpoint]) => endpoint.type === "query" || endpoint.type === "mutation",
99
+ );
100
+ };
101
+
102
+ const createOperation = (
103
+ refName: string,
104
+ endpointKey: string,
105
+ endpoint: SerializedEndpoint,
106
+ options: OpenApiDocumentOptions,
107
+ ): OpenApiOperation => {
108
+ const { paramArgs, searchArgs, bodyArgs, uploadArgs } = FetchClient.classifyHttpArgs(endpoint.args);
109
+ const description = options.resolveDescription?.(`${refName}.signal.${endpointKey}.desc`);
110
+ const guards = getProtectedGuards(endpoint.guards);
111
+ return {
112
+ tags: [refName],
113
+ operationId: endpointKey,
114
+ summary: options.resolveDescription?.(`${refName}.signal.${endpointKey}`) ?? endpointKey,
115
+ ...(description ? { description } : {}),
116
+ parameters: [
117
+ ...paramArgs.map((arg) => createParameter(refName, endpointKey, arg, "path", options)),
118
+ ...searchArgs.map((arg) => createParameter(refName, endpointKey, arg, "query", options)),
119
+ ],
120
+ ...(bodyArgs.length || uploadArgs.length ? { requestBody: createRequestBody(bodyArgs, uploadArgs) } : {}),
121
+ responses: {
122
+ "200": {
123
+ description: "Successful response",
124
+ content: createResponseContent(endpoint),
125
+ },
126
+ default: {
127
+ description: "Error response",
128
+ },
129
+ },
130
+ ...(guards.length ? { security: [{ bearerAuth: [] }], "x-akan-guards": guards } : {}),
131
+ "x-akan-signal": refName,
132
+ "x-akan-endpoint-type": endpoint.type,
133
+ };
134
+ };
135
+
136
+ const createParameter = (
137
+ refName: string,
138
+ endpointKey: string,
139
+ arg: SerializedArg,
140
+ location: "path" | "query",
141
+ options: OpenApiDocumentOptions,
142
+ ): OpenApiParameter => ({
143
+ name: arg.name,
144
+ in: location,
145
+ required: location === "path",
146
+ schema: createArgSchema(arg),
147
+ ...(arg.example !== undefined ? { example: arg.example } : {}),
148
+ ...(options.resolveDescription?.(`${refName}.signal.${endpointKey}.arg.${arg.name}.desc`)
149
+ ? { description: options.resolveDescription(`${refName}.signal.${endpointKey}.arg.${arg.name}.desc`) }
150
+ : {}),
151
+ });
152
+
153
+ const createRequestBody = (bodyArgs: SerializedArg[], uploadArgs: SerializedArg[]) => {
154
+ const required = [...bodyArgs, ...uploadArgs].filter((arg) => !arg.nullable).map((arg) => arg.name);
155
+ const schema = {
156
+ type: "object",
157
+ properties: Object.fromEntries(
158
+ [...bodyArgs, ...uploadArgs].map((arg) => [
159
+ arg.name,
160
+ uploadArgs.includes(arg) ? createUploadSchema(arg) : createArgSchema(arg),
161
+ ]),
162
+ ),
163
+ ...(required.length ? { required } : {}),
164
+ };
165
+ return {
166
+ required: required.length > 0,
167
+ content: {
168
+ [uploadArgs.length ? "multipart/form-data" : "application/json"]: {
169
+ schema,
170
+ },
171
+ },
172
+ };
173
+ };
174
+
175
+ const createResponseContent = (endpoint: SerializedEndpoint) =>
176
+ isBinaryResponseEndpoint(endpoint)
177
+ ? {
178
+ "application/octet-stream": {
179
+ schema: { type: "string", format: "binary" },
180
+ },
181
+ }
182
+ : {
183
+ "application/json": {
184
+ schema: createReturnSchema(endpoint.returns),
185
+ },
186
+ };
187
+
188
+ const createReturnSchema = (returns: SerializedReturns): OpenApiSchema =>
189
+ withNullable(
190
+ withArrayDepth(createRefSchema(returns.refName, returns.modelType), returns.arrDepth ?? 0),
191
+ !!returns.nullable,
192
+ );
193
+
194
+ const createArgSchema = (arg: SerializedArg): OpenApiSchema => {
195
+ const schema = arg.enum ? createEnumSchema(arg.enum) : createRefSchema(arg.refName, arg.modelType);
196
+ return withNullable(withArrayDepth(schema, arg.arrDepth ?? 0), !!arg.nullable);
197
+ };
198
+
199
+ const createUploadSchema = (arg: SerializedArg): OpenApiSchema => {
200
+ const fileSchema = { type: "string", format: "binary" };
201
+ return withNullable(withArrayDepth(fileSchema, arg.arrDepth ?? 0), !!arg.nullable);
202
+ };
203
+
204
+ const createRefSchema = (refName: string, modelType?: ConstantType): OpenApiSchema => {
205
+ if (!modelType) return createPrimitiveSchema(refName);
206
+ const modelRef = ConstantRegistry.getModelRef(refName, modelType);
207
+ return { $ref: `#/components/schemas/${ConstantRegistry.getModelName(modelRef as Cls)}` };
208
+ };
209
+
210
+ const createComponentSchemas = (): Record<string, OpenApiSchema> => {
211
+ const schemas: Record<string, OpenApiSchema> = {};
212
+ for (const [, database] of ConstantRegistry.database.entries()) {
213
+ [database.input, database.object, database.full, database.light, database.insight].forEach((modelRef) => {
214
+ schemas[ConstantRegistry.getModelName(modelRef)] = createModelSchema(modelRef);
215
+ });
216
+ }
217
+ for (const [, scalar] of ConstantRegistry.scalar.entries()) {
218
+ schemas[ConstantRegistry.getModelName(scalar.model)] = createModelSchema(scalar.model);
219
+ }
220
+ return schemas;
221
+ };
222
+
223
+ const createReferencedComponentSchemas = (paths: Record<string, OpenApiPathItem>): Record<string, OpenApiSchema> => {
224
+ const allSchemas = createComponentSchemas();
225
+ const referencedNames = collectSchemaRefNames(paths);
226
+ const pending = [...referencedNames];
227
+ for (let idx = 0; idx < pending.length; idx++) {
228
+ const schemaName = pending[idx];
229
+ if (!schemaName) continue;
230
+ const schema = allSchemas[schemaName];
231
+ if (!schema) continue;
232
+ for (const nestedName of collectSchemaRefNames(schema)) {
233
+ if (referencedNames.has(nestedName)) continue;
234
+ referencedNames.add(nestedName);
235
+ pending.push(nestedName);
236
+ }
237
+ }
238
+ return Object.fromEntries(
239
+ [...referencedNames]
240
+ .sort((a, b) => a.localeCompare(b))
241
+ .flatMap((name) => (allSchemas[name] ? ([[name, allSchemas[name]]] as const) : [])),
242
+ );
243
+ };
244
+
245
+ const collectSchemaRefNames = (value: unknown): Set<string> => {
246
+ const refs = new Set<string>();
247
+ const visit = (current: unknown) => {
248
+ if (!current || typeof current !== "object") return;
249
+ if (Array.isArray(current)) {
250
+ current.forEach(visit);
251
+ return;
252
+ }
253
+ const record = current as Record<string, unknown>;
254
+ if (typeof record.$ref === "string") {
255
+ const match = record.$ref.match(/^#\/components\/schemas\/(.+)$/);
256
+ if (match?.[1]) refs.add(match[1]);
257
+ }
258
+ Object.values(record).forEach(visit);
259
+ };
260
+ visit(value);
261
+ return refs;
262
+ };
263
+
264
+ const createModelSchema = (modelRef: ConstantCls): OpenApiSchema => {
265
+ const fields = (modelRef as { [FIELD_META]?: Record<string, ConstantField> })[FIELD_META] ?? {};
266
+ const properties: Record<string, OpenApiSchema> = {};
267
+ const required: string[] = [];
268
+ for (const [key, field] of Object.entries(fields)) {
269
+ const props = field.getProps();
270
+ properties[key] = createFieldSchema(field);
271
+ if (!props.nullable) required.push(key);
272
+ }
273
+ return {
274
+ type: "object",
275
+ properties,
276
+ ...(required.length ? { required } : {}),
277
+ additionalProperties: false,
278
+ };
279
+ };
280
+
281
+ const createFieldSchema = (field: ConstantField): OpenApiSchema => {
282
+ const props = field.getProps();
283
+ const schema = props.enum ? createInlineEnumSchema([...props.enum.values]) : createFieldRefSchema(props);
284
+ return withNullable(withArrayDepth(schema, props.arrDepth), props.nullable);
285
+ };
286
+
287
+ const createFieldRefSchema = (props: ReturnType<ConstantField["getProps"]>): OpenApiSchema => {
288
+ if (props.isMap) {
289
+ const [valueRef, valueArrDepth] = getNonArrayModel(props.of as Cls | Cls[]);
290
+ return {
291
+ type: "object",
292
+ additionalProperties: withArrayDepth(createModelRefSchema(valueRef as Cls), valueArrDepth),
293
+ };
294
+ }
295
+ return createModelRefSchema(props.modelRef as Cls);
296
+ };
297
+
298
+ const createModelRefSchema = (modelRef: Cls): OpenApiSchema => {
299
+ if (PrimitiveRegistry.has(modelRef))
300
+ return createPrimitiveSchema(PrimitiveRegistry.getName(modelRef as typeof PrimitiveScalar));
301
+ return { $ref: `#/components/schemas/${ConstantRegistry.getModelName(modelRef)}` };
302
+ };
303
+
304
+ const createPrimitiveSchema = (refName: string): OpenApiSchema => {
305
+ switch (refName) {
306
+ case "Boolean":
307
+ return { type: "boolean" };
308
+ case "Date":
309
+ return { type: "string", format: "date-time" };
310
+ case "Float":
311
+ return { type: "number" };
312
+ case "ID":
313
+ return { type: "string", pattern: "^[0-9a-fA-F]{24}$" };
314
+ case "Int":
315
+ return { type: "integer" };
316
+ case "Upload":
317
+ return { type: "string", format: "binary" };
318
+ case "Any":
319
+ return {};
320
+ case "String":
321
+ default:
322
+ return { type: "string" };
323
+ }
324
+ };
325
+
326
+ const createEnumSchema = (refName: string): OpenApiSchema => {
327
+ const enumRef = ConstantRegistry.enum.get(refName);
328
+ if (!enumRef) return { type: "string", "x-akan-enum": refName };
329
+ return createInlineEnumSchema([...enumRef.values]);
330
+ };
331
+
332
+ const createInlineEnumSchema = (values: unknown[]): OpenApiSchema => ({
333
+ type: values.every((value) => typeof value === "number") ? "number" : "string",
334
+ enum: values,
335
+ });
336
+
337
+ const withArrayDepth = (schema: OpenApiSchema, arrDepth: number): OpenApiSchema => {
338
+ let current = schema;
339
+ for (let idx = 0; idx < arrDepth; idx++) current = { type: "array", items: current };
340
+ return current;
341
+ };
342
+
343
+ const withNullable = (schema: OpenApiSchema, nullable: boolean): OpenApiSchema =>
344
+ nullable ? { anyOf: [schema, { type: "null" }] } : schema;
345
+
346
+ const getProtectedGuards = (guards?: string[]) =>
347
+ (guards ?? []).filter((guard) => guard !== "None" && guard !== "Public");
348
+
349
+ const isBinaryResponseEndpoint = (endpoint: SerializedEndpoint) =>
350
+ endpoint.returns.refName === "Upload" ||
351
+ (endpoint.returns.refName === "Any" &&
352
+ Boolean(endpoint.path?.includes("*") || endpoint.path?.toLowerCase().includes("blob")));
353
+
354
+ const toOpenApiPath = (path: string) => {
355
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
356
+ return normalizedPath.replace(/:([A-Za-z0-9_]+)/g, "{$1}");
357
+ };
358
+
359
+ const isNonStandardOpenApiPath = (path: string) => path.includes("*");
@@ -6,6 +6,7 @@ export interface AkanAppOptions {
6
6
  runtimeDir?: string;
7
7
  port?: number;
8
8
  wsBasePort?: number;
9
+ openapi?: boolean;
9
10
  }
10
11
  /** Gateway/orchestrator that starts Akan child servers and proxies HTTP/WebSocket traffic. */
11
12
  export declare class AkanApp {
@@ -7,6 +7,10 @@ export interface AkanServerProps extends AkanLibProps {
7
7
  env?: BaseEnv;
8
8
  prefix?: string;
9
9
  websocketPrefix?: string;
10
+ openapi?: boolean;
11
+ }
12
+ export interface AkanServerOptions {
13
+ openapi?: boolean;
10
14
  }
11
15
  export interface AkanServerConsoleInfo {
12
16
  name: string;
@@ -32,11 +36,13 @@ export declare class AkanServer {
32
36
  };
33
37
  prefix: string;
34
38
  websocketPrefix: string;
39
+ openapi: boolean;
35
40
  serverMode: "federation" | "batch" | "all";
36
41
  shutdownTimeoutMs: number;
37
- constructor(name?: string, env?: BaseEnv, serverMode?: "federation" | "batch" | "all", ...libs: AkanLib[]);
42
+ constructor(name?: string, env?: BaseEnv, serverMode?: "federation" | "batch" | "all", ...libsOrOptions: (AkanLib | AkanServerOptions)[]);
38
43
  setPrefix(prefix: string): this;
39
44
  setWebsocketPrefix(websocketPrefix: string): this;
45
+ setOpenApi(openapi?: boolean): this;
40
46
  setDatabaseConfig(database: DatabaseConfig): this;
41
47
  setSolidConfig(solid: SolidConfig): this;
42
48
  setShutdownTimeout(timeoutMs: number): this;
@@ -1,4 +1,4 @@
1
- import type { CssPayload, PagesBundlePayload } from "./ipcTypes.d.ts";
1
+ import type { CssPayload, DevBuildStatus, PagesBundlePayload } from "./ipcTypes.d.ts";
2
2
  import type { BuildRouteClientResult } from "./manifestTypes.d.ts";
3
3
  export interface BuilderRpcEventHandlers {
4
4
  /**
@@ -20,6 +20,8 @@ export interface BuilderRpcEventHandlers {
20
20
  * worker subprocess.
21
21
  */
22
22
  onPagesUpdated?: (bundle: PagesBundlePayload) => void;
23
+ /** Builder reported a dev build phase success/failure. */
24
+ onBuildStatus?: (status: DevBuildStatus) => void;
23
25
  }
24
26
  /**
25
27
  * Typed RPC facade around the raw `BuilderTransport`. Handles the
@@ -27,6 +27,24 @@ export interface CssPayload {
27
27
  generation?: number;
28
28
  changedFiles?: string[];
29
29
  }
30
+ export type DevChangeRole = "server" | "client" | "shared" | "barrel" | "config" | "css";
31
+ export type DevChangeAction = "restart-backend" | "rebuild-client" | "rebuild-css" | "sync-generated" | "restart-dev-host" | "report-error";
32
+ export interface DevChangePlan {
33
+ generation: number;
34
+ files: string[];
35
+ generatedFiles: string[];
36
+ roles: DevChangeRole[];
37
+ actions: DevChangeAction[];
38
+ reasonByFile: Record<string, string[]>;
39
+ }
40
+ export type BuildPhase = "scan" | "barrel" | "csr" | "pages" | "css" | "route" | "backend";
41
+ export interface DevBuildStatus {
42
+ generation: number;
43
+ phase: BuildPhase;
44
+ ok: boolean;
45
+ files: string[];
46
+ message?: string;
47
+ }
30
48
  export type BuilderReq = {
31
49
  type: "build-route";
32
50
  id: number;
@@ -63,11 +81,15 @@ export type BuilderEvent = {
63
81
  kinds: ("code" | "css" | "config")[];
64
82
  files: string[];
65
83
  generation?: number;
84
+ devPlan?: DevChangePlan;
66
85
  } | {
67
86
  type: "css-updated";
68
87
  data: CssPayload;
69
88
  } | {
70
89
  type: "pages-updated";
71
90
  data: PagesBundlePayload;
91
+ } | {
92
+ type: "build-status";
93
+ data: DevBuildStatus;
72
94
  };
73
95
  export type BuilderMessage = BuilderReq | BuilderRes | BuilderEvent;
@@ -1 +1 @@
1
- export declare const HMR_CLIENT_SCRIPT = "(function(){\n if (self.__AKAN_HMR_INSTALLED__) return;\n self.__AKAN_HMR_INSTALLED__ = true;\n var proto = location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n var url = proto + \"//\" + location.host + \"/_akan/hmr\";\n var attempts = 0;\n var socket = null;\n var lastBuildId = null;\n var refreshRuntimePromise = null;\n var refreshRuntime = null;\n var pendingRefreshRegistrations = [];\n var refreshQueue = Promise.resolve();\n var overlayEl = null;\n var overlayLabelEl = null;\n var overlayStyleEl = null;\n var overlayTimer = null;\n var overlayHideTimer = null;\n var overlayNextToken = 1;\n var overlayJobs = {};\n self.__AKAN_HMR_PHASE__ = null;\n\n // Bun's React Fast Refresh transform can emit top-level calls to these globals\n // even when we fall back to full reload instead of applying React Refresh.\n self.$RefreshReg$ = self.$RefreshReg$ || function(type, id){\n if (refreshRuntime) refreshRuntime.register(type, id);\n else pendingRefreshRegistrations.push([type, id]);\n };\n self.$RefreshSig$ = self.$RefreshSig$ || function(){ return function(type){ return type; }; };\n // Start installing React Refresh before the application module graph loads.\n // Injecting the runtime only on the first update is too late for React's renderer hook.\n ensureRefreshRuntime().catch(function(err){\n console.warn(\"[akan-hmr] React Refresh runtime preload failed\", err);\n });\n\n function connect(){\n try { socket = new WebSocket(url); }\n catch(e){ console.error(\"[akan-hmr] ws init failed\", e); schedule(); return; }\n socket.addEventListener(\"open\", function(){ attempts = 0; });\n socket.addEventListener(\"message\", function(ev){\n var msg;\n try { msg = JSON.parse(ev.data); } catch (e){ return; }\n if (!msg || typeof msg.type !== \"string\") return;\n if (msg.type === \"hello\") {\n if (lastBuildId !== null && msg.buildId !== lastBuildId) {\n location.reload();\n return;\n }\n lastBuildId = msg.buildId;\n return;\n }\n if (msg.type === \"reload\") {\n beginHmrOverlay(\"Reloading...\", true);\n try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}\n setTimeout(function(){ location.reload(); }, 30);\n return;\n }\n if (msg.type === \"rsc-refresh\") {\n refreshRsc(msg);\n return;\n }\n if (msg.type === \"client-refresh\") {\n refreshClient(msg);\n return;\n }\n if (msg.type === \"css-update\") {\n var cssUrl = selectCssUrl(msg.cssAssets);\n if (cssUrl) swapCss(cssUrl);\n else {\n beginHmrOverlay(\"Reloading...\", true);\n location.reload();\n }\n return;\n }\n if (msg.type === \"error\") { console.error(\"[akan-hmr]\", msg.message); return; }\n });\n socket.addEventListener(\"close\", function(){ socket = null; schedule(); });\n socket.addEventListener(\"error\", function(){ try { socket && socket.close(); } catch(e){} });\n }\n\n function schedule(){\n attempts = Math.min(attempts + 1, 6);\n var delay = Math.min(30000, 250 * Math.pow(2, attempts - 1));\n setTimeout(connect, delay);\n }\n\n function ensureOverlay(){\n if (overlayEl && overlayLabelEl) return overlayEl;\n if (!overlayStyleEl) {\n overlayStyleEl = document.createElement(\"style\");\n overlayStyleEl.textContent =\n \"@keyframes akan-hmr-spin{to{transform:rotate(360deg)}}\" +\n \".__akan_hmr_overlay{position:fixed;left:16px;bottom:16px;z-index:2147483647;display:flex;align-items:center;gap:9px;padding:10px 12px;border-radius:999px;background:rgba(17,24,39,.94);color:#fff;font:500 13px/1.2 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;box-shadow:0 10px 28px rgba(0,0,0,.28);pointer-events:none;opacity:0;transform:translateY(6px);transition:opacity .15s ease,transform .15s ease;backdrop-filter:blur(8px)}\" +\n \".__akan_hmr_overlay[data-show=true]{opacity:1;transform:translateY(0)}\" +\n \".__akan_hmr_spinner{width:14px;height:14px;border:2px solid rgba(255,255,255,.32);border-top-color:#fff;border-radius:999px;animation:akan-hmr-spin .75s linear infinite;flex:none}\" +\n \"@media (prefers-reduced-motion:reduce){.__akan_hmr_overlay{transition:none}.__akan_hmr_spinner{animation:none}}\";\n document.head.appendChild(overlayStyleEl);\n }\n overlayEl = document.createElement(\"div\");\n overlayEl.className = \"__akan_hmr_overlay\";\n overlayEl.setAttribute(\"role\", \"status\");\n overlayEl.setAttribute(\"aria-live\", \"polite\");\n overlayEl.innerHTML = '<span class=\"__akan_hmr_spinner\" aria-hidden=\"true\"></span><span data-akan-hmr-label>Updating...</span>';\n overlayLabelEl = overlayEl.querySelector(\"[data-akan-hmr-label]\");\n (document.body || document.documentElement).appendChild(overlayEl);\n return overlayEl;\n }\n\n function activeOverlayTokens(){\n return Object.keys(overlayJobs);\n }\n\n function latestOverlayLabel(){\n var keys = activeOverlayTokens();\n if (keys.length === 0) return \"Updating...\";\n return overlayJobs[keys[keys.length - 1]] || \"Updating...\";\n }\n\n function showOverlayNow(){\n overlayTimer = null;\n if (activeOverlayTokens().length === 0) return;\n var el = ensureOverlay();\n if (overlayHideTimer) {\n clearTimeout(overlayHideTimer);\n overlayHideTimer = null;\n }\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n requestAnimationFrame(function(){ el.setAttribute(\"data-show\", \"true\"); });\n }\n\n function beginHmrOverlay(label, immediate){\n var token = overlayNextToken++;\n overlayJobs[token] = label || \"Updating...\";\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n if (overlayHideTimer) {\n clearTimeout(overlayHideTimer);\n overlayHideTimer = null;\n }\n if (immediate) {\n if (overlayTimer) clearTimeout(overlayTimer);\n showOverlayNow();\n } else if (!overlayTimer && (!overlayEl || overlayEl.getAttribute(\"data-show\") !== \"true\")) {\n overlayTimer = setTimeout(showOverlayNow, 120);\n }\n return token;\n }\n\n function setHmrOverlayLabel(token, label){\n if (!overlayJobs[token]) return;\n overlayJobs[token] = label || \"Updating...\";\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n }\n\n function endHmrOverlay(token){\n delete overlayJobs[token];\n if (activeOverlayTokens().length > 0) {\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n return;\n }\n if (overlayTimer) {\n clearTimeout(overlayTimer);\n overlayTimer = null;\n }\n if (!overlayEl) return;\n overlayEl.setAttribute(\"data-show\", \"false\");\n if (overlayHideTimer) clearTimeout(overlayHideTimer);\n overlayHideTimer = setTimeout(function(){\n if (overlayEl && activeOverlayTokens().length === 0 && overlayEl.parentNode) {\n overlayEl.parentNode.removeChild(overlayEl);\n overlayEl = null;\n overlayLabelEl = null;\n }\n }, 180);\n }\n\n function refreshRsc(msg){\n var started = performance.now();\n var overlayToken = beginHmrOverlay(\"Refreshing page...\");\n try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}\n if (!self.__AKAN_RSC_REFRESH__) {\n console.warn(\"[akan-hmr] RSC refresh API unavailable, falling back to full reload\");\n setHmrOverlayLabel(overlayToken, \"Reloading...\");\n setTimeout(function(){ location.reload(); }, 30);\n return;\n }\n Promise.resolve(self.__AKAN_RSC_REFRESH__({ buildId: msg.buildId })).then(function(){\n lastBuildId = msg.buildId;\n endHmrOverlay(overlayToken);\n console.debug && console.debug(\"[akan-hmr] RSC refreshed\", {\n buildId: msg.buildId,\n generation: msg.generation,\n routeIds: msg.routeIds,\n changedFiles: msg.changedFiles && msg.changedFiles.length,\n durationMs: Math.round(performance.now() - started)\n });\n }, function(err){\n console.error(\"[akan-hmr] RSC refresh failed, falling back to full reload\", err);\n setHmrOverlayLabel(overlayToken, \"Update failed, reloading...\");\n setTimeout(function(){ location.reload(); }, 250);\n });\n }\n\n function ensureRefreshRuntime(){\n if (refreshRuntimePromise) return refreshRuntimePromise;\n refreshRuntimePromise = import(\"react-refresh/runtime\").then(function(mod){\n var runtime = mod.default || mod;\n if (!self.__AKAN_REACT_REFRESH_READY__) {\n refreshRuntime = runtime;\n runtime.injectIntoGlobalHook(self);\n self.$RefreshReg$ = function(type, id){ runtime.register(type, id); };\n self.$RefreshSig$ = runtime.createSignatureFunctionForTransform;\n for (var i = 0; i < pendingRefreshRegistrations.length; i++) {\n self.$RefreshReg$(pendingRefreshRegistrations[i][0], pendingRefreshRegistrations[i][1]);\n }\n pendingRefreshRegistrations = [];\n self.__AKAN_REACT_REFRESH_READY__ = true;\n self.__AKAN_REACT_REFRESH_RUNTIME__ = runtime;\n }\n return runtime;\n });\n return refreshRuntimePromise;\n }\n\n function refreshClient(msg){\n refreshQueue = refreshQueue.then(function(){ return doRefreshClient(msg); }, function(){ return doRefreshClient(msg); });\n }\n\n function setHmrPhase(phase){\n self.__AKAN_HMR_PHASE__ = phase;\n }\n\n function doRefreshClient(msg){\n var started = performance.now();\n var metadataAt = started;\n var importAt = started;\n var refreshAt = started;\n var overlayToken = beginHmrOverlay(\"Updating...\");\n var fallbackToRsc = false;\n return ensureRefreshRuntime().then(function(runtime){\n setHmrOverlayLabel(overlayToken, \"Fetching update...\");\n var endpoint = new URL(\"/_akan/hmr/client-refresh\", location.origin);\n endpoint.searchParams.set(\"url\", location.href);\n if (msg.buildId != null) endpoint.searchParams.set(\"buildId\", String(msg.buildId));\n return fetch(endpoint, { credentials: \"same-origin\", cache: \"no-store\" })\n .then(function(res){\n if (!res.ok) throw new Error(\"client-refresh metadata failed \" + res.status + \" \" + res.statusText);\n return res.json();\n })\n .then(function(info){\n metadataAt = performance.now();\n var chunks = Array.isArray(info.chunks) ? info.chunks : [];\n if (chunks.length === 0) throw new Error(\"no client chunks returned\");\n setHmrPhase(\"refresh-import\");\n setHmrOverlayLabel(overlayToken, \"Importing update...\");\n return Promise.all(chunks.map(function(chunk){ return import(chunk); })).then(function(){\n importAt = performance.now();\n setHmrPhase(\"react-refresh\");\n setHmrOverlayLabel(overlayToken, \"Applying update...\");\n try {\n runtime.performReactRefresh();\n } finally {\n setHmrPhase(null);\n }\n refreshAt = performance.now();\n lastBuildId = msg.buildId;\n console.debug && console.debug(\"[akan-hmr] React Fast Refresh applied\", {\n buildId: msg.buildId,\n generation: msg.generation,\n chunks: chunks.length,\n routeIds: info.routeIds || msg.routeIds,\n changedFiles: msg.changedFiles && msg.changedFiles.length,\n metadataMs: Math.round(metadataAt - started),\n importMs: Math.round(importAt - metadataAt),\n refreshMs: Math.round(refreshAt - importAt),\n durationMs: Math.round(refreshAt - started)\n });\n endHmrOverlay(overlayToken);\n }, function(err){\n setHmrPhase(null);\n throw err;\n });\n });\n }).catch(function(err){\n console.warn(\"[akan-hmr] React Fast Refresh failed, falling back to RSC refresh\", err);\n fallbackToRsc = true;\n endHmrOverlay(overlayToken);\n refreshRsc(msg);\n }).finally(function(){\n if (!fallbackToRsc) endHmrOverlay(overlayToken);\n });\n }\n\n function swapCss(href){\n var overlayToken = beginHmrOverlay(\"Updating styles...\");\n var link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = href;\n link.setAttribute(\"data-akan-css\", \"pending\");\n link.addEventListener(\"load\", function(){\n var prev = document.querySelectorAll(\"link[data-akan-css=active]\");\n for (var i = 0; i < prev.length; i++) prev[i].parentNode && prev[i].parentNode.removeChild(prev[i]);\n link.setAttribute(\"data-akan-css\", \"active\");\n endHmrOverlay(overlayToken);\n });\n link.addEventListener(\"error\", function(){\n if (link.parentNode) link.parentNode.removeChild(link);\n endHmrOverlay(overlayToken);\n });\n document.head.appendChild(link);\n }\n\n function selectCssUrl(cssAssets){\n if (!cssAssets || typeof cssAssets !== \"object\") return null;\n var parts = location.pathname.split(\"/\").filter(Boolean);\n for (var i = 0; i < parts.length; i++) {\n var asset = cssAssets[parts[i]];\n if (asset && asset.cssUrl) return asset.cssUrl;\n }\n return cssAssets[\"\"] && cssAssets[\"\"].cssUrl ? cssAssets[\"\"].cssUrl : null;\n }\n\n connect();\n})();";
1
+ export declare const HMR_CLIENT_SCRIPT = "(function(){\n if (self.__AKAN_HMR_INSTALLED__) return;\n self.__AKAN_HMR_INSTALLED__ = true;\n var proto = location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n var url = proto + \"//\" + location.host + \"/_akan/hmr\";\n var attempts = 0;\n var socket = null;\n var lastBuildId = null;\n var refreshRuntimePromise = null;\n var refreshRuntime = null;\n var pendingRefreshRegistrations = [];\n var refreshQueue = Promise.resolve();\n var overlayEl = null;\n var overlayLabelEl = null;\n var overlayDetailEl = null;\n var overlayStyleEl = null;\n var overlayTimer = null;\n var overlayHideTimer = null;\n var overlayNextToken = 1;\n var overlayJobs = {};\n var buildErrorStates = {};\n self.__AKAN_HMR_PHASE__ = null;\n\n // Bun's React Fast Refresh transform can emit top-level calls to these globals\n // even when we fall back to full reload instead of applying React Refresh.\n self.$RefreshReg$ = self.$RefreshReg$ || function(type, id){\n if (refreshRuntime) refreshRuntime.register(type, id);\n else pendingRefreshRegistrations.push([type, id]);\n };\n self.$RefreshSig$ = self.$RefreshSig$ || function(){ return function(type){ return type; }; };\n // Start installing React Refresh before the application module graph loads.\n // Injecting the runtime only on the first update is too late for React's renderer hook.\n ensureRefreshRuntime().catch(function(err){\n console.warn(\"[akan-hmr] React Refresh runtime preload failed\", err);\n });\n\n function connect(){\n try { socket = new WebSocket(url); }\n catch(e){ console.error(\"[akan-hmr] ws init failed\", e); schedule(); return; }\n socket.addEventListener(\"open\", function(){ attempts = 0; });\n socket.addEventListener(\"message\", function(ev){\n var msg;\n try { msg = JSON.parse(ev.data); } catch (e){ return; }\n if (!msg || typeof msg.type !== \"string\") return;\n if (msg.type === \"hello\") {\n if (lastBuildId !== null && msg.buildId !== lastBuildId) {\n location.reload();\n return;\n }\n lastBuildId = msg.buildId;\n return;\n }\n if (msg.type === \"reload\") {\n beginHmrOverlay(\"Reloading...\", true);\n try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}\n setTimeout(function(){ location.reload(); }, 30);\n return;\n }\n if (msg.type === \"rsc-refresh\") {\n refreshRsc(msg);\n return;\n }\n if (msg.type === \"client-refresh\") {\n refreshClient(msg);\n return;\n }\n if (msg.type === \"css-update\") {\n var cssUrl = selectCssUrl(msg.cssAssets);\n if (cssUrl) swapCss(cssUrl);\n else {\n beginHmrOverlay(\"Reloading...\", true);\n location.reload();\n }\n return;\n }\n if (msg.type === \"build-status\") { handleBuildStatus(msg); return; }\n if (msg.type === \"ok\") {\n clearBuildErrorOverlay({\n phase: \"build\",\n generation: typeof msg.generation === \"number\" ? msg.generation : Number.MAX_SAFE_INTEGER,\n files: 0\n });\n return;\n }\n if (msg.type === \"error\") {\n console.error(\"[akan-hmr]\", msg.message);\n showBuildErrorOverlay({ phase: \"build\", generation: 0, message: msg.message, files: 0 });\n return;\n }\n });\n socket.addEventListener(\"close\", function(){ socket = null; schedule(); });\n socket.addEventListener(\"error\", function(){ try { socket && socket.close(); } catch(e){} });\n }\n\n function schedule(){\n attempts = Math.min(attempts + 1, 6);\n var delay = Math.min(30000, 250 * Math.pow(2, attempts - 1));\n setTimeout(connect, delay);\n }\n\n function ensureOverlay(){\n if (overlayEl && overlayLabelEl) return overlayEl;\n if (!overlayStyleEl) {\n overlayStyleEl = document.createElement(\"style\");\n overlayStyleEl.textContent =\n \"@keyframes akan-hmr-spin{to{transform:rotate(360deg)}}\" +\n \".__akan_hmr_overlay{position:fixed;left:16px;bottom:16px;z-index:2147483647;display:flex;align-items:flex-start;gap:9px;max-width:min(420px,calc(100vw - 32px));padding:10px 12px;border-radius:16px;background:rgba(17,24,39,.94);color:#fff;font:500 13px/1.25 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;box-shadow:0 10px 28px rgba(0,0,0,.28);pointer-events:none;opacity:0;transform:translateY(6px);transition:opacity .15s ease,transform .15s ease;backdrop-filter:blur(8px)}\" +\n \".__akan_hmr_overlay[data-show=true]{opacity:1;transform:translateY(0)}\" +\n \".__akan_hmr_overlay[data-status=error]{background:rgba(127,29,29,.96);border:1px solid rgba(252,165,165,.55)}\" +\n \".__akan_hmr_overlay[data-status=ok]{background:rgba(6,95,70,.94);border:1px solid rgba(110,231,183,.45)}\" +\n \".__akan_hmr_spinner{width:14px;height:14px;margin-top:1px;border:2px solid rgba(255,255,255,.32);border-top-color:#fff;border-radius:999px;animation:akan-hmr-spin .75s linear infinite;flex:none}\" +\n \".__akan_hmr_overlay[data-status=error] .__akan_hmr_spinner,.__akan_hmr_overlay[data-status=ok] .__akan_hmr_spinner{animation:none;border-color:rgba(255,255,255,.72);border-top-color:rgba(255,255,255,.72)}\" +\n \".__akan_hmr_body{display:flex;flex-direction:column;gap:3px;min-width:0}\" +\n \".__akan_hmr_detail{font:400 12px/1.35 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:rgba(255,255,255,.82);white-space:pre-wrap;overflow-wrap:anywhere}\" +\n \"@media (prefers-reduced-motion:reduce){.__akan_hmr_overlay{transition:none}.__akan_hmr_spinner{animation:none}}\";\n document.head.appendChild(overlayStyleEl);\n }\n overlayEl = document.createElement(\"div\");\n overlayEl.className = \"__akan_hmr_overlay\";\n overlayEl.setAttribute(\"data-status\", \"updating\");\n overlayEl.setAttribute(\"role\", \"status\");\n overlayEl.setAttribute(\"aria-live\", \"polite\");\n overlayEl.innerHTML = '<span class=\"__akan_hmr_spinner\" aria-hidden=\"true\"></span><span class=\"__akan_hmr_body\"><span data-akan-hmr-label>Updating...</span><span class=\"__akan_hmr_detail\" data-akan-hmr-detail></span></span>';\n overlayLabelEl = overlayEl.querySelector(\"[data-akan-hmr-label]\");\n overlayDetailEl = overlayEl.querySelector(\"[data-akan-hmr-detail]\");\n (document.body || document.documentElement).appendChild(overlayEl);\n return overlayEl;\n }\n\n function activeOverlayTokens(){\n return Object.keys(overlayJobs);\n }\n\n function latestOverlayLabel(){\n var keys = activeOverlayTokens();\n if (keys.length === 0) return \"Updating...\";\n return overlayJobs[keys[keys.length - 1]] || \"Updating...\";\n }\n\n function showOverlayNow(){\n overlayTimer = null;\n if (activeOverlayTokens().length === 0) return;\n if (hasBuildErrors()) {\n renderBuildErrorOverlay();\n return;\n }\n var el = ensureOverlay();\n el.setAttribute(\"data-status\", \"updating\");\n if (overlayHideTimer) {\n clearTimeout(overlayHideTimer);\n overlayHideTimer = null;\n }\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n if (overlayDetailEl) overlayDetailEl.textContent = \"\";\n requestAnimationFrame(function(){ el.setAttribute(\"data-show\", \"true\"); });\n }\n\n function beginHmrOverlay(label, immediate){\n var token = overlayNextToken++;\n overlayJobs[token] = label || \"Updating...\";\n if (!hasBuildErrors() && overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n if (overlayHideTimer) {\n clearTimeout(overlayHideTimer);\n overlayHideTimer = null;\n }\n if (immediate) {\n if (overlayTimer) clearTimeout(overlayTimer);\n showOverlayNow();\n } else if (!overlayTimer && (!overlayEl || overlayEl.getAttribute(\"data-show\") !== \"true\")) {\n overlayTimer = setTimeout(showOverlayNow, 120);\n }\n return token;\n }\n\n function setHmrOverlayLabel(token, label){\n if (!overlayJobs[token]) return;\n overlayJobs[token] = label || \"Updating...\";\n if (!hasBuildErrors() && overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n }\n\n function endHmrOverlay(token){\n delete overlayJobs[token];\n if (hasBuildErrors()) {\n renderBuildErrorOverlay();\n return;\n }\n if (activeOverlayTokens().length > 0) {\n if (overlayLabelEl) overlayLabelEl.textContent = latestOverlayLabel();\n return;\n }\n if (overlayTimer) {\n clearTimeout(overlayTimer);\n overlayTimer = null;\n }\n if (!overlayEl) return;\n overlayEl.setAttribute(\"data-show\", \"false\");\n if (overlayHideTimer) clearTimeout(overlayHideTimer);\n overlayHideTimer = setTimeout(function(){\n if (overlayEl && activeOverlayTokens().length === 0 && overlayEl.parentNode) {\n overlayEl.parentNode.removeChild(overlayEl);\n overlayEl = null;\n overlayLabelEl = null;\n overlayDetailEl = null;\n }\n }, 180);\n }\n\n function handleBuildStatus(msg){\n if (msg.status === \"error\") {\n showBuildErrorOverlay(msg);\n return;\n }\n if (msg.status === \"ok\") clearBuildErrorOverlay(msg);\n }\n\n function showBuildErrorOverlay(msg){\n var phase = msg.phase || \"build\";\n var generation = typeof msg.generation === \"number\" ? msg.generation : 0;\n var previous = buildErrorStates[phase];\n if (previous && generation < previous.generation) return;\n buildErrorStates[phase] = {\n phase: msg.phase || \"build\",\n generation: generation,\n message: msg.message || \"Build failed\",\n files: typeof msg.files === \"number\" ? msg.files : 0\n };\n console.error(\"[akan-hmr] build failed\", buildErrorStates[phase]);\n renderBuildErrorOverlay();\n }\n\n function renderBuildErrorOverlay(){\n if (!hasBuildErrors()) return;\n if (overlayTimer) {\n clearTimeout(overlayTimer);\n overlayTimer = null;\n }\n if (overlayHideTimer) {\n clearTimeout(overlayHideTimer);\n overlayHideTimer = null;\n }\n var el = ensureOverlay();\n var phases = buildErrorPhases();\n var latest = latestBuildErrorState();\n el.setAttribute(\"data-status\", \"error\");\n if (overlayLabelEl) overlayLabelEl.textContent = \"Build failed: \" + phases.join(\", \");\n if (overlayDetailEl) overlayDetailEl.textContent = formatBuildStatusDetail(latest, phases.length);\n requestAnimationFrame(function(){ el.setAttribute(\"data-show\", \"true\"); });\n }\n\n function clearBuildErrorOverlay(msg){\n var phase = msg.phase || \"build\";\n var current = buildErrorStates[phase];\n if (!current) return;\n var generation = typeof msg.generation === \"number\" ? msg.generation : 0;\n var recovered = phase === \"backend\" ? generation >= current.generation : generation > current.generation;\n if (!recovered) return;\n delete buildErrorStates[phase];\n if (hasBuildErrors()) {\n renderBuildErrorOverlay();\n return;\n }\n if (overlayHideTimer) clearTimeout(overlayHideTimer);\n var el = ensureOverlay();\n el.setAttribute(\"data-status\", \"ok\");\n if (overlayLabelEl) overlayLabelEl.textContent = \"Build recovered\";\n if (overlayDetailEl) overlayDetailEl.textContent = formatBuildStatusDetail(msg);\n requestAnimationFrame(function(){ el.setAttribute(\"data-show\", \"true\"); });\n overlayHideTimer = setTimeout(function(){\n if (activeOverlayTokens().length > 0) {\n showOverlayNow();\n return;\n }\n if (!overlayEl) return;\n overlayEl.setAttribute(\"data-show\", \"false\");\n overlayHideTimer = setTimeout(function(){\n if (overlayEl && activeOverlayTokens().length === 0 && !hasBuildErrors() && overlayEl.parentNode) {\n overlayEl.parentNode.removeChild(overlayEl);\n overlayEl = null;\n overlayLabelEl = null;\n overlayDetailEl = null;\n }\n }, 180);\n }, 900);\n }\n\n function hasBuildErrors(){\n return Object.keys(buildErrorStates).length > 0;\n }\n\n function buildErrorPhases(){\n return Object.keys(buildErrorStates).sort();\n }\n\n function latestBuildErrorState(){\n var phases = buildErrorPhases();\n var latest = buildErrorStates[phases[0]];\n for (var i = 1; i < phases.length; i++) {\n var next = buildErrorStates[phases[i]];\n if (!latest || next.generation >= latest.generation) latest = next;\n }\n return latest;\n }\n\n function formatBuildStatusDetail(msg, failedPhaseCount){\n var parts = [];\n if (typeof msg.generation === \"number\" && msg.generation > 0) parts.push(\"generation \" + msg.generation);\n if (typeof msg.files === \"number\") parts.push(msg.files + \" file\" + (msg.files === 1 ? \"\" : \"s\"));\n if (failedPhaseCount > 1) parts.push(failedPhaseCount + \" failed phases\");\n var prefix = parts.length > 0 ? parts.join(\" \u00B7 \") : \"\";\n if (!msg.message) return prefix;\n return prefix ? prefix + \"\\n\" + msg.message : msg.message;\n }\n\n function refreshRsc(msg){\n var started = performance.now();\n var overlayToken = beginHmrOverlay(\"Refreshing page...\");\n try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}\n if (!self.__AKAN_RSC_REFRESH__) {\n console.warn(\"[akan-hmr] RSC refresh API unavailable, falling back to full reload\");\n setHmrOverlayLabel(overlayToken, \"Reloading...\");\n setTimeout(function(){ location.reload(); }, 30);\n return;\n }\n Promise.resolve(self.__AKAN_RSC_REFRESH__({ buildId: msg.buildId })).then(function(){\n lastBuildId = msg.buildId;\n endHmrOverlay(overlayToken);\n console.debug && console.debug(\"[akan-hmr] RSC refreshed\", {\n buildId: msg.buildId,\n generation: msg.generation,\n routeIds: msg.routeIds,\n changedFiles: msg.changedFiles && msg.changedFiles.length,\n durationMs: Math.round(performance.now() - started)\n });\n }, function(err){\n console.error(\"[akan-hmr] RSC refresh failed, falling back to full reload\", err);\n setHmrOverlayLabel(overlayToken, \"Update failed, reloading...\");\n setTimeout(function(){ location.reload(); }, 250);\n });\n }\n\n function ensureRefreshRuntime(){\n if (refreshRuntimePromise) return refreshRuntimePromise;\n refreshRuntimePromise = import(\"react-refresh/runtime\").then(function(mod){\n var runtime = mod.default || mod;\n if (!self.__AKAN_REACT_REFRESH_READY__) {\n refreshRuntime = runtime;\n runtime.injectIntoGlobalHook(self);\n self.$RefreshReg$ = function(type, id){ runtime.register(type, id); };\n self.$RefreshSig$ = runtime.createSignatureFunctionForTransform;\n for (var i = 0; i < pendingRefreshRegistrations.length; i++) {\n self.$RefreshReg$(pendingRefreshRegistrations[i][0], pendingRefreshRegistrations[i][1]);\n }\n pendingRefreshRegistrations = [];\n self.__AKAN_REACT_REFRESH_READY__ = true;\n self.__AKAN_REACT_REFRESH_RUNTIME__ = runtime;\n }\n return runtime;\n });\n return refreshRuntimePromise;\n }\n\n function refreshClient(msg){\n refreshQueue = refreshQueue.then(function(){ return doRefreshClient(msg); }, function(){ return doRefreshClient(msg); });\n }\n\n function setHmrPhase(phase){\n self.__AKAN_HMR_PHASE__ = phase;\n }\n\n function doRefreshClient(msg){\n var started = performance.now();\n var metadataAt = started;\n var importAt = started;\n var refreshAt = started;\n var overlayToken = beginHmrOverlay(\"Updating...\");\n var fallbackToRsc = false;\n return ensureRefreshRuntime().then(function(runtime){\n setHmrOverlayLabel(overlayToken, \"Fetching update...\");\n var endpoint = new URL(\"/_akan/hmr/client-refresh\", location.origin);\n endpoint.searchParams.set(\"url\", location.href);\n if (msg.buildId != null) endpoint.searchParams.set(\"buildId\", String(msg.buildId));\n return fetch(endpoint, { credentials: \"same-origin\", cache: \"no-store\" })\n .then(function(res){\n if (!res.ok) throw new Error(\"client-refresh metadata failed \" + res.status + \" \" + res.statusText);\n return res.json();\n })\n .then(function(info){\n metadataAt = performance.now();\n var chunks = Array.isArray(info.chunks) ? info.chunks : [];\n if (chunks.length === 0) throw new Error(\"no client chunks returned\");\n setHmrPhase(\"refresh-import\");\n setHmrOverlayLabel(overlayToken, \"Importing update...\");\n return Promise.all(chunks.map(function(chunk){ return import(chunk); })).then(function(){\n importAt = performance.now();\n setHmrPhase(\"react-refresh\");\n setHmrOverlayLabel(overlayToken, \"Applying update...\");\n try {\n runtime.performReactRefresh();\n } finally {\n setHmrPhase(null);\n }\n refreshAt = performance.now();\n lastBuildId = msg.buildId;\n console.debug && console.debug(\"[akan-hmr] React Fast Refresh applied\", {\n buildId: msg.buildId,\n generation: msg.generation,\n chunks: chunks.length,\n routeIds: info.routeIds || msg.routeIds,\n changedFiles: msg.changedFiles && msg.changedFiles.length,\n metadataMs: Math.round(metadataAt - started),\n importMs: Math.round(importAt - metadataAt),\n refreshMs: Math.round(refreshAt - importAt),\n durationMs: Math.round(refreshAt - started)\n });\n endHmrOverlay(overlayToken);\n }, function(err){\n setHmrPhase(null);\n throw err;\n });\n });\n }).catch(function(err){\n console.warn(\"[akan-hmr] React Fast Refresh failed, falling back to RSC refresh\", err);\n fallbackToRsc = true;\n endHmrOverlay(overlayToken);\n refreshRsc(msg);\n }).finally(function(){\n if (!fallbackToRsc) endHmrOverlay(overlayToken);\n });\n }\n\n function swapCss(href){\n var overlayToken = beginHmrOverlay(\"Updating styles...\");\n var link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = href;\n link.setAttribute(\"data-akan-css\", \"pending\");\n link.addEventListener(\"load\", function(){\n var prev = document.querySelectorAll(\"link[data-akan-css=active]\");\n for (var i = 0; i < prev.length; i++) prev[i].parentNode && prev[i].parentNode.removeChild(prev[i]);\n link.setAttribute(\"data-akan-css\", \"active\");\n endHmrOverlay(overlayToken);\n });\n link.addEventListener(\"error\", function(){\n if (link.parentNode) link.parentNode.removeChild(link);\n endHmrOverlay(overlayToken);\n });\n document.head.appendChild(link);\n }\n\n function selectCssUrl(cssAssets){\n if (!cssAssets || typeof cssAssets !== \"object\") return null;\n var parts = location.pathname.split(\"/\").filter(Boolean);\n for (var i = 0; i < parts.length; i++) {\n var asset = cssAssets[parts[i]];\n if (asset && asset.cssUrl) return asset.cssUrl;\n }\n return cssAssets[\"\"] && cssAssets[\"\"].cssUrl ? cssAssets[\"\"].cssUrl : null;\n }\n\n connect();\n})();";
@@ -1,9 +1,12 @@
1
- import { BuilderRpc, type ClientManifest, RouteClientCache, type RouteSeedIndex } from "../artifact.d.ts";
1
+ import { BuilderRpc, type ClientManifest, type DevBuildStatus, RouteClientCache, type RouteSeedIndex } from "../artifact.d.ts";
2
2
  import type { RscWorker } from "../rscWorkerHost.d.ts";
3
3
  import type { RenderState } from "../types.d.ts";
4
- import { type HmrWsData, HmrWsHub } from "./wsHub.d.ts";
4
+ import { type HmrMessage, type HmrWsData, HmrWsHub } from "./wsHub.d.ts";
5
5
  export declare function isAkanRuntimeMetadataFile(file: string): boolean;
6
6
  export declare function manifestClientEntriesForFiles(files: string[], clientManifest: ClientManifest, workspaceRoot?: string): Set<string>;
7
+ export declare function devBuildStatusToHmrMessage(status: DevBuildStatus, previous?: DevBuildStatus): Extract<HmrMessage, {
8
+ type: "build-status";
9
+ }> | null;
7
10
  export interface DevHmrControllerOptions {
8
11
  renderState: RenderState;
9
12
  rsc: RscWorker;
@@ -1,3 +1,4 @@
1
+ import type { BuildPhase } from "../artifact.d.ts";
1
2
  export interface HmrWsData {
2
3
  kind: "akan-hmr";
3
4
  openedAt: number;
@@ -30,6 +31,16 @@ export type HmrMessage = {
30
31
  cssUrl: string;
31
32
  cssRelPath: string;
32
33
  }>;
34
+ } | {
35
+ type: "ok";
36
+ generation?: number;
37
+ } | {
38
+ type: "build-status";
39
+ status: "building" | "error" | "ok";
40
+ generation: number;
41
+ phase: BuildPhase;
42
+ message?: string;
43
+ files?: number;
33
44
  } | {
34
45
  type: "error";
35
46
  message: string;
@@ -23,6 +23,7 @@ export interface ApiRouteInputs {
23
23
  prefix: string;
24
24
  websocketPrefix: string;
25
25
  routes: HttpRoutes;
26
+ builtinRoutes?: HttpRoutes;
26
27
  routeOptions?: Record<string, SignalRouteOptions>;
27
28
  renderEnvRoutes: HttpRoutes;
28
29
  /** Upgrades the incoming request into an app-signal WebSocket. */
@@ -49,7 +50,7 @@ export declare class ApiRouter {
49
50
  * 2. Prefix every endpoint-generated route with `prefix`.
50
51
  * 3. Merge render-env routes (CSR/SSR) last so they can catch-all `/*`.
51
52
  */
52
- static buildRoutes({ prefix, websocketPrefix, routes, routeOptions, renderEnvRoutes, upgradeAppWs, webProxyRunner, }: ApiRouteInputs): NonNullHttpRoutes;
53
+ static buildRoutes({ prefix, websocketPrefix, routes, builtinRoutes, routeOptions, renderEnvRoutes, upgradeAppWs, webProxyRunner, }: ApiRouteInputs): NonNullHttpRoutes;
53
54
  /**
54
55
  * Builds the `websocket` handler config for `Bun.serve`. Multiplexes two
55
56
  * logical channels on the same upgrade port:
@@ -9,6 +9,7 @@ export * from "./internal.d.ts";
9
9
  export * from "./internalArg.d.ts";
10
10
  export * from "./internalInfo.d.ts";
11
11
  export * from "./middleware.d.ts";
12
+ export * from "./openapi.d.ts";
12
13
  export * from "./serializer.d.ts";
13
14
  export * from "./serverSignal.d.ts";
14
15
  export * from "./signalContext.d.ts";
@@ -0,0 +1 @@
1
+ export * from "./openapi.d.ts";