akanjs 2.3.13 → 2.4.0-rc.1

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.
Files changed (49) hide show
  1. package/client/csrTypes.ts +21 -0
  2. package/client/frameConfig.ts +7 -1
  3. package/package.json +5 -1
  4. package/runtime/logs/serverGet-local-local-2026-07-21-gateway-0001.log +4 -0
  5. package/runtime/logs/serverGet-local-local-2026-07-21-gateway-0002.log +5 -0
  6. package/runtime/logs/serverGet-local-local-2026-07-21-gateway-0003.log +7 -0
  7. package/runtime/logs/serverGet-local-local-2026-07-21-gateway-0004.log +2 -0
  8. package/server/akanApp.ts +128 -26
  9. package/server/akanServer.ts +46 -8
  10. package/server/lifecycle/portInUse.ts +11 -0
  11. package/server/routeElementComposer.tsx +22 -3
  12. package/server/routeTreeBuilder.ts +5 -0
  13. package/server/rscWorker.tsx +16 -0
  14. package/server/rscWorkerCache.ts +2 -0
  15. package/server/ssrFromRscRenderer.tsx +2 -1
  16. package/server/ssrTypes.ts +14 -0
  17. package/server/webRouter.ts +1 -0
  18. package/service/ipcTypes.ts +3 -1
  19. package/signal/guard.ts +3 -3
  20. package/signal/signalContext.ts +5 -0
  21. package/store/baseSt.ts +1 -0
  22. package/types/client/csrTypes.d.ts +19 -0
  23. package/types/server/lifecycle/portInUse.d.ts +5 -0
  24. package/types/server/routeElementComposer.d.ts +7 -3
  25. package/types/server/rscWorkerCache.d.ts +2 -0
  26. package/types/server/ssrTypes.d.ts +14 -0
  27. package/types/service/ipcTypes.d.ts +5 -1
  28. package/types/signal/guard.d.ts +2 -2
  29. package/types/signal/signalContext.d.ts +1 -0
  30. package/types/ui/Load/Units.d.ts +1 -1
  31. package/types/ui/Model/LoadView.d.ts +10 -0
  32. package/types/ui/Model/index.d.ts +1 -0
  33. package/types/ui/Model/index_.d.ts +1 -0
  34. package/types/webkit/index.d.ts +4 -0
  35. package/types/webkit/useCamera.d.ts +19 -0
  36. package/types/webkit/useCodepush.d.ts +16 -0
  37. package/types/webkit/useContact.d.ts +11 -0
  38. package/types/webkit/useGeoLocation.d.ts +8 -0
  39. package/types/webkit/usePurchase.d.ts +19 -0
  40. package/ui/Load/Units.tsx +2 -2
  41. package/ui/Model/LoadView.tsx +11 -0
  42. package/ui/Model/index.ts +2 -0
  43. package/ui/Model/index_.tsx +1 -0
  44. package/webkit/index.ts +4 -0
  45. package/webkit/useCamera.tsx +103 -0
  46. package/webkit/useCodepush.tsx +99 -0
  47. package/webkit/useContact.tsx +52 -0
  48. package/webkit/useGeoLocation.tsx +24 -0
  49. package/webkit/usePurchase.tsx +156 -0
@@ -2,14 +2,14 @@ import type {
2
2
  Head,
3
3
  LayoutErrorRender,
4
4
  LayoutFallbackRoute,
5
- PageConfig,
6
5
  LayoutNotFoundRender,
6
+ PageConfig,
7
7
  PathRoute,
8
8
  ResolvedHead,
9
9
  RouteRender,
10
10
  } from "akanjs/client";
11
- import { getExplicitPageConfigKeys, resolvePageState } from "../client/frameConfig";
12
11
  import { Children, cloneElement, isValidElement, type ReactElement, type ReactNode, Suspense } from "react";
12
+ import { getExplicitPageConfigKeys, resolvePageState } from "../client/frameConfig";
13
13
  import { resolveHeadResult } from "./metadata";
14
14
  import { type AkanRouteSegmentState, createAkanRouteSegments, createAkanSegmentOutletKey } from "./routeState";
15
15
  import { isAkanRscPartialCommitEnabled } from "./rscPartialCommit";
