akanjs 3.1.1 → 3.1.2-rc.0
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/README.ko.md +22 -0
- package/README.md +22 -0
- package/client/cookie.ts +23 -7
- package/client/csrTypes.ts +1 -1
- package/client/makePageProto.tsx +10 -1
- package/client/router.ts +57 -16
- package/common/IpLiteral.ts +4 -2
- package/common/TrustedProxy.ts +45 -2
- package/common/akanNames.ts +169 -0
- package/common/codeAgentProfile.ts +51 -4
- package/common/index.ts +13 -0
- package/common/localeConfig.ts +30 -5
- package/common/localePath.ts +26 -0
- package/common/markdownSpans.ts +3 -1
- package/common/mcpExposure.ts +7 -7
- package/common/websocketContract.ts +50 -0
- package/constant/index.ts +1 -0
- package/constant/inputParser.ts +10 -2
- package/constant/requestValue.ts +47 -0
- package/dictionary/agent.dictionary.ts +2 -2
- package/dictionary/base.dictionary.ts +4 -0
- package/dictionary/dictInfo.ts +9 -9
- package/document/database.ts +1 -0
- package/document/documentQuery.ts +42 -6
- package/document/filterMeta.ts +56 -8
- package/document/queryEvaluator.ts +13 -8
- package/fetch/client/fetchClient.ts +28 -62
- package/fetch/client/httpClient.ts +1 -0
- package/fetch/client/sliceInitHandle.ts +18 -18
- package/fetch/index.ts +1 -0
- package/fetch/sessionCookie.ts +37 -0
- package/index.ts +8 -0
- package/package.json +13 -27
- package/server/akanApp.ts +108 -29
- package/server/akanServer.ts +18 -7
- package/server/assetHeaders.ts +20 -1
- package/server/cachePolicy.ts +18 -5
- package/server/devtools/depsSerializer.ts +2 -1
- package/server/devtools/devtoolsRouter.ts +3 -2
- package/server/devtools/signalSerializer.ts +5 -5
- package/server/hmr/clientReport.ts +10 -5
- package/server/imageOptimizer.ts +117 -22
- package/server/lifecycle/buildEnv.ts +11 -0
- package/server/lifecycle/loadBuildEnv.ts +3 -0
- package/server/logging/logControlSocket.ts +20 -14
- package/server/logging/logStreamRoute.ts +12 -8
- package/server/logging/rotatingLogWriter.ts +15 -1
- package/server/machine/machineToken.ts +4 -0
- package/server/mcp/McpAuth.ts +12 -4
- package/server/mcp/McpExecutionContext.ts +4 -0
- package/server/mcp/McpRouter.ts +25 -13
- package/server/mcp/PagePromptComposer.ts +4 -7
- package/server/oauth/OAuthClientIdMetadata.ts +29 -6
- package/server/oauth/OAuthRegistration.ts +9 -0
- package/server/ops/failureLimiter.ts +42 -0
- package/server/ops/opsCommand.ts +19 -7
- package/server/ops/opsRoute.ts +60 -9
- package/server/ops/opsToken.ts +5 -0
- package/server/ops/snapshotJobs.ts +3 -1
- package/server/ops/snapshotRestore.ts +23 -1
- package/server/ops/sqliteSnapshot.ts +98 -18
- package/server/ops/statusAccess.ts +37 -19
- package/server/proxy/hostBasePathWebProxy.ts +4 -7
- package/server/proxy/localeWebProxy.ts +36 -16
- package/server/resolver/database.resolver.ts +15 -11
- package/server/resolver/service.resolver.ts +17 -2
- package/server/resolver/signal.resolver.ts +50 -40
- package/server/routeElementComposer.tsx +42 -9
- package/server/routeShellGate.ts +42 -0
- package/server/routing/apiRouter.ts +64 -1
- package/server/routing/serverLimits.ts +151 -0
- package/server/routing/soloAppRoutes.ts +23 -12
- package/server/rscClient.tsx +5 -3
- package/server/rscScrollRestoration.ts +60 -9
- package/server/rscWorker.tsx +76 -24
- package/server/sitemap.ts +5 -7
- package/server/ssrFromRscRenderer.tsx +36 -4
- package/server/ssrTypes.ts +5 -2
- package/server/staticPath.ts +36 -1
- package/server/systemPageDocument.tsx +5 -3
- package/server/systemPages.tsx +14 -8
- package/server/tunnel/TunnelAgent.ts +19 -2
- package/server/tunnel/TunnelDataSocket.ts +4 -2
- package/server/tunnel/TunnelHttpStream.ts +7 -2
- package/server/tunnel/TunnelTcpStream.ts +6 -3
- package/server/tunnel/TunnelWebsocketStream.ts +9 -2
- package/server/tunnel/tunnelStream.ts +6 -0
- package/server/types.tsx +20 -2
- package/server/webRouter.ts +38 -18
- package/service/agent.service.ts +9 -7
- package/service/injectInfo.ts +2 -1
- package/service/liveSyncHub.ts +6 -0
- package/service/predefinedAdaptor/anthropicLlm.ts +13 -12
- package/service/predefinedAdaptor/database.adaptor.ts +4 -0
- package/service/predefinedAdaptor/llm.adaptor.ts +3 -0
- package/service/predefinedAdaptor/llmOverflow.ts +5 -1
- package/service/predefinedAdaptor/openaiDialect.ts +9 -8
- package/service/predefinedAdaptor/searchIndex.ts +21 -1
- package/service/predefinedAdaptor/sql/QueryCompiler.ts +28 -4
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +45 -11
- package/service/predefinedAdaptor/sql/dialect/postgres.ts +6 -1
- package/service/predefinedAdaptor/sql/types.ts +3 -0
- package/service/predefinedAdaptor/storage.adaptor.ts +11 -10
- package/service/serve.ts +3 -3
- package/service/serviceModule.ts +28 -24
- package/service/types.ts +3 -0
- package/signal/SignalFailure.ts +5 -2
- package/signal/agent/AgentCatalogue.ts +3 -2
- package/signal/agentMeter.ts +81 -9
- package/signal/agentRelayPolicy.ts +53 -2
- package/signal/agentTurnStream.ts +12 -1
- package/signal/guard.ts +2 -2
- package/signal/mcp/McpDocument.ts +3 -3
- package/signal/mcp/McpUriTemplate.ts +5 -5
- package/signal/middleware.ts +15 -1
- package/signal/roomKey.ts +53 -0
- package/signal/signalContext.ts +100 -40
- package/signal/slice.ts +27 -22
- package/signal/trace.ts +17 -6
- package/store/action.ts +20 -49
- package/store/actionTag.ts +2 -0
- package/store/agent/ScreenReader.ts +36 -4
- package/store/agent/ScreenTarget.ts +1 -1
- package/store/agent/StoreCatalogue.ts +7 -1
- package/store/agent/StoreSurfaceSource.ts +5 -0
- package/store/agentic/AgentConfirm.ts +30 -0
- package/store/agentic/StStateBuilder.ts +2 -2
- package/store/agentic/StToolBuilder.ts +7 -2
- package/store/agentic/formFields.ts +1 -2
- package/store/agentic/index.ts +1 -0
- package/store/agentic/useFieldTool.ts +4 -3
- package/store/deprecatedNames.ts +6 -0
- package/store/draftStore.ts +2 -2
- package/store/formUploadOf.ts +6 -5
- package/store/index.ts +1 -2
- package/store/sliceKeys.ts +9 -32
- package/store/state.ts +1 -1
- package/store/stateBuilder.ts +1 -1
- package/store/store.ts +1 -1
- package/store/storeInstance.ts +12 -8
- package/store/storeRegistry.ts +14 -2
- package/types/client/cookie.d.ts +7 -2
- package/types/client/csrTypes.d.ts +1 -1
- package/types/client/router.d.ts +4 -1
- package/types/common/akanNames.d.ts +149 -0
- package/types/common/codeAgentProfile.d.ts +4 -1
- package/types/common/index.d.ts +3 -1
- package/types/common/localeConfig.d.ts +8 -0
- package/types/common/localePath.d.ts +13 -0
- package/types/common/websocketContract.d.ts +7 -0
- package/types/constant/index.d.ts +1 -0
- package/types/constant/requestValue.d.ts +5 -0
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +3 -3
- package/types/document/database.d.ts +1 -0
- package/types/document/documentQuery.d.ts +3 -0
- package/types/document/filterMeta.d.ts +4 -0
- package/types/fetch/client/sliceInitHandle.d.ts +1 -2
- package/types/fetch/index.d.ts +1 -0
- package/types/fetch/sessionCookie.d.ts +12 -0
- package/types/index.d.ts +8 -0
- package/types/server/akanApp.d.ts +1 -0
- package/types/server/assetHeaders.d.ts +5 -0
- package/types/server/cachePolicy.d.ts +2 -0
- package/types/server/devtools/devtoolsRouter.d.ts +1 -1
- package/types/server/imageOptimizer.d.ts +4 -1
- package/types/server/lifecycle/buildEnv.d.ts +2 -0
- package/types/server/lifecycle/loadBuildEnv.d.ts +1 -0
- package/types/server/logging/logStreamRoute.d.ts +1 -1
- package/types/server/logging/rotatingLogWriter.d.ts +3 -0
- package/types/server/machine/machineToken.d.ts +2 -0
- package/types/server/mcp/McpAuth.d.ts +2 -1
- package/types/server/mcp/McpExecutionContext.d.ts +1 -0
- package/types/server/oauth/OAuthRegistration.d.ts +2 -0
- package/types/server/ops/failureLimiter.d.ts +11 -0
- package/types/server/ops/opsRoute.d.ts +4 -2
- package/types/server/ops/opsToken.d.ts +2 -0
- package/types/server/ops/snapshotRestore.d.ts +3 -1
- package/types/server/ops/sqliteSnapshot.d.ts +7 -1
- package/types/server/ops/statusAccess.d.ts +8 -4
- package/types/server/proxy/hostBasePathWebProxy.d.ts +1 -0
- package/types/server/resolver/signal.resolver.d.ts +0 -1
- package/types/server/routeElementComposer.d.ts +10 -4
- package/types/server/routeShellGate.d.ts +12 -0
- package/types/server/routing/apiRouter.d.ts +6 -1
- package/types/server/routing/serverLimits.d.ts +23 -0
- package/types/server/routing/soloAppRoutes.d.ts +1 -1
- package/types/server/rscScrollRestoration.d.ts +1 -0
- package/types/server/ssrTypes.d.ts +5 -2
- package/types/server/staticPath.d.ts +1 -0
- package/types/server/systemPageDocument.d.ts +1 -0
- package/types/server/tunnel/TunnelAgent.d.ts +6 -0
- package/types/server/tunnel/TunnelDataSocket.d.ts +2 -0
- package/types/server/tunnel/TunnelTcpStream.d.ts +1 -1
- package/types/server/tunnel/tunnelStream.d.ts +1 -0
- package/types/server/types.d.ts +11 -2
- package/types/service/agent.service.d.ts +1 -1
- package/types/service/liveSyncHub.d.ts +1 -0
- package/types/service/predefinedAdaptor/anthropicLlm.d.ts +1 -1
- package/types/service/predefinedAdaptor/llm.adaptor.d.ts +1 -0
- package/types/service/predefinedAdaptor/openaiDialect.d.ts +1 -1
- package/types/service/predefinedAdaptor/searchIndex.d.ts +5 -1
- package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +1 -0
- package/types/service/predefinedAdaptor/sql/types.d.ts +3 -0
- package/types/service/types.d.ts +3 -0
- package/types/signal/SignalFailure.d.ts +1 -1
- package/types/signal/agentMeter.d.ts +4 -1
- package/types/signal/agentRelayPolicy.d.ts +12 -0
- package/types/signal/guard.d.ts +1 -1
- package/types/signal/roomKey.d.ts +8 -0
- package/types/signal/signalContext.d.ts +20 -6
- package/types/signal/slice.d.ts +1 -0
- package/types/signal/trace.d.ts +4 -0
- package/types/store/actionTag.d.ts +2 -0
- package/types/store/agent/ScreenReader.d.ts +2 -1
- package/types/store/agent/StoreCatalogue.d.ts +5 -0
- package/types/store/agentic/AgentConfirm.d.ts +15 -0
- package/types/store/agentic/StToolBuilder.d.ts +3 -1
- package/types/store/agentic/index.d.ts +1 -0
- package/types/store/deprecatedNames.d.ts +5 -0
- package/types/store/index.d.ts +1 -2
- package/types/store/sliceKeys.d.ts +12 -2
- package/types/ui/Agent/agentSessionOf.d.ts +2 -0
- package/types/ui/Agent/useAccountBoundSession.d.ts +2 -0
- package/types/ui/Agent/useScreenActions.d.ts +1 -1
- package/types/ui/Icon.d.ts +15 -0
- package/types/ui/animated.d.ts +3 -3
- package/types/ui/sliceNamesOf.d.ts +4 -4
- package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +10 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +6 -0
- package/types/vendor/@akanjs/native/packages/core/src/kernel.d.ts +6 -1
- package/types/vendor/@akanjs/native/packages/desktop/src/paths.d.ts +7 -0
- package/types/vendor/@akanjs/native/plugins/single-instance/src/socket.d.ts +2 -0
- package/types/vendor/@akanjs/native/plugins/updates/src/desktop.d.ts +2 -0
- package/types/vendor/use-agentic/AgentSession.d.ts +6 -1
- package/types/vendor/use-agentic/ToolRunner.d.ts +7 -2
- package/types/vendor/use-agentic/types.d.ts +2 -0
- package/types/vendor/use-agentic/useAgentTool.d.ts +2 -1
- package/types/webkit/useCsrValues.d.ts +3 -3
- package/ui/Agent/Chat.tsx +2 -0
- package/ui/Agent/Zone.tsx +2 -0
- package/ui/Agent/agentSessionOf.ts +54 -5
- package/ui/Agent/useAccountBoundSession.ts +26 -0
- package/ui/Agent/useScreenActions.ts +1 -1
- package/ui/BottomSheet.tsx +1 -1
- package/ui/Button.tsx +5 -5
- package/ui/Data/Item.tsx +2 -8
- package/ui/Data/ListContainer.tsx +4 -4
- package/ui/Data/QueryMaker.tsx +3 -2
- package/ui/Data/RefPicker.tsx +3 -2
- package/ui/Data/UnlistedEditModal.tsx +2 -1
- package/ui/Data/ViewActions.tsx +4 -4
- package/ui/Data/dataExport.ts +3 -0
- package/ui/Data/useViewModal.ts +10 -15
- package/ui/Dialog/LegacyModal.tsx +1 -1
- package/ui/Dialog/Provider.tsx +2 -2
- package/ui/DragAction.tsx +1 -1
- package/ui/Dropdown.tsx +2 -2
- package/ui/Field/lightRefCache.ts +2 -2
- package/ui/Field/useRelationOptions.ts +3 -10
- package/ui/Icon.tsx +87 -0
- package/ui/KeyboardAvoiding.tsx +1 -1
- package/ui/Layout/Sider.tsx +3 -3
- package/ui/Link/Anchor.tsx +4 -1
- package/ui/Load/Edit.tsx +2 -2
- package/ui/Load/Edit_Client.tsx +3 -3
- package/ui/Load/Pagination.tsx +3 -3
- package/ui/Load/View.tsx +15 -14
- package/ui/Loading/ProgressBar.tsx +1 -1
- package/ui/Model/DraftBar.tsx +7 -14
- package/ui/Model/EditModal.tsx +9 -40
- package/ui/Model/EditWrapper.tsx +3 -5
- package/ui/Model/NewWrapper_Client.tsx +3 -7
- package/ui/Model/Remove.tsx +2 -2
- package/ui/Model/RemoveWrapper.tsx +2 -4
- package/ui/Model/SureToRemove.tsx +2 -2
- package/ui/Model/ViewEditModal.tsx +9 -20
- package/ui/Model/ViewModal.tsx +4 -11
- package/ui/Model/ViewWrapper.tsx +4 -6
- package/ui/ScreenNavigator.tsx +2 -2
- package/ui/Signal/WebSocket.tsx +2 -2
- package/ui/System/CSR.tsx +3 -3
- package/ui/System/Reconnect.tsx +2 -2
- package/ui/System/SSR.tsx +2 -2
- package/ui/System/SelectLanguage.tsx +1 -1
- package/ui/System/ThemeToggle.tsx +1 -1
- package/ui/Tab/Provider.tsx +1 -0
- package/ui/Toast.tsx +7 -15
- package/ui/index.ts +0 -1
- package/ui/sliceNamesOf.ts +25 -15
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +2 -1
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeKernel.kt +10 -2
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeVectors.kt +4 -0
- package/vendor/@akanjs/native/native/desktop/src/acl.rs +166 -0
- package/vendor/@akanjs/native/native/desktop/src/lib.rs +25 -6
- package/vendor/@akanjs/native/native/desktop/src/navigation.rs +22 -10
- package/vendor/@akanjs/native/native/desktop/src/routes.rs +2 -2
- package/vendor/@akanjs/native/native/desktop/src/vectors.rs +28 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeBridge.swift +1 -1
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeKernel.swift +10 -2
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeVectors.swift +4 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeViewController.swift +1 -0
- package/vendor/@akanjs/native/packages/cli/src/config.ts +10 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +2 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/csp.ts +5 -3
- package/vendor/@akanjs/native/packages/cli/src/lib/prepare.ts +4 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +2 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +30 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/vectors.ts +1 -1
- package/vendor/@akanjs/native/packages/core/src/kernel.ts +18 -2
- package/vendor/@akanjs/native/packages/core/vectors/navigation.json +22 -1
- package/vendor/@akanjs/native/packages/core/vectors/routes.json +4 -0
- package/vendor/@akanjs/native/packages/desktop/src/main.ts +5 -1
- package/vendor/@akanjs/native/packages/desktop/src/paths.ts +24 -1
- package/vendor/@akanjs/native/plugins/external-view/src/desktop.ts +4 -5
- package/vendor/@akanjs/native/plugins/file-picker/src/desktop.ts +6 -8
- package/vendor/@akanjs/native/plugins/filesystem/src/desktop.ts +4 -2
- package/vendor/@akanjs/native/plugins/single-instance/src/socket.ts +22 -3
- package/vendor/@akanjs/native/plugins/updates/android/UpdatesPlugin.kt +6 -0
- package/vendor/@akanjs/native/plugins/updates/ios/UpdatesPlugin.swift +6 -0
- package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +106 -18
- package/vendor/use-agentic/AgentSession.ts +7 -2
- package/vendor/use-agentic/ToolRunner.ts +44 -5
- package/vendor/use-agentic/types.ts +2 -0
- package/vendor/use-agentic/useAgentTool.ts +4 -2
- package/webkit/McpAppAgent.ts +2 -1
- package/webkit/bootCsr.tsx +1 -1
- package/webkit/useCsrValues.ts +5 -1
- package/webkit/usePageTool.tsx +1 -0
- package/store/databaseStateNames.ts +0 -24
- package/store/formSetterNames.ts +0 -18
- package/types/store/databaseStateNames.d.ts +0 -18
- package/types/store/formSetterNames.d.ts +0 -11
|
@@ -12,6 +12,7 @@ import { Children, cloneElement, isValidElement, type ReactElement, type ReactNo
|
|
|
12
12
|
import { getExplicitPageConfigKeys, resolvePageState } from "../client/frameConfig";
|
|
13
13
|
import { RouteGuards } from "../client/route/RouteGuards";
|
|
14
14
|
import { resolveHeadResult } from "./head";
|
|
15
|
+
import { RouteShellGate } from "./routeShellGate";
|
|
15
16
|
import {
|
|
16
17
|
type AkanRouteSegmentState,
|
|
17
18
|
createAkanRouteSegments,
|
|
@@ -73,12 +74,14 @@ export class RouteElementComposer {
|
|
|
73
74
|
searchParams,
|
|
74
75
|
navKey,
|
|
75
76
|
notFound,
|
|
77
|
+
gate,
|
|
76
78
|
}: {
|
|
77
79
|
pathRoute: PathRoute;
|
|
78
80
|
params: Record<string, string>;
|
|
79
81
|
searchParams: Record<string, string | string[]>;
|
|
80
82
|
navKey?: string;
|
|
81
83
|
notFound?: RouteNotFoundInPlace;
|
|
84
|
+
gate?: RouteShellGate;
|
|
82
85
|
}): ReactNode {
|
|
83
86
|
return RouteElementComposer.composeRenders({
|
|
84
87
|
renders: RouteElementComposer.#getRenderStack(pathRoute),
|
|
@@ -87,6 +90,7 @@ export class RouteElementComposer {
|
|
|
87
90
|
searchParams,
|
|
88
91
|
navKey,
|
|
89
92
|
notFound,
|
|
93
|
+
gate,
|
|
90
94
|
});
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -205,6 +209,7 @@ export class RouteElementComposer {
|
|
|
205
209
|
pathname,
|
|
206
210
|
error,
|
|
207
211
|
digest,
|
|
212
|
+
gate,
|
|
208
213
|
}: {
|
|
209
214
|
kind: "not-found" | "error";
|
|
210
215
|
route: PathRoute | LayoutFallbackRoute;
|
|
@@ -213,6 +218,7 @@ export class RouteElementComposer {
|
|
|
213
218
|
pathname: string;
|
|
214
219
|
error?: unknown;
|
|
215
220
|
digest?: string;
|
|
221
|
+
gate?: RouteShellGate;
|
|
216
222
|
}): Promise<ReactNode | null> {
|
|
217
223
|
const layoutStack = [...route.renderRootLayouts, ...route.renderLayouts];
|
|
218
224
|
for (let index = layoutStack.length - 1; index >= 0; index--) {
|
|
@@ -225,7 +231,7 @@ export class RouteElementComposer {
|
|
|
225
231
|
...layoutStack.slice(0, index + 1),
|
|
226
232
|
RouteElementComposer.#makeFallbackRouteRender({ kind, fallback, pathname, error, digest }),
|
|
227
233
|
];
|
|
228
|
-
return RouteElementComposer.composeRenders({ renders, params, searchParams });
|
|
234
|
+
return RouteElementComposer.composeRenders({ renders, params, searchParams, gate });
|
|
229
235
|
}
|
|
230
236
|
return null;
|
|
231
237
|
}
|
|
@@ -238,6 +244,7 @@ export class RouteElementComposer {
|
|
|
238
244
|
searchParams,
|
|
239
245
|
navKey,
|
|
240
246
|
notFound,
|
|
247
|
+
gate,
|
|
241
248
|
}: {
|
|
242
249
|
renders: RouteRender[];
|
|
243
250
|
/** Layouts above `renders` that this element leaves out (a patch) but whose `.notFound()` still applies. */
|
|
@@ -247,7 +254,10 @@ export class RouteElementComposer {
|
|
|
247
254
|
searchParams: Record<string, string | string[]>;
|
|
248
255
|
navKey?: string;
|
|
249
256
|
notFound?: RouteNotFoundInPlace;
|
|
257
|
+
/** Holds the response until every level above the first `.loading()` has rendered. */
|
|
258
|
+
gate?: RouteShellGate;
|
|
250
259
|
}): ReactNode {
|
|
260
|
+
const levelGates = gate ? RouteElementComposer.#gateLevels(renders, gate) : [];
|
|
251
261
|
let element: ReactNode = null;
|
|
252
262
|
for (let i = renders.length - 1; i >= 0; i--) {
|
|
253
263
|
const routeRender = renders[i];
|
|
@@ -255,16 +265,23 @@ export class RouteElementComposer {
|
|
|
255
265
|
const loadingFallback = RouteElementComposer.#composeLoadingFallback(renders.slice(i), params);
|
|
256
266
|
const suspenseKey =
|
|
257
267
|
navKey && loadingFallback != null && i === renders.length - 1 ? `akan-loading:${navKey}` : undefined;
|
|
268
|
+
const levelGate = levelGates[i];
|
|
269
|
+
const level = (
|
|
270
|
+
<RouteElementComposer.AsyncRender
|
|
271
|
+
routeRender={routeRender}
|
|
272
|
+
params={params}
|
|
273
|
+
searchParams={searchParams}
|
|
274
|
+
notFound={notFound ? { ...notFound, owners: [...owners, ...renders.slice(0, i)] } : undefined}
|
|
275
|
+
gate={levelGate}
|
|
276
|
+
>
|
|
277
|
+
{element}
|
|
278
|
+
</RouteElementComposer.AsyncRender>
|
|
279
|
+
);
|
|
280
|
+
|
|
258
281
|
element = (
|
|
259
282
|
<Suspense key={suspenseKey} fallback={loadingFallback}>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
params={params}
|
|
263
|
-
searchParams={searchParams}
|
|
264
|
-
notFound={notFound ? { ...notFound, owners: [...owners, ...renders.slice(0, i)] } : undefined}
|
|
265
|
-
>
|
|
266
|
-
{element}
|
|
267
|
-
</RouteElementComposer.AsyncRender>
|
|
283
|
+
{level}
|
|
284
|
+
{routeRender.Loading && levelGate ? <RouteShellGate.Wait until={levelGate.opened} /> : null}
|
|
268
285
|
</Suspense>
|
|
269
286
|
);
|
|
270
287
|
const segment = segments?.[i];
|
|
@@ -286,13 +303,16 @@ export class RouteElementComposer {
|
|
|
286
303
|
params,
|
|
287
304
|
searchParams,
|
|
288
305
|
notFound,
|
|
306
|
+
gate,
|
|
289
307
|
}: {
|
|
290
308
|
routeRender: RouteRender;
|
|
291
309
|
children: ReactNode;
|
|
292
310
|
params: Record<string, string>;
|
|
293
311
|
searchParams: Record<string, string | string[]>;
|
|
294
312
|
notFound?: RouteNotFoundInPlace & { owners: RouteRender[] };
|
|
313
|
+
gate?: RouteShellGate;
|
|
295
314
|
}) => {
|
|
315
|
+
gate?.enter();
|
|
296
316
|
try {
|
|
297
317
|
return RouteElementComposer.#normalizeReactNode(
|
|
298
318
|
await routeRender.render({ children, params, searchParams } as never),
|
|
@@ -301,6 +321,8 @@ export class RouteElementComposer {
|
|
|
301
321
|
if (!notFound || (error as { digest?: unknown } | null)?.digest !== "AKAN_NOT_FOUND") throw error;
|
|
302
322
|
notFound.onNotFound();
|
|
303
323
|
return await RouteElementComposer.#renderNotFoundInPlace(notFound, params, searchParams);
|
|
324
|
+
} finally {
|
|
325
|
+
gate?.leave();
|
|
304
326
|
}
|
|
305
327
|
};
|
|
306
328
|
|
|
@@ -380,6 +402,17 @@ export class RouteElementComposer {
|
|
|
380
402
|
return [...layoutConfigs, ...(pageConfig ? [pageConfig] : [])];
|
|
381
403
|
}
|
|
382
404
|
|
|
405
|
+
static #gateLevels(renders: RouteRender[], gate: RouteShellGate): RouteShellGate[] {
|
|
406
|
+
let current = gate;
|
|
407
|
+
const gates = renders.map((routeRender) => {
|
|
408
|
+
if (routeRender?.Loading) current = new RouteShellGate();
|
|
409
|
+
return current;
|
|
410
|
+
});
|
|
411
|
+
for (const levelGate of new Set([gate, ...gates]))
|
|
412
|
+
levelGate.hold(gates.filter((held, index) => held === levelGate && renders[index]).length);
|
|
413
|
+
return gates;
|
|
414
|
+
}
|
|
415
|
+
|
|
383
416
|
static #composeLoadingFallback(renders: RouteRender[], params: Record<string, string>): ReactNode {
|
|
384
417
|
let element: ReactNode = null;
|
|
385
418
|
for (let i = renders.length - 1; i >= 0; i--) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export class RouteShellGate {
|
|
2
|
+
|
|
3
|
+
static Wait = async ({ until }: { until: Promise<null> }) => await until;
|
|
4
|
+
|
|
5
|
+
readonly opened: Promise<null>;
|
|
6
|
+
#open: () => void = () => undefined;
|
|
7
|
+
#holding = false;
|
|
8
|
+
#rendering = 0;
|
|
9
|
+
#activity = 0;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.opened = new Promise<null>((resolve) => {
|
|
13
|
+
this.#open = () => resolve(null);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get holding(): boolean {
|
|
18
|
+
return this.#holding;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
hold(levels: number): void {
|
|
22
|
+
this.#holding = levels > 0;
|
|
23
|
+
if (levels === 0) this.#open();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
enter(): void {
|
|
27
|
+
this.#rendering += 1;
|
|
28
|
+
this.#activity += 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
leave(): void {
|
|
32
|
+
this.#rendering -= 1;
|
|
33
|
+
const activity = ++this.#activity;
|
|
34
|
+
setImmediate(() =>
|
|
35
|
+
setImmediate(() => {
|
|
36
|
+
if (this.#rendering > 0 || this.#activity !== activity) return;
|
|
37
|
+
this.#holding = false;
|
|
38
|
+
this.#open();
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -15,6 +15,7 @@ import { SignalResolver } from "../resolver";
|
|
|
15
15
|
import type { HttpRoutes, SignalRouteOptions, WebsocketRoutes } from "../types";
|
|
16
16
|
import { AppWsData } from "./appWsData";
|
|
17
17
|
import type { HostAllowlist } from "./hostAllowlist";
|
|
18
|
+
import { ServerLimits } from "./serverLimits";
|
|
18
19
|
import { WsUpgrade } from "./wsUpgrade";
|
|
19
20
|
|
|
20
21
|
export interface HmrStateSource {
|
|
@@ -43,6 +44,10 @@ export interface ApiRouteInputs {
|
|
|
43
44
|
upgradeAppWs: (req: Request, data: AppWsData) => boolean;
|
|
44
45
|
webProxyRunner?: WebProxyRunner | null;
|
|
45
46
|
hostAllowlist?: HostAllowlist | null;
|
|
47
|
+
bodyLimits?: ServerLimits | null;
|
|
48
|
+
/** Off behind the gateway, which already counted the chunked body it streams on. */
|
|
49
|
+
countChunkedBodies?: boolean;
|
|
50
|
+
idleTimeout?: number;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
type RouteValue = NonNullHttpRoutes[keyof NonNullHttpRoutes];
|
|
@@ -72,6 +77,9 @@ export class ApiRouter {
|
|
|
72
77
|
upgradeAppWs,
|
|
73
78
|
webProxyRunner,
|
|
74
79
|
hostAllowlist,
|
|
80
|
+
bodyLimits,
|
|
81
|
+
countChunkedBodies = true,
|
|
82
|
+
idleTimeout = 0,
|
|
75
83
|
}: ApiRouteInputs): NonNullHttpRoutes {
|
|
76
84
|
const endpointEntries = Object.entries(routes ?? {}).map(
|
|
77
85
|
([p, handler]) =>
|
|
@@ -109,7 +117,11 @@ export class ApiRouter {
|
|
|
109
117
|
served,
|
|
110
118
|
new Set([...endpointEntries.map(([path]) => path), ...Object.keys(renderEnvRoutes ?? {})]),
|
|
111
119
|
);
|
|
112
|
-
|
|
120
|
+
const limited = bodyLimits ? ApiRouter.#limitBodies(swept, bodyLimits, countChunkedBodies) : swept;
|
|
121
|
+
const routed = ApiRouter.#dropForgedForwarding(
|
|
122
|
+
hostAllowlist ? ApiRouter.#guardHosts(limited, hostAllowlist) : limited,
|
|
123
|
+
);
|
|
124
|
+
return idleTimeout ? ApiRouter.#holdOpen(routed, idleTimeout) : routed;
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
static buildWebsocketHandlers({
|
|
@@ -257,6 +269,57 @@ export class ApiRouter {
|
|
|
257
269
|
) as NonNullHttpRoutes;
|
|
258
270
|
}
|
|
259
271
|
|
|
272
|
+
static #limitBodies(routes: NonNullHttpRoutes, limits: ServerLimits, countChunked: boolean): NonNullHttpRoutes {
|
|
273
|
+
return Object.fromEntries(
|
|
274
|
+
Object.entries(routes).map(([path, route]) => [
|
|
275
|
+
path,
|
|
276
|
+
route instanceof Response
|
|
277
|
+
? route
|
|
278
|
+
: ApiRouter.#mapRoute(route, (handler) => (req) => {
|
|
279
|
+
const refused = limits.refuseDeclared(req);
|
|
280
|
+
if (refused) return refused;
|
|
281
|
+
const limit = countChunked && !req.headers.has("content-length") ? limits.bodyLimitOf(req) : null;
|
|
282
|
+
if (limit === null || !req.body) return handler(req);
|
|
283
|
+
let exceeded = false;
|
|
284
|
+
const body = ServerLimits.limitBody(req.body, limit, () => {
|
|
285
|
+
exceeded = true;
|
|
286
|
+
});
|
|
287
|
+
const counted = new Request(req.url, {
|
|
288
|
+
method: req.method,
|
|
289
|
+
headers: req.headers,
|
|
290
|
+
body,
|
|
291
|
+
signal: req.signal,
|
|
292
|
+
duplex: "half",
|
|
293
|
+
} as RequestInit);
|
|
294
|
+
return (async () => {
|
|
295
|
+
try {
|
|
296
|
+
const response = await handler(copyBunRequestFields(counted, req));
|
|
297
|
+
return exceeded ? ServerLimits.payloadTooLarge() : response;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (exceeded) return ServerLimits.payloadTooLarge();
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
})();
|
|
303
|
+
}),
|
|
304
|
+
]),
|
|
305
|
+
) as NonNullHttpRoutes;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
static #holdOpen(routes: NonNullHttpRoutes, idleTimeout: number): NonNullHttpRoutes {
|
|
309
|
+
return Object.fromEntries(
|
|
310
|
+
Object.entries(routes).map(([path, route]) => [
|
|
311
|
+
path,
|
|
312
|
+
route instanceof Response
|
|
313
|
+
? route
|
|
314
|
+
: ApiRouter.#mapRoute(
|
|
315
|
+
route,
|
|
316
|
+
(handler) => (req, server?: Bun.Server<unknown>) =>
|
|
317
|
+
server ? ServerLimits.holdOpen(server, req, idleTimeout, () => handler(req)) : handler(req),
|
|
318
|
+
),
|
|
319
|
+
]),
|
|
320
|
+
) as NonNullHttpRoutes;
|
|
321
|
+
}
|
|
322
|
+
|
|
260
323
|
static #guardHosts(routes: NonNullHttpRoutes, allowlist: HostAllowlist): NonNullHttpRoutes {
|
|
261
324
|
return Object.fromEntries(
|
|
262
325
|
Object.entries(routes).map(([path, route]) => [
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { Logger } from "akanjs/common";
|
|
3
|
+
|
|
4
|
+
type Env = Record<string, string | undefined>;
|
|
5
|
+
|
|
6
|
+
export class ServerLimits {
|
|
7
|
+
|
|
8
|
+
static readonly maxIdleSeconds = 255;
|
|
9
|
+
static readonly defaultIdleSeconds = 120;
|
|
10
|
+
static readonly defaultUploadBodyBytes = 128 * 1024 * 1024;
|
|
11
|
+
static readonly defaultRequestBodyBytes = 16 * 1024 * 1024;
|
|
12
|
+
static readonly loopbackHost = "127.0.0.1";
|
|
13
|
+
static readonly #agentTurnPath = "/runAgentTurn";
|
|
14
|
+
static readonly #tooLarge = new Error("Request body is over the server's limit");
|
|
15
|
+
static readonly #logger = new Logger("ServerLimits");
|
|
16
|
+
static readonly #warned = new Set<string>();
|
|
17
|
+
|
|
18
|
+
readonly idleTimeout: number;
|
|
19
|
+
readonly wsIdleTimeout: number;
|
|
20
|
+
readonly uploadBodyBytes: number;
|
|
21
|
+
readonly requestBodyBytes: number | null;
|
|
22
|
+
|
|
23
|
+
constructor(env: Env = process.env) {
|
|
24
|
+
const devHosted = env.AKAN_COMMAND_TYPE === "start";
|
|
25
|
+
this.idleTimeout = ServerLimits.#seconds(env, "AKAN_IDLE_TIMEOUT", devHosted ? 0 : ServerLimits.defaultIdleSeconds);
|
|
26
|
+
this.wsIdleTimeout = ServerLimits.#seconds(env, "AKAN_WS_IDLE_TIMEOUT", ServerLimits.defaultIdleSeconds);
|
|
27
|
+
this.uploadBodyBytes = ServerLimits.#bytes(env, "AKAN_MAX_UPLOAD_BODY", ServerLimits.defaultUploadBodyBytes);
|
|
28
|
+
const requestBody = env.AKAN_MAX_REQUEST_BODY?.trim();
|
|
29
|
+
this.requestBodyBytes =
|
|
30
|
+
requestBody === "0" || requestBody?.toLowerCase() === "off"
|
|
31
|
+
? null
|
|
32
|
+
: Math.min(
|
|
33
|
+
ServerLimits.#bytes(env, "AKAN_MAX_REQUEST_BODY", ServerLimits.defaultRequestBodyBytes),
|
|
34
|
+
this.uploadBodyBytes,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static async holdOpen<T extends Response | undefined>(
|
|
39
|
+
server: Pick<Bun.Server<unknown>, "timeout">,
|
|
40
|
+
req: Request,
|
|
41
|
+
idleTimeout: number,
|
|
42
|
+
handle: () => T | Promise<T>,
|
|
43
|
+
): Promise<T> {
|
|
44
|
+
if (!idleTimeout) return await handle();
|
|
45
|
+
server.timeout(req, 0);
|
|
46
|
+
let upgraded = false;
|
|
47
|
+
try {
|
|
48
|
+
const response = await handle();
|
|
49
|
+
upgraded = response === undefined;
|
|
50
|
+
return response;
|
|
51
|
+
} finally {
|
|
52
|
+
if (!upgraded) server.timeout(req, idleTimeout);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static listenHostname(env: Env = process.env, inContainer = ServerLimits.#inContainer(env)): string | undefined {
|
|
57
|
+
const explicit = env.AKAN_LISTEN_HOST?.trim();
|
|
58
|
+
if (explicit) return explicit === "*" ? undefined : explicit;
|
|
59
|
+
return env.AKAN_COMMAND_TYPE === "start" && !inContainer ? ServerLimits.loopbackHost : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static serveOwnerOnly<T>(socketPath: string, serve: () => T): T {
|
|
63
|
+
const previous = process.umask(0o177);
|
|
64
|
+
try {
|
|
65
|
+
return serve();
|
|
66
|
+
} finally {
|
|
67
|
+
process.umask(previous);
|
|
68
|
+
try {
|
|
69
|
+
fs.chmodSync(socketPath, 0o600);
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static #inContainer(env: Env): boolean {
|
|
76
|
+
if (env.KUBERNETES_SERVICE_HOST) return true;
|
|
77
|
+
return fs.existsSync("/.dockerenv") || fs.existsSync("/run/.containerenv");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static #seconds(env: Env, name: string, fallback: number): number {
|
|
81
|
+
const raw = env[name]?.trim();
|
|
82
|
+
if (!raw) return fallback;
|
|
83
|
+
const match = /^(\d+)\s*s?$/i.exec(raw);
|
|
84
|
+
if (match) return Math.min(Number(match[1]), ServerLimits.maxIdleSeconds);
|
|
85
|
+
ServerLimits.#warnUnread(name, raw, "seconds such as 30 or 30s", `${fallback} s`);
|
|
86
|
+
return fallback;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static #bytes(env: Env, name: string, fallback: number): number {
|
|
90
|
+
const raw = env[name]?.trim();
|
|
91
|
+
if (!raw) return fallback;
|
|
92
|
+
const match = /^(\d+(?:\.\d+)?)\s*(?:([kmg])(?:i?b)?|b)?$/i.exec(raw);
|
|
93
|
+
const unit = match?.[2]?.toLowerCase();
|
|
94
|
+
const scale = unit === "g" ? 1024 ** 3 : unit === "m" ? 1024 ** 2 : unit === "k" ? 1024 : 1;
|
|
95
|
+
const bytes = match ? Math.floor(Number(match[1]) * scale) : 0;
|
|
96
|
+
if (bytes > 0) return bytes;
|
|
97
|
+
ServerLimits.#warnUnread(name, raw, "a size such as 512mb, 1.5g or 16MiB", `${fallback / 1024 ** 2} MiB`);
|
|
98
|
+
return fallback;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static #warnUnread(name: string, raw: string, expected: string, fallback: string) {
|
|
102
|
+
const key = `${name}=${raw}`;
|
|
103
|
+
if (ServerLimits.#warned.has(key)) return;
|
|
104
|
+
ServerLimits.#warned.add(key);
|
|
105
|
+
ServerLimits.#logger.warn(`${name}="${raw}" is not ${expected}; using ${fallback}.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
bodyLimitOf(req: Request): number | null {
|
|
109
|
+
if (this.requestBodyBytes === null || req.method === "GET" || req.method === "HEAD") return null;
|
|
110
|
+
const type = req.headers.get("content-type")?.toLowerCase() ?? "";
|
|
111
|
+
if (type.startsWith("multipart/form-data")) return null;
|
|
112
|
+
return new URL(req.url).pathname.endsWith(ServerLimits.#agentTurnPath) ? null : this.requestBodyBytes;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
refuseDeclared(req: Request): Response | null {
|
|
116
|
+
const limit = this.bodyLimitOf(req);
|
|
117
|
+
const declared = req.headers.get("content-length");
|
|
118
|
+
if (limit === null || declared === null) return null;
|
|
119
|
+
return Number(declared) > limit ? ServerLimits.payloadTooLarge() : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static limitBody(
|
|
123
|
+
body: ReadableStream<Uint8Array>,
|
|
124
|
+
limit: number,
|
|
125
|
+
onExceeded?: () => void,
|
|
126
|
+
): ReadableStream<Uint8Array> {
|
|
127
|
+
let seen = 0;
|
|
128
|
+
return body.pipeThrough(
|
|
129
|
+
new TransformStream<Uint8Array, Uint8Array>({
|
|
130
|
+
transform(chunk, controller) {
|
|
131
|
+
seen += chunk.byteLength;
|
|
132
|
+
if (seen <= limit) return controller.enqueue(chunk);
|
|
133
|
+
onExceeded?.();
|
|
134
|
+
controller.error(ServerLimits.#tooLarge);
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static payloadTooLarge(): Response {
|
|
141
|
+
return new Response("Payload Too Large", {
|
|
142
|
+
status: 413,
|
|
143
|
+
headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", connection: "close" },
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
static isPayloadTooLarge(error: unknown): boolean {
|
|
148
|
+
if (error === ServerLimits.#tooLarge) return true;
|
|
149
|
+
return !!error && typeof error === "object" && (error as { cause?: unknown }).cause === ServerLimits.#tooLarge;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AkanChildRole, AkanMetricsReport } from "akanjs/service";
|
|
2
|
+
import { SignalContext } from "akanjs/signal";
|
|
2
3
|
import { AppInfo } from "../ops/appInfo";
|
|
3
4
|
import type { OpsRoute } from "../ops/opsRoute";
|
|
4
5
|
import { StatusAccess } from "../ops/statusAccess";
|
|
@@ -14,7 +15,7 @@ export interface SoloAppStatus {
|
|
|
14
15
|
|
|
15
16
|
export const createSoloAppRoutes = (
|
|
16
17
|
read: () => SoloAppStatus,
|
|
17
|
-
logStream: { handle(req: Request): Response } | null = null,
|
|
18
|
+
logStream: { handle(req: Request, client?: string | null): Response } | null = null,
|
|
18
19
|
ops: OpsRoute | null = null,
|
|
19
20
|
access = StatusAccess.fromEnv(),
|
|
20
21
|
): HttpRoutes => {
|
|
@@ -32,22 +33,32 @@ export const createSoloAppRoutes = (
|
|
|
32
33
|
return {
|
|
33
34
|
"/_akan/app/health": {
|
|
34
35
|
GET: (req: Request) =>
|
|
35
|
-
access.health(
|
|
36
|
+
access.health(
|
|
37
|
+
req,
|
|
38
|
+
() => ({ status: read().status, pid: process.pid, solo: true, children: [child()] }),
|
|
39
|
+
SignalContext.clientIpOf(req),
|
|
40
|
+
),
|
|
36
41
|
},
|
|
37
42
|
"/_akan/app/metrics": {
|
|
38
43
|
GET: (req: Request) =>
|
|
39
|
-
access.metrics(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
access.metrics(
|
|
45
|
+
req,
|
|
46
|
+
() => ({
|
|
47
|
+
rooms: 0,
|
|
48
|
+
sockets: 0,
|
|
49
|
+
solo: true,
|
|
50
|
+
gateway: null,
|
|
51
|
+
proxyHop: null,
|
|
52
|
+
children: [{ ...child(), metrics: read().metrics }],
|
|
53
|
+
}),
|
|
54
|
+
SignalContext.clientIpOf(req),
|
|
55
|
+
),
|
|
47
56
|
},
|
|
48
57
|
[AppInfo.publicPath]: { GET: () => AppInfo.handlePublic() },
|
|
49
58
|
"/_akan/bench/ping": { GET: () => new Response("ok") },
|
|
50
|
-
...(logStream
|
|
51
|
-
|
|
59
|
+
...(logStream
|
|
60
|
+
? { "/_akan/app/logs": { GET: (req: Request) => logStream.handle(req, SignalContext.clientIpOf(req)) } }
|
|
61
|
+
: {}),
|
|
62
|
+
...(ops ? { "/_akan/ops/*": (req: Request) => ops.handle(req, SignalContext.clientIpOf(req)) } : {}),
|
|
52
63
|
};
|
|
53
64
|
};
|
package/server/rscClient.tsx
CHANGED
|
@@ -365,10 +365,11 @@ function forgetPagesOfAnotherAccount(): void {
|
|
|
365
365
|
|
|
366
366
|
function Root(): ReactNode {
|
|
367
367
|
const [thenable, setThenable] = useState<RscThenable>(initialThenable);
|
|
368
|
-
const [scrollRequest, setScrollRequest] = useState<{ scrollY: number } | null>(null);
|
|
368
|
+
const [scrollRequest, setScrollRequest] = useState<{ scrollY: number; hash?: string } | null>(null);
|
|
369
369
|
|
|
370
370
|
useLayoutEffect(() => {
|
|
371
|
-
if (scrollRequest) scrollRestoration.
|
|
371
|
+
if (scrollRequest?.hash) scrollRestoration.scrollToHash(scrollRequest.hash, scrollRequest.scrollY);
|
|
372
|
+
else if (scrollRequest) scrollRestoration.scrollTo(scrollRequest.scrollY);
|
|
372
373
|
}, [scrollRequest]);
|
|
373
374
|
|
|
374
375
|
globalThis.__AKAN_RSC_CLEAR_CACHE__ = () => {
|
|
@@ -458,7 +459,8 @@ function Root(): ReactNode {
|
|
|
458
459
|
let ownsEntry = !!options.replace;
|
|
459
460
|
const replaceOnRedirect = () => ownsEntry;
|
|
460
461
|
const scrollY = options.scrollY ?? ((options.scrollToTop ?? true) ? 0 : undefined);
|
|
461
|
-
const
|
|
462
|
+
const hash = options.scrollY === undefined ? new URL(target).hash : "";
|
|
463
|
+
const requestScroll = scrollY === undefined ? undefined : () => setScrollRequest({ scrollY, hash });
|
|
462
464
|
try {
|
|
463
465
|
let nextNode = rscCache.get(target);
|
|
464
466
|
let notFound = false;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const RESTORE_WINDOW_MS = 2000;
|
|
2
|
+
const FRAGMENT_WAIT_MS = 10_000;
|
|
2
3
|
const PERSIST_DELAY_MS = 200;
|
|
3
4
|
const userScrollEvents = ["wheel", "touchstart", "keydown", "pointerdown"] as const;
|
|
4
5
|
|
|
@@ -68,7 +69,55 @@ export class RscScrollRestoration {
|
|
|
68
69
|
const observer = new ResizeObserver(() => {
|
|
69
70
|
if (Math.abs(window.scrollY - scrollY) > 1) window.scrollTo(0, scrollY);
|
|
70
71
|
});
|
|
71
|
-
|
|
72
|
+
observer.observe(document.body);
|
|
73
|
+
this.#holdWhile(observer, RESTORE_WINDOW_MS);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
scrollToHash(hash: string, fallbackScrollY = 0) {
|
|
77
|
+
this.#cancelPending?.();
|
|
78
|
+
const id = RscScrollRestoration.#fragmentIdOf(hash);
|
|
79
|
+
const shownTarget = () => {
|
|
80
|
+
const target = id ? document.getElementById(id) : null;
|
|
81
|
+
return target?.closest("[hidden]") ? null : target;
|
|
82
|
+
};
|
|
83
|
+
const target = shownTarget();
|
|
84
|
+
if (target) {
|
|
85
|
+
target.scrollIntoView();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
window.scrollTo(0, fallbackScrollY);
|
|
89
|
+
if (!id || typeof MutationObserver === "undefined") return;
|
|
90
|
+
const observer = new MutationObserver(() => {
|
|
91
|
+
const appeared = shownTarget();
|
|
92
|
+
if (!appeared) return;
|
|
93
|
+
cancel();
|
|
94
|
+
appeared.scrollIntoView();
|
|
95
|
+
});
|
|
96
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
97
|
+
const cancel = this.#holdWhile(observer, FRAGMENT_WAIT_MS);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
restoreDocumentLoad() {
|
|
101
|
+
if (window.history.scrollRestoration === "manual") {
|
|
102
|
+
this.#manual = true;
|
|
103
|
+
const { scrollY } = RscScrollRestoration.#entryOf(window.history.state);
|
|
104
|
+
if (scrollY !== undefined) {
|
|
105
|
+
this.scrollTo(scrollY);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (window.location.hash && window.scrollY === 0 && !RscScrollRestoration.#isRevisit())
|
|
111
|
+
this.scrollToHash(window.location.hash);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
static #isRevisit(): boolean {
|
|
115
|
+
const [entry] = (performance.getEntriesByType?.("navigation") ?? []) as PerformanceNavigationTiming[];
|
|
116
|
+
return entry?.type === "reload" || entry?.type === "back_forward";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#holdWhile(observer: { disconnect: () => void }, windowMs: number): () => void {
|
|
120
|
+
const timer = setTimeout(() => cancel(), windowMs);
|
|
72
121
|
const cancel = () => {
|
|
73
122
|
observer.disconnect();
|
|
74
123
|
clearTimeout(timer);
|
|
@@ -76,15 +125,8 @@ export class RscScrollRestoration {
|
|
|
76
125
|
if (this.#cancelPending === cancel) this.#cancelPending = null;
|
|
77
126
|
};
|
|
78
127
|
for (const type of userScrollEvents) window.addEventListener(type, cancel, { capture: true, passive: true });
|
|
79
|
-
observer.observe(document.body);
|
|
80
128
|
this.#cancelPending = cancel;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
restoreDocumentLoad() {
|
|
84
|
-
if (window.history.scrollRestoration !== "manual") return;
|
|
85
|
-
this.#manual = true;
|
|
86
|
-
const { scrollY } = RscScrollRestoration.#entryOf(window.history.state);
|
|
87
|
-
if (scrollY !== undefined) this.scrollTo(scrollY);
|
|
129
|
+
return cancel;
|
|
88
130
|
}
|
|
89
131
|
|
|
90
132
|
#schedulePersist() {
|
|
@@ -123,6 +165,15 @@ export class RscScrollRestoration {
|
|
|
123
165
|
window.history.replaceState({ ...state, __akanRouter: { ...state.__akanRouter, ...fields } }, "");
|
|
124
166
|
}
|
|
125
167
|
|
|
168
|
+
static #fragmentIdOf(hash: string): string {
|
|
169
|
+
const raw = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
170
|
+
try {
|
|
171
|
+
return decodeURIComponent(raw);
|
|
172
|
+
} catch {
|
|
173
|
+
return raw;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
126
177
|
static #mintKey() {
|
|
127
178
|
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
128
179
|
}
|