akanjs 3.0.0-alpha.67 → 3.0.0-alpha.69
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/common/mcpExposure.ts +14 -1
- package/constant/crystalize.ts +45 -1
- package/constant/deserialize.ts +3 -2
- package/constant/types.ts +5 -1
- package/dictionary/base.dictionary.ts +18 -0
- package/fetch/client/fetchClient.ts +23 -2
- package/fetch/client/httpClient.ts +77 -9
- package/fetch/fetchType/appliedReturn.type.ts +48 -44
- package/fetch/fetchType/endpointFetch.type.ts +6 -3
- package/fetch/fetchType/sliceFetch.type.ts +27 -50
- package/index.ts +25 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +2 -2
- package/server/akanApp.ts +93 -24
- package/server/akanAppHeaders.ts +2 -0
- package/server/assetEncoding.ts +4 -24
- package/server/console.ts +9 -101
- package/server/consoleEvaluator.ts +51 -0
- package/server/consolePasteFilter.ts +91 -0
- package/server/consoleSession.ts +198 -0
- package/server/contentEncoding.ts +83 -0
- package/server/imageOptimizer.ts +122 -12
- package/server/mcp/McpRouter.ts +28 -2
- package/server/routing/apiRouter.ts +35 -11
- package/server/semaphore.ts +22 -0
- package/server/types.tsx +3 -0
- package/signal/mcp/McpDocument.ts +43 -3
- package/signal/mcp/McpUriTemplate.ts +1 -6
- package/signal/serializer/fetch.serializer.ts +14 -0
- package/signal/signalContext.ts +164 -33
- package/signal/slice.ts +32 -1
- package/signal/types.ts +34 -3
- package/store/action.ts +13 -13
- package/types/common/mcpExposure.d.ts +5 -1
- package/types/constant/constantRegistry.d.ts +2 -10
- package/types/constant/crystalize.d.ts +2 -0
- package/types/constant/types.d.ts +3 -3
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +3 -3
- package/types/fetch/agentTurn.d.ts +1 -119
- package/types/fetch/fetchType/appliedReturn.type.d.ts +21 -18
- package/types/fetch/fetchType/endpointFetch.type.d.ts +2 -1
- package/types/fetch/fetchType/sliceFetch.type.d.ts +11 -15
- package/types/index.d.ts +23 -0
- package/types/server/assetEncoding.d.ts +1 -1
- package/types/server/console.d.ts +1 -1
- package/types/server/consoleEvaluator.d.ts +2 -0
- package/types/server/consolePasteFilter.d.ts +11 -0
- package/types/server/consoleSession.d.ts +12 -0
- package/types/server/contentEncoding.d.ts +28 -0
- package/types/server/mcp/McpRouter.d.ts +2 -0
- package/types/server/semaphore.d.ts +5 -0
- package/types/server/types.d.ts +2 -0
- package/types/signal/mcp/McpDocument.d.ts +19 -0
- package/types/signal/mcp/McpUriTemplate.d.ts +0 -1
- package/types/signal/signalContext.d.ts +8 -1
- package/types/signal/slice.d.ts +25 -0
- package/types/signal/types.d.ts +33 -3
- package/types/store/action.d.ts +7 -5
- package/types/store/baseSt.d.ts +2 -6
- package/types/ui/System/Client.d.ts +1 -2
- package/types/ui/System/Common.d.ts +3 -4
- package/ui/Load/Units.tsx +4 -2
- package/ui/Signal/Doc.tsx +1 -1
- package/ui/Signal/RestApi.tsx +1 -1
- package/ui/System/Client.tsx +6 -2
- package/ui/System/Common.tsx +3 -4
package/common/mcpExposure.ts
CHANGED
|
@@ -16,9 +16,13 @@ export interface McpExposureEndpoint {
|
|
|
16
16
|
args: { name: string; refName: string; type: string; arrDepth?: number; nullable?: boolean }[];
|
|
17
17
|
guards?: string[];
|
|
18
18
|
fileUpload?: boolean;
|
|
19
|
+
mcp?: boolean;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export interface McpExposureOption {
|
|
23
|
+
/** The model the endpoint belongs to and the name it is published under: one rule reads the key, not the shape. */
|
|
24
|
+
refName: string;
|
|
25
|
+
key: string;
|
|
22
26
|
/**
|
|
23
27
|
* The read-only deployment valve, which is server configuration. The browser explorer cannot know it and so
|
|
24
28
|
* badges what the code decided; the boot log is where a read-only deployment says what it dropped.
|
|
@@ -56,10 +60,19 @@ export const mcpHintsOf = (key: string, endpoint: { type: string }) => {
|
|
|
56
60
|
};
|
|
57
61
|
|
|
58
62
|
/** The sentence explaining why this endpoint is not in the catalogue, or `null` when it is. */
|
|
59
|
-
export const mcpRefusalOf = (
|
|
63
|
+
export const mcpRefusalOf = (
|
|
64
|
+
endpoint: McpExposureEndpoint,
|
|
65
|
+
{ refName, key, readOnly }: McpExposureOption,
|
|
66
|
+
): string | null => {
|
|
67
|
+
|
|
68
|
+
if (endpoint.mcp === false)
|
|
69
|
+
return "it declares `mcp: false`, so it is deliberately off the agent shelf. HTTP still serves it.";
|
|
60
70
|
|
|
61
71
|
if (!endpoint.guards?.length)
|
|
62
72
|
return "it declares no guards, and exposure follows them — write `guards: [Public]` if anonymous access is the intent.";
|
|
73
|
+
|
|
74
|
+
if (key === `light${capitalize(refName)}`)
|
|
75
|
+
return `it reads the same document as \`${refName}\` in a smaller shape — call \`${refName}\` instead.`;
|
|
63
76
|
if (endpoint.type === "prompt") return mcpPromptRefusalOf(endpoint);
|
|
64
77
|
if (endpoint.type === "pubsub" || endpoint.type === "message")
|
|
65
78
|
return `\`${endpoint.type}\` rides the websocket, and its internal arguments read a socket an MCP request does not have.`;
|
package/constant/crystalize.ts
CHANGED
|
@@ -11,6 +11,50 @@ import type { FieldProps } from ".";
|
|
|
11
11
|
|
|
12
12
|
export type CrystalizeFunc<Model> = (self: GetStateObject<Model>, isChild?: boolean) => Model;
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* The relation instances one hydration pass has already built, by model class and then document id.
|
|
16
|
+
*
|
|
17
|
+
* A listing hands the same relation to every row that references it — twenty users wearing one avatar — and
|
|
18
|
+
* each row would otherwise build its own copy of it, re-parsing every `Date` on the way. Sharing is safe
|
|
19
|
+
* because a constant model is `[immerable]`: a store write copies before it mutates.
|
|
20
|
+
*
|
|
21
|
+
* Module-scoped rather than passed down, because `crystalize` is reached through a model's own constructor,
|
|
22
|
+
* which has nowhere to carry it. A pass is wholly synchronous, so no other one can observe this mid-flight.
|
|
23
|
+
*/
|
|
24
|
+
let sharedInstances: Map<Cls, Map<string, object>> | null = null;
|
|
25
|
+
|
|
26
|
+
/** Runs one hydration pass, sharing a relation instance across every value in it that names the same id. */
|
|
27
|
+
export const withSharedInstances = <T>(hydrate: () => T): T => {
|
|
28
|
+
if (sharedInstances) return hydrate();
|
|
29
|
+
sharedInstances = new Map();
|
|
30
|
+
try {
|
|
31
|
+
return hydrate();
|
|
32
|
+
} finally {
|
|
33
|
+
sharedInstances = null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type ModelCls = Cls<{ set: (obj: object) => object }>;
|
|
38
|
+
|
|
39
|
+
const relationIdOf = (value: object): string | null => {
|
|
40
|
+
const id = (value as { id?: unknown }).id;
|
|
41
|
+
return typeof id === "string" && id ? id : null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const crystalizeModel = (field: FieldProps, value: object): object => {
|
|
45
|
+
const modelRef = field.modelRef as ModelCls;
|
|
46
|
+
|
|
47
|
+
if (value instanceof modelRef) return value;
|
|
48
|
+
|
|
49
|
+
const id = field.isScalar ? null : relationIdOf(value);
|
|
50
|
+
if (!sharedInstances || !id) return new modelRef().set(value);
|
|
51
|
+
const byId = sharedInstances.get(modelRef) ?? new Map<string, object>();
|
|
52
|
+
if (!sharedInstances.has(modelRef)) sharedInstances.set(modelRef, byId);
|
|
53
|
+
const shared = byId.get(id) ?? new modelRef().set(value);
|
|
54
|
+
byId.set(id, shared);
|
|
55
|
+
return shared;
|
|
56
|
+
};
|
|
57
|
+
|
|
14
58
|
export const crystalize = (field: FieldProps, value: unknown): unknown => {
|
|
15
59
|
if (value === undefined || value === null) return value as undefined | null;
|
|
16
60
|
if (field.isArray && Array.isArray(value))
|
|
@@ -41,7 +85,7 @@ export const crystalize = (field: FieldProps, value: unknown): unknown => {
|
|
|
41
85
|
]),
|
|
42
86
|
);
|
|
43
87
|
}
|
|
44
|
-
if (field.isClass) return
|
|
88
|
+
if (field.isClass) return crystalizeModel(field, value as object);
|
|
45
89
|
if (field.modelRef === Date) return dayjs(value as Date);
|
|
46
90
|
return crystalizeValue(value);
|
|
47
91
|
};
|
package/constant/deserialize.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Any, applyFnToArrayObjects, type Cls, FIELD_META, PrimitiveRegistry, type PrimitiveScalar } from "akanjs/base";
|
|
2
2
|
|
|
3
|
-
import { type ConstantCls, type ConstantModelRef, ConstantRegistry, type FieldProps } from ".";
|
|
3
|
+
import { type ConstantCls, type ConstantModelRef, ConstantRegistry, type FieldProps, withSharedInstances } from ".";
|
|
4
4
|
|
|
5
5
|
const getDeserializeFn = (inputRef: ConstantModelRef | PrimitiveScalar) => {
|
|
6
6
|
const deserializeFn = PrimitiveRegistry.has(inputRef as Cls)
|
|
@@ -66,5 +66,6 @@ export const deserialize = (
|
|
|
66
66
|
if (nullable && (value === null || value === undefined)) return null;
|
|
67
67
|
else if (!nullable && (value === null || value === undefined) && argRef !== Any)
|
|
68
68
|
throw new Error(`Invalid Value (Nullable) in ${key} ${argRef} for value ${value}`);
|
|
69
|
-
|
|
69
|
+
|
|
70
|
+
return withSharedInstances(() => deserializeInput(value, argRef, arrDepth, convertFn)) as object[];
|
|
70
71
|
};
|
package/constant/types.ts
CHANGED
|
@@ -30,7 +30,11 @@ export type DocumentModel<T> = unknown extends T
|
|
|
30
30
|
: Docify<T>;
|
|
31
31
|
|
|
32
32
|
export type FieldState<T> = T extends { id: string } ? T | null : T;
|
|
33
|
-
export type DefaultOf<S> =
|
|
33
|
+
export type DefaultOf<S> = {
|
|
34
|
+
[K in keyof S as S[K] extends (...args: never[]) => unknown ? never : K extends "prototype" ? never : K]: FieldState<
|
|
35
|
+
S[K]
|
|
36
|
+
>;
|
|
37
|
+
};
|
|
34
38
|
|
|
35
39
|
export type DefaultOfSchema<Schema, RelationKey = never> = [RelationKey] extends [never]
|
|
36
40
|
? Schema
|
|
@@ -27,6 +27,24 @@ export const baseDictionary = serviceDictionary(["en", "ko"])
|
|
|
27
27
|
})),
|
|
28
28
|
pubsubPing: fn(["Pubsub Ping", "Pubsub Ping"]),
|
|
29
29
|
}))
|
|
30
|
+
.error({
|
|
31
|
+
serverUnreachable: [
|
|
32
|
+
"Cannot reach the server. Check your connection and try again.",
|
|
33
|
+
"서버에 연결할 수 없습니다. 네트워크 상태를 확인한 뒤 다시 시도해주세요.",
|
|
34
|
+
],
|
|
35
|
+
serverUnavailable: [
|
|
36
|
+
"The server is restarting. Please try again in a moment.",
|
|
37
|
+
"서버가 재시작 중입니다. 잠시 후 다시 시도해주세요.",
|
|
38
|
+
],
|
|
39
|
+
gatewayTimeout: [
|
|
40
|
+
"The server took too long to answer. Please try again in a moment.",
|
|
41
|
+
"서버 응답이 지연되고 있습니다. 잠시 후 다시 시도해주세요.",
|
|
42
|
+
],
|
|
43
|
+
unexpectedResponse: [
|
|
44
|
+
"The server returned an unexpected response ({status}).",
|
|
45
|
+
"서버가 예상하지 못한 응답을 보냈습니다. ({status})",
|
|
46
|
+
],
|
|
47
|
+
})
|
|
30
48
|
.translate({
|
|
31
49
|
somethingWrong: ["Something's wrong!", "문제가 생겼어요!"],
|
|
32
50
|
connecting: ["Connecting...", "연결 중..."],
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { DataList, getEnv, PrimitiveRegistry, type PromiseOrObject } from "akanjs/base";
|
|
2
2
|
import { capitalize, type FetchPolicy, fileUploadContract, Logger, resolveFileUploadCapability } from "akanjs/common";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type BaseInsight,
|
|
5
|
+
type BaseObject,
|
|
6
|
+
ConstantRegistry,
|
|
7
|
+
deserialize,
|
|
8
|
+
serialize,
|
|
9
|
+
withSharedInstances,
|
|
10
|
+
} from "akanjs/constant";
|
|
4
11
|
import type {
|
|
5
12
|
DatabaseSignal,
|
|
6
13
|
SerializedArg,
|
|
@@ -436,6 +443,9 @@ export class FetchClient {
|
|
|
436
443
|
const createGuards = signal.createGuards ?? signal.cruGuards;
|
|
437
444
|
const updateGuards = signal.updateGuards ?? signal.cruGuards;
|
|
438
445
|
const removeGuards = signal.removeGuards ?? signal.cruGuards;
|
|
446
|
+
|
|
447
|
+
const mcp = (verb: keyof NonNullable<SerializedSignal["mcp"]>) =>
|
|
448
|
+
signal.mcp?.[verb] === false ? { mcp: false as const } : {};
|
|
439
449
|
const endpoint: { [key: string]: SerializedEndpoint } = {};
|
|
440
450
|
if (signal.getGuards) {
|
|
441
451
|
endpoint[names.model] = {
|
|
@@ -443,12 +453,14 @@ export class FetchClient {
|
|
|
443
453
|
args: [{ type: "param", name: names.modelId, refName: "ID" }],
|
|
444
454
|
returns: { refName, modelType: "full" },
|
|
445
455
|
guards: signal.getGuards,
|
|
456
|
+
...mcp("get"),
|
|
446
457
|
};
|
|
447
458
|
endpoint[names.lightModel] = {
|
|
448
459
|
type: "query",
|
|
449
460
|
args: [{ type: "param", name: names.modelId, refName: "ID" }],
|
|
450
461
|
returns: { refName, modelType: "light" },
|
|
451
462
|
guards: signal.getGuards,
|
|
463
|
+
...mcp("get"),
|
|
452
464
|
};
|
|
453
465
|
}
|
|
454
466
|
if (createGuards) {
|
|
@@ -457,6 +469,7 @@ export class FetchClient {
|
|
|
457
469
|
args: [{ type: "body", name: "data", refName, modelType: "input" }],
|
|
458
470
|
returns: { refName, modelType: "full" },
|
|
459
471
|
guards: createGuards,
|
|
472
|
+
...mcp("create"),
|
|
460
473
|
};
|
|
461
474
|
}
|
|
462
475
|
if (updateGuards) {
|
|
@@ -468,6 +481,7 @@ export class FetchClient {
|
|
|
468
481
|
],
|
|
469
482
|
returns: { refName, modelType: "full" },
|
|
470
483
|
guards: updateGuards,
|
|
484
|
+
...mcp("update"),
|
|
471
485
|
};
|
|
472
486
|
}
|
|
473
487
|
if (removeGuards) {
|
|
@@ -476,6 +490,7 @@ export class FetchClient {
|
|
|
476
490
|
args: [{ type: "param", name: names.modelId, refName: "ID" }],
|
|
477
491
|
returns: { refName, modelType: "full" },
|
|
478
492
|
guards: removeGuards,
|
|
493
|
+
...mcp("remove"),
|
|
479
494
|
};
|
|
480
495
|
}
|
|
481
496
|
return endpoint;
|
|
@@ -595,18 +610,22 @@ export class FetchClient {
|
|
|
595
610
|
list: `${refName}List${capSuffix}`,
|
|
596
611
|
insight: `${refName}Insight${capSuffix}`,
|
|
597
612
|
};
|
|
613
|
+
|
|
614
|
+
const mcp = slice.mcp === false ? { mcp: false as const } : {};
|
|
598
615
|
const endpoint: { [key: string]: SerializedEndpoint } = {
|
|
599
616
|
[names.list]: {
|
|
600
617
|
type: "query",
|
|
601
618
|
args: [...slice.args, ...FetchClient.paginationArgs],
|
|
602
619
|
returns: { refName, modelType: "light", arrDepth: 1 },
|
|
603
620
|
guards: slice.guards,
|
|
621
|
+
...mcp,
|
|
604
622
|
},
|
|
605
623
|
[names.insight]: {
|
|
606
624
|
type: "query",
|
|
607
625
|
args: [...slice.args],
|
|
608
626
|
returns: { refName, modelType: "insight" },
|
|
609
627
|
guards: slice.guards,
|
|
628
|
+
...mcp,
|
|
610
629
|
},
|
|
611
630
|
};
|
|
612
631
|
return endpoint;
|
|
@@ -646,7 +665,9 @@ export class FetchClient {
|
|
|
646
665
|
listFn(...fetchQueryArgs, skip, limit, sort, { ...option, crystalize: false }),
|
|
647
666
|
fetchInsight ? insightFn(...fetchQueryArgs, { ...option, crystalize: false }) : null,
|
|
648
667
|
])) as unknown as [BaseObject[], BaseInsight];
|
|
649
|
-
const modelList = new DataList(
|
|
668
|
+
const modelList = new DataList(
|
|
669
|
+
withSharedInstances(() => modelObjList.map((modelObj) => new cnst.light(modelObj))),
|
|
670
|
+
);
|
|
650
671
|
const modelInsight = new cnst.insight(modelObjInsight);
|
|
651
672
|
const lastPage = modelObjInsight?.count
|
|
652
673
|
? Math.max(Math.floor((modelObjInsight.count - 1) / (limit || 20)) + 1, 1)
|
|
@@ -27,6 +27,19 @@ interface FetchOptions {
|
|
|
27
27
|
baseUrl?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const jsonContentType = /^application\/(?:[\w.+-]+\+)?json\b/i;
|
|
31
|
+
|
|
32
|
+
const transportErrorKeyMap = {
|
|
33
|
+
408: "base.error.gatewayTimeout",
|
|
34
|
+
502: "base.error.serverUnavailable",
|
|
35
|
+
503: "base.error.serverUnavailable",
|
|
36
|
+
504: "base.error.gatewayTimeout",
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
const serverUnreachableKey = "base.error.serverUnreachable";
|
|
40
|
+
const unexpectedResponseKey = "base.error.unexpectedResponse";
|
|
41
|
+
const transportDetailLimit = 200;
|
|
42
|
+
|
|
30
43
|
export class HttpClient {
|
|
31
44
|
readonly baseUrl: string;
|
|
32
45
|
constructor(
|
|
@@ -42,11 +55,17 @@ export class HttpClient {
|
|
|
42
55
|
#resolveBaseUrl(baseUrl?: string) {
|
|
43
56
|
return (baseUrl ?? this.baseUrl).replace(/\/$/, "");
|
|
44
57
|
}
|
|
58
|
+
#resolveUrl(url: string, options: FetchOptions) {
|
|
59
|
+
return `${this.#resolveBaseUrl(options.baseUrl)}${url}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static #makeHeaders(headers: Record<string, string>, options: FetchOptions) {
|
|
63
|
+
return { Accept: "application/json", ...headers, ...options.headers };
|
|
64
|
+
}
|
|
45
65
|
async get<Returns = unknown>(url: string, options: FetchOptions = {}): Promise<Returns> {
|
|
46
|
-
|
|
47
|
-
headers: { "Content-Type": "application/json",
|
|
66
|
+
return await this.#request<Returns>(this.#resolveUrl(url, options), {
|
|
67
|
+
headers: HttpClient.#makeHeaders({ "Content-Type": "application/json" }, options),
|
|
48
68
|
});
|
|
49
|
-
return await this.#readJsonResponse<Returns>(res);
|
|
50
69
|
}
|
|
51
70
|
#makeReqContent(data: FormData | Record<string, unknown>): { body: BodyInit; headers: Record<string, string> } {
|
|
52
71
|
|
|
@@ -60,12 +79,11 @@ export class HttpClient {
|
|
|
60
79
|
options: FetchOptions = {},
|
|
61
80
|
): Promise<Returns> {
|
|
62
81
|
const { body, headers } = this.#makeReqContent(data);
|
|
63
|
-
|
|
82
|
+
return await this.#request<Returns>(this.#resolveUrl(url, options), {
|
|
64
83
|
method,
|
|
65
84
|
body,
|
|
66
|
-
headers:
|
|
85
|
+
headers: HttpClient.#makeHeaders(headers, options),
|
|
67
86
|
});
|
|
68
|
-
return await this.#readJsonResponse<Returns>(res);
|
|
69
87
|
}
|
|
70
88
|
async put<Returns = unknown>(
|
|
71
89
|
url: string,
|
|
@@ -82,19 +100,69 @@ export class HttpClient {
|
|
|
82
100
|
return await this.send<Returns>("POST", url, data, options);
|
|
83
101
|
}
|
|
84
102
|
async delete<Returns = unknown>(url: string, options: FetchOptions = {}): Promise<Returns> {
|
|
85
|
-
|
|
103
|
+
return await this.#request<Returns>(this.#resolveUrl(url, options), {
|
|
86
104
|
method: "DELETE",
|
|
87
|
-
headers: { "Content-Type": "application/json",
|
|
105
|
+
headers: HttpClient.#makeHeaders({ "Content-Type": "application/json" }, options),
|
|
88
106
|
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async #request<Returns>(url: string, init: RequestInit): Promise<Returns> {
|
|
110
|
+
const res = await this.#fetch(url, init);
|
|
89
111
|
return await this.#readJsonResponse<Returns>(res);
|
|
90
112
|
}
|
|
91
113
|
|
|
114
|
+
/**
|
|
115
|
+
* `fetch` rejects only when no response arrived at all — a refused connection, a DNS failure, a socket
|
|
116
|
+
* dropped mid-flight. That is the server being unreachable, not an error this API reported, so it is
|
|
117
|
+
* restored as one rather than surfacing the runtime's own `TypeError: Failed to fetch`.
|
|
118
|
+
*/
|
|
119
|
+
async #fetch(url: string, init: RequestInit) {
|
|
120
|
+
try {
|
|
121
|
+
return await fetch(url, init);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
124
|
+
throw this.#restoreError({ error: serverUnreachableKey, details: String(error) }, 503);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
92
128
|
async #readJsonResponse<Returns>(res: Response): Promise<Returns> {
|
|
93
|
-
const body = await res
|
|
129
|
+
const body = await this.#readBody(res);
|
|
94
130
|
if (res.ok) return body as Returns;
|
|
95
131
|
throw this.#restoreError(body, res.status);
|
|
96
132
|
}
|
|
97
133
|
|
|
134
|
+
/**
|
|
135
|
+
* A proxy answers a restarting upstream with a page of its own — nginx's `504 Gateway Time-out` HTML,
|
|
136
|
+
* the federation gateway's plain-text 503. That body is not this API's, so parsing it would surface the
|
|
137
|
+
* parser's complaint (`Unexpected token '<'`) instead of the fact that the server is down.
|
|
138
|
+
*/
|
|
139
|
+
async #readBody(res: Response) {
|
|
140
|
+
if (jsonContentType.test(res.headers.get("content-type") ?? "")) {
|
|
141
|
+
try {
|
|
142
|
+
return (await res.json()) as unknown;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw this.#transportError(res.status, String(error));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const raw = await res.text();
|
|
148
|
+
const parsed = HttpClient.#parseJson(raw);
|
|
149
|
+
if (parsed === undefined) throw this.#transportError(res.status, raw);
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static #parseJson(raw: string): unknown {
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(raw) as unknown;
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#transportError(status: number, detail: string): RestoredError {
|
|
162
|
+
const error = transportErrorKeyMap[status as keyof typeof transportErrorKeyMap] ?? unexpectedResponseKey;
|
|
163
|
+
return this.#restoreError({ error, data: { status }, details: detail.slice(0, transportDetailLimit) }, status);
|
|
164
|
+
}
|
|
165
|
+
|
|
98
166
|
#restoreError(body: unknown, fallbackStatusCode: number): RestoredError {
|
|
99
167
|
const payload =
|
|
100
168
|
body && typeof body === "object" && "error" in body
|
|
@@ -25,33 +25,45 @@ export interface QuerySetting {
|
|
|
25
25
|
queryArgs?: unknown[] | (() => unknown[]);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
type ServerInitShape<
|
|
29
29
|
RefName extends string,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
_LightObj = GetStateObject<Light>,
|
|
36
|
-
_InsightObj = GetStateObject<Insight>,
|
|
37
|
-
_Sort = ExtractSort<Filter>,
|
|
30
|
+
QueryArgs,
|
|
31
|
+
CapRefName extends string,
|
|
32
|
+
LightObj,
|
|
33
|
+
InsightObj,
|
|
34
|
+
Sort,
|
|
38
35
|
> = SliceMeta & {
|
|
39
|
-
[K in `${RefName}ObjList`]:
|
|
36
|
+
[K in `${RefName}ObjList`]: LightObj[];
|
|
40
37
|
} & {
|
|
41
|
-
[K in `${RefName}ObjInsight`]:
|
|
38
|
+
[K in `${RefName}ObjInsight`]: InsightObj;
|
|
42
39
|
} & {
|
|
43
|
-
[K in `pageOf${
|
|
40
|
+
[K in `pageOf${CapRefName}`]: number;
|
|
44
41
|
} & {
|
|
45
|
-
[K in `lastPageOf${
|
|
42
|
+
[K in `lastPageOf${CapRefName}`]: number;
|
|
46
43
|
} & {
|
|
47
|
-
[K in `limitOf${
|
|
44
|
+
[K in `limitOf${CapRefName}`]: number;
|
|
48
45
|
} & {
|
|
49
|
-
[K in `queryArgsOf${
|
|
46
|
+
[K in `queryArgsOf${CapRefName}`]: QueryArgs;
|
|
50
47
|
} & {
|
|
51
|
-
[K in `sortOf${
|
|
48
|
+
[K in `sortOf${CapRefName}`]: Sort;
|
|
52
49
|
} & {
|
|
53
50
|
[K in `${RefName}InitAt`]: Date;
|
|
54
51
|
};
|
|
52
|
+
|
|
53
|
+
export type ServerInit<
|
|
54
|
+
RefName extends string,
|
|
55
|
+
Light,
|
|
56
|
+
Insight = any,
|
|
57
|
+
QueryArgs = any,
|
|
58
|
+
Filter extends FilterInstance = any,
|
|
59
|
+
> = ServerInitShape<
|
|
60
|
+
RefName,
|
|
61
|
+
QueryArgs,
|
|
62
|
+
Capitalize<RefName>,
|
|
63
|
+
GetStateObject<Light>,
|
|
64
|
+
GetStateObject<Insight>,
|
|
65
|
+
ExtractSort<Filter>
|
|
66
|
+
>;
|
|
55
67
|
/** Client/server-friendly return type for initialized list and insight data. */
|
|
56
68
|
export type ClientInit<
|
|
57
69
|
RefName extends string,
|
|
@@ -59,13 +71,7 @@ export type ClientInit<
|
|
|
59
71
|
Insight = any,
|
|
60
72
|
QueryArgs = any,
|
|
61
73
|
Filter extends FilterInstance = any,
|
|
62
|
-
|
|
63
|
-
_LightObj = GetStateObject<Light>,
|
|
64
|
-
_InsightObj = GetStateObject<Insight>,
|
|
65
|
-
_Sort = ExtractSort<Filter>,
|
|
66
|
-
> = PromiseOrObject<
|
|
67
|
-
ServerInit<RefName, Light, Insight, QueryArgs, Filter, _CapitalizedRefName, _LightObj, _InsightObj, _Sort>
|
|
68
|
-
>;
|
|
74
|
+
> = PromiseOrObject<ServerInit<RefName, Light, Insight, QueryArgs, Filter>>;
|
|
69
75
|
|
|
70
76
|
export type ServerView<RefName extends string, Model> = { refName: RefName } & {
|
|
71
77
|
[K in `${RefName}Obj`]: GetStateObject<Model>;
|
|
@@ -94,6 +100,18 @@ export type EditReturn<RefName extends string, Full> = {
|
|
|
94
100
|
[K in `${RefName}Edit`]: ServerEdit<RefName, Full>;
|
|
95
101
|
};
|
|
96
102
|
|
|
103
|
+
type InitReturnShape<
|
|
104
|
+
RefName extends string,
|
|
105
|
+
CapSuffix extends string,
|
|
106
|
+
Init,
|
|
107
|
+
ListItem extends { id: string },
|
|
108
|
+
Insight,
|
|
109
|
+
> = {
|
|
110
|
+
[K in `${RefName}Init${CapSuffix}`]: Init;
|
|
111
|
+
} & {
|
|
112
|
+
[K in `${RefName}List${CapSuffix}`]: DataList<ListItem>;
|
|
113
|
+
} & { [K in `${RefName}Insight${CapSuffix}`]: Insight };
|
|
114
|
+
|
|
97
115
|
export type InitReturn<
|
|
98
116
|
RefName extends string,
|
|
99
117
|
Suffix extends string,
|
|
@@ -101,25 +119,11 @@ export type InitReturn<
|
|
|
101
119
|
Insight,
|
|
102
120
|
Args,
|
|
103
121
|
Filter extends FilterInstance,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
[K in `${RefName}Init${_CapitalizedSuffix}`]: ServerInit<
|
|
112
|
-
RefName,
|
|
113
|
-
Light,
|
|
114
|
-
Insight,
|
|
115
|
-
Args,
|
|
116
|
-
Filter,
|
|
117
|
-
_CapitalizedRefName,
|
|
118
|
-
_LightObj,
|
|
119
|
-
_InsightObj,
|
|
120
|
-
_Sort
|
|
121
|
-
>;
|
|
122
|
-
} & {
|
|
123
|
-
[K in `${RefName}List${_CapitalizedSuffix}`]: DataList<_Light>;
|
|
124
|
-
} & { [K in `${RefName}Insight${_CapitalizedSuffix}`]: Insight };
|
|
122
|
+
> = InitReturnShape<
|
|
123
|
+
RefName,
|
|
124
|
+
Capitalize<Suffix>,
|
|
125
|
+
ServerInit<RefName, Light, Insight, Args, Filter>,
|
|
126
|
+
Light extends { id: string } ? Light : { id: string },
|
|
127
|
+
Insight
|
|
128
|
+
>;
|
|
125
129
|
|
|
@@ -33,9 +33,12 @@ type EndpointClientReturns<E, SlceCls extends SliceCls | never> = [SlceCls] exte
|
|
|
33
33
|
SlceCnstInsight<SlceCls>
|
|
34
34
|
>;
|
|
35
35
|
|
|
36
|
-
type
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
type OrNull<T, Nullable> = Nullable extends true ? T | null : T;
|
|
37
|
+
|
|
38
|
+
type EndpInfoReturns<E, SlceCls extends SliceCls | never> = OrNull<
|
|
39
|
+
EndpointClientReturns<E, SlceCls>,
|
|
40
|
+
EndpInfoNullable<E>
|
|
41
|
+
>;
|
|
39
42
|
|
|
40
43
|
type QueryOrMutationFetchFn<E, SlceCls extends SliceCls | never> = (
|
|
41
44
|
...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SLICE_META } from "akanjs/base";
|
|
2
2
|
import type { FetchPolicy } from "akanjs/common";
|
|
3
3
|
import type { ConstantModel, DefaultOf, ProtoFile, PurifiedModel } from "akanjs/constant";
|
|
4
4
|
import type { DatabaseModel, ExtractSort, FilterInstance } from "akanjs/document";
|
|
5
5
|
import type {
|
|
6
6
|
SlceCnstCapitalizedRefName,
|
|
7
|
-
SlceCnstDefaultInput,
|
|
8
7
|
SlceCnstFull,
|
|
9
8
|
SlceCnstInput,
|
|
10
9
|
SlceCnstInsight,
|
|
@@ -26,11 +25,9 @@ type _Full<S extends SliceCls> = SlceCnstFull<S>;
|
|
|
26
25
|
type _Light<S extends SliceCls> = SlceCnstLight<S>;
|
|
27
26
|
type _Insight<S extends SliceCls> = SlceCnstInsight<S>;
|
|
28
27
|
type _PurifiedInput<S extends SliceCls> = SlceCnstPurifiedInput<S>;
|
|
29
|
-
type _DefaultInput<S extends SliceCls> = SlceCnstDefaultInput<S>;
|
|
30
28
|
type _Filter<S extends SliceCls> = SlceDbFilter<S>;
|
|
31
29
|
type _Sort<S extends SliceCls> = SlceDbSort<S>;
|
|
32
|
-
type
|
|
33
|
-
type _SliceFetchInitOption<S extends SliceCls> = FetchInitOption<_Input<S>, _Filter<S>, _DefaultInput<S>, _Sort<S>>;
|
|
30
|
+
type SliceInitOption<S extends SliceCls> = FetchInitOption<_Input<S>, _Filter<S>>;
|
|
34
31
|
|
|
35
32
|
type SliceListFetch<S extends SliceCls> = {
|
|
36
33
|
[Suffix in keyof _SliceMap<S> as Suffix extends string ? `${_RefName<S>}List${Capitalize<Suffix>}` : never]: (
|
|
@@ -50,43 +47,33 @@ type SliceInsightFetch<S extends SliceCls> = {
|
|
|
50
47
|
) => Promise<_Insight<S>>;
|
|
51
48
|
};
|
|
52
49
|
|
|
50
|
+
type SliceInit<S extends SliceCls, Suffix extends keyof _SliceMap<S>> = InitReturn<
|
|
51
|
+
_RefName<S>,
|
|
52
|
+
Suffix & string,
|
|
53
|
+
_Light<S>,
|
|
54
|
+
_Insight<S>,
|
|
55
|
+
SliceInfoArgs<_SliceMap<S>[Suffix]>,
|
|
56
|
+
_Filter<S>
|
|
57
|
+
>;
|
|
58
|
+
|
|
59
|
+
type SliceServerInit<S extends SliceCls, Suffix extends keyof _SliceMap<S>> = ServerInit<
|
|
60
|
+
_RefName<S>,
|
|
61
|
+
_Light<S>,
|
|
62
|
+
_Insight<S>,
|
|
63
|
+
SliceInfoArgs<_SliceMap<S>[Suffix]>,
|
|
64
|
+
_Filter<S>
|
|
65
|
+
>;
|
|
66
|
+
|
|
53
67
|
type SliceInitFetch<S extends SliceCls> = {
|
|
54
68
|
[Suffix in keyof _SliceMap<S> as Suffix extends string ? `init${_Cap<S>}${Capitalize<Suffix>}` : never]: (
|
|
55
|
-
...args: [...SliceInfoArgs<_SliceMap<S>[Suffix]>, option?:
|
|
56
|
-
) => Promise<
|
|
57
|
-
InitReturn<
|
|
58
|
-
_RefName<S>,
|
|
59
|
-
Suffix & string,
|
|
60
|
-
_Light<S>,
|
|
61
|
-
_Insight<S>,
|
|
62
|
-
SliceInfoArgs<_SliceMap<S>[Suffix]>,
|
|
63
|
-
_Filter<S>,
|
|
64
|
-
_Cap<S>,
|
|
65
|
-
Suffix extends string ? Capitalize<Suffix> : never,
|
|
66
|
-
_LightWithId<S>,
|
|
67
|
-
GetStateObject<_LightWithId<S>>,
|
|
68
|
-
GetStateObject<_Insight<S>>,
|
|
69
|
-
_Sort<S>
|
|
70
|
-
>
|
|
71
|
-
>;
|
|
69
|
+
...args: [...SliceInfoArgs<_SliceMap<S>[Suffix]>, option?: SliceInitOption<S>]
|
|
70
|
+
) => Promise<SliceInit<S, Suffix>>;
|
|
72
71
|
};
|
|
73
72
|
|
|
74
73
|
type SliceGetInitFetch<S extends SliceCls> = {
|
|
75
74
|
[Suffix in keyof _SliceMap<S> as Suffix extends string ? `get${_Cap<S>}Init${Capitalize<Suffix>}` : never]: (
|
|
76
|
-
...args: [...SliceInfoArgs<_SliceMap<S>[Suffix]>, option?:
|
|
77
|
-
) => Promise<
|
|
78
|
-
ServerInit<
|
|
79
|
-
_RefName<S>,
|
|
80
|
-
_Light<S>,
|
|
81
|
-
_Insight<S>,
|
|
82
|
-
SliceInfoArgs<_SliceMap<S>[Suffix]>,
|
|
83
|
-
_Filter<S>,
|
|
84
|
-
_Cap<S>,
|
|
85
|
-
GetStateObject<_LightWithId<S>>,
|
|
86
|
-
GetStateObject<_Insight<S>>,
|
|
87
|
-
_Sort<S>
|
|
88
|
-
>
|
|
89
|
-
>;
|
|
75
|
+
...args: [...SliceInfoArgs<_SliceMap<S>[Suffix]>, option?: SliceInitOption<S>]
|
|
76
|
+
) => Promise<SliceServerInit<S, Suffix>>;
|
|
90
77
|
};
|
|
91
78
|
|
|
92
79
|
export type GetFetchTypeFromSlice<SlceCls extends SliceCls> = SlceCls["srv"]["cnst"] extends ConstantModel
|
|
@@ -164,23 +151,13 @@ type AppliedBaseSliceFetchType<
|
|
|
164
151
|
) => Promise<Full>;
|
|
165
152
|
};
|
|
166
153
|
|
|
167
|
-
export interface FetchInitForm<
|
|
168
|
-
Input,
|
|
169
|
-
Filter extends FilterInstance,
|
|
170
|
-
_DefaultInput = DefaultOf<Input>,
|
|
171
|
-
_Sort = ExtractSort<Filter>,
|
|
172
|
-
> {
|
|
154
|
+
export interface FetchInitForm<Input, Filter extends FilterInstance> {
|
|
173
155
|
page?: number;
|
|
174
156
|
limit?: number;
|
|
175
|
-
sort?:
|
|
176
|
-
default?: Partial<
|
|
157
|
+
sort?: ExtractSort<Filter>;
|
|
158
|
+
default?: Partial<DefaultOf<Input>>;
|
|
177
159
|
invalidate?: boolean;
|
|
178
160
|
insight?: boolean;
|
|
179
161
|
}
|
|
180
162
|
|
|
181
|
-
type FetchInitOption<
|
|
182
|
-
Input,
|
|
183
|
-
Filter extends FilterInstance,
|
|
184
|
-
_DefaultInput = DefaultOf<Input>,
|
|
185
|
-
_Sort = ExtractSort<Filter>,
|
|
186
|
-
> = FetchPolicy & FetchInitForm<Input, Filter, _DefaultInput, _Sort>;
|
|
163
|
+
type FetchInitOption<Input, Filter extends FilterInstance> = FetchPolicy & FetchInitForm<Input, Filter>;
|