@@ -61,16 +61,19 @@ export class RouteElementComposer {
61
61
  pathRoute,
62
62
  params,
63
63
  searchParams,
64
+ navKey,
64
65
  }: {
65
66
  pathRoute: PathRoute;
66
67
  params: Record<string, string>;
67
68
  searchParams: Record<string, string | string[]>;
69
+ navKey?: string;
68
70
  }): ReactNode {
69
71
  return RouteElementComposer.composeRenders({
70
72
  renders: RouteElementComposer.#getRenderStack(pathRoute),
71
73
  segments: isAkanRscPartialCommitEnabled() ? createAkanRouteSegments(pathRoute) : undefined,
72
74
  params,
73
75
  searchParams,
76
+ navKey,
74
77
  });
75
78
  }
76
79
 
@@ -79,11 +82,13 @@ export class RouteElementComposer {
79
82
  params,
80
83
  searchParams,
81
84
  patchStartIndex,
85
+ navKey,
82
86
  }: {
83
87
  pathRoute: PathRoute;
84
88
  params: Record<string, string>;
85
89
  searchParams: Record<string, string | string[]>;
86
90
  patchStartIndex: number;
91
+ navKey?: string;
87
92
  }): ReactNode | null {
88
93
  const renders = RouteElementComposer.#getRenderStack(pathRoute);
89
94
  if (!Number.isInteger(patchStartIndex) || patchStartIndex < 0 || patchStartIndex >= renders.length) return null;
@@ -91,9 +96,18 @@ export class RouteElementComposer {
91
96
  renders: renders.slice(patchStartIndex),
92
97
  params,
93
98
  searchParams,
99
+ navKey,
94
100
  });
95
101
  }
96
102
 
