akanjs 3.0.0-beta.17 → 3.0.0-beta.18

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 (91) hide show
  1. package/base/base.ts +1 -1
  2. package/client/csrTypes.ts +1 -31
  3. package/client/frameConfig.ts +2 -2
  4. package/client/route/RootLayoutDefinition.ts +0 -6
  5. package/client/route/RouteDefinition.ts +1 -13
  6. package/common/codeAgentProfile.ts +36 -0
  7. package/common/index.ts +3 -0
  8. package/common/routeConvention.ts +0 -3
  9. package/constant/constantRegistry.ts +8 -11
  10. package/document/dataLoader.ts +48 -15
  11. package/document/loaderInfo.ts +28 -7
  12. package/fetch/client/fetchClient.ts +14 -12
  13. package/fetch/client/httpClient.ts +1 -1
  14. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  15. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  16. package/package.json +1 -1
  17. package/server/akanServer.ts +3 -1
  18. package/server/console.ts +9 -9
  19. package/server/decorators.ts +3 -64
  20. package/server/devtools/depsSerializer.ts +1 -4
  21. package/server/di/diLifecycle.ts +1 -3
  22. package/server/head.tsx +60 -0
  23. package/server/imageOptimizer.ts +3 -1
  24. package/server/resolver/database.resolver.ts +29 -26
  25. package/server/routeElementComposer.tsx +3 -3
  26. package/server/routeTreeBuilder.ts +1 -24
  27. package/server/rscWorker.tsx +10 -21
  28. package/server/webRouter.ts +12 -8
  29. package/service/injectInfo.ts +86 -72
  30. package/service/predefinedAdaptor/cache.adaptor.ts +42 -8
  31. package/service/predefinedAdaptor/storage.adaptor.ts +6 -6
  32. package/signal/endpointCache.ts +75 -0
  33. package/signal/guard.ts +4 -3
  34. package/signal/middleware.ts +0 -77
  35. package/signal/signalContext.ts +4 -14
  36. package/signal/types.ts +4 -5
  37. package/types/client/csrTypes.d.ts +1 -31
  38. package/types/client/route/RootLayoutDefinition.d.ts +0 -1
  39. package/types/client/route/RouteDefinition.d.ts +1 -3
  40. package/types/common/codeAgentProfile.d.ts +37 -0
  41. package/types/common/index.d.ts +1 -1
  42. package/types/constant/constantRegistry.d.ts +2 -2
  43. package/types/document/dataLoader.d.ts +6 -1
  44. package/types/document/loaderInfo.d.ts +9 -4
  45. package/types/server/console.d.ts +1 -1
  46. package/types/server/decorators.d.ts +1 -8
  47. package/types/server/head.d.ts +10 -0
  48. package/types/server/routeElementComposer.d.ts +1 -1
  49. package/types/service/injectInfo.d.ts +17 -14
  50. package/types/service/predefinedAdaptor/cache.adaptor.d.ts +1 -0
  51. package/types/service/predefinedAdaptor/storage.adaptor.d.ts +1 -2
  52. package/types/signal/endpointCache.d.ts +19 -0
  53. package/types/signal/guard.d.ts +4 -3
  54. package/types/signal/middleware.d.ts +0 -20
  55. package/types/signal/signalContext.d.ts +1 -6
  56. package/types/signal/types.d.ts +4 -5
  57. package/types/ui/Agent/Menu.d.ts +2 -2
  58. package/types/ui/Button.d.ts +1 -1
  59. package/types/ui/CsrImage.d.ts +2 -1
  60. package/types/ui/Data/Item.d.ts +1 -1
  61. package/types/ui/Link/CsrLink.d.ts +1 -1
  62. package/types/ui/Link/types.d.ts +2 -0
  63. package/types/ui/System/CSR.d.ts +2 -2
  64. package/types/ui/System/Client.d.ts +2 -3
  65. package/types/ui/System/Common.d.ts +0 -2
  66. package/types/ui/System/SSR.d.ts +2 -2
  67. package/types/ui/recipe/inputRecipe.d.ts +1 -1
  68. package/ui/Agent/Menu.tsx +2 -2
  69. package/ui/Button.tsx +1 -1
  70. package/ui/Constant/Doc.tsx +121 -77
  71. package/ui/Constant/Graph.tsx +11 -3
  72. package/ui/CsrImage.tsx +3 -2
  73. package/ui/Data/Item.tsx +1 -1
  74. package/ui/Field/Choice.tsx +1 -1
  75. package/ui/Field/Collection.tsx +4 -3
  76. package/ui/Image.tsx +1 -0
  77. package/ui/Link/CsrLink.tsx +5 -1
  78. package/ui/Link/types.ts +2 -0
  79. package/ui/System/CSR.tsx +1 -9
  80. package/ui/System/Client.tsx +2 -4
  81. package/ui/System/Common.tsx +0 -2
  82. package/ui/System/SSR.tsx +1 -9
  83. package/ui/recipe/inputRecipe.ts +1 -1
  84. package/webkit/bootCsr.tsx +0 -12
  85. package/webkit/useCamera.tsx +4 -3
  86. package/webkit/useCsrValues.ts +3 -3
  87. package/webkit/useFetch.ts +6 -7
  88. package/server/metadata.tsx +0 -198
  89. package/types/server/metadata.d.ts +0 -22
  90. package/types/ui/System/Gtag.d.ts +0 -4
  91. package/ui/System/Gtag.tsx +0 -69
