akanjs 2.3.0 → 2.3.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/base/primitiveRegistry.ts +19 -12
- package/client/csrTypes.ts +16 -0
- package/constant/fieldInfo.ts +11 -9
- package/constant/getDefault.ts +1 -1
- package/fetch/requestStorage.ts +5 -0
- package/package.json +4 -4
- package/server/akanApp.ts +26 -3
- package/server/akanServer.ts +5 -1
- package/server/cachePolicy.ts +99 -5
- package/server/imageOptimizer.ts +14 -1
- package/server/metadata.tsx +117 -33
- package/server/resolver/database.resolver.ts +4 -4
- package/server/routeElementComposer.tsx +46 -14
- package/server/routeState.ts +379 -0
- package/server/routeTreeBuilder.ts +3 -2
- package/server/rscClient.tsx +316 -46
- package/server/rscClientFetch.ts +57 -0
- package/server/rscClientPatch.ts +157 -0
- package/server/rscHeadPatch.ts +80 -0
- package/server/rscNavigationState.ts +315 -0
- package/server/rscPartialCommit.ts +3 -0
- package/server/rscPatchSafety.ts +57 -0
- package/server/rscSegmentOutlet.tsx +69 -0
- package/server/rscSegmentOutletReference.ts +24 -0
- package/server/rscWorker.tsx +380 -53
- package/server/rscWorkerCache.ts +180 -0
- package/server/rscWorkerHost.ts +40 -12
- package/server/rscWorkerReplay.ts +11 -2
- package/server/ssrFromRscRenderer.tsx +15 -10
- package/server/ssrTypes.ts +18 -0
- package/server/types.tsx +4 -0
- package/server/webRouter.ts +198 -42
- package/service/predefinedAdaptor/database.adaptor.ts +72 -25
- package/signal/signalContext.ts +1 -1
- package/types/base/primitiveRegistry.d.ts +6 -6
- package/types/client/csrTypes.d.ts +16 -0
- package/types/constant/fieldInfo.d.ts +8 -7
- package/types/fetch/requestStorage.d.ts +2 -0
- package/types/server/cachePolicy.d.ts +36 -0
- package/types/server/metadata.d.ts +10 -1
- package/types/server/routeElementComposer.d.ts +9 -1
- package/types/server/routeState.d.ts +94 -0
- package/types/server/rscClient.d.ts +1 -0
- package/types/server/rscClientFetch.d.ts +24 -0
- package/types/server/rscClientPatch.d.ts +21 -0
- package/types/server/rscHeadPatch.d.ts +12 -0
- package/types/server/rscNavigationState.d.ts +78 -0
- package/types/server/rscPartialCommit.d.ts +1 -0
- package/types/server/rscPatchSafety.d.ts +8 -0
- package/types/server/rscSegmentOutlet.d.ts +17 -0
- package/types/server/rscSegmentOutletReference.d.ts +2 -0
- package/types/server/rscWorker.d.ts +5 -0
- package/types/server/rscWorkerCache.d.ts +63 -0
- package/types/server/rscWorkerHost.d.ts +8 -4
- package/types/server/rscWorkerReplay.d.ts +3 -0
- package/types/server/ssrFromRscRenderer.d.ts +1 -0
- package/types/server/ssrTypes.d.ts +17 -0
- package/types/server/types.d.ts +4 -0
- package/types/server/webRouter.d.ts +7 -3
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +6 -0
- package/types/ui/Button.d.ts +1 -1
- package/types/ui/ClientSide.d.ts +1 -1
- package/types/ui/Constant/Doc.d.ts +6 -6
- package/types/ui/Constant/Mermaid.d.ts +1 -1
- package/types/ui/Constant/index.d.ts +1 -1
- package/types/ui/Constant/schemaDoc.d.ts +1 -1
- package/types/ui/Copy.d.ts +1 -1
- package/types/ui/CsrImage.d.ts +1 -1
- package/types/ui/Data/CardList.d.ts +1 -1
- package/types/ui/Data/Dashboard.d.ts +1 -1
- package/types/ui/Data/Insight.d.ts +1 -1
- package/types/ui/Data/Item.d.ts +6 -6
- package/types/ui/Data/ListContainer.d.ts +1 -1
- package/types/ui/Data/Pagination.d.ts +1 -1
- package/types/ui/Data/TableList.d.ts +1 -1
- package/types/ui/DatePicker.d.ts +3 -3
- package/types/ui/Dialog/Close.d.ts +1 -1
- package/types/ui/Dialog/Content.d.ts +1 -1
- package/types/ui/Dialog/Provider.d.ts +1 -1
- package/types/ui/Dialog/Trigger.d.ts +1 -1
- package/types/ui/Dialog/index.d.ts +3 -3
- package/types/ui/DragAction.d.ts +4 -4
- package/types/ui/DraggableList.d.ts +3 -3
- package/types/ui/Dropdown.d.ts +1 -1
- package/types/ui/Empty.d.ts +1 -1
- package/types/ui/Field.d.ts +22 -22
- package/types/ui/Image.d.ts +1 -1
- package/types/ui/InfiniteScroll.d.ts +1 -1
- package/types/ui/Input.d.ts +6 -6
- package/types/ui/KeyboardAvoiding.d.ts +1 -1
- package/types/ui/Layout/BottomAction.d.ts +1 -1
- package/types/ui/Layout/BottomInset.d.ts +1 -1
- package/types/ui/Layout/BottomTab.d.ts +1 -1
- package/types/ui/Layout/Header.d.ts +1 -1
- package/types/ui/Layout/LeftSider.d.ts +1 -1
- package/types/ui/Layout/Navbar.d.ts +1 -1
- package/types/ui/Layout/RightSider.d.ts +1 -1
- package/types/ui/Layout/Sider.d.ts +1 -1
- package/types/ui/Layout/Template.d.ts +1 -1
- package/types/ui/Layout/TopLeftAction.d.ts +1 -1
- package/types/ui/Layout/Unit.d.ts +1 -1
- package/types/ui/Layout/View.d.ts +1 -1
- package/types/ui/Layout/Zone.d.ts +1 -1
- package/types/ui/Layout/index.d.ts +12 -12
- package/types/ui/Link/Back.d.ts +1 -1
- package/types/ui/Link/Close.d.ts +1 -1
- package/types/ui/Link/CsrLink.d.ts +1 -1
- package/types/ui/Link/Lang.d.ts +1 -1
- package/types/ui/Link/SsrLink.d.ts +1 -1
- package/types/ui/Link/index.d.ts +1 -1
- package/types/ui/Load/Edit.d.ts +1 -1
- package/types/ui/Load/Edit_Client.d.ts +1 -1
- package/types/ui/Load/PageCSR.d.ts +1 -1
- package/types/ui/Load/Pagination.d.ts +1 -1
- package/types/ui/Load/Units.d.ts +1 -1
- package/types/ui/Load/View.d.ts +1 -1
- package/types/ui/Loading/Area.d.ts +1 -1
- package/types/ui/Loading/Button.d.ts +1 -1
- package/types/ui/Loading/Input.d.ts +1 -1
- package/types/ui/Loading/ProgressBar.d.ts +1 -1
- package/types/ui/Loading/Skeleton.d.ts +1 -1
- package/types/ui/Loading/Spin.d.ts +1 -1
- package/types/ui/Loading/index.d.ts +6 -6
- package/types/ui/Menu.d.ts +1 -1
- package/types/ui/Modal.d.ts +1 -1
- package/types/ui/Model/AdminPanel.d.ts +1 -1
- package/types/ui/Model/Edit.d.ts +1 -1
- package/types/ui/Model/EditModal.d.ts +1 -1
- package/types/ui/Model/EditWrapper.d.ts +1 -1
- package/types/ui/Model/LoadInit.d.ts +1 -1
- package/types/ui/Model/New.d.ts +1 -1
- package/types/ui/Model/NewWrapper.d.ts +1 -1
- package/types/ui/Model/NewWrapper_Client.d.ts +1 -1
- package/types/ui/Model/Remove.d.ts +1 -1
- package/types/ui/Model/RemoveWrapper.d.ts +1 -1
- package/types/ui/Model/SureToRemove.d.ts +1 -1
- package/types/ui/Model/View.d.ts +1 -1
- package/types/ui/Model/ViewEditModal.d.ts +1 -1
- package/types/ui/Model/ViewModal.d.ts +1 -1
- package/types/ui/Model/ViewWrapper.d.ts +1 -1
- package/types/ui/More.d.ts +1 -1
- package/types/ui/ObjectId.d.ts +1 -1
- package/types/ui/Popconfirm.d.ts +1 -1
- package/types/ui/Radio.d.ts +2 -2
- package/types/ui/RecentTime.d.ts +1 -1
- package/types/ui/Refresh.d.ts +1 -1
- package/types/ui/ScreenNavigator.d.ts +3 -3
- package/types/ui/Select.d.ts +1 -1
- package/types/ui/Signal/Arg.d.ts +13 -13
- package/types/ui/Signal/Doc.d.ts +6 -6
- package/types/ui/Signal/Listener.d.ts +2 -2
- package/types/ui/Signal/Message.d.ts +4 -4
- package/types/ui/Signal/Object.d.ts +4 -4
- package/types/ui/Signal/PubSub.d.ts +4 -4
- package/types/ui/Signal/Request.d.ts +2 -2
- package/types/ui/Signal/Response.d.ts +3 -3
- package/types/ui/Signal/RestApi.d.ts +5 -5
- package/types/ui/Signal/WebSocket.d.ts +2 -2
- package/types/ui/System/CSR.d.ts +5 -5
- package/types/ui/System/Client.d.ts +8 -8
- package/types/ui/System/Common.d.ts +2 -2
- package/types/ui/System/DevModeToggle.d.ts +1 -1
- package/types/ui/System/Gtag.d.ts +1 -1
- package/types/ui/System/Messages.d.ts +1 -1
- package/types/ui/System/Reconnect.d.ts +1 -1
- package/types/ui/System/Root.d.ts +1 -1
- package/types/ui/System/SSR.d.ts +4 -4
- package/types/ui/System/SelectLanguage.d.ts +1 -1
- package/types/ui/System/ThemeToggle.d.ts +1 -1
- package/types/ui/System/index.d.ts +7 -7
- package/types/ui/Tab/Menu.d.ts +1 -1
- package/types/ui/Tab/Menus.d.ts +1 -1
- package/types/ui/Tab/Panel.d.ts +1 -1
- package/types/ui/Tab/Provider.d.ts +1 -1
- package/types/ui/Tab/index.d.ts +4 -4
- package/types/ui/Table.d.ts +1 -1
- package/types/ui/ToggleSelect.d.ts +2 -2
- package/types/ui/Unauthorized.d.ts +1 -1
- package/ui/Constant/schemaDoc.ts +1 -1
- package/server/resolver/resolver.contract.fixture.ts +0 -222
|
@@ -206,12 +206,12 @@ declare global {
|
|
|
206
206
|
[PRIMITIVE_CLIENT_VALUE]: boolean;
|
|
207
207
|
[PRIMITIVE_DEFAULT_VALUE]: boolean;
|
|
208
208
|
[PRIMITIVE_EXAMPLE_VALUE]: boolean;
|
|
209
|
-
validate(value: boolean): boolean;
|
|
210
|
-
parseValue(input: boolean): boolean;
|
|
211
|
-
serializeValue(value: boolean): boolean;
|
|
212
|
-
_parse(input: boolean): boolean;
|
|
213
|
-
_serialize(value: boolean): boolean;
|
|
214
|
-
_checkValue(value: boolean): void;
|
|
209
|
+
validate(value: boolean | number): boolean;
|
|
210
|
+
parseValue(input: boolean | number): boolean | number;
|
|
211
|
+
serializeValue(value: boolean | number): boolean | number;
|
|
212
|
+
_parse(input: boolean | number): boolean;
|
|
213
|
+
_serialize(value: boolean | number): boolean;
|
|
214
|
+
_checkValue(value: boolean | number): void;
|
|
215
215
|
}
|
|
216
216
|
interface DateConstructor {
|
|
217
217
|
refName: "Date";
|
|
@@ -301,19 +301,26 @@ Object.assign(String, scalarPrimitiveStatics, {
|
|
|
301
301
|
});
|
|
302
302
|
PrimitiveRegistry.register(String);
|
|
303
303
|
|
|
304
|
+
const normalizeBooleanPrimitiveValue = (value: boolean | number): boolean | null => {
|
|
305
|
+
if (typeof value === "boolean") return value;
|
|
306
|
+
if (value === 1) return true;
|
|
307
|
+
if (value === 0) return false;
|
|
308
|
+
return null;
|
|
309
|
+
};
|
|
310
|
+
|
|
304
311
|
Object.assign(Boolean, {
|
|
305
312
|
...scalarPrimitiveStatics,
|
|
306
313
|
refName: "Boolean",
|
|
307
314
|
[PRIMITIVE_DEFAULT_VALUE]: false,
|
|
308
315
|
[PRIMITIVE_EXAMPLE_VALUE]: true,
|
|
309
|
-
validate(value: boolean) {
|
|
310
|
-
return
|
|
316
|
+
validate(value: boolean | number) {
|
|
317
|
+
return normalizeBooleanPrimitiveValue(value) !== null;
|
|
311
318
|
},
|
|
312
|
-
parseValue(input: boolean) {
|
|
313
|
-
return
|
|
319
|
+
parseValue(input: boolean | number) {
|
|
320
|
+
return normalizeBooleanPrimitiveValue(input) ?? input;
|
|
314
321
|
},
|
|
315
|
-
serializeValue(value: boolean) {
|
|
316
|
-
return
|
|
322
|
+
serializeValue(value: boolean | number) {
|
|
323
|
+
return normalizeBooleanPrimitiveValue(value) ?? value;
|
|
317
324
|
},
|
|
318
325
|
});
|
|
319
326
|
PrimitiveRegistry.register(Boolean);
|
package/client/csrTypes.ts
CHANGED
|
@@ -18,6 +18,12 @@ export interface PageConfig {
|
|
|
18
18
|
bottomInset?: boolean | number;
|
|
19
19
|
gesture?: boolean;
|
|
20
20
|
cache?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Opt in to guarded RSC page suffix commits when the page does not require
|
|
23
|
+
* head/metadata updates and the retained route chain head is invariant for
|
|
24
|
+
* sibling navigations under the same layout.
|
|
25
|
+
*/
|
|
26
|
+
rscPatchHeadSafe?: boolean;
|
|
21
27
|
topSafeAreaColor?: string;
|
|
22
28
|
bottomSafeAreaColor?: string;
|
|
23
29
|
}
|
|
@@ -58,9 +64,19 @@ export interface LayoutErrorProps extends LayoutNotFoundProps {
|
|
|
58
64
|
}
|
|
59
65
|
export type Head = ReactNode;
|
|
60
66
|
export type GenerateHead = (props: PageProps) => PromiseOrObject<Head | null | undefined>;
|
|
67
|
+
export interface AkanHeadSnapshotNode {
|
|
68
|
+
tag: "title" | "meta" | "link";
|
|
69
|
+
attrs?: Record<string, string>;
|
|
70
|
+
text?: string;
|
|
71
|
+
}
|
|
72
|
+
export interface AkanHeadSnapshotV1 {
|
|
73
|
+
version: 1;
|
|
74
|
+
nodes: AkanHeadSnapshotNode[];
|
|
75
|
+
}
|
|
61
76
|
export interface ResolvedHead {
|
|
62
77
|
node: Head | null | undefined;
|
|
63
78
|
hasExplicitLanguageAlternates: boolean;
|
|
79
|
+
headSnapshot?: AkanHeadSnapshotV1;
|
|
64
80
|
}
|
|
65
81
|
export type ResolveHeadResult = Head | ResolvedHead | null | undefined;
|
|
66
82
|
export type ResolveHead = (props: PageProps) => PromiseOrObject<ResolveHeadResult>;
|
package/constant/fieldInfo.ts
CHANGED
|
@@ -56,8 +56,10 @@ export type ExtractFieldInfoObject<Obj extends FieldInfoObject> = {
|
|
|
56
56
|
: never;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
export type ConstantFieldKind = "property" | "hidden" | "secret" | "resolve";
|
|
60
|
+
|
|
59
61
|
export interface ConstantFieldProps<
|
|
60
|
-
FieldType extends
|
|
62
|
+
FieldType extends ConstantFieldKind = ConstantFieldKind,
|
|
61
63
|
FieldValue = any,
|
|
62
64
|
MapValue = any,
|
|
63
65
|
Metadata = { [key: string]: any },
|
|
@@ -87,7 +89,7 @@ export const fieldPresets = ["email", "password", "url"] as const;
|
|
|
87
89
|
export type FieldPreset = (typeof fieldPresets)[number];
|
|
88
90
|
|
|
89
91
|
class FieldInfo<
|
|
90
|
-
FieldType extends
|
|
92
|
+
FieldType extends ConstantFieldKind = any,
|
|
91
93
|
Value extends ConstantFieldTypeInput | null = null,
|
|
92
94
|
ExplicitType = unknown,
|
|
93
95
|
MapValue = Value extends MapConstructor ? typeof PrimitiveScalar : never,
|
|
@@ -121,7 +123,7 @@ class FieldInfo<
|
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
interface ConstantFieldBuildProps<
|
|
124
|
-
FieldType extends
|
|
126
|
+
FieldType extends ConstantFieldKind = any,
|
|
125
127
|
FieldValue = any,
|
|
126
128
|
MapValue = any,
|
|
127
129
|
Metadata = any,
|
|
@@ -172,7 +174,7 @@ export type FieldInfoObjectToFieldObject<Obj extends FieldInfoObject> = {
|
|
|
172
174
|
|
|
173
175
|
/** Runtime metadata for a single Akan constant field. */
|
|
174
176
|
export class ConstantField<
|
|
175
|
-
FieldType extends
|
|
177
|
+
FieldType extends ConstantFieldKind = ConstantFieldKind,
|
|
176
178
|
Value extends ConstantFieldTypeInput | null = any,
|
|
177
179
|
FieldValue = any,
|
|
178
180
|
MapValue = any,
|
|
@@ -244,7 +246,7 @@ export class ConstantField<
|
|
|
244
246
|
}
|
|
245
247
|
|
|
246
248
|
static fromFieldInfo<
|
|
247
|
-
FieldType extends
|
|
249
|
+
FieldType extends ConstantFieldKind = any,
|
|
248
250
|
Value extends ConstantFieldTypeInput | null = null,
|
|
249
251
|
FieldValue = any,
|
|
250
252
|
MapValue = any,
|
|
@@ -341,11 +343,11 @@ type FieldOption<
|
|
|
341
343
|
_FieldToValue = FieldToValue<Value, MapValue> | null | undefined,
|
|
342
344
|
> =
|
|
343
345
|
| Omit<
|
|
344
|
-
ConstantFieldProps<
|
|
346
|
+
ConstantFieldProps<ConstantFieldKind, _FieldToValue, MapValue, Metadata>,
|
|
345
347
|
"enum" | "meta" | "nullable" | "fieldType" | "select"
|
|
346
348
|
>
|
|
347
349
|
| Omit<
|
|
348
|
-
ConstantFieldProps<
|
|
350
|
+
ConstantFieldProps<ConstantFieldKind, SingleValue<_FieldToValue>, MapValue, Metadata>,
|
|
349
351
|
"enum" | "meta" | "nullable" | "fieldType" | "select"
|
|
350
352
|
>[];
|
|
351
353
|
|
|
@@ -392,9 +394,9 @@ field.secret = <
|
|
|
392
394
|
value: Value,
|
|
393
395
|
option: FieldOption<Value, MapValue> = {},
|
|
394
396
|
) =>
|
|
395
|
-
new FieldInfo<"
|
|
397
|
+
new FieldInfo<"secret", Value | null, ExplicitType | null, MapValue>(value, {
|
|
396
398
|
...option,
|
|
397
|
-
fieldType: "
|
|
399
|
+
fieldType: "secret",
|
|
398
400
|
select: false,
|
|
399
401
|
nullable: true,
|
|
400
402
|
});
|
package/constant/getDefault.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { DefaultOf } from "./types";
|
|
|
5
5
|
export const getDefault = <T>(fieldObj: FieldObject): DefaultOf<T> => {
|
|
6
6
|
const result: Record<string, unknown> = {};
|
|
7
7
|
for (const [key, field] of Object.entries(fieldObj)) {
|
|
8
|
-
if (field.fieldType === "hidden") result[key] = null;
|
|
8
|
+
if (field.fieldType === "hidden" || field.fieldType === "secret") result[key] = null;
|
|
9
9
|
else if (field.default !== undefined && field.default !== null) {
|
|
10
10
|
if (typeof field.default === "function") result[key] = (field.default as () => object)();
|
|
11
11
|
else result[key] = field.default as object;
|
package/fetch/requestStorage.ts
CHANGED
|
@@ -149,6 +149,11 @@ export function updateRequestPolicy(
|
|
|
149
149
|
return policy;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/** @internal Route cache tag collection is reserved for framework-owned cache policy experiments. */
|
|
153
|
+
export function cacheTag(...tags: string[]): AkanRequestPolicy | undefined {
|
|
154
|
+
return updateRequestPolicy({ tags: tags.filter(Boolean) });
|
|
155
|
+
}
|
|
156
|
+
|
|
152
157
|
export function getRequestDynamicUsage(): AkanDynamicUsage | undefined {
|
|
153
158
|
return getRequestStore()?.dynamicUsage;
|
|
154
159
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akanjs",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1-rc.1",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -187,12 +187,12 @@
|
|
|
187
187
|
"mermaid": "^11.15.0",
|
|
188
188
|
"postgres": "^3.4.9",
|
|
189
189
|
"protobufjs": "^8.4.0",
|
|
190
|
-
"react": "19.2.
|
|
190
|
+
"react": "19.2.7",
|
|
191
191
|
"react-datepicker": "^9.1.0",
|
|
192
|
-
"react-dom": "19.2.
|
|
192
|
+
"react-dom": "19.2.7",
|
|
193
193
|
"react-icons": "^5.6.0",
|
|
194
194
|
"react-refresh": "^0.18.0",
|
|
195
|
-
"react-server-dom-webpack": "^19.2.
|
|
195
|
+
"react-server-dom-webpack": "^19.2.7",
|
|
196
196
|
"react-simple-pull-to-refresh": "^1.3.4",
|
|
197
197
|
"react-spring": "^10.0.4",
|
|
198
198
|
"scheduler": "^0.27.0",
|
package/server/akanApp.ts
CHANGED
|
@@ -67,6 +67,7 @@ export class AkanApp {
|
|
|
67
67
|
readonly #artifactDir: string;
|
|
68
68
|
readonly #replica: AkanReplicaConfig;
|
|
69
69
|
readonly #runtimeDir: string;
|
|
70
|
+
readonly #socketRunId = `${process.pid}-${Date.now().toString(36)}`;
|
|
70
71
|
readonly #port: number;
|
|
71
72
|
readonly #wsBasePort: number;
|
|
72
73
|
readonly #children = new Map<number, ChildState>();
|
|
@@ -273,7 +274,7 @@ export class AkanApp {
|
|
|
273
274
|
|
|
274
275
|
child.restartPending = true;
|
|
275
276
|
child.ready = false;
|
|
276
|
-
child.status = reason === "health-timeout" ? "unhealthy" : "exited";
|
|
277
|
+
child.status = reason === "health-timeout" || reason === "upstream-open-failed" ? "unhealthy" : "exited";
|
|
277
278
|
child.upstream = undefined;
|
|
278
279
|
child.healthPath = undefined;
|
|
279
280
|
this.#invalidateFederationChildCache();
|
|
@@ -336,6 +337,7 @@ export class AkanApp {
|
|
|
336
337
|
}
|
|
337
338
|
|
|
338
339
|
async #stopChildForRestart(child: ChildState, proc: Bun.Subprocess<"ignore", "pipe", "pipe">, reason: string) {
|
|
340
|
+
if (reason.startsWith("exit:") || proc.killed) return;
|
|
339
341
|
if (!proc.killed) {
|
|
340
342
|
this.#sendToChild(child, { type: "shutdown", signal: reason } satisfies AkanIpcMessage);
|
|
341
343
|
}
|
|
@@ -572,12 +574,27 @@ export class AkanApp {
|
|
|
572
574
|
redirect: "manual",
|
|
573
575
|
});
|
|
574
576
|
return this.#proxyResponse(upstreamRes);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
if (AkanApp.#isUpstreamOpenFailure(error)) {
|
|
579
|
+
this.logger.error(
|
|
580
|
+
`Child ${child.idx}/${child.role} upstream is unreachable (${child.upstream.socketPath}); restarting`,
|
|
581
|
+
);
|
|
582
|
+
this.#scheduleChildRestart(child, child.proc, "upstream-open-failed");
|
|
583
|
+
return new Response("Federation child upstream is unreachable; restarting", { status: 503 });
|
|
584
|
+
}
|
|
585
|
+
throw error;
|
|
575
586
|
} finally {
|
|
576
587
|
child.metrics.activeRequests = Math.max(0, (child.metrics.activeRequests ?? 1) - 1);
|
|
577
588
|
if (traced) this.#recordProxyHop(performance.now() - hopStart);
|
|
578
589
|
}
|
|
579
590
|
}
|
|
580
591
|
|
|
592
|
+
static #isUpstreamOpenFailure(error: unknown): boolean {
|
|
593
|
+
if (!error || typeof error !== "object") return false;
|
|
594
|
+
const candidate = error as { code?: unknown; message?: unknown };
|
|
595
|
+
return candidate.code === "FailedToOpenSocket" || String(candidate.message ?? "").includes("FailedToOpenSocket");
|
|
596
|
+
}
|
|
597
|
+
|
|
581
598
|
/**
|
|
582
599
|
* Gateway-observed upstream round-trip time. The pure proxy overhead is this value
|
|
583
600
|
* minus the child handler time captured in the per-request trace.
|
|
@@ -705,7 +722,7 @@ export class AkanApp {
|
|
|
705
722
|
|
|
706
723
|
#getChildUpstream(idx: number, role: AkanChildRole): GatewayUpstream {
|
|
707
724
|
return {
|
|
708
|
-
http: { type: "unix", socketPath: path.join(this.#runtimeDir, `akan-child-${idx}.sock`) },
|
|
725
|
+
http: { type: "unix", socketPath: path.join(this.#runtimeDir, `akan-child-${this.#socketRunId}-${idx}.sock`) },
|
|
709
726
|
ws: role === "batch" ? undefined : { type: "tcp", host: "127.0.0.1", port: this.#wsBasePort + idx },
|
|
710
727
|
};
|
|
711
728
|
}
|
|
@@ -927,11 +944,17 @@ export class AkanApp {
|
|
|
927
944
|
void this.#scheduleChildRestart(child, child.proc, "health-timeout");
|
|
928
945
|
return;
|
|
929
946
|
}
|
|
930
|
-
this.#sendToChild(child, {
|
|
947
|
+
const sent = this.#sendToChild(child, {
|
|
931
948
|
type: "health.ping",
|
|
932
949
|
nonce: crypto.randomUUID(),
|
|
933
950
|
sentAt: now,
|
|
934
951
|
} satisfies AkanIpcMessage);
|
|
952
|
+
if (!sent) {
|
|
953
|
+
child.status = "unhealthy";
|
|
954
|
+
this.#invalidateFederationChildCache();
|
|
955
|
+
void this.#scheduleChildRestart(child, child.proc, "health-send-failed");
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
935
958
|
}
|
|
936
959
|
}
|
|
937
960
|
|
package/server/akanServer.ts
CHANGED
|
@@ -348,7 +348,11 @@ export class AkanServer {
|
|
|
348
348
|
sentAt: message.sentAt,
|
|
349
349
|
pid: process.pid,
|
|
350
350
|
} satisfies AkanIpcMessage);
|
|
351
|
-
else if (message.type === "shutdown")
|
|
351
|
+
else if (message.type === "shutdown") {
|
|
352
|
+
void this.stop()
|
|
353
|
+
.then(() => process.exit(0))
|
|
354
|
+
.catch(() => process.exit(1));
|
|
355
|
+
}
|
|
352
356
|
}
|
|
353
357
|
|
|
354
358
|
#startMetricsReporting() {
|
package/server/cachePolicy.ts
CHANGED
|
@@ -10,17 +10,53 @@ export interface RouteCacheKeyInput {
|
|
|
10
10
|
|
|
11
11
|
export interface RouteCacheRenderState {
|
|
12
12
|
cacheable: boolean;
|
|
13
|
+
routeId?: string;
|
|
13
14
|
revalidate?: number | false;
|
|
14
15
|
tags?: string[];
|
|
15
16
|
dynamicUsage?: AkanDynamicUsage;
|
|
16
17
|
reason?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
export interface RouteCacheMetadata {
|
|
21
|
+
pathname: string;
|
|
22
|
+
routeId?: string;
|
|
23
|
+
tags?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RouteCacheInvalidation {
|
|
27
|
+
tags?: string[];
|
|
28
|
+
paths?: string[];
|
|
29
|
+
reason?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
export interface RouteCacheEntry {
|
|
20
33
|
key: string;
|
|
21
34
|
ttl: number;
|
|
22
35
|
}
|
|
23
36
|
|
|
37
|
+
export interface PublicRouteCacheEntryInput extends RouteCacheKeyInput {
|
|
38
|
+
env: {
|
|
39
|
+
enabled?: string | null;
|
|
40
|
+
ttl?: string | null;
|
|
41
|
+
allow?: string | null;
|
|
42
|
+
deny?: string | null;
|
|
43
|
+
};
|
|
44
|
+
defaultTtl?: number;
|
|
45
|
+
defaultEnabled?: boolean;
|
|
46
|
+
defaultAllow?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type RouteCacheBypassReason =
|
|
50
|
+
| "env-opt-out"
|
|
51
|
+
| "dev-default-off"
|
|
52
|
+
| "ttl-disabled"
|
|
53
|
+
| "path-excluded"
|
|
54
|
+
| "request-not-public";
|
|
55
|
+
|
|
56
|
+
export type RouteCacheEntryDecision =
|
|
57
|
+
| { entry: RouteCacheEntry; reason?: undefined }
|
|
58
|
+
| { entry: null; reason: RouteCacheBypassReason };
|
|
59
|
+
|
|
24
60
|
export type RouteCacheRenderControlType = "redirect" | "not-found" | "error";
|
|
25
61
|
|
|
26
62
|
export function parsePositiveInt(value: string | undefined | null): number | null {
|
|
@@ -39,8 +75,10 @@ export function resolveAutoRouteCacheTtl(input: {
|
|
|
39
75
|
enabled?: string | null;
|
|
40
76
|
ttl?: string | null;
|
|
41
77
|
defaultTtl?: number;
|
|
78
|
+
defaultEnabled?: boolean;
|
|
42
79
|
}): number | null {
|
|
43
|
-
if (input.enabled
|
|
80
|
+
if (input.enabled === "0") return null;
|
|
81
|
+
if (input.enabled !== "1" && !input.defaultEnabled) return null;
|
|
44
82
|
return normalizeRouteCacheTtl(input.ttl, input.defaultTtl ?? DEFAULT_ROUTE_CACHE_TTL_SECONDS);
|
|
45
83
|
}
|
|
46
84
|
|
|
@@ -78,7 +116,7 @@ export function isPublicRouteCacheableRequest(request: Request): boolean {
|
|
|
78
116
|
|
|
79
117
|
export function isRouteCachePathAllowed(
|
|
80
118
|
pathname: string,
|
|
81
|
-
options: { allow?: string | null; deny?: string | null } = {},
|
|
119
|
+
options: { allow?: string | null; deny?: string | null; defaultAllow?: boolean } = {},
|
|
82
120
|
): boolean {
|
|
83
121
|
const matches = (raw: string | null | undefined) => {
|
|
84
122
|
const prefixes = (raw ?? "")
|
|
@@ -92,6 +130,7 @@ export function isRouteCachePathAllowed(
|
|
|
92
130
|
};
|
|
93
131
|
if (matches(options.deny)) return false;
|
|
94
132
|
const allow = options.allow ?? "";
|
|
133
|
+
if (!allow.trim() && options.defaultAllow) return true;
|
|
95
134
|
return matches(allow);
|
|
96
135
|
}
|
|
97
136
|
|
|
@@ -112,6 +151,33 @@ export function createRouteCacheEntry(input: RouteCacheKeyInput & { ttl: number
|
|
|
112
151
|
return { key: createRouteCacheKey(input), ttl: input.ttl };
|
|
113
152
|
}
|
|
114
153
|
|
|
154
|
+
export function resolvePublicRouteCacheEntryDecision(input: PublicRouteCacheEntryInput): RouteCacheEntryDecision {
|
|
155
|
+
if (input.env.enabled === "0") return { entry: null, reason: "env-opt-out" };
|
|
156
|
+
if (input.env.enabled !== "1" && !input.defaultEnabled) return { entry: null, reason: "dev-default-off" };
|
|
157
|
+
const ttl = resolveAutoRouteCacheTtl({
|
|
158
|
+
enabled: input.env.enabled,
|
|
159
|
+
ttl: input.env.ttl,
|
|
160
|
+
defaultTtl: input.defaultTtl,
|
|
161
|
+
defaultEnabled: input.defaultEnabled,
|
|
162
|
+
});
|
|
163
|
+
if (ttl === null) return { entry: null, reason: "ttl-disabled" };
|
|
164
|
+
if (
|
|
165
|
+
!isRouteCachePathAllowed(input.url.pathname, {
|
|
166
|
+
allow: input.env.allow,
|
|
167
|
+
deny: input.env.deny,
|
|
168
|
+
defaultAllow: input.defaultAllow,
|
|
169
|
+
})
|
|
170
|
+
) {
|
|
171
|
+
return { entry: null, reason: "path-excluded" };
|
|
172
|
+
}
|
|
173
|
+
if (!isPublicRouteCacheableRequest(input.request)) return { entry: null, reason: "request-not-public" };
|
|
174
|
+
return { entry: createRouteCacheEntry({ request: input.request, url: input.url, theme: input.theme, ttl }) };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function resolvePublicRouteCacheEntry(input: PublicRouteCacheEntryInput): RouteCacheEntry | null {
|
|
178
|
+
return resolvePublicRouteCacheEntryDecision(input).entry;
|
|
179
|
+
}
|
|
180
|
+
|
|
115
181
|
export function resolveRouteCacheStoreTtl(baseTtl: number, state: RouteCacheRenderState): number | null {
|
|
116
182
|
if (!state.cacheable || state.revalidate === false) return null;
|
|
117
183
|
if (typeof state.revalidate !== "number") return baseTtl;
|
|
@@ -126,6 +192,7 @@ export function shouldStoreRouteCache(input: {
|
|
|
126
192
|
lateRedirect?: boolean;
|
|
127
193
|
}): RouteCacheRenderState {
|
|
128
194
|
const dynamicUsage = input.dynamicUsage ? { ...input.dynamicUsage } : undefined;
|
|
195
|
+
const routeId = input.policy?.routeId;
|
|
129
196
|
const tags = input.policy ? [...input.policy.tags] : undefined;
|
|
130
197
|
const revalidate = combineMinRevalidate(input.policy?.revalidate);
|
|
131
198
|
if (input.renderControlType) {
|
|
@@ -133,11 +200,38 @@ export function shouldStoreRouteCache(input: {
|
|
|
133
200
|
input.renderControlType === "redirect" && input.lateRedirect
|
|
134
201
|
? "late-redirect"
|
|
135
202
|
: `render-${input.renderControlType}`;
|
|
136
|
-
return { cacheable: false, revalidate, tags, dynamicUsage, reason };
|
|
203
|
+
return { cacheable: false, routeId, revalidate, tags, dynamicUsage, reason };
|
|
137
204
|
}
|
|
138
205
|
if (dynamicUsage?.headers || dynamicUsage?.cookies)
|
|
139
|
-
return { cacheable: false, revalidate, tags, dynamicUsage, reason: "dynamic-request-api" };
|
|
140
|
-
return { cacheable: input.policy?.cacheable !== false, revalidate, tags, dynamicUsage };
|
|
206
|
+
return { cacheable: false, routeId, revalidate, tags, dynamicUsage, reason: "dynamic-request-api" };
|
|
207
|
+
return { cacheable: input.policy?.cacheable !== false, routeId, revalidate, tags, dynamicUsage };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function hasRouteCacheInvalidationScope(invalidation?: RouteCacheInvalidation): boolean {
|
|
211
|
+
return Boolean(invalidation?.tags?.length || invalidation?.paths?.length);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function shouldInvalidateRouteCacheEntry(
|
|
215
|
+
metadata: RouteCacheMetadata,
|
|
216
|
+
invalidation: RouteCacheInvalidation,
|
|
217
|
+
): boolean {
|
|
218
|
+
if (invalidation.tags?.length) {
|
|
219
|
+
const entryTags = new Set(metadata.tags ?? []);
|
|
220
|
+
if (invalidation.tags.some((tag) => entryTags.has(tag))) return true;
|
|
221
|
+
}
|
|
222
|
+
if (invalidation.paths?.length) {
|
|
223
|
+
return invalidation.paths.some((path) => {
|
|
224
|
+
if (!path) return false;
|
|
225
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
226
|
+
return (
|
|
227
|
+
metadata.pathname === normalized ||
|
|
228
|
+
metadata.pathname.startsWith(normalized.endsWith("/") ? normalized : `${normalized}/`) ||
|
|
229
|
+
metadata.routeId === normalized ||
|
|
230
|
+
Boolean(metadata.routeId?.startsWith(normalized.endsWith("/") ? normalized : `${normalized}/`))
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
141
235
|
}
|
|
142
236
|
|
|
143
237
|
export class LruTtlCache<T> {
|
package/server/imageOptimizer.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import sharp from "sharp";
|
|
5
4
|
import { ImageOptimizerError } from "./imageOptimizerError";
|
|
6
5
|
import {
|
|
7
6
|
type AkanImageConfig,
|
|
@@ -11,6 +10,18 @@ import {
|
|
|
11
10
|
mergeAkanImageConfig,
|
|
12
11
|
} from "./types";
|
|
13
12
|
|
|
13
|
+
type SharpFactory = typeof import("sharp");
|
|
14
|
+
|
|
15
|
+
let sharpLoad: Promise<SharpFactory> | null = null;
|
|
16
|
+
|
|
17
|
+
function loadSharp(): Promise<SharpFactory> {
|
|
18
|
+
sharpLoad ??= import("sharp").then((mod) => {
|
|
19
|
+
const loaded = mod as unknown as { default?: SharpFactory } & SharpFactory;
|
|
20
|
+
return loaded.default ?? loaded;
|
|
21
|
+
});
|
|
22
|
+
return sharpLoad;
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
export interface ImageOptimizerOptions {
|
|
15
26
|
publicDir: string;
|
|
16
27
|
cacheDir: string;
|
|
@@ -274,6 +285,7 @@ export class ImageOptimizer {
|
|
|
274
285
|
buffer: Buffer,
|
|
275
286
|
options: { width: number; quality: number; contentType: string },
|
|
276
287
|
): Promise<Buffer> {
|
|
288
|
+
const sharp = await loadSharp();
|
|
277
289
|
if (sharp.concurrency() > 1) sharp.concurrency(Math.max(Math.floor(sharp.concurrency() / 2), 1));
|
|
278
290
|
const transformer = sharp(buffer, {
|
|
279
291
|
limitInputPixels: 268_402_689,
|
|
@@ -371,6 +383,7 @@ export class ImageOptimizer {
|
|
|
371
383
|
)
|
|
372
384
|
return false;
|
|
373
385
|
try {
|
|
386
|
+
const sharp = await loadSharp();
|
|
374
387
|
const metadata = await sharp(buffer, { animated: true }).metadata();
|
|
375
388
|
return Boolean(metadata.pages && metadata.pages > 1);
|
|
376
389
|
} catch {
|