103
+ static async resolveSuffixLoadings(pathRoute: PathRoute, patchStartIndex: number): Promise<void> {
104
+ const renders = RouteElementComposer.#getRenderStack(pathRoute).slice(Math.max(patchStartIndex, 0));
105
+
106
+ await Promise.all(
107
+ renders.map((routeRender) => Promise.resolve(routeRender?.resolveLoading?.()).catch(() => undefined)),
108
+ );
109
+ }
110
+
97
111
  static async resolveHead({
98
112
  pathRoute,
99
113
  params,
@@ -170,18 +184,23 @@ export class RouteElementComposer {
170
184
  segments,
171
185
  params,
172
186
  searchParams,
187
+ navKey,
173
188
  }: {
174
189
  renders: RouteRender[];
175
190
  segments?: AkanRouteSegmentState[];
176
191
  params: Record<string, string>;
177
192
  searchParams: Record<string, string | string[]>;
193
+ navKey?: string;
178
194
  }): ReactNode {
179
195
  let element: ReactNode = null;
180
196
  for (let i = renders.length - 1; i >= 0; i--) {
181
197
  const routeRender = renders[i];
182
198
  if (!routeRender) continue;
199
+ const loadingFallback = RouteElementComposer.#composeLoadingFallback(renders.slice(i), params);
200
+ const suspenseKey =
201
+ navKey && loadingFallback != null && i === renders.length - 1 ? `akan-loading:${navKey}` : undefined;
183
202
  element = (
184
- <Suspense fallback={RouteElementComposer.#composeLoadingFallback(renders.slice(i), params)}>
203
+ <Suspense key={suspenseKey} fallback={loadingFallback}>
185
204
  <RouteElementComposer.AsyncRender routeRender={routeRender} params={params} searchParams={searchParams}>
186
205
  {element}
187
206
  </RouteElementComposer.AsyncRender>
@@ -34,6 +34,7 @@ export const defaultPageState: PageState = {
34
34
  bottomInset: 0,
35
35
  gesture: true,
36
36
  cache: false,
37
+ ssr: "stream",
37
38
  topSafeAreaColor: "transparent",
38
39
  bottomSafeAreaColor: "transparent",
39
40
  };
@@ -341,6 +342,10 @@ export class RouteTreeBuilder {
341
342
  const loadModule = RouteTreeBuilder.#makeLazyModule(key, kind, loader);
342
343
  const routeRender: RouteRender = {
343
344
  isAsync: true,
345
+ resolveLoading: async () => {
346
+ const mod = await loadModule();
347
+ routeRender.Loading = mod.Loading as never;
348
+ },
344
349
  render: async (props: LayoutProps | PageProps) => {
345
350
  const mod = await loadModule();
346
351
  routeRender.Loading = mod.Loading as never;
@@ -2,6 +2,7 @@ import type {
2
2
  AkanNotFoundError,
3
3
  AkanRedirectError,
4
4
  LayoutFallbackRoute,
5
+ PageState,
5
6
  PathRoute,
6
7
  RedirectStatus,
7
8
  ResolvedHead,
@@ -9,6 +10,7 @@ import type {
9
10
  import { type AkanI18nConfig, DEFAULT_AKAN_I18N, getBasePathFromPathname, Logger } from "akanjs/common";
10
11
  import {
11
12
  getRequestDynamicUsage,
13
+ getRequestFrameState,
12
14
  getRequestPolicy,
13
15
  getRequestTheme,
14
16
  requestStorage,
@@ -214,6 +216,11 @@ export class RscRenderer {
214
216
  }
215
217
  this.#send = process.send.bind(process) as (message: unknown) => void;
216
218
  process.on("message", (msg: InMsg) => this.#handleMessage(msg));
219
+
220
+ process.on("disconnect", () => {
221
+ this.#logger.warn("parent IPC channel closed; exiting rsc worker");
222
+ process.exit(0);
223
+ });
217
224
  this.#logger.verbose(`constructed (pid=${process.pid})`);
218
225
  }
219
226
 
@@ -525,6 +532,7 @@ export class RscRenderer {
525
532
  patchStartSegment: undefined,
526
533
  patchHeadSafe: undefined,
527
534
  patchHeadSnapshot: undefined,
535
+ ssrBlocking: cached.ssrBlocking,
528
536
  },
529
537
  send: (message) => this.#send(message),
530
538
  isCancelled: () => this.#cancelledRenderRequests.has(requestId),
@@ -553,9 +561,11 @@ export class RscRenderer {
553
561
  else element = await this.#renderNotFound(urlObj);
554
562
  const traceCacheKey =
555
563
  effectivePatchDecision.status === "patch" ? (patchCacheEntry?.key ?? cacheEntry?.key) : cacheEntry?.key;
564
+ const ssrBlocking = getRequestFrameState<PageState>()?.ssr === "block";
556
565
  const trace: RscTraceMetadata = {
557
566
  ...createTraceBase(effectivePatchDecision, traceCacheKey),
558
567
  cache: cacheEntry ? "miss" : "bypass",
568
+ ssrBlocking,
559
569
  };
560
570
  this.#logger.verbose(`render[${requestId}] starting Flight stream`);
561
571
  const result = await this.#renderFlightElement(element, msg.clientManifest ?? this.#clientManifest, {
@@ -597,6 +607,7 @@ export class RscRenderer {
597
607
  routeId,
598
608
  tags: cacheState.tags,
599
609
  theme: getRequestTheme(),
610
+ ssrBlocking,
600
611
  cacheState,
601
612
  patch: createCachedRscPatchMetadata({
602
613
  targetRouterState,
@@ -616,6 +627,7 @@ export class RscRenderer {
616
627
  routeId,
617
628
  tags: cacheState.tags,
618
629
  theme: getRequestTheme(),
630
+ ssrBlocking,
619
631
  cacheState,
620
632
  },
621
633
  storeTtl,
@@ -1243,6 +1255,7 @@ export class RscRenderer {
1243
1255
  pathRoute,
1244
1256
  params: match.params,
1245
1257
  searchParams,
1258
+ navKey: url.pathname + url.search,
1246
1259
  });
1247
1260
  return (
1248
1261
  <html
@@ -1283,11 +1296,14 @@ export class RscRenderer {
1283
1296
  basePath: this.#getBasePath(url),
1284
1297
  });
1285
1298
  setRequestFrameState(pathRoute.pageState);
1299
+
1300
+ await RouteElementComposer.resolveSuffixLoadings(pathRoute, patchStartIndex);
1286
1301
  return RouteElementComposer.composeSuffix({
1287
1302
  pathRoute,
1288
1303
  params: match.params,
1289
1304
  searchParams,
1290
1305
  patchStartIndex,
1306
+ navKey: url.pathname + url.search,
1291
1307
  });
1292
1308
  }
1293
1309
 
@@ -16,6 +16,8 @@ export interface CachedRscResult {
16
16
  routeId?: string;
17
17
  tags?: string[];
18
18
  theme?: string;
19
+ /** Whether the matched route opted into blocking SSR (`pageConfig.ssr: "block"`). */
20
+ ssrBlocking?: boolean;
19
21
  cacheState: RouteCacheRenderState;
20
22
  patch?: CachedRscPatchMetadata;
21
23
  }
@@ -594,12 +594,13 @@ export class SsrFromRscRenderer {
594
594
  ? `${SsrFromRscRenderer.#clientBootstrap}\n${input.extraBootstrapInline}`
595
595
  : SsrFromRscRenderer.#clientBootstrap;
596
596
 
597
+ const waitForAllReady = input.waitForAllReady || process.env.AKAN_SSR_WAIT_FOR_ALL_READY === "1";
597
598
  const renderHtml = async () => {
598
599
  const root = await thenable;
599
600
  const stream = await renderToReadableStream(root, {
600
601
  bootstrapScriptContent: bootstrap,
601
602
  });
602
- await stream.allReady;
603
+ if (waitForAllReady) await stream.allReady;
603
604
  return stream;
604
605
  };
605
606
  const requestContext = input.requestStore ?? input.request;
@@ -42,6 +42,13 @@ export interface RscTraceMetadata {
42
42
  patchHeadSafe?: boolean;
43
43
  patchHeadSnapshot?: string;
44
44
  routeState?: string;
45
+ /**
46
+ * Resolved from the matched route's `pageConfig.ssr === "block"`. Rides the
47
+ * trace so the host (which never resolves pageConfig itself) can decide
48
+ * whether the full-document SSR pass buffers until every Suspense boundary
49
+ * resolves. Absent/`false` means stream the shell first.
50
+ */
51
+ ssrBlocking?: boolean;
45
52
  }
46
53
 
47
54
  export interface SsrFromRscInput {
@@ -73,4 +80,11 @@ export interface SsrFromRscInput {
73
80
  injectThemeInitScript?: boolean;
74
81
  lateControl?: Promise<SsrLateRedirect | null>;
75
82
  onCancel?: (reason?: unknown) => void;
83
+ /**
84
+ * When true, buffer the whole document until every Suspense boundary resolves
85
+ * (`stream.allReady`) before emitting a byte, matching `pageConfig.ssr:
86
+ * "block"`. Defaults to shell-first streaming so `Loading` fallbacks surface.
87
+ * The `AKAN_SSR_WAIT_FOR_ALL_READY=1` env var forces blocking globally.
88
+ */
89
+ waitForAllReady?: boolean;
76
90
  }
@@ -578,6 +578,7 @@ export class WebRouter {
578
578
  importmap: this.#artifact.vendorMap,
579
579
  theme: themeCookieExists ? undefined : (rscResult.theme ?? "system"),
580
580
  lateControl: rscResult.lateControl,
581
+ waitForAllReady: rscResult.trace?.ssrBlocking ?? false,
581
582
  onCancel: (reason: unknown) => {
582
583
  rscResult.cancel(reason);
583
584
  },
@@ -1,6 +1,6 @@
1
1
  export type AkanChildRole = "all" | "federation" | "batch";
2
2
 
3
- export type AkanChildStatus = "starting" | "ready" | "healthy" | "draining" | "unhealthy" | "exited";
3
+ export type AkanChildStatus = "starting" | "ready" | "healthy" | "draining" | "unhealthy" | "exited" | "crashed";
4
4
 
5
5
  export type AkanUpstream = { type: "unix"; socketPath: string } | { type: "tcp"; host: string; port: number };
6
6
 
@@ -110,6 +110,8 @@ export type AkanIpcMessage =
110
110
  replicaIdx: number;
111
111
  role: AkanChildRole;
112
112
  upstream?: AkanUpstream;
113
+ /** Actual websocket upstream the child bound; may differ from the preferred port when it was in use. */
114
+ wsUpstream?: Extract<AkanUpstream, { type: "tcp" }>;
113
115
  healthPath?: string;
114
116
  }
115
117
  | { type: "backend-ready"; pid: number }
package/signal/guard.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { Cls } from "akanjs/base";
1
+ import type { Cls, PromiseOrObject } from "akanjs/base";
2
2
  import type { SignalContext } from "./signalContext";
3
3
 
4
4
  export interface Guard {
5
- canPass(context: SignalContext): boolean;
5
+ canPass(context: SignalContext): PromiseOrObject<boolean>;
6
6
  }
7
7
 
8
8
  export type GuardCls<Name extends string = string> = Cls<Guard, { readonly name: Name }>;
@@ -11,7 +11,7 @@ export type GuardCls<Name extends string = string> = Cls<Guard, { readonly name:
11
11
  export const guard = <T extends string>(name: T): GuardCls<T> => {
12
12
  return class Guard {
13
13
  static name = name;
14
- canPass(context: SignalContext): boolean {
14
+ canPass(context: SignalContext): PromiseOrObject<boolean> {
15
15
  return true;
16
16
  }
17
17
  };
@@ -99,6 +99,11 @@ export class SignalContext<
99
99
  }
100
100
  return instance as T;
101
101
  }
102
+ getService<T>(refName: string): T {
103
+ const service = this.#live.service.get(refName);
104
+ if (!service) throw new Exception.Error(`Service "${refName}" not found in live registry`);
105
+ return service as T;
106
+ }
102
107
  async init() {
103
108
  if (this.trace) {
104
109
  const start = performance.now();
package/store/baseSt.ts CHANGED
@@ -44,6 +44,7 @@ export class BaseStore extends store("base" as const, () => ({
44
44
  bottomInset: 0,
45
45
  gesture: true,
46
46
  cache: false,
47
+ ssr: "stream",
47
48
  topSafeAreaColor: "var(--color-base-100, Canvas)",
48
49
  bottomSafeAreaColor: "var(--color-base-100, Canvas)",
49
50
  } as PageState,
@@ -10,6 +10,19 @@ export type PageSafeAreaConfig = boolean | "top" | "bottom" | {
10
10
  bottom?: boolean;
11
11
  android?: "auto" | "edge-to-edge" | "none";
12
12
  };
13
+ /**
14
+ * Server-render strategy for the initial full-document SSR pass.
15
+ * - `"stream"` (default): flush the shell — including any `Loading` Suspense
16
+ * fallback — as soon as it is ready, then stream the resolved page. Redirects
17
+ * decided in the shell still become real HTTP redirects; redirects thrown
18
+ * inside suspended content degrade to soft (client) redirects.
19
+ * - `"block"`: buffer the whole document until every Suspense boundary resolves
20
+ * before sending a byte. The `Loading` fallback never reaches the browser, but
21
+ * a non-redirect error thrown in slow content can still yield a clean error
22
+ * page. Use only for routes that need that guarantee and do not care about SEO
23
+ * or first-paint of the fallback.
24
+ */
25
+ export type SsrRenderMode = "stream" | "block";
13
26
  /** Per-page CSR configuration for transition, safe-area, and gesture behavior. */
14
27
  export interface PageConfig {
15
28
  transition?: TransitionType;
@@ -20,6 +33,8 @@ export interface PageConfig {
20
33
  bottomInset?: number | boolean;
21
34
  gesture?: boolean;
22
35
  cache?: boolean;
36
+ /** Initial full-document SSR strategy. Defaults to `"stream"`. */
37
+ ssr?: SsrRenderMode;
23
38
  /**
24
39
  * Opt in to guarded RSC page suffix commits when the page does not require
25
40
  * head/metadata updates and the retained route chain head is invariant for
@@ -37,6 +52,7 @@ export interface CsrState {
37
52
  bottomInset: number;
38
53
  gesture: boolean;
39
54
  cache: boolean;
55
+ ssr: SsrRenderMode;
40
56
  topSafeAreaColor?: string;
41
57
  bottomSafeAreaColor?: string;
42
58
  }
@@ -98,6 +114,9 @@ export interface RouteRender {
98
114
  render: LayoutRender | PageRender;
99
115
  isAsync?: boolean;
100
116
  Loading?: LayoutLoadingRender | PageLoadingRender;
117
+ /** Loads the module and populates `Loading` without running `render`/`resolveHead`.
118
+ * Used by the patch (suffix) compose path, which never calls `resolveHead`. */
119
+ resolveLoading?: () => void | Promise<void>;
101
120
  NotFound?: LayoutNotFoundRender;
102
121
  Error?: LayoutErrorRender;
103
122
  resolveNotFound?: () => PromiseOrObject<LayoutNotFoundRender | undefined>;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Detects "address already in use" listen failures across the shapes Bun throws them in
3
+ * (`code: "EADDRINUSE"` on newer versions, message-only on older ones).
4
+ */
5
+ export declare const isPortInUseError: (error: unknown) => boolean;
@@ -11,17 +11,20 @@ export declare class RouteElementComposer {
11
11
  route: PathRoute | LayoutFallbackRoute;
12
12
  basePath?: string | null;
13
13
  }): Promise<import("akanjs/client").PageState>;
14
- static compose({ pathRoute, params, searchParams, }: {
14
+ static compose({ pathRoute, params, searchParams, navKey, }: {
15
15
  pathRoute: PathRoute;
16
16
  params: Record<string, string>;
17
17
  searchParams: Record<string, string | string[]>;
18
+ navKey?: string;
18
19
  }): ReactNode;
19
- static composeSuffix({ pathRoute, params, searchParams, patchStartIndex, }: {
20
+ static composeSuffix({ pathRoute, params, searchParams, patchStartIndex, navKey, }: {
20
21
  pathRoute: PathRoute;
21
22
  params: Record<string, string>;
22
23
  searchParams: Record<string, string | string[]>;
23
24
  patchStartIndex: number;
25
+ navKey?: string;
24
26
  }): ReactNode | null;
27
+ static resolveSuffixLoadings(pathRoute: PathRoute, patchStartIndex: number): Promise<void>;
25
28
  static resolveHead({ pathRoute, params, searchParams, }: {
26
29
  pathRoute: PathRoute;
27
30
  params: Record<string, string>;
@@ -41,11 +44,12 @@ export declare class RouteElementComposer {
41
44
  error?: unknown;
42
45
  digest?: string;
43
46
  }): Promise<ReactNode | null>;
44
- static composeRenders({ renders, segments, params, searchParams, }: {
47
+ static composeRenders({ renders, segments, params, searchParams, navKey, }: {
45
48
  renders: RouteRender[];
46
49
  segments?: AkanRouteSegmentState[];
47
50
  params: Record<string, string>;
48
51
  searchParams: Record<string, string | string[]>;
52
+ navKey?: string;
49
53
  }): ReactNode;
50
54
  static renderAsync({ routeRender, children, params, searchParams, }: {
51
55
  routeRender: RouteRender;
@@ -8,6 +8,8 @@ export interface CachedRscResult {
8
8
  routeId?: string;
9
9
  tags?: string[];
10
10
  theme?: string;
11
+ /** Whether the matched route opted into blocking SSR (`pageConfig.ssr: "block"`). */
12
+ ssrBlocking?: boolean;
11
13
  cacheState: RouteCacheRenderState;
12
14
  patch?: CachedRscPatchMetadata;
13
15
  }
@@ -40,6 +40,13 @@ export interface RscTraceMetadata {
40
40
  patchHeadSafe?: boolean;
41
41
  patchHeadSnapshot?: string;
42
42
  routeState?: string;
43
+ /**
44
+ * Resolved from the matched route's `pageConfig.ssr === "block"`. Rides the
45
+ * trace so the host (which never resolves pageConfig itself) can decide
46
+ * whether the full-document SSR pass buffers until every Suspense boundary
47
+ * resolves. Absent/`false` means stream the shell first.
48
+ */
49
+ ssrBlocking?: boolean;
43
50
  }
44
51
  export interface SsrFromRscInput {
45
52
  request?: Request;
@@ -70,4 +77,11 @@ export interface SsrFromRscInput {
70
77
  injectThemeInitScript?: boolean;
71
78
  lateControl?: Promise<SsrLateRedirect | null>;
72
79
  onCancel?: (reason?: unknown) => void;
80
+ /**
81
+ * When true, buffer the whole document until every Suspense boundary resolves
82
+ * (`stream.allReady`) before emitting a byte, matching `pageConfig.ssr:
83
+ * "block"`. Defaults to shell-first streaming so `Loading` fallbacks surface.
84
+ * The `AKAN_SSR_WAIT_FOR_ALL_READY=1` env var forces blocking globally.
85
+ */
86
+ waitForAllReady?: boolean;
73
87
  }
@@ -1,5 +1,5 @@
1
1
  export type AkanChildRole = "all" | "federation" | "batch";
2
- export type AkanChildStatus = "starting" | "ready" | "healthy" | "draining" | "unhealthy" | "exited";
2
+ export type AkanChildStatus = "starting" | "ready" | "healthy" | "draining" | "unhealthy" | "exited" | "crashed";
3
3
  export type AkanUpstream = {
4
4
  type: "unix";
5
5
  socketPath: string;
@@ -122,6 +122,10 @@ export type AkanIpcMessage = {
122
122
  replicaIdx: number;
123
123
  role: AkanChildRole;
124
124
  upstream?: AkanUpstream;
125
+ /** Actual websocket upstream the child bound; may differ from the preferred port when it was in use. */
126
+ wsUpstream?: Extract<AkanUpstream, {
127
+ type: "tcp";
128
+ }>;
125
129
  healthPath?: string;
126
130
  } | {
127
131
  type: "backend-ready";
@@ -1,7 +1,7 @@
1
- import type { Cls } from "akanjs/base";
1
+ import type { Cls, PromiseOrObject } from "akanjs/base";
2
2
  import type { SignalContext } from "./signalContext.d.ts";
3
3
  export interface Guard {
4
- canPass(context: SignalContext): boolean;
4
+ canPass(context: SignalContext): PromiseOrObject<boolean>;
5
5
  }
6
6
  export type GuardCls<Name extends string = string> = Cls<Guard, {
7
7
  readonly name: Name;
@@ -30,6 +30,7 @@ export declare class SignalContext<Ctx extends HttpExecutionContext | WebSocketE
30
30
  middleware: Map<string, MiddlewareCls>;
31
31
  });
32
32
  getAdaptor<T extends Adaptor>(adaptorCls: AdaptorCls<T>): T;
33
+ getService<T>(refName: string): T;
33
34
  init(): Promise<this>;
34
35
  exec(): Promise<Response | undefined>;
35
36
  static try(endpoint: Adaptor, endpointInfo: EndpointInfo, key: string, fn: () => Promise<Response | undefined>): Promise<Response | undefined>;
@@ -13,7 +13,7 @@ interface DefaultProps<L extends {
13
13
  loading?: ReactNode;
14
14
  filter?: (item: L, idx: number) => boolean;
15
15
  sort?: (a: L, b: L) => number;
16
- renderEmpty?: null | (() => ReactNode);
16
+ renderEmpty?: null | (() => ReactNode) | false;
17
17
  renderItem?: (item: L, idx: number) => ReactNode;
18
18
  renderList?: (list: DataList<L>) => ReactNode;
19
19
  reverse?: boolean;
@@ -0,0 +1,10 @@
1
+ import type { ClientView } from "akanjs/fetch";
2
+ interface LoadViewProps<T extends string, Model extends {
3
+ id: string;
4
+ }> {
5
+ view: ClientView<T, Model>;
6
+ }
7
+ export default function LoadView<T extends string, Light extends {
8
+ id: string;
9
+ }>({ view }: LoadViewProps<T, Light>): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -8,6 +8,7 @@ export declare const Model: {
8
8
  Remove: typeof import("./Remove.d.ts").default;
9
9
  RemoveWrapper: typeof import("./RemoveWrapper.d.ts").default;
10
10
  LoadInit: typeof import("./LoadInit.d.ts").default;
11
+ LoadView: typeof import("./LoadView.d.ts").default;
11
12
  ViewWrapper: typeof import("./ViewWrapper.d.ts").default;
12
13
  ViewEditModal: typeof import("./ViewEditModal.d.ts").default;
13
14
  Edit: typeof import("./Edit.d.ts").default;
@@ -7,6 +7,7 @@ export declare const NewWrapper: typeof import("./NewWrapper.d.ts").default;
7
7
  export declare const EditWrapper: typeof import("./EditWrapper.d.ts").default;
8
8
  export declare const RemoveWrapper: typeof import("./RemoveWrapper.d.ts").default;
9
9
  export declare const LoadInit: typeof import("./LoadInit.d.ts").default;
10
+ export declare const LoadView: typeof import("./LoadView.d.ts").default;
10
11
  export declare const ViewWrapper: typeof import("./ViewWrapper.d.ts").default;
11
12
  export declare const ViewEditModal: typeof import("./ViewEditModal.d.ts").default;
12
13
  export declare const Edit: typeof import("./Edit.d.ts").default;
@@ -3,9 +3,13 @@ export { createRobotPage } from "./createRobotPage.d.ts";
3
3
  export { createSitemapPage } from "./createSitemapPage.d.ts";
4
4
  export { lazy } from "./lazy.d.ts";
5
5
  export type * from "./types.d.ts";
6
+ export { useCamera } from "./useCamera.d.ts";
7
+ export { useCodepush } from "./useCodepush.d.ts";
8
+ export { useContact } from "./useContact.d.ts";
6
9
  export { useCsrValues } from "./useCsrValues.d.ts";
7
10
  export { useDebounce } from "./useDebounce.d.ts";
8
11
  export { useFetch, useFetchFn } from "./useFetch.d.ts";
12
+ export { useGeoLocation } from "./useGeoLocation.d.ts";
9
13
  export { useHistory } from "./useHistory.d.ts";
10
14
  export { useInterval } from "./useInterval.d.ts";
11
15
  export { useLocation } from "./useLocation.d.ts";
@@ -0,0 +1,19 @@
1
+ import { type CapacitorPermissionState } from "akanjs/client/capacitor";
2
+ type PermissionStatus = {
3
+ camera: CapacitorPermissionState;
4
+ photos: CapacitorPermissionState;
5
+ };
6
+ /** Capacitor camera/photos hook with permission checks and app-settings fallback. */
7
+ export declare const useCamera: () => {
8
+ permissions: PermissionStatus;
9
+ getPhoto: (src?: "prompt" | "camera" | "photos") => Promise<{
10
+ [key: string]: unknown;
11
+ dataUrl?: string;
12
+ } | undefined>;
13
+ pickImage: () => Promise<{
14
+ [key: string]: unknown;
15
+ photos: unknown[];
16
+ }>;
17
+ checkPermission: (type: "photos" | "camera" | "all") => Promise<void>;
18
+ };
19
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { ProtoAppInfo, ProtoFile } from "akanjs/constant";
2
+ export declare const useCodepush: ({ serverUrl }: {
3
+ serverUrl: string;
4
+ }) => {
5
+ update: boolean;
6
+ version: string;
7
+ initialize: () => Promise<void>;
8
+ checkNewRelease: () => Promise<{
9
+ release: ProtoAppInfo & {
10
+ appBuild: string;
11
+ };
12
+ bundleFile: ProtoFile;
13
+ } | undefined>;
14
+ codepush: () => Promise<void>;
15
+ statManager: () => Promise<void>;
16
+ };
@@ -0,0 +1,11 @@
1
+ import { type CapacitorPermissionState } from "akanjs/client/capacitor";
2
+ type PermissionStatus = {
3
+ contacts: CapacitorPermissionState;
4
+ };
5
+ /** Capacitor contacts hook with permission checks and contact loading helpers. */
6
+ export declare const useContact: () => {
7
+ permissions: PermissionStatus;
8
+ getContacts: () => Promise<unknown[]>;
9
+ checkPermission: () => Promise<void>;
10
+ };
11
+ export {};
@@ -0,0 +1,8 @@
1
+ /** Capacitor geolocation hook with permission checks and current position lookup. */
2
+ export declare const useGeoLocation: () => {
3
+ checkPermission: () => Promise<{
4
+ geolocation: string;
5
+ coarseLocation: string;
6
+ }>;
7
+ getPosition: () => Promise<unknown>;
8
+ };
@@ -0,0 +1,19 @@
1
+ import "cordova-plugin-purchase/www/store";
2
+ export type PlatformType = "android" | "ios" | "all";
3
+ export interface ProductType {
4
+ id: string;
5
+ type: keyof typeof CdvPurchase.ProductType;
6
+ }
7
+ export type CdvProductType = CdvPurchase.ProductType;
8
+ export declare const usePurchase: ({ platform, productInfo, url, onPay, onSubscribe, }: {
9
+ platform: PlatformType;
10
+ productInfo: ProductType[];
11
+ url: string;
12
+ onPay?: (transaction: CdvPurchase.Transaction) => void | Promise<void>;
13
+ onSubscribe?: (transaction: CdvPurchase.Transaction) => void | Promise<void>;
14
+ }) => {
15
+ isLoading: boolean;
16
+ products: CdvPurchase.Product[];
17
+ purchaseProduct: (product: CdvPurchase.Product) => Promise<void>;
18
+ restorePurchases: () => Promise<void>;
19
+ };
package/ui/Load/Units.tsx CHANGED
@@ -23,7 +23,7 @@ interface DefaultProps<L extends { id: string }> {
23
23
  loading?: ReactNode;
24
24
  filter?: (item: L, idx: number) => boolean;
25
25
  sort?: (a: L, b: L) => number;
26
- renderEmpty?: null | (() => ReactNode);
26
+ renderEmpty?: null | (() => ReactNode) | false;
27
27
  renderItem?: (item: L, idx: number) => ReactNode;
28
28
  renderList?: (list: DataList<L>) => ReactNode;
29
29
  reverse?: boolean;
@@ -172,7 +172,7 @@ function Render<RefName extends string, Light extends { id: string }>({
172
172
  if (renderList)
173
173
  return (
174
174
  <>
175
- {modelDataList.length ? (
175
+ {modelDataList.length || renderEmpty === false ? (
176
176
  <ContainerWrapper
177
177
  containerRef={containerRef}
178
178
  className={clsx(className, {
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import type { ClientView } from "akanjs/fetch";
3
+
4
+ import { Load } from "../Load";
5
+
6
+ interface LoadViewProps<T extends string, Model extends { id: string }> {
7
+ view: ClientView<T, Model>;
8
+ }
9
+ export default function LoadView<T extends string, Light extends { id: string }>({ view }: LoadViewProps<T, Light>) {
10
+ return <Load.View view={view} renderView={() => null} loading={null} />;
11
+ }