package/base/base.ts CHANGED
@@ -166,7 +166,7 @@ export class DataList<Light extends { id: string }> {
166
166
  return this.values.flatMap(fn);
167
167
  }
168
168
  sort(fn: (a: Light, b: Light) => number) {
169
- return new DataList(this.values.sort(fn));
169
+ return new DataList([...this.values].sort(fn));
170
170
  }
171
171
  filter(fn: (value: Light, idx: number) => boolean) {
172
172
  return new DataList(this.values.filter(fn));
@@ -46,7 +46,7 @@ export interface PageConfig {
46
46
  ssr?: SsrRenderMode;
47
47
  /**
48
48
  * Opt in to guarded RSC page suffix commits when the page does not require
49
- * head/metadata updates and the retained route chain head is invariant for
49
+ * head updates and the retained route chain head is invariant for
50
50
  * sibling navigations under the same layout.
51
51
  */
52
52
  rscPatchHeadSafe?: boolean;
@@ -109,7 +109,6 @@ export interface AkanHeadSnapshotV1 {
109
109
  }
110
110
  export interface ResolvedHead {
111
111
  node: Head | null | undefined;
112
- hasExplicitLanguageAlternates: boolean;
113
112
  headSnapshot?: AkanHeadSnapshotV1;
114
113
  }
115
114
  export type ResolveHeadResult = Head | ResolvedHead | null | undefined;
@@ -163,53 +162,24 @@ export interface WebAppManifest {
163
162
  screenshots?: WebAppManifestIcon[];
164
163
  [key: string]: unknown;
165
164
  }
166
- export interface AkanMetadata {
167
- title?: string;
168
- description?: string;
169
- robots?: string;
170
- openGraph?: {
171
- title?: string;
172
- description?: string;
173
- type?: string;
174
- url?: string;
175
- siteName?: string;
176
- images?: string | string[];
177
- };
178
- twitter?: {
179
- card?: "summary" | "summary_large_image" | "app" | "player" | (string & {});
180
- title?: string;
181
- description?: string;
182
- images?: string | string[];
183
- };
184
- alternates?: {
185
- canonical?: string;
186
- languages?: Record<string, string>;
187
- };
188
- }
189
- export type GenerateMetadata = (props: PageProps) => PromiseOrObject<AkanMetadata | null | undefined>;
190
165
  export interface PageModule {
191
166
  default?: PageRender;
192
167
  pageConfig?: PageConfig;
193
168
  head?: Head;
194
- metadata?: AkanMetadata;
195
169
  generateHead?: GenerateHead;
196
- generateMetadata?: GenerateMetadata;
197
170
  Loading?: PageLoadingRender;
198
171
  }
199
172
  export interface LayoutModule {
200
173
  default?: LayoutRender;
201
174
  pageConfig?: PageConfig;
202
175
  head?: Head;
203
- metadata?: AkanMetadata;
204
176
  generateHead?: GenerateHead;
205
- generateMetadata?: GenerateMetadata;
206
177
  fonts?: ReactFont[];
207
178
  manifest?: WebAppManifest;
208
179
  theme?: string;
209
180
  reconnect?: boolean;
210
181
  wsConnect?: boolean;
211
182
  layoutStyle?: "mobile" | "web";
212
- gaTrackingId?: string;
213
183
  Loading?: LayoutLoadingRender;
214
184
  NotFound?: LayoutNotFoundRender;
215
185
  Error?: LayoutErrorRender;
@@ -165,8 +165,8 @@ export function resolvePageState({
165
165
  : (config.gesture ?? false),
166
166
  cache: config.cache ?? false,
167
167
  ssr: config.ssr ?? "stream",
168
- topSafeAreaColor: config.topSafeAreaColor ?? "var(color-background, Canvas)",
169
- bottomSafeAreaColor: config.bottomSafeAreaColor ?? "var(color-background, Canvas)",
168
+ topSafeAreaColor: config.topSafeAreaColor ?? defaultPageState.topSafeAreaColor,
169
+ bottomSafeAreaColor: config.bottomSafeAreaColor ?? defaultPageState.bottomSafeAreaColor,
170
170
  };
171
171
  }
172
172
 
@@ -12,7 +12,6 @@ export class RootLayoutDefinition<Args extends RouteArgsShape = Record<never, ne
12
12
  #reconnect?: boolean;
13
13
  #wsConnect?: boolean;
14
14
  #layoutStyle?: "mobile" | "web";
15
- #gaTrackingId?: string;
16
15
 
17
16
  fonts(fonts: ReactFont[]) {
18
17
  this.#fonts = fonts;
@@ -38,10 +37,6 @@ export class RootLayoutDefinition<Args extends RouteArgsShape = Record<never, ne
38
37
  this.#layoutStyle = style;
39
38
  return this;
40
39
  }
41
- gaTrackingId(id: string) {
42
- this.#gaTrackingId = id;
43
- return this;
44
- }
45
40
 
46
41
  protected override extendModule(module: PageModule & LayoutModule): PageModule & LayoutModule {
47
42
  return {
@@ -52,7 +47,6 @@ export class RootLayoutDefinition<Args extends RouteArgsShape = Record<never, ne
52
47
  ...(this.#reconnect !== undefined ? { reconnect: this.#reconnect } : {}),
53
48
  ...(this.#wsConnect !== undefined ? { wsConnect: this.#wsConnect } : {}),
54
49
  ...(this.#layoutStyle ? { layoutStyle: this.#layoutStyle } : {}),
55
- ...(this.#gaTrackingId ? { gaTrackingId: this.#gaTrackingId } : {}),
56
50
  };
57
51
  }
58
52
  }
@@ -9,7 +9,7 @@ import {
9
9
  import { parseAkanI18nEnv } from "akanjs/common";
10
10
  import { deserialize } from "akanjs/constant";
11
11
  import type { ReactNode } from "react";
12
- import type { AkanMetadata, Head, LayoutModule, PageConfig, PageModule } from "../csrTypes";
12
+ import type { Head, LayoutModule, PageConfig, PageModule } from "../csrTypes";
13
13
  import { AkanNotFoundError } from "../router";
14
14
  import { RouteArgError, type RouteArgInfo, type RouteArgInput, type RouteBaseArgs } from "./routeArgs";
15
15
 
@@ -19,7 +19,6 @@ export type RouteKind = "page" | "layout" | "rootLayout";
19
19
  export const routeDefinitionMarker = Symbol.for("akan.routeDefinition");
20
20
 
21
21
  type HeadStage<Args> = Head | ((args: Args) => PromiseOrObject<Head | null | undefined>);
22
- type MetadataStage<Args> = AkanMetadata | ((args: Args) => PromiseOrObject<AkanMetadata | null | undefined>);
23
22
 
24
23
  interface RouteRenderProps {
25
24
  params: Record<string, string>;
@@ -46,7 +45,6 @@ export abstract class RouteDefinition<
46
45
  #config?: PageConfig;
47
46
 
48
47
  #head?: HeadStage<never>;
49
- #metadata?: MetadataStage<never>;
50
48
  #loading?: (args: never) => PromiseOrObject<ReactNode>;
51
49
  #render?: (args: never) => PromiseOrObject<ReactNode>;
52
50
 
@@ -58,10 +56,6 @@ export abstract class RouteDefinition<
58
56
  this.#head = head as HeadStage<never>;
59
57
  return this;
60
58
  }
61
- metadata(metadata: MetadataStage<RouteBaseArgs & Args>) {
62
- this.#metadata = metadata as MetadataStage<never>;
63
- return this;
64
- }
65
59
  loading(render: (args: RouteBaseArgs & Args & Extra) => PromiseOrObject<ReactNode>) {
66
60
  this.#loading = render as (args: never) => PromiseOrObject<ReactNode>;
67
61
  return this;
@@ -136,7 +130,6 @@ export abstract class RouteDefinition<
136
130
  if (!render)
137
131
  throw new Error(`[route-convention] a ${this.kind}() chain ends with .render(), and this one has none`);
138
132
  const head = this.#head;
139
- const metadata = this.#metadata;
140
133
  const loading = this.#loading;
141
134
  const module: PageModule & LayoutModule = {
142
135
  default: (async (props: RouteRenderProps) => await render(this.#argsOf(props) as never)) as never,
@@ -146,11 +139,6 @@ export abstract class RouteDefinition<
146
139
  : typeof head === "function"
147
140
  ? { generateHead: async (props: RouteRenderProps) => await head(this.#argsOf(props) as never) }
148
141
  : { head }),
149
- ...(metadata === undefined
150
- ? {}
151
- : typeof metadata === "function"
152
- ? { generateMetadata: async (props: RouteRenderProps) => await metadata(this.#argsOf(props) as never) }
153
- : { metadata }),
154
142
  ...(loading
155
143
  ? {
156
144
  Loading: ((props: RouteRenderProps) =>
@@ -21,6 +21,22 @@ export type CodeAgentApprovalPolicy = "never" | "writes" | "commands" | "all";
21
21
  */
22
22
  export type CodeAgentInteractionMode = "await" | "suspend";
23
23
 
24
+ export interface CodeAgentModelInfo {
25
+ id: string;
26
+ name: string;
27
+ contextWindow?: number;
28
+ /** Whether this is the one the session is running on, which a listing has to mark rather than repeat. */
29
+ current: boolean;
30
+ }
31
+
32
+ /** One provider of the model catalogue, and whether a credential for it has been configured. */
33
+ export interface CodeAgentProviderInfo {
34
+ id: string;
35
+ name: string;
36
+ authorized: boolean;
37
+ models: CodeAgentModelInfo[];
38
+ }
39
+
24
40
  export interface CodeAgentMcpServerRef {
25
41
  name: string;
26
42
  transport: "stdio" | "http";
@@ -29,8 +45,26 @@ export interface CodeAgentMcpServerRef {
29
45
  args?: string[];
30
46
  url?: string;
31
47
  env?: Record<string, string>;
48
+ /** Sent on every http request. A server behind a static token needs nothing else. */
49
+ headers?: Record<string, string>;
50
+ /**
51
+ * What to use when the server answers `401` and asks for OAuth.
52
+ *
53
+ * Everything here is optional because the spec is discoverable: the challenge names the resource metadata,
54
+ * that names the authorization server, and most hosted servers register a client on demand. It is for the
55
+ * ones that do not — a provider issuing client ids by hand, or one whose scopes cannot be guessed.
56
+ */
57
+ oauth?: { clientId?: string; clientSecret?: string; scope?: string };
32
58
  }
33
59
 
60
+ /**
61
+ * Where a server stands with its credential.
62
+ *
63
+ * `required` is not a failure: it is a server that answered correctly and said who may talk to it. Reporting
64
+ * it as unreachable would send someone to debug a server that is working exactly as designed.
65
+ */
66
+ export type CodeAgentMcpAuthState = "none" | "authorized" | "required";
67
+
34
68
  /**
35
69
  * One declared MCP server, as the session found it.
36
70
  *
@@ -47,6 +81,8 @@ export interface CodeAgentMcpStatus {
47
81
  tools: string[];
48
82
  /** Why it published none, when it published none. */
49
83
  error?: string;
84
+ /** Whether a credential was needed, and whether one was held. */
85
+ auth?: CodeAgentMcpAuthState;
50
86
  }
51
87
 
52
88
  export interface CodeAgentSubagentBudget {
package/common/index.ts CHANGED
@@ -21,10 +21,13 @@ export {
21
21
  type CodeAgentApprovalPolicy,
22
22
  type CodeAgentBuiltinTool,
23
23
  type CodeAgentInteractionMode,
24
+ type CodeAgentMcpAuthState,
24
25
  type CodeAgentMcpServerRef,
25
26
  type CodeAgentMcpStatus,
27
+ type CodeAgentModelInfo,
26
28
  type CodeAgentPresetName,
27
29
  type CodeAgentProfile,
30
+ type CodeAgentProviderInfo,
28
31
  type CodeAgentSubagentBudget,
29
32
  codeAgentDeniedPaths,
30
33
  codeAgentPresets,
@@ -13,9 +13,7 @@ export const PAGE_ROUTE_EXPORTS: ReadonlySet<string> = new Set([
13
13
  "default",
14
14
  "pageConfig",
15
15
  "head",
16
- "metadata",
17
16
  "generateHead",
18
- "generateMetadata",
19
17
  "Loading",
20
18
  ]);
21
19
  export const LAYOUT_ROUTE_EXPORTS: ReadonlySet<string> = new Set([...PAGE_ROUTE_EXPORTS, "NotFound", "Error"]);
@@ -27,7 +25,6 @@ export const ROOT_LAYOUT_ROUTE_EXPORTS: ReadonlySet<string> = new Set([
27
25
  "reconnect",
28
26
  "wsConnect",
29
27
  "layoutStyle",
30
- "gaTrackingId",
31
28
  ]);
32
29
  /** Root-layout exports that are plain config rather than components, so a PascalCase check cannot allow them. */
33
30
  export const RESERVED_ROUTE_CONFIG_EXPORTS: ReadonlySet<string> = new Set(
@@ -238,16 +238,15 @@ export class ConstantRegistry {
238
238
  PurifiedModel<Model>
239
239
  >;
240
240
  }
241
- static serialize<Value>(modelRef: Cls | Cls[], value: Value, nullable: boolean = false): Value {
241
+ static serialize<Value>(modelRef: Cls | Cls[], value: Value, nullable: boolean = false, of?: Cls | Cls[]): Value {
242
242
  if (Array.isArray(value) && Array.isArray(modelRef)) {
243
243
  const singleModelRef = modelRef.at(0);
244
244
  if (!singleModelRef) throw new Error("No model ref found");
245
245
  return value.map((v: object) => ConstantRegistry.serialize(singleModelRef as Cls, v)) as unknown as Value;
246
246
  } else if (modelRef === Map && value instanceof Map) {
247
+ if (!of) throw new Error("A Map needs its value type (of) to serialize");
247
248
  return Object.fromEntries(
248
- [...value.entries()].map(([key, value]: [string, unknown]) => {
249
- return [key, ConstantRegistry.serialize(value as Cls, value)];
250
- }),
249
+ [...value.entries()].map(([key, entry]: [string, unknown]) => [key, ConstantRegistry.serialize(of, entry)]),
251
250
  ) as unknown as Value;
252
251
  } else if (PrimitiveRegistry.has(modelRef as Cls)) {
253
252
  return (modelRef as typeof PrimitiveScalar)._serialize(value as PrimitiveValue) as unknown as Value;
@@ -255,17 +254,15 @@ export class ConstantRegistry {
255
254
  return serialize(modelRef as ConstantCls, 0, value, "object", { nullable }) as unknown as Value;
256
255
  } else throw new Error(`No serialize function for modelRef: ${modelRef}`);
257
256
  }
258
- static deserialize<Value>(modelRef: Cls | Cls[], value: Value, nullable: boolean = false): Value {
257
+ static deserialize<Value>(modelRef: Cls | Cls[], value: Value, nullable: boolean = false, of?: Cls | Cls[]): Value {
259
258
  if (Array.isArray(value) && Array.isArray(modelRef)) {
260
259
  const singleModelRef = modelRef.at(0);
261
260
  if (!singleModelRef) throw new Error("No model ref found");
262
261
  return value.map((v: object) => ConstantRegistry.deserialize(singleModelRef as Cls, v)) as unknown as Value;
263
- } else if (modelRef === Map && value instanceof Map) {
264
- return new Map(
265
- Object.entries(value).map(([key, value]: [string, unknown]) => {
266
- return [key, ConstantRegistry.deserialize(value as Cls, value)];
267
- }),
268
- ) as unknown as Value;
262
+ } else if (modelRef === Map && value !== null && typeof value === "object") {
263
+ if (!of) throw new Error("A Map needs its value type (of) to deserialize");
264
+ const entries: [string, unknown][] = value instanceof Map ? [...value.entries()] : Object.entries(value);
265
+ return new Map(entries.map(([key, entry]) => [key, ConstantRegistry.deserialize(of, entry)])) as unknown as Value;
269
266
  } else if (PrimitiveRegistry.has(modelRef as Cls)) {
270
267
  return (modelRef as typeof PrimitiveScalar)._parse(value as PrimitiveValue) as unknown as Value;
271
268
  } else if (ConstantRegistry.modelRefNameMap.has(modelRef as Cls)) {
@@ -11,7 +11,12 @@ type BatchLoadFn<Key, Value> = (
11
11
  ) => PromiseLike<ReadonlyArray<Value | Error>> | ReadonlyArray<Value | Error>;
12
12
 
13
13
  interface DataLoaderOptions<Key, CacheKey> {
14
- cache?: boolean;
14
+ /**
15
+ * How long a loaded key is answered from memory: `false` (the default) remembers nothing past its own batch,
16
+ * a number keeps each key for that many milliseconds, and `true` keeps it for the life of the loader. A model's
17
+ * loaders live as long as the process, so anything but `false` serves a stale document after it changes.
18
+ */
19
+ cache?: boolean | number;
15
20
  cacheKeyFn?: (key: Key) => CacheKey;
16
21
  batch?: boolean;
17
22
  batchScheduleFn?: (callback: () => void) => void;
@@ -27,20 +32,27 @@ interface BatchItem<Key, Value> {
27
32
 
28
33
  /** Minimal DataLoader-compatible batch loader used by Akan document resolvers. */
29
34
  export class DataLoader<Key, Value, CacheKey = Key> {
35
+ static readonly #minSweepSize = 1024;
30
36
  readonly name?: string;
31
37
  readonly #batchLoadFn: BatchLoadFn<Key, Value>;
32
- readonly #cache: boolean;
38
+ readonly #ttl: number;
33
39
  readonly #cacheKeyFn: (key: Key) => CacheKey;
34
40
  readonly #batch: boolean;
35
41
  readonly #batchScheduleFn: (callback: () => void) => void;
36
42
  readonly #maxBatchSize: number;
37
- readonly #promiseCache = new Map<CacheKey, Promise<Value>>();
43
+ readonly #promiseCache = new Map<CacheKey, { promise: Promise<Value>; expiresAt: number }>();
44
+ #sweepAt = DataLoader.#minSweepSize;
38
45
  #queue: BatchItem<Key, Value>[] = [];
39
46
  #scheduled = false;
40
47
 
41
48
  constructor(batchLoadFn: BatchLoadFn<Key, Value>, options: DataLoaderOptions<Key, CacheKey> = {}) {
42
49
  this.#batchLoadFn = batchLoadFn;
43
- this.#cache = options.cache !== false;
50
+ this.#ttl =
51
+ options.cache === true
52
+ ? Number.POSITIVE_INFINITY
53
+ : typeof options.cache === "number" && options.cache > 0
54
+ ? options.cache
55
+ : 0;
44
56
  this.#cacheKeyFn = options.cacheKeyFn ?? ((key) => key as unknown as CacheKey);
45
57
  this.#batch = options.batch !== false;
46
58
  this.#batchScheduleFn = options.batchScheduleFn ?? ((callback) => queueMicrotask(callback));
@@ -50,17 +62,21 @@ export class DataLoader<Key, Value, CacheKey = Key> {
50
62
 
51
63
  load(key: Key): Promise<Value> {
52
64
  const cacheKey = this.#cacheKeyFn(key);
53
- if (this.#cache) {
54
- const cached = this.#promiseCache.get(cacheKey);
55
- if (cached) return cached;
56
- }
65
+ const cached = this.#cached(cacheKey);
66
+ if (cached) return cached;
57
67
 
58
68
  const promise = new Promise<Value>((resolve, reject) => {
59
69
  this.#queue.push({ key, resolve, reject });
60
70
  if (this.#batch) this.#schedule();
61
71
  else this.#dispatch();
62
72
  });
63
- if (this.#cache) this.#promiseCache.set(cacheKey, promise);
73
+ if (this.#ttl > 0) {
74
+ this.#remember(cacheKey, promise);
75
+
76
+ promise.catch(() => {
77
+ if (this.#promiseCache.get(cacheKey)?.promise === promise) this.#promiseCache.delete(cacheKey);
78
+ });
79
+ }
64
80
  return promise;
65
81
  }
66
82
 
@@ -80,13 +96,30 @@ export class DataLoader<Key, Value, CacheKey = Key> {
80
96
  }
81
97
 
82
98
  prime(key: Key, value: Value | Error): this {
83
- if (!this.#cache) return this;
99
+ if (this.#ttl <= 0) return this;
84
100
  const cacheKey = this.#cacheKeyFn(key);
85
- if (this.#promiseCache.has(cacheKey)) return this;
86
- this.#promiseCache.set(cacheKey, value instanceof Error ? Promise.reject(value) : Promise.resolve(value));
101
+ if (this.#cached(cacheKey)) return this;
102
+ this.#remember(cacheKey, value instanceof Error ? Promise.reject(value) : Promise.resolve(value));
87
103
  return this;
88
104
  }
89
105
 
106
+ #cached(cacheKey: CacheKey): Promise<Value> | undefined {
107
+ if (this.#ttl <= 0) return undefined;
108
+ const entry = this.#promiseCache.get(cacheKey);
109
+ if (!entry) return undefined;
110
+ if (entry.expiresAt > Date.now()) return entry.promise;
111
+ this.#promiseCache.delete(cacheKey);
112
+ return undefined;
113
+ }
114
+
115
+ #remember(cacheKey: CacheKey, promise: Promise<Value>) {
116
+ this.#promiseCache.set(cacheKey, { promise, expiresAt: Date.now() + this.#ttl });
117
+ if (this.#promiseCache.size < this.#sweepAt) return;
118
+ const now = Date.now();
119
+ for (const [key, entry] of this.#promiseCache) if (entry.expiresAt <= now) this.#promiseCache.delete(key);
120
+ this.#sweepAt = Math.max(DataLoader.#minSweepSize, this.#promiseCache.size * 2);
121
+ }
122
+
90
123
  #schedule() {
91
124
  if (this.#scheduled) return;
92
125
  this.#scheduled = true;
@@ -159,7 +192,7 @@ export const createLoader = <Key, Value>(model: LoaderModel, fieldName = "id", d
159
192
  });
160
193
  return data as unknown as Promise<Value[]>;
161
194
  },
162
- { name: "dataloader", cache: false },
195
+ { name: "dataloader" },
163
196
  );
164
197
  };
165
198
  export const createArrayLoader = <K, V>(model: LoaderModel, fieldName = "id", defaultQuery: QueryOf<unknown> = {}) => {
@@ -194,7 +227,7 @@ export const createArrayElementLoader = <K, V>(
194
227
  });
195
228
  return data as unknown as Promise<V[]>;
196
229
  },
197
- { name: "dataloader", cache: false },
230
+ { name: "dataloader" },
198
231
  );
199
232
  };
200
233
 
@@ -214,7 +247,7 @@ export const createQueryLoader = <Key, Value>(
214
247
  });
215
248
  return data as unknown as Promise<Value[]>;
216
249
  },
217
- { name: "dataloader", cache: false },
250
+ { name: "dataloader" },
218
251
  );
219
252
  };
220
253
 
@@ -15,25 +15,46 @@ export type ModelCls<
15
15
  _libsOnSchema: (schema: SchemaOf<any, any>) => void;
16
16
  }
17
17
  >;
18
+ export interface LoaderOption {
19
+ /** `false` by default; a number of milliseconds, or `true` for as long as the process runs. */
20
+ cache?: boolean | number;
21
+ }
22
+
18
23
  export class LoaderInfo<Doc = any, Key extends keyof Doc = keyof Doc, QueryArg = Doc[Key]> {
19
24
  type: LoaderType;
20
25
  field: Key | Key[];
21
26
  defaultQuery: QueryOf<unknown>;
27
+ cache: boolean | number;
22
28
  queryArg: QueryArg | undefined;
23
- constructor(type: LoaderType, field: Key | Key[], defaultQuery: QueryOf<unknown> = {}) {
29
+ constructor(
30
+ type: LoaderType,
31
+ field: Key | Key[],
32
+ defaultQuery: QueryOf<unknown> = {},
33
+ { cache = false }: LoaderOption = {},
34
+ ) {
24
35
  this.type = type;
25
36
  this.field = field;
26
37
  this.defaultQuery = defaultQuery;
38
+ this.cache = cache;
27
39
  }
28
40
  }
29
41
 
30
42
  export const makeLoaderBuilder = <Doc>() => ({
31
- byField: <Key extends keyof Doc & string>(fieldName: Key, defaultQuery: QueryOf<unknown> = {}) =>
32
- new LoaderInfo<Doc, Key>("field", fieldName, defaultQuery),
33
- byArrayField: <Key extends keyof Doc & string>(fieldName: Key, defaultQuery: QueryOf<unknown> = {}) =>
34
- new LoaderInfo<Doc, Key>("arrayField", fieldName, defaultQuery),
35
- byQuery: <Key extends keyof Doc & string>(queryKeys: readonly Key[], defaultQuery: QueryOf<unknown> = {}) =>
36
- new LoaderInfo<Doc, Key, Pick<Doc, Key>>("query", queryKeys as Key[], defaultQuery),
43
+ byField: <Key extends keyof Doc & string>(
44
+ fieldName: Key,
45
+ defaultQuery: QueryOf<unknown> = {},
46
+ option: LoaderOption = {},
47
+ ) => new LoaderInfo<Doc, Key>("field", fieldName, defaultQuery, option),
48
+ byArrayField: <Key extends keyof Doc & string>(
49
+ fieldName: Key,
50
+ defaultQuery: QueryOf<unknown> = {},
51
+ option: LoaderOption = {},
52
+ ) => new LoaderInfo<Doc, Key>("arrayField", fieldName, defaultQuery, option),
53
+ byQuery: <Key extends keyof Doc & string>(
54
+ queryKeys: readonly Key[],
55
+ defaultQuery: QueryOf<unknown> = {},
56
+ option: LoaderOption = {},
57
+ ) => new LoaderInfo<Doc, Key, Pick<Doc, Key>>("query", queryKeys as Key[], defaultQuery, option),
37
58
  });
38
59
 
39
60
  export type LoaderBuilder<Doc = any> = (builder: ReturnType<typeof makeLoaderBuilder<Doc>>) => {
@@ -544,7 +544,7 @@ export class FetchClient {
544
544
 
545
545
  const anyCruGuards = signal.cruGuards ?? signal.createGuards ?? signal.updateGuards ?? signal.removeGuards;
546
546
  const updateGuards = signal.updateGuards ?? signal.cruGuards;
547
- if (anyCruGuards) {
547
+ if (signal.getGuards) {
548
548
  this.#setHandlerFactory(names.viewModel, () =>
549
549
  this.#makeModelHandleFn(refName, names.model, names.viewModel, `${refName}View`),
550
550
  );
@@ -557,6 +557,8 @@ export class FetchClient {
557
557
  return { refName, [`${refName}Obj`]: modelObj, [`${refName}ViewAt`]: new Date() };
558
558
  }) as FetchHandler,
559
559
  );
560
+ }
561
+ if (signal.getGuards && anyCruGuards) {
560
562
  this.#setHandlerFactory(names.editModel, () =>
561
563
  this.#makeModelHandleFn(refName, names.model, names.editModel, `${refName}Edit`),
562
564
  );
@@ -569,17 +571,17 @@ export class FetchClient {
569
571
  return { refName, [`${refName}Obj`]: modelObj, [`${refName}ViewAt`]: new Date() };
570
572
  }) as FetchHandler,
571
573
  );
572
- if (updateGuards) {
573
- this.#setHandlerFactory(
574
- names.mergeModel,
575
- () =>
576
- (async (modelOrId: string | { id: string }, data: UnknownRecord, option?: FetchPolicy) => {
577
- const id = typeof modelOrId === "string" ? modelOrId : modelOrId.id;
578
- const updateFn = this.#requireHandler(names.updateModel, names.mergeModel);
579
- return await updateFn(id, data, option);
580
- }) as FetchHandler,
581
- );
582
- }
574
+ }
575
+ if (updateGuards) {
576
+ this.#setHandlerFactory(
577
+ names.mergeModel,
578
+ () =>
579
+ (async (modelOrId: string | { id: string }, data: UnknownRecord, option?: FetchPolicy) => {
580
+ const id = typeof modelOrId === "string" ? modelOrId : modelOrId.id;
581
+ const updateFn = this.#requireHandler(names.updateModel, names.mergeModel);
582
+ return await updateFn(id, data, option);
583
+ }) as FetchHandler,
584
+ );
583
585
  }
584
586
 
585
587
  this.#setHandlerFactory(
@@ -207,7 +207,7 @@ export class HttpClient {
207
207
  const searchPath = searchParams.size > 0 ? `?${searchParams.toString()}` : "";
208
208
  const paramedPath = path.replace(/:(\w+)/g, (match, p1) => {
209
209
  const value = argMap.get(p1);
210
- return value === null || value === undefined ? match : String(value);
210
+ return value === null || value === undefined ? match : encodeURIComponent(String(value));
211
211
  });
212
212
  return `${paramedPath}${searchPath}`;
213
213
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-beta.17",
3
+ "version": "3.0.0-beta.18",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -268,7 +268,9 @@ export class AkanServer {
268
268
  }
269
269
  setMcp(mcp: boolean | McpServerOption = true) {
270
270
  if (this.status !== "stopped") throw new Error("MCP config must be set before app initialization.");
271
- this.mcp = typeof mcp === "boolean" ? mcp : (mcp.enabled ?? true);
271
+
272
+ const requested = typeof mcp === "boolean" ? mcp : (mcp.enabled ?? this.mcp);
273
+ this.mcp = requested && !AkanServer.#isEnvOff("AKAN_MCP", "AKAN_PUBLIC_MCP");
272
274
  if (typeof mcp === "boolean") return this;
273
275
  const { enabled: _enabled, readOnly, auth, ...rest } = mcp;
274
276
  if (readOnly !== undefined) this.mcpReadOnly = readOnly;
package/server/console.ts CHANGED
@@ -124,16 +124,16 @@ export interface AkanConsoleContext extends Record<string, unknown> {
124
124
  debug: () => ReturnType<AkanServer["inspectConsole"]>;
125
125
  }
126
126
 
127
- export const assertAkanConsoleAllowed = (
128
- env: Pick<BaseEnv, "environment" | "operationMode"> = {
129
- environment: (process.env.AKAN_PUBLIC_ENV ?? "debug") as BaseEnv["environment"],
130
- operationMode: (process.env.AKAN_PUBLIC_OPERATION_MODE ?? "cloud") as BaseEnv["operationMode"],
131
- },
132
- ) => {
127
+ export const assertAkanConsoleAllowed = (env: Partial<Pick<BaseEnv, "environment" | "operationMode">> = {}) => {
128
+ const environment = env.environment ?? ((process.env.AKAN_PUBLIC_ENV ?? "debug") as BaseEnv["environment"]);
129
+ const operationMode =
130
+ env.operationMode ??
131
+ ((process.env.AKAN_PUBLIC_OPERATION_MODE ??
132
+ (environment === "local" ? "local" : "cloud")) as BaseEnv["operationMode"]);
133
133
  const isProductionLike =
134
- env.environment === "main" ||
135
- env.operationMode === "cloud" ||
136
- env.operationMode === "edge" ||
134
+ environment === "main" ||
135
+ operationMode === "cloud" ||
136
+ operationMode === "edge" ||
137
137
  process.env.NODE_ENV === "production";
138
138
  if (!isProductionLike || process.env.AKAN_CONSOLE === "1") return;
139
139