akanjs 3.0.0-beta.16 → 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.
- package/base/base.ts +1 -1
- package/client/csrTypes.ts +1 -31
- package/client/frameConfig.ts +2 -2
- package/client/route/RootLayoutDefinition.ts +0 -6
- package/client/route/RouteDefinition.ts +1 -13
- package/common/CodeTranscript.ts +12 -0
- package/common/codeAgentProfile.ts +54 -0
- package/common/codeAgentWire.ts +31 -0
- package/common/index.ts +5 -0
- package/common/routeConvention.ts +0 -3
- package/constant/constantRegistry.ts +8 -11
- package/document/dataLoader.ts +48 -15
- package/document/loaderInfo.ts +28 -7
- package/fetch/client/fetchClient.ts +14 -12
- package/fetch/client/httpClient.ts +1 -1
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/server/akanServer.ts +3 -1
- package/server/console.ts +9 -9
- package/server/decorators.ts +3 -64
- package/server/devtools/depsSerializer.ts +1 -4
- package/server/di/diLifecycle.ts +1 -3
- package/server/head.tsx +60 -0
- package/server/imageOptimizer.ts +3 -1
- package/server/resolver/database.resolver.ts +29 -26
- package/server/routeElementComposer.tsx +3 -3
- package/server/routeTreeBuilder.ts +1 -24
- package/server/rscWorker.tsx +10 -21
- package/server/webRouter.ts +12 -8
- package/service/injectInfo.ts +86 -72
- package/service/predefinedAdaptor/cache.adaptor.ts +42 -8
- package/service/predefinedAdaptor/storage.adaptor.ts +6 -6
- package/signal/endpointCache.ts +75 -0
- package/signal/guard.ts +4 -3
- package/signal/middleware.ts +0 -77
- package/signal/signalContext.ts +4 -14
- package/signal/types.ts +4 -5
- package/types/client/csrTypes.d.ts +1 -31
- package/types/client/route/RootLayoutDefinition.d.ts +0 -1
- package/types/client/route/RouteDefinition.d.ts +1 -3
- package/types/common/CodeTranscript.d.ts +3 -1
- package/types/common/codeAgentProfile.d.ts +54 -0
- package/types/common/codeAgentWire.d.ts +28 -0
- package/types/common/index.d.ts +2 -2
- package/types/constant/constantRegistry.d.ts +2 -2
- package/types/document/dataLoader.d.ts +6 -1
- package/types/document/loaderInfo.d.ts +9 -4
- package/types/server/console.d.ts +1 -1
- package/types/server/decorators.d.ts +1 -8
- package/types/server/head.d.ts +10 -0
- package/types/server/routeElementComposer.d.ts +1 -1
- package/types/service/injectInfo.d.ts +17 -14
- package/types/service/predefinedAdaptor/cache.adaptor.d.ts +1 -0
- package/types/service/predefinedAdaptor/storage.adaptor.d.ts +1 -2
- package/types/signal/endpointCache.d.ts +19 -0
- package/types/signal/guard.d.ts +4 -3
- package/types/signal/middleware.d.ts +0 -20
- package/types/signal/signalContext.d.ts +1 -6
- package/types/signal/types.d.ts +4 -5
- package/types/ui/Agent/Menu.d.ts +2 -2
- package/types/ui/Button.d.ts +1 -1
- package/types/ui/CsrImage.d.ts +2 -1
- package/types/ui/Data/Item.d.ts +1 -1
- package/types/ui/Link/CsrLink.d.ts +1 -1
- package/types/ui/Link/types.d.ts +2 -0
- package/types/ui/System/CSR.d.ts +2 -2
- package/types/ui/System/Client.d.ts +2 -3
- package/types/ui/System/Common.d.ts +0 -2
- package/types/ui/System/SSR.d.ts +2 -2
- package/types/ui/recipe/inputRecipe.d.ts +1 -1
- package/ui/Agent/Menu.tsx +2 -2
- package/ui/Button.tsx +1 -1
- package/ui/Constant/Doc.tsx +121 -77
- package/ui/Constant/Graph.tsx +11 -3
- package/ui/CsrImage.tsx +3 -2
- package/ui/Data/Item.tsx +1 -1
- package/ui/Field/Choice.tsx +1 -1
- package/ui/Field/Collection.tsx +4 -3
- package/ui/Image.tsx +1 -0
- package/ui/Link/CsrLink.tsx +5 -1
- package/ui/Link/types.ts +2 -0
- package/ui/System/CSR.tsx +1 -9
- package/ui/System/Client.tsx +2 -4
- package/ui/System/Common.tsx +0 -2
- package/ui/System/SSR.tsx +1 -9
- package/ui/recipe/inputRecipe.ts +1 -1
- package/webkit/bootCsr.tsx +0 -12
- package/webkit/useCamera.tsx +4 -3
- package/webkit/useCsrValues.ts +3 -3
- package/webkit/useFetch.ts +6 -7
- package/server/metadata.tsx +0 -198
- package/types/server/metadata.d.ts +0 -22
- package/types/ui/System/Gtag.d.ts +0 -4
- package/ui/System/Gtag.tsx +0 -69
|
@@ -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
|
}
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/server/akanServer.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
package/server/decorators.ts
CHANGED
|
@@ -1,27 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Method decorators for a server class: `@Transaction`, `@Try
|
|
2
|
+
* Method decorators for a server class: `@Transaction`, `@Try`. Kept because `libs/util`'s
|
|
3
3
|
* storage adaptors use `@Try` on their remote calls; new code follows the adaptor rule instead
|
|
4
4
|
* (`catch` → `logger.error` → `return null`).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
type DecoratedInstance = {
|
|
8
|
-
logger?: { warn?: (message: string) => void
|
|
8
|
+
logger?: { warn?: (message: string) => void };
|
|
9
9
|
__database?: { transaction?: <T>(fn: () => Promise<T>) => Promise<T> };
|
|
10
|
-
__databaseModel?: {
|
|
11
|
-
__database?: { transaction?: <T>(fn: () => Promise<T>) => Promise<T> };
|
|
12
|
-
__model?: { modelName: string };
|
|
13
|
-
__cache?: {
|
|
14
|
-
get: (key: string) => Promise<string | null>;
|
|
15
|
-
set: (key: string, value: string, option: unknown) => unknown;
|
|
16
|
-
};
|
|
17
|
-
};
|
|
10
|
+
__databaseModel?: { __database?: { transaction?: <T>(fn: () => Promise<T>) => Promise<T> } };
|
|
18
11
|
database?: { transaction?: <T>(fn: () => Promise<T>) => Promise<T> };
|
|
19
12
|
connection?: { transaction?: <T>(fn: () => Promise<T>) => Promise<T> };
|
|
20
|
-
__model?: { modelName: string };
|
|
21
|
-
__cache?: {
|
|
22
|
-
get: (key: string) => Promise<string | null>;
|
|
23
|
-
set: (key: string, value: string, option: unknown) => unknown;
|
|
24
|
-
};
|
|
25
13
|
};
|
|
26
14
|
|
|
27
15
|
/** Method decorator that catches errors and logs a warning instead of throwing. */
|
|
@@ -53,52 +41,3 @@ export const Transaction = (): MethodDecorator => {
|
|
|
53
41
|
return descriptor;
|
|
54
42
|
}) as MethodDecorator;
|
|
55
43
|
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Method decorator that caches method results for the given timeout in milliseconds.
|
|
59
|
-
*
|
|
60
|
-
* Not `Cache` — `akanjs/signal` exports a `Cache` **middleware class**, and an app importing both barrels got
|
|
61
|
-
* one name meaning two things.
|
|
62
|
-
*/
|
|
63
|
-
export const CacheMethod = (timeout = 1000, getCacheKey?: (...args: unknown[]) => string): MethodDecorator => {
|
|
64
|
-
return ((_target: unknown, key: string, descriptor: PropertyDescriptor) => {
|
|
65
|
-
const originMethod = descriptor.value as (this: unknown, ...args: unknown[]) => unknown;
|
|
66
|
-
const cacheMap = new Map<string, unknown>();
|
|
67
|
-
const timerMap = new Map<string, NodeJS.Timeout>();
|
|
68
|
-
descriptor.value = async function (...args: unknown[]) {
|
|
69
|
-
const instance = this as DecoratedInstance;
|
|
70
|
-
const classType = instance.__model ? "doc" : instance.__databaseModel ? "service" : "class";
|
|
71
|
-
const model = instance.__model ?? instance.__databaseModel?.__model;
|
|
72
|
-
const cache = instance.__cache ?? instance.__databaseModel?.__cache;
|
|
73
|
-
const cacheKeyValue = getCacheKey ? getCacheKey(...args) : JSON.stringify(args);
|
|
74
|
-
const cacheKey = `${classType}:${model?.modelName ?? "unknown"}:${key}:${cacheKeyValue}`;
|
|
75
|
-
const getCache = async (cacheKey: string) => {
|
|
76
|
-
if (classType === "class") return cacheMap.get(cacheKey);
|
|
77
|
-
const cached = (await cache?.get(cacheKey)) as string | null;
|
|
78
|
-
if (cached) return JSON.parse(cached);
|
|
79
|
-
return null;
|
|
80
|
-
};
|
|
81
|
-
const setCache = async (cacheKey: string, value: unknown) => {
|
|
82
|
-
if (classType === "class") {
|
|
83
|
-
const existingTimer = timerMap.get(cacheKey);
|
|
84
|
-
if (existingTimer) clearTimeout(existingTimer);
|
|
85
|
-
cacheMap.set(cacheKey, value);
|
|
86
|
-
const timer = setTimeout(() => {
|
|
87
|
-
cacheMap.delete(cacheKey);
|
|
88
|
-
timerMap.delete(cacheKey);
|
|
89
|
-
}, timeout);
|
|
90
|
-
timerMap.set(cacheKey, timer);
|
|
91
|
-
} else await cache?.set(cacheKey, JSON.stringify(value), { PX: timeout });
|
|
92
|
-
};
|
|
93
|
-
const cachedData = await getCache(cacheKey);
|
|
94
|
-
if (cachedData) {
|
|
95
|
-
instance.logger?.trace?.(`${model?.modelName ?? "unknown"} cache hit: ${cacheKey}`);
|
|
96
|
-
return cachedData;
|
|
97
|
-
}
|
|
98
|
-
const result = await originMethod.apply(this, args);
|
|
99
|
-
await setCache(cacheKey, result);
|
|
100
|
-
instance.logger?.trace?.(`${model?.modelName ?? "unknown"} cache set: ${cacheKey}`);
|
|
101
|
-
return result;
|
|
102
|
-
};
|
|
103
|
-
}) as MethodDecorator;
|
|
104
|
-
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type BaseEnv, INJECT_META } from "akanjs/base";
|
|
2
2
|
import type { AdaptorCls, InjectInfo, ServiceCls } from "akanjs/service";
|
|
3
3
|
import type { DiLifecycle } from "../di/diLifecycle";
|
|
4
|
-
import { DevtoolsJson } from "./devtoolsJson";
|
|
5
4
|
import type { DepEdge, DepEdgeKind, DepNode, DepNodeKind, DepsData } from "./types";
|
|
6
5
|
|
|
7
6
|
export interface DepsSerializerContext {
|
|
@@ -193,9 +192,7 @@ export class DepsSerializer {
|
|
|
193
192
|
detail: {
|
|
194
193
|
local: Boolean(injectInfo.local),
|
|
195
194
|
isMap: Boolean(injectInfo.isMap),
|
|
196
|
-
...(injectInfo.
|
|
197
|
-
? { expireAt: DevtoolsJson.toSafe(injectInfo.cacheOption.expireAt) }
|
|
198
|
-
: {}),
|
|
195
|
+
...(injectInfo.ttl !== undefined ? { ttl: injectInfo.ttl } : {}),
|
|
199
196
|
},
|
|
200
197
|
};
|
|
201
198
|
}
|
package/server/di/diLifecycle.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { agentTurnConstant, agentTurnDocument } from "../../signal/agentTurn";
|
|
|
17
17
|
import { Base, BaseEndpoint, BaseInternal } from "../../signal/base.signal";
|
|
18
18
|
import type { Endpoint } from "../../signal/endpoint";
|
|
19
19
|
import type { Internal } from "../../signal/internal";
|
|
20
|
-
import {
|
|
20
|
+
import { Logging, type MiddlewareCls, Timeout } from "../../signal/middleware";
|
|
21
21
|
import type { ServerSignal, ServerSignalCls } from "../../signal/serverSignal";
|
|
22
22
|
import { SignalRegistry } from "../../signal/signalRegistry";
|
|
23
23
|
import type { AkanLib, DatabaseModule, ScalarModule, ServiceModule } from "../akanLib";
|
|
@@ -128,8 +128,6 @@ export class DiLifecycle {
|
|
|
128
128
|
this.#middleware.set(Logging.refName, Logging);
|
|
129
129
|
|
|
130
130
|
this.#middleware.set(Timeout.refName, Timeout);
|
|
131
|
-
|
|
132
|
-
this.#middleware.set(Cache.refName, Cache);
|
|
133
131
|
const defaultOption = createDefaultAkanOption();
|
|
134
132
|
defaultOption.getMiddlewares().forEach((middleware) => {
|
|
135
133
|
this.#middleware.set(middleware.refName, middleware);
|
package/server/head.tsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Head, ResolvedHead, ResolveHeadResult } from "akanjs/client";
|
|
2
|
+
import { AKAN_RSC_HEAD_SNAPSHOT_VERSION, type AkanHeadSnapshotV1 } from "./routeState";
|
|
3
|
+
|
|
4
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
5
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createAkanLocaleAlternateHeadSnapshot(languages: Record<string, string>): AkanHeadSnapshotV1 {
|
|
9
|
+
return {
|
|
10
|
+
version: AKAN_RSC_HEAD_SNAPSHOT_VERSION,
|
|
11
|
+
nodes: Object.entries(languages).map(([lang, href]) => ({
|
|
12
|
+
tag: "link",
|
|
13
|
+
attrs: { rel: "alternate", hrefLang: lang, href },
|
|
14
|
+
})),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function mergeAkanHeadSnapshots(
|
|
19
|
+
...snapshots: Array<AkanHeadSnapshotV1 | null | undefined>
|
|
20
|
+
): AkanHeadSnapshotV1 | undefined {
|
|
21
|
+
const nodes = snapshots.flatMap((snapshot) => snapshot?.nodes ?? []);
|
|
22
|
+
return snapshots.some(Boolean) ? { version: AKAN_RSC_HEAD_SNAPSHOT_VERSION, nodes } : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function renderAkanHeadSnapshot(snapshot: AkanHeadSnapshotV1): Head {
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
{snapshot.nodes.map((node, index) => {
|
|
29
|
+
const marker = {
|
|
30
|
+
"data-akan-head": "route",
|
|
31
|
+
"data-akan-head-key": `${node.tag}:${index}`,
|
|
32
|
+
};
|
|
33
|
+
if (node.tag === "title") {
|
|
34
|
+
return (
|
|
35
|
+
<title key={`${node.tag}:${index}`} {...marker}>
|
|
36
|
+
{node.text ?? ""}
|
|
37
|
+
</title>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (node.tag === "meta") {
|
|
41
|
+
return <meta key={`${node.tag}:${index}`} {...node.attrs} {...marker} />;
|
|
42
|
+
}
|
|
43
|
+
return <link key={`${node.tag}:${index}`} {...node.attrs} {...marker} />;
|
|
44
|
+
})}
|
|
45
|
+
</>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function shouldRenderLocaleAlternates(options: { isSpecialRoute?: boolean }): boolean {
|
|
50
|
+
return options.isSpecialRoute !== true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isResolvedHead(value: unknown): value is ResolvedHead {
|
|
54
|
+
return isRecord(value) && "node" in value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveHeadResult(value: ResolveHeadResult): ResolvedHead {
|
|
58
|
+
if (isResolvedHead(value)) return value;
|
|
59
|
+
return { node: value };
|
|
60
|
+
}
|
package/server/imageOptimizer.ts
CHANGED
|
@@ -61,6 +61,7 @@ export class ImageOptimizer {
|
|
|
61
61
|
static readonly #avif = "image/avif";
|
|
62
62
|
static readonly #heic = "image/heic";
|
|
63
63
|
static readonly #bypassTypes = new Set(["image/x-icon", "image/x-icns", "image/bmp", "image/jxl", "image/heic"]);
|
|
64
|
+
static readonly #encodableTypes = new Set(["image/jpeg", "image/png", "image/webp", "image/avif"]);
|
|
64
65
|
|
|
65
66
|
#publicDir: string;
|
|
66
67
|
#cacheDir: string;
|
|
@@ -329,6 +330,7 @@ export class ImageOptimizer {
|
|
|
329
330
|
shouldBypass || !params.outputType || inputType === ImageOptimizer.#webp || inputType === ImageOptimizer.#avif
|
|
330
331
|
? inputType
|
|
331
332
|
: params.outputType;
|
|
333
|
+
const shouldEncode = !shouldBypass && ImageOptimizer.#encodableTypes.has(outputType);
|
|
332
334
|
const cachePath = this.#getCachePath({
|
|
333
335
|
href: params.href,
|
|
334
336
|
width: params.width,
|
|
@@ -351,7 +353,7 @@ export class ImageOptimizer {
|
|
|
351
353
|
let buffer = source.buffer;
|
|
352
354
|
let contentType = inputType;
|
|
353
355
|
let cacheable = true;
|
|
354
|
-
if (
|
|
356
|
+
if (shouldEncode) {
|
|
355
357
|
try {
|
|
356
358
|
buffer = await this.#semaphore.run(() =>
|
|
357
359
|
ImageOptimizer.#optimize(source.buffer, {
|
|
@@ -155,37 +155,40 @@ export class DatabaseResolver {
|
|
|
155
155
|
});
|
|
156
156
|
Object.entries(getLoaderInfos(database.model)).forEach(([key, loaderInfo]) => {
|
|
157
157
|
Object.assign(this, {
|
|
158
|
-
[key]: new DataLoader<any, any>(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
[key]: new DataLoader<any, any>(
|
|
159
|
+
async (keys) => {
|
|
160
|
+
traceDataLoaderBatch(keys.length);
|
|
161
|
+
if (loaderInfo.type === "query") {
|
|
162
|
+
const fields = loaderInfo.field as string[];
|
|
163
|
+
const query = { kind: "any", queries: keys } as QueryOf<unknown>;
|
|
164
|
+
const docs = await timedQuery(() =>
|
|
165
|
+
this.__store.find(documentQueryHelper.all(loaderInfo.defaultQuery, query)),
|
|
166
|
+
);
|
|
167
|
+
const byKey = new Map(docs.map((doc) => [fields.map((field) => String(doc[field])).join(""), doc]));
|
|
168
|
+
return keys.map(
|
|
169
|
+
(queryKey) => byKey.get(fields.map((field) => String(queryKey[field])).join("")) ?? null,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const field = loaderInfo.field as string;
|
|
173
|
+
const query = {
|
|
174
|
+
[field]: documentQueryHelper.oneOf([...keys]),
|
|
175
|
+
};
|
|
163
176
|
const docs = await timedQuery(() =>
|
|
164
177
|
this.__store.find(documentQueryHelper.all(loaderInfo.defaultQuery, query)),
|
|
165
178
|
);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
[field]: documentQueryHelper.oneOf([...keys]),
|
|
174
|
-
};
|
|
175
|
-
const docs = await timedQuery(() =>
|
|
176
|
-
this.__store.find(documentQueryHelper.all(loaderInfo.defaultQuery, query)),
|
|
177
|
-
);
|
|
178
|
-
if (loaderInfo.type === "arrayField") {
|
|
179
|
-
const byKey = new Map<string, unknown>();
|
|
180
|
-
for (const doc of docs) {
|
|
181
|
-
const values = Array.isArray(doc[field]) ? doc[field] : [];
|
|
182
|
-
for (const value of values) if (!byKey.has(String(value))) byKey.set(String(value), doc);
|
|
179
|
+
if (loaderInfo.type === "arrayField") {
|
|
180
|
+
const byKey = new Map<string, unknown>();
|
|
181
|
+
for (const doc of docs) {
|
|
182
|
+
const values = Array.isArray(doc[field]) ? doc[field] : [];
|
|
183
|
+
for (const value of values) if (!byKey.has(String(value))) byKey.set(String(value), doc);
|
|
184
|
+
}
|
|
185
|
+
return keys.map((key) => byKey.get(String(key)) ?? null);
|
|
183
186
|
}
|
|
187
|
+
const byKey = new Map(docs.map((doc) => [String(doc[field]), doc]));
|
|
184
188
|
return keys.map((key) => byKey.get(String(key)) ?? null);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}),
|
|
189
|
+
},
|
|
190
|
+
{ name: key, cache: loaderInfo.cache },
|
|
191
|
+
),
|
|
189
192
|
});
|
|
190
193
|
});
|
|
191
194
|
}
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
} from "akanjs/client";
|
|
11
11
|
import { Children, cloneElement, isValidElement, type ReactElement, type ReactNode, Suspense } from "react";
|
|
12
12
|
import { getExplicitPageConfigKeys, resolvePageState } from "../client/frameConfig";
|
|
13
|
-
import { resolveHeadResult } from "./
|
|
13
|
+
import { resolveHeadResult } from "./head";
|
|
14
14
|
import { type AkanRouteSegmentState, createAkanRouteSegments, createAkanSegmentOutletKey } from "./routeState";
|
|
15
15
|
import { isAkanRscPartialCommitEnabled } from "./rscPartialCommit";
|
|
16
16
|
import { AkanSegmentOutletReference } from "./rscSegmentOutletReference";
|
|
@@ -118,7 +118,7 @@ export class RouteElementComposer {
|
|
|
118
118
|
searchParams: Record<string, string | string[]>;
|
|
119
119
|
}): Promise<Head | null | undefined> {
|
|
120
120
|
return (
|
|
121
|
-
await RouteElementComposer.
|
|
121
|
+
await RouteElementComposer.resolveHeadWithSnapshot({
|
|
122
122
|
pathRoute,
|
|
123
123
|
params,
|
|
124
124
|
searchParams,
|
|
@@ -126,7 +126,7 @@ export class RouteElementComposer {
|
|
|
126
126
|
).node;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
static async
|
|
129
|
+
static async resolveHeadWithSnapshot({
|
|
130
130
|
pathRoute,
|
|
131
131
|
params,
|
|
132
132
|
searchParams,
|
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
type RouteModuleSource,
|
|
27
27
|
resolveRouteModule,
|
|
28
28
|
} from "../client/route/resolveRouteModule";
|
|
29
|
-
import { resolveHeadExport, resolveMetadataHead } from "./metadata";
|
|
30
29
|
|
|
31
30
|
type RouteModuleKindWithOverrides = "page" | "layout" | "overrides";
|
|
32
31
|
|
|
@@ -295,18 +294,6 @@ export class RouteTreeBuilder {
|
|
|
295
294
|
if ("head" in mod && "generateHead" in mod) {
|
|
296
295
|
throw new Error(`[route-convention] head and generateHead cannot both be exported in ${key}`);
|
|
297
296
|
}
|
|
298
|
-
if (
|
|
299
|
-
!parsed.isInternalRootLayout &&
|
|
300
|
-
("head" in mod || "generateHead" in mod) &&
|
|
301
|
-
("metadata" in mod || "generateMetadata" in mod)
|
|
302
|
-
) {
|
|
303
|
-
throw new Error(
|
|
304
|
-
`[route-convention] head/generateHead and metadata/generateMetadata cannot both be exported in ${key}`,
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
if ("metadata" in mod && "generateMetadata" in mod) {
|
|
308
|
-
throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${key}`);
|
|
309
|
-
}
|
|
310
297
|
}
|
|
311
298
|
|
|
312
299
|
static #makeRouteRender(key: string, kind: "page" | "layout", loader: () => Promise<RouteModuleSource>): RouteRender {
|
|
@@ -336,17 +323,7 @@ export class RouteTreeBuilder {
|
|
|
336
323
|
routeRender.NotFound = layoutMod.NotFound;
|
|
337
324
|
routeRender.Error = layoutMod.Error;
|
|
338
325
|
}
|
|
339
|
-
|
|
340
|
-
const head = await mod.generateHead(props);
|
|
341
|
-
if (head !== null && head !== undefined) return resolveHeadExport(head, { includeHeadSnapshot: false });
|
|
342
|
-
}
|
|
343
|
-
if (mod.generateMetadata) {
|
|
344
|
-
const metadata = await mod.generateMetadata(props);
|
|
345
|
-
return metadata === null || metadata === undefined ? metadata : resolveMetadataHead(metadata);
|
|
346
|
-
}
|
|
347
|
-
if (mod.head !== undefined)
|
|
348
|
-
return mod.head === null ? null : resolveHeadExport(mod.head, { includeHeadSnapshot: false });
|
|
349
|
-
return mod.metadata === undefined ? undefined : resolveMetadataHead(mod.metadata);
|
|
326
|
+
return mod.generateHead ? await mod.generateHead(props) : mod.head;
|
|
350
327
|
},
|
|
351
328
|
};
|
|
352
329
|
if (kind === "page") {
|
package/server/rscWorker.tsx
CHANGED
|
@@ -36,13 +36,13 @@ import {
|
|
|
36
36
|
resolveRouteCacheStoreTtl,
|
|
37
37
|
shouldStoreRouteCache,
|
|
38
38
|
} from "./cachePolicy";
|
|
39
|
-
import { LogForwarder } from "./logging/logForwarder";
|
|
40
39
|
import {
|
|
41
40
|
createAkanLocaleAlternateHeadSnapshot,
|
|
42
41
|
mergeAkanHeadSnapshots,
|
|
43
42
|
renderAkanHeadSnapshot,
|
|
44
43
|
shouldRenderLocaleAlternates,
|
|
45
|
-
} from "./
|
|
44
|
+
} from "./head";
|
|
45
|
+
import { LogForwarder } from "./logging/logForwarder";
|
|
46
46
|
import { ProcessMetricsCollector } from "./processMetricsCollector";
|
|
47
47
|
import { RouteElementComposer } from "./routeElementComposer";
|
|
48
48
|
import {
|
|
@@ -1311,15 +1311,13 @@ export class RscRenderer {
|
|
|
1311
1311
|
if (!body) return null;
|
|
1312
1312
|
const routeHead =
|
|
1313
1313
|
"resolveHead" in route
|
|
1314
|
-
? await RouteElementComposer.
|
|
1314
|
+
? await RouteElementComposer.resolveHeadWithSnapshot({
|
|
1315
1315
|
pathRoute: route,
|
|
1316
1316
|
params,
|
|
1317
1317
|
searchParams,
|
|
1318
1318
|
})
|
|
1319
|
-
: { node: undefined
|
|
1320
|
-
const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead, {
|
|
1321
|
-
hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
|
|
1322
|
-
});
|
|
1319
|
+
: { node: undefined };
|
|
1320
|
+
const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead, {});
|
|
1323
1321
|
return (
|
|
1324
1322
|
<html lang={params.lang ?? RscRenderer.#getLocale(pathname, this.#i18n)} suppressHydrationWarning>
|
|
1325
1323
|
<head key="head">
|
|
@@ -1329,10 +1327,7 @@ export class RscRenderer {
|
|
|
1329
1327
|
{routeHeadSnapshot
|
|
1330
1328
|
? renderAkanHeadSnapshot(routeHeadSnapshot)
|
|
1331
1329
|
: (routeHead.node ?? this.#renderDefaultHead())}
|
|
1332
|
-
{
|
|
1333
|
-
shouldRenderLocaleAlternates({ hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates })
|
|
1334
|
-
? this.#renderLocaleAlternates(url)
|
|
1335
|
-
: null}
|
|
1330
|
+
{routeHeadSnapshot ? null : this.#renderLocaleAlternates(url)}
|
|
1336
1331
|
{this.#renderStylesheet(pathname)}
|
|
1337
1332
|
</head>
|
|
1338
1333
|
<body key="body">{body}</body>
|
|
@@ -1353,14 +1348,13 @@ export class RscRenderer {
|
|
|
1353
1348
|
basePath: this.#getBasePath(url),
|
|
1354
1349
|
});
|
|
1355
1350
|
setRequestFrameState(pathRoute.pageState);
|
|
1356
|
-
const routeHead = await RouteElementComposer.
|
|
1351
|
+
const routeHead = await RouteElementComposer.resolveHeadWithSnapshot({
|
|
1357
1352
|
pathRoute,
|
|
1358
1353
|
params: match.params,
|
|
1359
1354
|
searchParams,
|
|
1360
1355
|
});
|
|
1361
1356
|
const routeHeadSnapshot = this.#createRouteHeadSnapshot(url, routeHead, {
|
|
1362
1357
|
isSpecialRoute: pathRoute.isSpecialRoute,
|
|
1363
|
-
hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
|
|
1364
1358
|
});
|
|
1365
1359
|
const body = RouteElementComposer.compose({
|
|
1366
1360
|
pathRoute,
|
|
@@ -1377,11 +1371,7 @@ export class RscRenderer {
|
|
|
1377
1371
|
{routeHeadSnapshot
|
|
1378
1372
|
? renderAkanHeadSnapshot(routeHeadSnapshot)
|
|
1379
1373
|
: (routeHead.node ?? this.#renderDefaultHead())}
|
|
1380
|
-
{!routeHeadSnapshot &&
|
|
1381
|
-
shouldRenderLocaleAlternates({
|
|
1382
|
-
isSpecialRoute: pathRoute.isSpecialRoute,
|
|
1383
|
-
hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
|
|
1384
|
-
})
|
|
1374
|
+
{!routeHeadSnapshot && shouldRenderLocaleAlternates({ isSpecialRoute: pathRoute.isSpecialRoute })
|
|
1385
1375
|
? this.#renderLocaleAlternates(url)
|
|
1386
1376
|
: null}
|
|
1387
1377
|
{this.#renderStylesheet(url.pathname)}
|
|
@@ -1470,21 +1460,20 @@ export class RscRenderer {
|
|
|
1470
1460
|
match: { pathRoute: PathRoute; params: Record<string, string> },
|
|
1471
1461
|
searchParams: Record<string, string | string[]>,
|
|
1472
1462
|
): Promise<ResolvedHead["headSnapshot"]> {
|
|
1473
|
-
const routeHead = await RouteElementComposer.
|
|
1463
|
+
const routeHead = await RouteElementComposer.resolveHeadWithSnapshot({
|
|
1474
1464
|
pathRoute: match.pathRoute,
|
|
1475
1465
|
params: match.params,
|
|
1476
1466
|
searchParams,
|
|
1477
1467
|
});
|
|
1478
1468
|
return this.#createRouteHeadSnapshot(url, routeHead, {
|
|
1479
1469
|
isSpecialRoute: match.pathRoute.isSpecialRoute,
|
|
1480
|
-
hasExplicitLanguageAlternates: routeHead.hasExplicitLanguageAlternates,
|
|
1481
1470
|
});
|
|
1482
1471
|
}
|
|
1483
1472
|
|
|
1484
1473
|
#createRouteHeadSnapshot(
|
|
1485
1474
|
url: URL,
|
|
1486
1475
|
routeHead: ResolvedHead,
|
|
1487
|
-
options: { isSpecialRoute?: boolean
|
|
1476
|
+
options: { isSpecialRoute?: boolean },
|
|
1488
1477
|
): ResolvedHead["headSnapshot"] {
|
|
1489
1478
|
if (!routeHead.headSnapshot) return undefined;
|
|
1490
1479
|
return mergeAkanHeadSnapshots(
|
package/server/webRouter.ts
CHANGED
|
@@ -1209,18 +1209,22 @@ export class WebRouter {
|
|
|
1209
1209
|
if (details.length === 0) return null;
|
|
1210
1210
|
return WebRouter.#jsonResponse({ applinks: { apps: [], details } }, cacheControl);
|
|
1211
1211
|
}
|
|
1212
|
+
|
|
1213
|
+
const packageSuffixes = process.env.AKAN_PUBLIC_ENV === "main" ? [""] : ["", ".debug"];
|
|
1212
1214
|
const assetLinks = associations
|
|
1213
1215
|
.filter(
|
|
1214
1216
|
(association) => association.domains.length > 0 && (association.androidSha256CertFingerprints?.length ?? 0) > 0,
|
|
1215
1217
|
)
|
|
1216
|
-
.
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1218
|
+
.flatMap((association) =>
|
|
1219
|
+
packageSuffixes.map((suffix) => ({
|
|
1220
|
+
relation: ["delegate_permission/common.handle_all_urls"],
|
|
1221
|
+
target: {
|
|
1222
|
+
namespace: "android_app",
|
|
1223
|
+
package_name: `${association.appId}${suffix}`,
|
|
1224
|
+
sha256_cert_fingerprints: association.androidSha256CertFingerprints,
|
|
1225
|
+
},
|
|
1226
|
+
})),
|
|
1227
|
+
);
|
|
1224
1228
|
if (assetLinks.length === 0) return null;
|
|
1225
1229
|
return WebRouter.#jsonResponse(assetLinks, cacheControl);
|
|
1226
1230
|
}
|