akanjs 3.0.2 → 3.1.0-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/CHANGELOG.md +26 -0
- package/base/baseEnv.ts +0 -4
- package/base/primitiveRegistry.ts +2 -2
- package/client/cookie.ts +5 -1
- package/client/csrLocation.ts +122 -0
- package/client/csrTypes.ts +36 -4
- package/client/device.ts +2 -1
- package/client/frameConfig.ts +78 -7
- package/client/index.ts +3 -0
- package/client/makePageProto.tsx +3 -2
- package/client/native/external-view.ts +1 -0
- package/client/route/RootLayoutDefinition.ts +7 -1
- package/client/route/RouteHead.ts +108 -0
- package/client/route/index.ts +1 -0
- package/client/router.ts +36 -23
- package/client/rscNavigation.ts +8 -2
- package/client/viewportMeta.ts +60 -0
- package/client/webManifest.ts +18 -0
- package/common/Logger.ts +7 -2
- package/common/PipelineNames.ts +31 -0
- package/common/PipelinePattern.ts +70 -0
- package/common/PipelinePlanSchema.ts +490 -0
- package/common/backoff.ts +14 -0
- package/common/index.ts +39 -0
- package/common/pipelinePlan.ts +259 -0
- package/common/routeConvention.ts +1 -0
- package/common/types.ts +17 -0
- package/constant/constantRegistry.ts +26 -6
- package/dictionary/base.dictionary.ts +12 -0
- package/dictionary/trans.ts +9 -1
- package/document/into.ts +5 -0
- package/fetch/client/fetchClient.ts +135 -31
- package/fetch/client/httpClient.ts +45 -0
- package/fetch/client/wsClient.ts +125 -16
- package/fetch/fetchType/buildFetch.type.ts +1 -0
- package/index.ts +243 -10
- package/package.json +1 -5
- package/server/akanApp.ts +5 -2
- package/server/akanServer.ts +44 -8
- package/server/artifact/builderRpc.ts +8 -3
- package/server/artifact/ipcTypes.ts +5 -2
- package/server/di/diLifecycle.ts +39 -3
- package/server/di/utils.ts +28 -1
- package/server/ed25519Key.ts +48 -0
- package/server/index.ts +1 -0
- package/server/machine/index.ts +2 -0
- package/server/machine/machineAccount.ts +27 -0
- package/server/machine/machineToken.ts +151 -0
- package/server/mcp/McpApps.ts +256 -0
- package/server/mcp/McpDispatcher.ts +31 -7
- package/server/mcp/McpRouter.ts +209 -46
- package/server/ops/opsToken.ts +3 -10
- package/server/resolver/database.resolver.ts +3 -0
- package/server/resolver/signal.resolver.ts +40 -3
- package/server/routeElementComposer.tsx +78 -2
- package/server/routeState.ts +6 -0
- package/server/routeTreeBuilder.ts +26 -9
- package/server/routing/apiRouter.ts +6 -3
- package/server/routing/roomReplay.ts +88 -0
- package/server/rscClient.tsx +53 -8
- package/server/rscWorker.tsx +141 -27
- package/server/rscWorkerHost.ts +18 -5
- package/server/sitemap.ts +14 -0
- package/server/ssrFromRscRenderer.tsx +12 -6
- package/server/ssrTypes.ts +3 -1
- package/server/systemPageDocument.tsx +38 -30
- package/server/types.tsx +2 -0
- package/server/webRouter.ts +74 -5
- package/service/adapt.ts +1 -1
- package/service/injectInfo.ts +9 -1
- package/service/ipcTypes.ts +2 -1
- package/service/predefinedAdaptor/database.adaptor.ts +1 -146
- package/service/predefinedAdaptor/queue.adaptor.ts +44 -6
- package/service/predefinedAdaptor/queueBackoff.ts +12 -0
- package/service/predefinedAdaptor/solidQueue.adaptor.ts +17 -13
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +44 -2
- package/service/predefinedAdaptor/sql/dialect/postgres.ts +3 -0
- package/service/predefinedAdaptor/sql/dialect/sqlite.ts +5 -2
- package/service/predefinedAdaptor/sql/search/fts5.ts +1 -1
- package/service/predefinedAdaptor/sql/types.ts +8 -6
- package/service/predefinedAdaptor/sql/values.ts +0 -23
- package/service/serve.ts +5 -1
- package/signal/agent.signal.ts +1 -1
- package/signal/base.signal.ts +2 -2
- package/signal/endpointIdempotency.ts +53 -0
- package/signal/internalArg.ts +14 -0
- package/signal/mcp/McpDocument.ts +38 -1
- package/signal/mcp/mcpProtocol.ts +18 -0
- package/signal/signalContext.ts +58 -30
- package/signal/signalRegistry.ts +14 -4
- package/signal/types.ts +18 -3
- package/store/action.ts +17 -19
- package/store/agent/StoreSurfaceSource.ts +1 -1
- package/store/agentic/StToolBuilder.ts +3 -0
- package/test/conformance.ts +9 -33
- package/types/base/baseEnv.d.ts +0 -4
- package/types/base/primitiveRegistry.d.ts +1 -1
- package/types/client/csrLocation.d.ts +32 -0
- package/types/client/csrTypes.d.ts +43 -4
- package/types/client/frameConfig.d.ts +14 -2
- package/types/client/index.d.ts +3 -0
- package/types/client/native/external-view.d.ts +1 -0
- package/types/client/route/RootLayoutDefinition.d.ts +2 -1
- package/types/client/route/RouteHead.d.ts +13 -0
- package/types/client/route/index.d.ts +1 -0
- package/types/client/router.d.ts +6 -0
- package/types/client/rscNavigation.d.ts +4 -0
- package/types/client/viewportMeta.d.ts +18 -0
- package/types/client/webManifest.d.ts +4 -0
- package/types/common/PipelineNames.d.ts +6 -0
- package/types/common/PipelinePattern.d.ts +14 -0
- package/types/common/PipelinePlanSchema.d.ts +9 -0
- package/types/common/backoff.d.ts +10 -0
- package/types/common/index.d.ts +5 -0
- package/types/common/pipelinePlan.d.ts +224 -0
- package/types/common/types.d.ts +16 -0
- package/types/constant/constantRegistry.d.ts +5 -2
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/document/into.d.ts +9 -0
- package/types/fetch/client/fetchClient.d.ts +31 -4
- package/types/fetch/client/httpClient.d.ts +8 -0
- package/types/fetch/client/wsClient.d.ts +26 -2
- package/types/fetch/fetchType/buildFetch.type.d.ts +1 -0
- package/types/index.d.ts +230 -9
- package/types/server/akanServer.d.ts +16 -1
- package/types/server/artifact/builderRpc.d.ts +7 -2
- package/types/server/artifact/ipcTypes.d.ts +5 -1
- package/types/server/di/utils.d.ts +1 -0
- package/types/server/ed25519Key.d.ts +13 -0
- package/types/server/index.d.ts +1 -0
- package/types/server/machine/index.d.ts +2 -0
- package/types/server/machine/machineAccount.d.ts +5 -0
- package/types/server/machine/machineToken.d.ts +29 -0
- package/types/server/machine.d.ts +1 -0
- package/types/server/mcp/McpApps.d.ts +50 -0
- package/types/server/mcp/McpDispatcher.d.ts +4 -1
- package/types/server/mcp/McpRouter.d.ts +4 -0
- package/types/server/resolver/signal.resolver.d.ts +2 -1
- package/types/server/routeElementComposer.d.ts +24 -4
- package/types/server/routeState.d.ts +4 -1
- package/types/server/routing/roomReplay.d.ts +24 -0
- package/types/server/rscWorkerHost.d.ts +5 -2
- package/types/server/sitemap.d.ts +2 -0
- package/types/server/ssrFromRscRenderer.d.ts +3 -3
- package/types/server/ssrTypes.d.ts +4 -1
- package/types/server/systemPageDocument.d.ts +2 -0
- package/types/server/types.d.ts +4 -1
- package/types/server/webRouter.d.ts +3 -1
- package/types/service/adapt.d.ts +1 -1
- package/types/service/ipcTypes.d.ts +2 -0
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +1 -24
- package/types/service/predefinedAdaptor/queue.adaptor.d.ts +10 -3
- package/types/service/predefinedAdaptor/queueBackoff.d.ts +4 -0
- package/types/service/predefinedAdaptor/solidQueue.adaptor.d.ts +3 -3
- package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +6 -1
- package/types/service/predefinedAdaptor/sql/dialect/postgres.d.ts +1 -0
- package/types/service/predefinedAdaptor/sql/dialect/sqlite.d.ts +1 -0
- package/types/service/predefinedAdaptor/sql/search/fts5.d.ts +1 -1
- package/types/service/predefinedAdaptor/sql/types.d.ts +9 -5
- package/types/service/predefinedAdaptor/sql/values.d.ts +0 -3
- package/types/service/serve.d.ts +3 -0
- package/types/signal/endpointIdempotency.d.ts +7 -0
- package/types/signal/internalArg.d.ts +10 -0
- package/types/signal/mcp/McpDocument.d.ts +8 -0
- package/types/signal/mcp/mcpProtocol.d.ts +17 -0
- package/types/signal/signalContext.d.ts +5 -3
- package/types/signal/signalRegistry.d.ts +4 -4
- package/types/signal/types.d.ts +21 -1
- package/types/store/agentic/StToolBuilder.d.ts +2 -0
- package/types/test/conformance.d.ts +7 -10
- package/types/ui/Agent/Chat.d.ts +5 -1
- package/types/ui/Agent/EmbedApproval.d.ts +1 -0
- package/types/ui/Agent/index.d.ts +3 -0
- package/types/ui/Agent/useKeyboardFrame.d.ts +2 -0
- package/types/ui/Agent/useScreenActions.d.ts +18 -0
- package/types/ui/BottomSheet.d.ts +1 -0
- package/types/ui/Constant/Doc.d.ts +11 -17
- package/types/ui/Constant/index.d.ts +7 -2
- package/types/ui/Constant/schemaDoc.d.ts +11 -0
- package/types/ui/Dialog/Modal.d.ts +6 -1
- package/types/ui/Dialog/index.d.ts +1 -1
- package/types/ui/Dialog/modalSurface.d.ts +12 -0
- package/types/ui/Layout/BottomInset.d.ts +1 -1
- package/types/ui/Layout/BottomTab.d.ts +11 -2
- package/types/ui/Layout/FrameSlotFallback.d.ts +9 -0
- package/types/ui/Layout/Navbar.d.ts +4 -1
- package/types/ui/Layout/TopInset.d.ts +1 -1
- package/types/ui/Layout/index.d.ts +4 -4
- package/types/ui/Link/Action.d.ts +5 -1
- package/types/ui/Link/Anchor.d.ts +5 -3
- package/types/ui/Link/activePath.d.ts +13 -0
- package/types/ui/Link/index.d.ts +1 -1
- package/types/ui/Modal.d.ts +6 -1
- package/types/ui/Model/EditModal.d.ts +6 -1
- package/types/ui/Model/New.d.ts +6 -1
- package/types/ui/Model/ViewEditModal.d.ts +6 -1
- package/types/ui/Model/ViewModal.d.ts +6 -1
- package/types/ui/Model/newFormOrigin.d.ts +7 -0
- package/types/ui/Portal.d.ts +3 -1
- package/types/ui/Reference/origin.d.ts +12 -0
- package/types/ui/Signal/Doc.d.ts +23 -18
- package/types/ui/Signal/Listener.d.ts +2 -6
- package/types/ui/Signal/Object.d.ts +6 -10
- package/types/ui/Signal/Response.d.ts +3 -6
- package/types/ui/Signal/RestApi.d.ts +11 -15
- package/types/ui/Signal/WebSocket.d.ts +11 -21
- package/types/ui/Signal/endpointEntries.d.ts +8 -0
- package/types/ui/Signal/index.d.ts +35 -12
- package/types/ui/Signal/signalText.d.ts +178 -0
- package/types/ui/System/CSR.d.ts +2 -2
- package/types/ui/System/Client.d.ts +4 -3
- package/types/ui/System/Common.d.ts +27 -6
- package/types/ui/System/SSR.d.ts +6 -6
- package/types/ui/Tooltip.d.ts +4 -2
- package/types/ui/index.d.ts +2 -1
- package/types/ui/overlayLayer.d.ts +6 -0
- package/types/ui/overlayPosition.d.ts +17 -0
- package/types/vendor/@akanjs/native/packages/cli/src/api.d.ts +14 -3
- package/types/vendor/@akanjs/native/packages/cli/src/commands/update.d.ts +1 -1
- package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +30 -5
- package/types/vendor/@akanjs/native/packages/cli/src/lib/compat.d.ts +8 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.d.ts +5 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/distribution.d.ts +22 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/project.d.ts +4 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/publish.d.ts +16 -10
- package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +51 -7
- package/types/vendor/@akanjs/native/packages/cli/src/lib/vectors.d.ts +1 -1
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.d.ts +12 -4
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows.d.ts +5 -0
- package/types/vendor/@akanjs/native/packages/core/src/kernel.d.ts +36 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/dispatcher.d.ts +6 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/plugin.d.ts +2 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/server.d.ts +11 -0
- package/types/vendor/@akanjs/native/plugins/external-view/src/desktop.d.ts +17 -0
- package/types/vendor/@akanjs/native/plugins/external-view/src/index.d.ts +132 -0
- package/types/vendor/@akanjs/native/plugins/external-view/src.d.ts +1 -0
- package/types/vendor/@akanjs/native/plugins/single-instance/src/socket.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/updates/src/index.d.ts +8 -0
- package/types/vendor/@akanjs/native/plugins/window/src/desktop.d.ts +4 -1
- package/types/vendor/@akanjs/native/plugins/window/src/index.d.ts +49 -0
- package/types/vendor/use-agentic/AgentActivity.d.ts +3 -1
- package/types/vendor/use-agentic/AgenticSurface.d.ts +2 -2
- package/types/vendor/use-agentic/types.d.ts +2 -0
- package/types/webkit/CsrStack.d.ts +1 -1
- package/types/webkit/EmbeddedLayout.d.ts +10 -0
- package/types/webkit/McpAppAgent.d.ts +14 -0
- package/types/webkit/McpAppApprovals.d.ts +8 -0
- package/types/webkit/McpAppBridge.d.ts +101 -0
- package/types/webkit/McpAppView.d.ts +21 -0
- package/types/webkit/RenderLayer.d.ts +3 -1
- package/types/webkit/index.d.ts +1 -0
- package/types/webkit/useLocation.d.ts +1 -1
- package/types/webkit/useVisualViewportFrame.d.ts +13 -0
- package/ui/Agent/Chat.tsx +29 -10
- package/ui/Agent/Composer.tsx +2 -1
- package/ui/Agent/EmbedApproval.tsx +17 -0
- package/ui/Agent/RichInput.tsx +1 -1
- package/ui/Agent/index.ts +2 -0
- package/ui/Agent/useKeyboardFrame.ts +10 -0
- package/ui/Agent/useScreenActions.ts +45 -0
- package/ui/BottomSheet.tsx +82 -53
- package/ui/Constant/Doc.tsx +113 -60
- package/ui/Constant/index.ts +2 -2
- package/ui/Constant/schemaDoc.ts +86 -26
- package/ui/Dialog/LegacyModal.tsx +53 -52
- package/ui/Dialog/Modal.tsx +93 -49
- package/ui/Dialog/modalSurface.ts +61 -0
- package/ui/DragAction.tsx +1 -1
- package/ui/DraggableList.tsx +1 -1
- package/ui/Dropdown.tsx +4 -6
- package/ui/KeyboardAvoiding.tsx +1 -1
- package/ui/Layout/BottomInset.tsx +32 -16
- package/ui/Layout/BottomTab.tsx +42 -16
- package/ui/Layout/FrameSlotFallback.tsx +59 -0
- package/ui/Layout/Navbar.tsx +56 -15
- package/ui/Layout/Sider.tsx +1 -1
- package/ui/Layout/TopInset.tsx +22 -17
- package/ui/Link/Action.tsx +15 -5
- package/ui/Link/Anchor.tsx +25 -13
- package/ui/Link/activePath.ts +25 -0
- package/ui/Link/index.tsx +1 -1
- package/ui/Load/Pagination.tsx +1 -1
- package/ui/Load/Units.tsx +1 -1
- package/ui/Loading/ProgressBar.tsx +1 -1
- package/ui/Modal.tsx +9 -0
- package/ui/Model/EditModal.tsx +16 -1
- package/ui/Model/New.tsx +25 -2
- package/ui/Model/NewWrapper_Client.tsx +2 -0
- package/ui/Model/ViewEditModal.tsx +25 -10
- package/ui/Model/ViewModal.tsx +16 -1
- package/ui/Model/newFormOrigin.ts +45 -0
- package/ui/Popconfirm.tsx +5 -7
- package/ui/Portal.tsx +4 -7
- package/ui/Reference/origin.ts +31 -0
- package/ui/ScreenNavigator.tsx +1 -1
- package/ui/Select.tsx +14 -14
- package/ui/Signal/Arg.tsx +2 -2
- package/ui/Signal/Doc.tsx +125 -63
- package/ui/Signal/Endpoint.tsx +11 -10
- package/ui/Signal/Listener.tsx +7 -10
- package/ui/Signal/Object.tsx +7 -14
- package/ui/Signal/Response.tsx +24 -24
- package/ui/Signal/RestApi.tsx +63 -52
- package/ui/Signal/WebSocket.tsx +65 -72
- package/ui/Signal/endpointEntries.ts +25 -0
- package/ui/Signal/index.ts +23 -6
- package/ui/Signal/signalText.ts +59 -0
- package/ui/System/CSR.tsx +41 -9
- package/ui/System/Client.tsx +37 -5
- package/ui/System/Common.tsx +76 -32
- package/ui/System/SSR.tsx +73 -31
- package/ui/Toast.tsx +1 -1
- package/ui/Tooltip.tsx +84 -30
- package/ui/index.ts +11 -1
- package/ui/overlayLayer.ts +25 -2
- package/ui/overlayPosition.ts +90 -0
- package/ui/recipe/inputRecipe.ts +8 -1
- package/ui/styles.css +152 -0
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +22 -2
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeKernel.kt +37 -0
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeUpdateReports.kt +169 -0
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeUpdates.kt +110 -38
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeVectors.kt +20 -0
- package/vendor/@akanjs/native/native/desktop/Cargo.lock +16 -16
- package/vendor/@akanjs/native/native/desktop/src/external.rs +651 -0
- package/vendor/@akanjs/native/native/desktop/src/json.rs +23 -0
- package/vendor/@akanjs/native/native/desktop/src/lib.rs +228 -5
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeApp.swift +2 -1
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeKernel.swift +47 -1
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeUpdateReports.swift +145 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeUpdates.swift +112 -43
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeVectors.swift +18 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeViewController.swift +24 -2
- package/vendor/@akanjs/native/packages/cli/src/api.ts +22 -6
- package/vendor/@akanjs/native/packages/cli/src/commands/build.ts +24 -8
- package/vendor/@akanjs/native/packages/cli/src/commands/update.ts +11 -9
- package/vendor/@akanjs/native/packages/cli/src/config.ts +35 -7
- package/vendor/@akanjs/native/packages/cli/src/lib/compat.ts +2 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +6 -4
- package/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.ts +11 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/distribution.ts +73 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +15 -3
- package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +70 -23
- package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +159 -23
- package/vendor/@akanjs/native/packages/cli/src/lib/vectors.ts +2 -0
- package/vendor/@akanjs/native/packages/cli/src/platforms/android.ts +9 -4
- package/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.ts +1 -1
- package/vendor/@akanjs/native/packages/cli/src/platforms/desktop.ts +4 -2
- package/vendor/@akanjs/native/packages/cli/src/platforms/ios.ts +9 -4
- package/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.ts +60 -21
- package/vendor/@akanjs/native/packages/cli/src/platforms/windows.ts +14 -3
- package/vendor/@akanjs/native/packages/core/src/kernel.ts +49 -0
- package/vendor/@akanjs/native/packages/core/vectors/updates.json +31 -0
- package/vendor/@akanjs/native/packages/desktop/src/dispatcher.ts +13 -5
- package/vendor/@akanjs/native/packages/desktop/src/main.ts +3 -0
- package/vendor/@akanjs/native/packages/desktop/src/plugin.ts +2 -0
- package/vendor/@akanjs/native/packages/desktop/src/server.ts +70 -6
- package/vendor/@akanjs/native/plugins/external-view/native-plugin.json +23 -0
- package/vendor/@akanjs/native/plugins/external-view/src/desktop.ts +321 -0
- package/vendor/@akanjs/native/plugins/external-view/src/index.ts +106 -0
- package/vendor/@akanjs/native/plugins/single-instance/src/desktop.ts +4 -0
- package/vendor/@akanjs/native/plugins/single-instance/src/socket.ts +12 -5
- package/vendor/@akanjs/native/plugins/updates/android/UpdatesPlugin.kt +133 -41
- package/vendor/@akanjs/native/plugins/updates/ios/UpdatesPlugin.swift +101 -36
- package/vendor/@akanjs/native/plugins/updates/native-plugin.json +4 -2
- package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +15 -1
- package/vendor/@akanjs/native/plugins/updates/src/index.ts +7 -1
- package/vendor/@akanjs/native/plugins/window/native-plugin.json +4 -0
- package/vendor/@akanjs/native/plugins/window/src/desktop.ts +68 -1
- package/vendor/@akanjs/native/plugins/window/src/index.ts +47 -0
- package/vendor/use-agentic/AgentActivity.tsx +4 -2
- package/vendor/use-agentic/AgenticSurface.ts +3 -3
- package/vendor/use-agentic/ToolRunner.ts +8 -1
- package/vendor/use-agentic/types.ts +2 -0
- package/webkit/CsrRouteTable.ts +9 -1
- package/webkit/CsrStack.ts +9 -2
- package/webkit/EmbeddedLayout.tsx +77 -0
- package/webkit/McpAppAgent.ts +138 -0
- package/webkit/McpAppApprovals.ts +56 -0
- package/webkit/McpAppBridge.ts +291 -0
- package/webkit/McpAppView.ts +258 -0
- package/webkit/RenderLayer.tsx +20 -5
- package/webkit/bootCsr.tsx +54 -5
- package/webkit/index.ts +1 -0
- package/webkit/useCsrValues.ts +16 -10
- package/webkit/useFrameRuntime.ts +10 -1
- package/webkit/useLocation.ts +4 -3
- package/webkit/useVisualViewportFrame.tsx +52 -0
- package/service/predefinedAdaptor/sql/driver/libsql.ts +0 -45
- package/types/service/predefinedAdaptor/sql/driver/libsql.d.ts +0 -20
- package/types/ui/Agent/useKeyboardInset.d.ts +0 -1
- package/types/ui/ServerPortal.d.ts +0 -15
- package/ui/Agent/useKeyboardInset.ts +0 -19
- package/ui/ServerPortal.tsx +0 -49
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type FetchPolicy,
|
|
5
5
|
fileUploadContract,
|
|
6
6
|
Logger,
|
|
7
|
+
type RetryPolicy,
|
|
7
8
|
readAuthToken,
|
|
8
9
|
resolveFileUploadCapability,
|
|
9
10
|
} from "akanjs/common";
|
|
@@ -28,10 +29,10 @@ import {
|
|
|
28
29
|
import type { GetSliceMetaObjFromDatabaseSignals } from "../types";
|
|
29
30
|
import { FetchHandle } from "./fetchHandle";
|
|
30
31
|
import { HttpClient } from "./httpClient";
|
|
31
|
-
import type { ErrorConstructor } from "./remoteError";
|
|
32
|
+
import type { ErrorConstructor, RestoredError } from "./remoteError";
|
|
32
33
|
import { SliceInitHandle } from "./sliceInitHandle";
|
|
33
34
|
import { expandQueryArgs, normalizeQueryArgs } from "./sliceQueryArgs";
|
|
34
|
-
import { WsClient } from "./wsClient";
|
|
35
|
+
import { WsClient, type WsClientOptions } from "./wsClient";
|
|
35
36
|
|
|
36
37
|
type FetchHandler = (...args: unknown[]) => PromiseOrObject<unknown>;
|
|
37
38
|
type FetchHandlerFactory = () => FetchHandler;
|
|
@@ -67,6 +68,21 @@ const sharedSignalRegistry: SharedSignalRegistry = globalWithSharedSignal[SHARED
|
|
|
67
68
|
};
|
|
68
69
|
globalWithSharedSignal[SHARED_SIGNAL_KEY] = sharedSignalRegistry;
|
|
69
70
|
|
|
71
|
+
export interface FetchBridgeCall {
|
|
72
|
+
key: string;
|
|
73
|
+
type: "query" | "mutation";
|
|
74
|
+
args: UnknownRecord;
|
|
75
|
+
returns: SerializedReturns;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A transport that is not HTTP: a view inside an MCP host has no network, and reaches the server through the host. */
|
|
79
|
+
export interface FetchBridge {
|
|
80
|
+
call(request: FetchBridgeCall): Promise<unknown>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const FETCH_BRIDGE_KEY = Symbol.for("akanjs.fetch.bridge");
|
|
84
|
+
const globalWithFetchBridge = globalThis as typeof globalThis & { [FETCH_BRIDGE_KEY]?: FetchBridge | null };
|
|
85
|
+
|
|
70
86
|
type ClientSignalMap<SigType extends { fetch: any }> = {
|
|
71
87
|
[K in keyof SigType as SigType[K] extends DatabaseSignal<any, any, any, any>
|
|
72
88
|
? K
|
|
@@ -89,13 +105,17 @@ export class FetchClient {
|
|
|
89
105
|
readonly sortValueMap = new Map<string, { [key: string]: { [path: string]: 1 | -1 } }>();
|
|
90
106
|
readonly filterQueryMap = new Map<string, { [queryKey: string]: SerializedArg[] }>();
|
|
91
107
|
readonly #originWs = new Map<string, WsClient>();
|
|
108
|
+
#wsOptions: WsClientOptions = {};
|
|
92
109
|
readonly #handlerStore: Record<string, FetchHandler> = {};
|
|
93
110
|
readonly #handlerFactory = new Map<string, FetchHandlerFactory>();
|
|
94
111
|
#sharedRegistryAppliedVersion = 0;
|
|
95
112
|
#clientIp: string | null = null;
|
|
113
|
+
/** Cloned toward another server: the request being served is this server's visitor, not that server's caller. */
|
|
114
|
+
#foreign = false;
|
|
96
115
|
static #testClientIps = false;
|
|
97
116
|
static #testClientIpSeq = 0;
|
|
98
117
|
serializedSignal: { [key: string]: SerializedSignal } = {};
|
|
118
|
+
#ownSignal: { [key: string]: SerializedSignal } = {};
|
|
99
119
|
jwt: string | null = null;
|
|
100
120
|
|
|
101
121
|
constructor(
|
|
@@ -119,6 +139,13 @@ export class FetchClient {
|
|
|
119
139
|
sharedSignalRegistry.signal = {};
|
|
120
140
|
sharedSignalRegistry.version++;
|
|
121
141
|
}
|
|
142
|
+
/** Every client in this tab sends its queries and mutations through `bridge`, and opens no socket; `null` restores HTTP. */
|
|
143
|
+
static setBridge(bridge: FetchBridge | null) {
|
|
144
|
+
globalWithFetchBridge[FETCH_BRIDGE_KEY] = bridge;
|
|
145
|
+
}
|
|
146
|
+
static get bridged() {
|
|
147
|
+
return !!globalWithFetchBridge[FETCH_BRIDGE_KEY];
|
|
148
|
+
}
|
|
122
149
|
static resetSharedClient() {
|
|
123
150
|
sharedClientState.proxy = null;
|
|
124
151
|
sharedClientState.origin = null;
|
|
@@ -151,27 +178,43 @@ export class FetchClient {
|
|
|
151
178
|
signal: SerializedSignal,
|
|
152
179
|
) {
|
|
153
180
|
const current = serializedSignal[refName];
|
|
154
|
-
serializedSignal[refName] = current
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
: signal
|
|
181
|
+
serializedSignal[refName] = current ? FetchClient.#definedOnly(FetchClient.#merged(current, signal)) : signal;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
static #definedOnly(signal: SerializedSignal): SerializedSignal {
|
|
185
|
+
return Object.fromEntries(Object.entries(signal).filter(([, value]) => value !== undefined)) as SerializedSignal;
|
|
186
|
+
}
|
|
187
|
+
static #merged(current: SerializedSignal, signal: SerializedSignal): SerializedSignal {
|
|
188
|
+
return {
|
|
189
|
+
...current,
|
|
190
|
+
...signal,
|
|
191
|
+
endpoint: { ...current.endpoint, ...signal.endpoint },
|
|
192
|
+
slice: current.slice || signal.slice ? { ...current.slice, ...signal.slice } : undefined,
|
|
193
|
+
filter:
|
|
194
|
+
current.filter || signal.filter
|
|
195
|
+
? {
|
|
196
|
+
filter: { ...current.filter?.filter, ...signal.filter?.filter },
|
|
197
|
+
sortKeys: [...new Set([...(current.filter?.sortKeys ?? []), ...(signal.filter?.sortKeys ?? [])])],
|
|
198
|
+
sorts: { ...current.filter?.sorts, ...signal.filter?.sorts },
|
|
199
|
+
}
|
|
200
|
+
: undefined,
|
|
201
|
+
getGuards: signal.getGuards ?? current.getGuards,
|
|
202
|
+
cruGuards: signal.cruGuards ?? current.cruGuards,
|
|
203
|
+
createGuards: signal.createGuards ?? current.createGuards,
|
|
204
|
+
updateGuards: signal.updateGuards ?? current.updateGuards,
|
|
205
|
+
removeGuards: signal.removeGuards ?? current.removeGuards,
|
|
206
|
+
origin:
|
|
207
|
+
current.origin || signal.origin
|
|
208
|
+
? [...new Set([...(current.origin ?? []), ...(signal.origin ?? [])])]
|
|
209
|
+
: undefined,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* What this client was built with and applied itself, never what it synced from another client in the process:
|
|
214
|
+
* a `getSerializedSignal` macro reads this, so a lib's bundle cannot depend on which app's signals loaded first.
|
|
215
|
+
*/
|
|
216
|
+
get ownSerializedSignal(): { [key: string]: SerializedSignal } {
|
|
217
|
+
return this.#ownSignal;
|
|
175
218
|
}
|
|
176
219
|
/** For calls neither the caller nor the endpoint budgets; `false` waits as long as the runtime will (minutes). */
|
|
177
220
|
setTimeout(timeout?: number | false) {
|
|
@@ -185,8 +228,10 @@ export class FetchClient {
|
|
|
185
228
|
}
|
|
186
229
|
applySignal(serializedSignal: { [key: string]: SerializedSignal }, { share = true }: { share?: boolean } = {}) {
|
|
187
230
|
if (share && Object.keys(serializedSignal).length > 0) {
|
|
188
|
-
for (const [refName, signal] of Object.entries(serializedSignal))
|
|
231
|
+
for (const [refName, signal] of Object.entries(serializedSignal)) {
|
|
189
232
|
FetchClient.#mergeSerializedSignalInto(sharedSignalRegistry.signal, refName, signal);
|
|
233
|
+
FetchClient.#mergeSerializedSignalInto(this.#ownSignal, refName, signal);
|
|
234
|
+
}
|
|
190
235
|
sharedSignalRegistry.version++;
|
|
191
236
|
this.#sharedRegistryAppliedVersion = sharedSignalRegistry.version;
|
|
192
237
|
}
|
|
@@ -246,7 +291,9 @@ export class FetchClient {
|
|
|
246
291
|
this.#handlerFactory.set(key, factory);
|
|
247
292
|
delete this.#handlerStore[key];
|
|
248
293
|
}
|
|
294
|
+
|
|
249
295
|
connect() {
|
|
296
|
+
if (FetchClient.bridged) return;
|
|
250
297
|
this.ws.connect();
|
|
251
298
|
}
|
|
252
299
|
disconnect() {
|
|
@@ -260,14 +307,23 @@ export class FetchClient {
|
|
|
260
307
|
connect = !(origin && typeof window === "undefined"),
|
|
261
308
|
jwt,
|
|
262
309
|
clientIp,
|
|
310
|
+
ws,
|
|
311
|
+
retry,
|
|
263
312
|
}: {
|
|
264
313
|
origin?: string;
|
|
265
314
|
connect?: boolean;
|
|
266
315
|
jwt?: string;
|
|
267
316
|
/** Sent as `x-real-ip`, which a server believes only from a trusted proxy — a test's loopback is one. */
|
|
268
317
|
clientIp?: string | null;
|
|
318
|
+
/** The clone's sockets: reconnect backoff, upgrade headers and TLS, and state, revocation and error hooks. */
|
|
319
|
+
ws?: WsClientOptions;
|
|
320
|
+
/** Every call's `retry` unless it names its own: transient failures, a mutation under one Idempotency-Key. */
|
|
321
|
+
retry?: RetryPolicy | false;
|
|
269
322
|
} = {}) {
|
|
270
323
|
const instance = new FetchClient(origin ?? this.origin, {}, this.serializedSignal, this.ErrorCls);
|
|
324
|
+
if (ws) instance.configureWs(ws);
|
|
325
|
+
if (retry !== undefined) instance.http.setRetry(retry);
|
|
326
|
+
instance.#foreign = this.#foreign || (!!origin && !FetchClient.#sameOrigin(origin, this.origin));
|
|
271
327
|
instance.#clientIp =
|
|
272
328
|
clientIp !== undefined ? clientIp : FetchClient.#testClientIps ? FetchClient.#nextTestClientIp() : this.#clientIp;
|
|
273
329
|
Object.entries(this.handler).forEach(([key, handler]) => {
|
|
@@ -277,6 +333,12 @@ export class FetchClient {
|
|
|
277
333
|
if (connect) instance.connect();
|
|
278
334
|
return FetchClient.#makeProxy(instance);
|
|
279
335
|
}
|
|
336
|
+
/** Every socket this client holds or opens later: its own and one per `FetchPolicy.origin`. */
|
|
337
|
+
configureWs(options: WsClientOptions) {
|
|
338
|
+
this.#wsOptions = { ...this.#wsOptions, ...options };
|
|
339
|
+
this.ws.configure(options);
|
|
340
|
+
for (const ws of this.#originWs.values()) ws.configure(options);
|
|
341
|
+
}
|
|
280
342
|
setJwt(jwt: string | null) {
|
|
281
343
|
this.jwt = jwt;
|
|
282
344
|
this.ws.setJwt(jwt);
|
|
@@ -289,19 +351,19 @@ export class FetchClient {
|
|
|
289
351
|
if (target === this.origin.replace(/\/+$/, "")) return this.ws;
|
|
290
352
|
const cached = this.#originWs.get(target);
|
|
291
353
|
if (cached) return cached;
|
|
292
|
-
const ws = new WsClient(FetchClient.#makeWsUri(target), this.ErrorCls);
|
|
354
|
+
const ws = new WsClient(FetchClient.#makeWsUri(target), this.ErrorCls).configure(this.#wsOptions);
|
|
293
355
|
this.#originWs.set(target, ws);
|
|
294
356
|
ws.setJwt(this.jwt);
|
|
295
357
|
ws.connect();
|
|
296
358
|
return ws;
|
|
297
359
|
}
|
|
298
360
|
#makeRequestHeaders(option?: FetchPolicy): Record<string, string> {
|
|
299
|
-
const clientIp = this.#clientIp ? { "x-real-ip": this.#clientIp } :
|
|
361
|
+
const clientIp = this.#clientIp ? { "x-real-ip": this.#clientIp } : this.#forwardedClientIp(option);
|
|
300
362
|
return { ...this.#makeAuthHeaders(option), ...clientIp };
|
|
301
363
|
}
|
|
302
364
|
|
|
303
|
-
|
|
304
|
-
if (option
|
|
365
|
+
#forwardedClientIp(option?: FetchPolicy): Record<string, string> {
|
|
366
|
+
if (!this.#forwardsVisitor(option)) return {};
|
|
305
367
|
try {
|
|
306
368
|
if (getEnv().side !== "server") return {};
|
|
307
369
|
const clientIp = requestHeaders().get("x-real-ip");
|
|
@@ -310,10 +372,17 @@ export class FetchClient {
|
|
|
310
372
|
return {};
|
|
311
373
|
}
|
|
312
374
|
}
|
|
375
|
+
static #sameOrigin(a: string, b: string) {
|
|
376
|
+
return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#forwardsVisitor(option?: FetchPolicy) {
|
|
380
|
+
return !this.#foreign && (!option?.origin || FetchClient.#sameOrigin(option.origin, this.origin));
|
|
381
|
+
}
|
|
313
382
|
#makeAuthHeaders(option?: FetchPolicy): Record<string, string> {
|
|
314
383
|
if (option?.token) return { Authorization: `Bearer ${option.token}` };
|
|
315
384
|
try {
|
|
316
|
-
if (getEnv().side === "server") {
|
|
385
|
+
if (getEnv().side === "server" && this.#forwardsVisitor(option)) {
|
|
317
386
|
const authorization = requestHeaders().get("authorization");
|
|
318
387
|
if (authorization) return { Authorization: authorization };
|
|
319
388
|
const token = readAuthToken((key) => requestCookies().get(key)?.value);
|
|
@@ -344,11 +413,21 @@ export class FetchClient {
|
|
|
344
413
|
case "query":
|
|
345
414
|
return async (...argData: unknown[]) => {
|
|
346
415
|
const { option, argMap, url } = requestOf(argData);
|
|
416
|
+
const bridge = option?.origin ? null : globalWithFetchBridge[FETCH_BRIDGE_KEY];
|
|
417
|
+
if (bridge) {
|
|
418
|
+
const value = await this.#bridged(bridge, {
|
|
419
|
+
key,
|
|
420
|
+
type: "query",
|
|
421
|
+
args: Object.fromEntries(argMap),
|
|
422
|
+
returns: endpoint.returns,
|
|
423
|
+
});
|
|
424
|
+
return parseReturn(value, { crystalize: option?.crystalize ?? true });
|
|
425
|
+
}
|
|
347
426
|
const headers = this.#makeRequestHeaders(option);
|
|
348
427
|
const baseUrl = this.#baseUrlOf(endpoint, option?.origin);
|
|
349
428
|
const timeout = option?.timeout ?? endpoint.timeout;
|
|
350
429
|
|
|
351
|
-
const requestQuery = () => this.http.get(url, { headers, baseUrl, timeout });
|
|
430
|
+
const requestQuery = () => this.http.get(url, { headers, baseUrl, timeout, retry: option?.retry });
|
|
352
431
|
|
|
353
432
|
const claim = baseUrl
|
|
354
433
|
? { value: requestQuery(), owned: true }
|
|
@@ -361,11 +440,23 @@ export class FetchClient {
|
|
|
361
440
|
case "mutation":
|
|
362
441
|
return async (...argData: unknown[]) => {
|
|
363
442
|
const { option, argMap, url } = requestOf(argData);
|
|
443
|
+
const bridge = option?.origin ? null : globalWithFetchBridge[FETCH_BRIDGE_KEY];
|
|
444
|
+
if (bridge) {
|
|
445
|
+
const value = await this.#bridged(bridge, {
|
|
446
|
+
key,
|
|
447
|
+
type: "mutation",
|
|
448
|
+
args: Object.fromEntries(argMap),
|
|
449
|
+
returns: endpoint.returns,
|
|
450
|
+
});
|
|
451
|
+
return parseReturn(value, { crystalize: option?.crystalize ?? true });
|
|
452
|
+
}
|
|
364
453
|
const body = HttpClient.makeBody(bodyArgs, uploadArgs, argMap);
|
|
365
454
|
const response = await this.http.send(endpoint.method ?? "POST", url, body, {
|
|
366
455
|
headers: this.#makeRequestHeaders(option),
|
|
367
456
|
baseUrl: this.#baseUrlOf(endpoint, option?.origin),
|
|
368
457
|
timeout: option?.timeout ?? endpoint.timeout,
|
|
458
|
+
retry: option?.retry,
|
|
459
|
+
idempotencyKey: option?.idempotencyKey,
|
|
369
460
|
});
|
|
370
461
|
return parseReturn(response, { crystalize: option?.crystalize ?? true });
|
|
371
462
|
};
|
|
@@ -387,6 +478,8 @@ export class FetchClient {
|
|
|
387
478
|
const parseReturn = this.#makeReturnParser(endpoint.returns);
|
|
388
479
|
const wrappedListeners = new WeakMap<(data: unknown) => void, (data: unknown) => void>();
|
|
389
480
|
return (...argData: unknown[]) => {
|
|
481
|
+
|
|
482
|
+
if (FetchClient.bridged) return () => undefined;
|
|
390
483
|
const args = argData.slice(0, roomArgLength);
|
|
391
484
|
const handleEvent = argData[roomArgLength] as (data: unknown) => void;
|
|
392
485
|
const fetchPolicy = argData[roomArgLength + 1] as FetchPolicy | undefined;
|
|
@@ -416,6 +509,7 @@ export class FetchClient {
|
|
|
416
509
|
const msgArgLength = msgArgs.length;
|
|
417
510
|
const serializerMap = this.#makeArgSerializer(endpoint.args);
|
|
418
511
|
return (...argData: unknown[]) => {
|
|
512
|
+
if (FetchClient.bridged) return;
|
|
419
513
|
const args = argData.slice(0, msgArgLength);
|
|
420
514
|
const fetchPolicy = argData[msgArgLength] as FetchPolicy | undefined;
|
|
421
515
|
const data = msgArgs.map((arg, idx) => serializerMap.get(arg.name)?.(args[idx]) ?? null);
|
|
@@ -443,6 +537,15 @@ export class FetchClient {
|
|
|
443
537
|
break;
|
|
444
538
|
}
|
|
445
539
|
}
|
|
540
|
+
|
|
541
|
+
async #bridged(bridge: FetchBridge, request: FetchBridgeCall) {
|
|
542
|
+
try {
|
|
543
|
+
return await bridge.call(request);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
const payload = (error as RestoredError | null)?.toJSON?.();
|
|
546
|
+
throw payload && this.ErrorCls ? this.ErrorCls.fromJSON(payload) : error;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
446
549
|
static #makeWsUri(origin: string) {
|
|
447
550
|
return `${origin.replace("http://", "ws://").replace("https://", "wss://")}${getWsPrefix()}`;
|
|
448
551
|
}
|
|
@@ -817,7 +920,7 @@ export class FetchClient {
|
|
|
817
920
|
);
|
|
818
921
|
} else {
|
|
819
922
|
Object.assign(handler, signal.handler);
|
|
820
|
-
Object.entries(signal.serializedSignal).forEach(([refName, signal]) => {
|
|
923
|
+
Object.entries(signal.ownSerializedSignal ?? signal.serializedSignal).forEach(([refName, signal]) => {
|
|
821
924
|
FetchClient.#mergeSerializedSignalInto(serializedSignal, refName, signal);
|
|
822
925
|
});
|
|
823
926
|
}
|
|
@@ -882,6 +985,7 @@ export class FetchClient {
|
|
|
882
985
|
get(target, prop) {
|
|
883
986
|
if (prop in target) return (target as any)[prop];
|
|
884
987
|
else if (prop === "instance") return instance;
|
|
988
|
+
else if (prop === "ownSerializedSignal") return instance.ownSerializedSignal;
|
|
885
989
|
else if (typeof prop === "string") {
|
|
886
990
|
const handler = instance.#getOrCreateHandler(prop);
|
|
887
991
|
if (handler) return handler;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Backoff, type RetryPolicy } from "akanjs/common";
|
|
1
2
|
import type { HttpMutationMethod, SerializedArg } from "akanjs/signal";
|
|
2
3
|
import { type ErrorConstructor, type RestoredError, restoreRemoteError } from "./remoteError";
|
|
3
4
|
|
|
@@ -10,6 +11,9 @@ export interface HttpClientOptions {
|
|
|
10
11
|
|
|
11
12
|
interface FetchOptions extends Pick<HttpClientOptions, "headers" | "timeout"> {
|
|
12
13
|
baseUrl?: string;
|
|
14
|
+
/** Over the client's default; `false` sends once. */
|
|
15
|
+
retry?: RetryPolicy | false;
|
|
16
|
+
idempotencyKey?: string;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
const jsonContentType = /^application\/(?:[\w.+-]+\+)?json\b/i;
|
|
@@ -23,6 +27,14 @@ const transportErrorKeyMap = {
|
|
|
23
27
|
} as const;
|
|
24
28
|
|
|
25
29
|
const serverUnreachableKey = "base.error.serverUnreachable";
|
|
30
|
+
|
|
31
|
+
const retryableKeys = new Set([
|
|
32
|
+
serverUnreachableKey,
|
|
33
|
+
"base.error.serverUnavailable",
|
|
34
|
+
"base.error.gatewayTimeout",
|
|
35
|
+
"base.error.tooManyRequests",
|
|
36
|
+
"base.error.idempotencyInFlight",
|
|
37
|
+
]);
|
|
26
38
|
const unexpectedResponseKey = "base.error.unexpectedResponse";
|
|
27
39
|
const transportDetailLimit = 200;
|
|
28
40
|
|
|
@@ -32,6 +44,7 @@ export class HttpClient {
|
|
|
32
44
|
readonly baseUrl: string;
|
|
33
45
|
#headers: Record<string, string>;
|
|
34
46
|
#timeout?: number | false;
|
|
47
|
+
#retry: RetryPolicy | false = false;
|
|
35
48
|
private ErrorCls?: ErrorConstructor;
|
|
36
49
|
constructor(baseUrl: string, options: HttpClientOptions = {}) {
|
|
37
50
|
this.baseUrl = baseUrl;
|
|
@@ -47,6 +60,10 @@ export class HttpClient {
|
|
|
47
60
|
setTimeout(timeout?: number | false) {
|
|
48
61
|
this.#timeout = timeout;
|
|
49
62
|
}
|
|
63
|
+
/** What every call that names no `retry` of its own does; `false`, the default, sends once. */
|
|
64
|
+
setRetry(retry: RetryPolicy | false) {
|
|
65
|
+
this.#retry = retry;
|
|
66
|
+
}
|
|
50
67
|
#resolveUrl(url: string, options: FetchOptions) {
|
|
51
68
|
return `${(options.baseUrl ?? this.baseUrl).replace(/\/$/, "")}${url}`;
|
|
52
69
|
}
|
|
@@ -102,6 +119,34 @@ export class HttpClient {
|
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
async #request<Returns>(url: string, init: RequestInit, options: FetchOptions = {}): Promise<Returns> {
|
|
122
|
+
const retry = options.retry ?? this.#retry;
|
|
123
|
+
const mutation = !!init.method && init.method !== "GET";
|
|
124
|
+
const idempotencyKey = options.idempotencyKey ?? (retry && mutation ? crypto.randomUUID() : undefined);
|
|
125
|
+
const sent: RequestInit = idempotencyKey
|
|
126
|
+
? { ...init, headers: { ...(init.headers as Record<string, string>), "Idempotency-Key": idempotencyKey } }
|
|
127
|
+
: init;
|
|
128
|
+
const policy = retry && !(init.body instanceof FormData) ? retry : null;
|
|
129
|
+
for (let attempt = 1; ; attempt++) {
|
|
130
|
+
try {
|
|
131
|
+
return await this.#requestOnce<Returns>(url, sent, options);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const wait = policy ? HttpClient.retryWait(error, attempt, policy) : null;
|
|
134
|
+
if (wait === null) throw error;
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The wait before the next try, or `null` when this error is final or the tries are spent. */
|
|
141
|
+
static retryWait(error: unknown, attempt: number, { attempts = 4, ...backoff }: RetryPolicy): number | null {
|
|
142
|
+
const restored = error as RestoredError | undefined;
|
|
143
|
+
if (attempt >= attempts || !retryableKeys.has(restored?.error ?? "")) return null;
|
|
144
|
+
const wait = Backoff.delay(attempt, { min: 500, ...backoff });
|
|
145
|
+
const seconds = (restored?.data as { seconds?: unknown } | undefined)?.seconds;
|
|
146
|
+
return typeof seconds === "number" && seconds > 0 ? Math.max(wait, seconds * 1000) : wait;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async #requestOnce<Returns>(url: string, init: RequestInit, options: FetchOptions): Promise<Returns> {
|
|
105
150
|
const res = await this.#fetch(url, this.#withTimeout(init, options));
|
|
106
151
|
const body = await this.#readBody(res);
|
|
107
152
|
if (res.ok) return body as Returns;
|
package/fetch/client/wsClient.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Backoff,
|
|
2
3
|
Logger,
|
|
3
4
|
type WebsocketHeartbeatAckData,
|
|
4
5
|
websocketAuthContract,
|
|
@@ -17,17 +18,40 @@ import { websocketRoomContract } from "../../common/websocketContract";
|
|
|
17
18
|
import { type ErrorConstructor, restoreRemoteError } from "./remoteError";
|
|
18
19
|
|
|
19
20
|
export interface WsClientReconnectOptions {
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
/** The first wait in ms; each attempt in a row doubles it. */
|
|
22
|
+
min?: number;
|
|
23
|
+
max?: number;
|
|
24
|
+
/** The share of each wait drawn at random, 0 to 1, so clients one deploy dropped together come back apart. */
|
|
25
|
+
jitter?: number;
|
|
26
|
+
/** Attempts in a row before the client gives up; unset, it never does. */
|
|
22
27
|
maxAttempts?: number;
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
export type WsClientState = "connecting" | "open" | "closed" | "gaveUp";
|
|
31
|
+
|
|
32
|
+
export interface WsClientOptions {
|
|
33
|
+
/** `false` stays closed after a drop. */
|
|
34
|
+
reconnect?: WsClientReconnectOptions | false;
|
|
35
|
+
/** Sent with the upgrade by a server-side client, the token among them as `authorization`; a browser sends none. */
|
|
36
|
+
headers?: Record<string, string>;
|
|
37
|
+
tls?: { rejectUnauthorized?: boolean; ca?: string | string[] };
|
|
38
|
+
onStateChange?: (state: WsClientState) => void;
|
|
39
|
+
/** A room the server took back when the credential changed. */
|
|
40
|
+
onRevoked?: (roomId: string) => void;
|
|
41
|
+
/** A frame the server answered with an error, and the subscribe or message key it answered. */
|
|
42
|
+
onError?: (error: unknown, key?: string) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
interface SubscribeOption {
|
|
26
46
|
key: string;
|
|
27
47
|
data: unknown[];
|
|
28
48
|
listener: Set<(data: unknown) => void>;
|
|
29
|
-
/** After a resubscribe following a drop: what was published meanwhile is gone
|
|
49
|
+
/** After a resubscribe following a drop, or a hole in the frames: what was published meanwhile is gone. */
|
|
30
50
|
resync: Set<() => void>;
|
|
51
|
+
/** The numbering of the frames the room delivered, while the server numbers them. */
|
|
52
|
+
cursor: { epoch: string | null; seq: number | null };
|
|
53
|
+
/** Resubscribed after a drop: the ack says whether what was missed came back. */
|
|
54
|
+
recovering: boolean;
|
|
31
55
|
}
|
|
32
56
|
interface Listener {
|
|
33
57
|
callback: (data: unknown) => void;
|
|
@@ -68,6 +92,8 @@ export class WsClient {
|
|
|
68
92
|
#jwt: string | null = null;
|
|
69
93
|
#heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
70
94
|
#lastInboundAt = 0;
|
|
95
|
+
#options: WsClientOptions = {};
|
|
96
|
+
#state: WsClientState = "closed";
|
|
71
97
|
connected = false;
|
|
72
98
|
|
|
73
99
|
constructor(
|
|
@@ -81,6 +107,26 @@ export class WsClient {
|
|
|
81
107
|
this.ErrorCls = ErrorCls;
|
|
82
108
|
}
|
|
83
109
|
|
|
110
|
+
/** Merged over what was set before; read at the next connect and the next drop. */
|
|
111
|
+
configure(options: WsClientOptions) {
|
|
112
|
+
this.#options = { ...this.#options, ...options };
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get state(): WsClientState {
|
|
117
|
+
return this.#state;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#setState(state: WsClientState) {
|
|
121
|
+
if (this.#state === state) return;
|
|
122
|
+
this.#state = state;
|
|
123
|
+
this.#options.onStateChange?.(state);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static reconnectDelay(attempt: number, policy: WsClientReconnectOptions = {}) {
|
|
127
|
+
return Backoff.delay(attempt, policy);
|
|
128
|
+
}
|
|
129
|
+
|
|
84
130
|
/**
|
|
85
131
|
* For clients holding the token in memory (native, cross-origin): the handshake carries only a same-origin cookie.
|
|
86
132
|
* `null` drops that cookie server-side and revokes the rooms it authorized.
|
|
@@ -107,8 +153,9 @@ export class WsClient {
|
|
|
107
153
|
#connect() {
|
|
108
154
|
if (this.#destroyed) return;
|
|
109
155
|
|
|
156
|
+
this.#setState("connecting");
|
|
110
157
|
try {
|
|
111
|
-
this.#ws =
|
|
158
|
+
this.#ws = this.#open();
|
|
112
159
|
} catch (error) {
|
|
113
160
|
this.logger.error(`Cannot open a WebSocket to ${this.url}: ${error instanceof Error ? error.message : error}`);
|
|
114
161
|
return;
|
|
@@ -118,16 +165,25 @@ export class WsClient {
|
|
|
118
165
|
this.#reconnectAttempts = 0;
|
|
119
166
|
this.connected = true;
|
|
120
167
|
this.logger.debug(`WebSocket connected`);
|
|
168
|
+
this.#setState("open");
|
|
121
169
|
this.#startHeartbeat();
|
|
122
170
|
|
|
123
171
|
if (this.#jwt) this.#sendAuth();
|
|
124
172
|
const reconnected = this.#hadConnection;
|
|
125
173
|
this.#hadConnection = true;
|
|
126
174
|
this.#roomSubscribeMap.forEach((option) => {
|
|
127
|
-
const
|
|
175
|
+
const { epoch, seq } = option.cursor;
|
|
176
|
+
const since = reconnected && epoch !== null && seq !== null ? { epoch, seq } : undefined;
|
|
177
|
+
const data: WebsocketReqData = {
|
|
178
|
+
key: option.key,
|
|
179
|
+
data: option.data,
|
|
180
|
+
subscribe: true,
|
|
181
|
+
...(since ? { since } : {}),
|
|
182
|
+
};
|
|
128
183
|
this.#ws?.send(JSON.stringify(data));
|
|
129
|
-
|
|
130
|
-
|
|
184
|
+
|
|
185
|
+
option.cursor = { epoch: null, seq: null };
|
|
186
|
+
option.recovering = reconnected;
|
|
131
187
|
});
|
|
132
188
|
const queued = this.#outbox;
|
|
133
189
|
this.#outbox = [];
|
|
@@ -142,8 +198,12 @@ export class WsClient {
|
|
|
142
198
|
else this.logger.warn("Unknown binary WebSocket frame");
|
|
143
199
|
return;
|
|
144
200
|
}
|
|
145
|
-
const parsed = JSON.parse(e.data) as { error?: unknown } & WebsocketResData;
|
|
146
|
-
if (parsed?.error)
|
|
201
|
+
const parsed = JSON.parse(e.data) as { error?: unknown; key?: string } & WebsocketResData;
|
|
202
|
+
if (parsed?.error) {
|
|
203
|
+
const error = restoreRemoteError(parsed, 500, this.ErrorCls);
|
|
204
|
+
this.#options.onError?.(error, parsed.key);
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
147
207
|
const type = (parsed as WebsocketResData | WebsocketHeartbeatAckData).type;
|
|
148
208
|
switch (type) {
|
|
149
209
|
case "msg": {
|
|
@@ -158,11 +218,12 @@ export class WsClient {
|
|
|
158
218
|
else this.#roomAliasMap.delete(sub.roomId);
|
|
159
219
|
}
|
|
160
220
|
this.logger.verbose(`Websocket ${sub.subscribe ? "subscribe" : "unsubscribe"} accepted: ${sub.roomId}`);
|
|
221
|
+
if (sub.subscribe) this.#acknowledge(sub);
|
|
161
222
|
break;
|
|
162
223
|
}
|
|
163
224
|
case "pub": {
|
|
164
225
|
const publishData = parsed as WebsocketPublishData;
|
|
165
|
-
this.#handlePubsub(publishData.roomId, publishData.data);
|
|
226
|
+
this.#handlePubsub(publishData.roomId, publishData.data, publishData.seq);
|
|
166
227
|
break;
|
|
167
228
|
}
|
|
168
229
|
case "pong":
|
|
@@ -170,9 +231,11 @@ export class WsClient {
|
|
|
170
231
|
case "auth": {
|
|
171
232
|
const ack = parsed as WebsocketAuthAck;
|
|
172
233
|
for (const roomId of ack.revokedRooms) {
|
|
173
|
-
this.#
|
|
234
|
+
const requestRoomId = this.#roomAliasMap.get(roomId) ?? roomId;
|
|
235
|
+
this.#roomSubscribeMap.delete(requestRoomId);
|
|
174
236
|
this.#roomAliasMap.delete(roomId);
|
|
175
237
|
this.logger.warn(`Websocket room ${roomId} is no longer authorized`);
|
|
238
|
+
this.#options.onRevoked?.(requestRoomId);
|
|
176
239
|
}
|
|
177
240
|
break;
|
|
178
241
|
}
|
|
@@ -193,6 +256,7 @@ export class WsClient {
|
|
|
193
256
|
this.logger.debug(`WebSocket closed: ${event.code} ${event.reason}`);
|
|
194
257
|
this.connected = false;
|
|
195
258
|
this.#stopHeartbeat();
|
|
259
|
+
if (!this.#destroyed) this.#setState("closed");
|
|
196
260
|
this.#scheduleReconnect();
|
|
197
261
|
};
|
|
198
262
|
}
|
|
@@ -225,15 +289,46 @@ export class WsClient {
|
|
|
225
289
|
|
|
226
290
|
#scheduleReconnect() {
|
|
227
291
|
if (this.#destroyed || !this.#ws) return;
|
|
228
|
-
|
|
292
|
+
this.#ws = null;
|
|
293
|
+
const { reconnect } = this.#options;
|
|
229
294
|
this.#reconnectAttempts += 1;
|
|
295
|
+
if (reconnect === false || this.#reconnectAttempts > (reconnect?.maxAttempts ?? Number.POSITIVE_INFINITY)) {
|
|
296
|
+
this.logger.warn(`WebSocket to ${this.url} gave up after ${this.#reconnectAttempts - 1} attempt(s)`);
|
|
297
|
+
this.#setState("gaveUp");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const interval = WsClient.reconnectDelay(this.#reconnectAttempts, reconnect || undefined);
|
|
230
301
|
this.logger.debug(`WebSocket reconnecting in ${interval}ms (attempt ${this.#reconnectAttempts})`);
|
|
231
|
-
|
|
232
|
-
this.#ws = null;
|
|
233
302
|
this.#reconnectTimer = setTimeout(() => {
|
|
234
303
|
this.#reconnectTimer = null;
|
|
235
304
|
this.#connect();
|
|
236
305
|
}, interval);
|
|
306
|
+
|
|
307
|
+
this.#reconnectTimer.unref?.();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#open(): WebSocket {
|
|
311
|
+
if (typeof window !== "undefined") return new WebSocket(this.url);
|
|
312
|
+
const { headers, tls } = this.#options;
|
|
313
|
+
const authorization: Record<string, string> = this.#jwt ? { authorization: `Bearer ${this.#jwt}` } : {};
|
|
314
|
+
const BunWebSocket = WebSocket as unknown as new (
|
|
315
|
+
url: string,
|
|
316
|
+
options: { headers?: Record<string, string>; tls?: WsClientOptions["tls"] },
|
|
317
|
+
) => WebSocket;
|
|
318
|
+
return new BunWebSocket(this.url, { headers: { ...headers, ...authorization }, ...(tls ? { tls } : {}) });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#acknowledge(ack: WebsocketSubscribeAck) {
|
|
322
|
+
const option = this.#roomSubscribeMap.get(ack.requestRoomId);
|
|
323
|
+
if (!option) return;
|
|
324
|
+
if (ack.cursor) {
|
|
325
|
+
option.cursor.epoch = ack.cursor.epoch;
|
|
326
|
+
if (option.cursor.seq === null || ack.cursor.seq > option.cursor.seq) option.cursor.seq = ack.cursor.seq;
|
|
327
|
+
}
|
|
328
|
+
if (!option.recovering) return;
|
|
329
|
+
option.recovering = false;
|
|
330
|
+
if (ack.replayed === true) return;
|
|
331
|
+
for (const handler of option.resync) handler();
|
|
237
332
|
}
|
|
238
333
|
#handleMessage(key: string, data: unknown) {
|
|
239
334
|
const listenerSet = this.#listenerMap.get(key);
|
|
@@ -246,10 +341,16 @@ export class WsClient {
|
|
|
246
341
|
}
|
|
247
342
|
}
|
|
248
343
|
}
|
|
249
|
-
#handlePubsub(roomId: string, data: unknown) {
|
|
344
|
+
#handlePubsub(roomId: string, data: unknown, seq?: number) {
|
|
250
345
|
const roomSubscribe = this.#roomSubscribeMap.get(this.#roomAliasMap.get(roomId) ?? roomId);
|
|
251
346
|
if (!roomSubscribe) return;
|
|
347
|
+
const last = roomSubscribe.cursor.seq;
|
|
348
|
+
|
|
349
|
+
if (seq !== undefined && last !== null && seq <= last) return;
|
|
350
|
+
if (seq !== undefined) roomSubscribe.cursor.seq = seq;
|
|
252
351
|
for (const listener of roomSubscribe.listener) listener(data);
|
|
352
|
+
|
|
353
|
+
if (seq !== undefined && last !== null && seq > last + 1) for (const handler of roomSubscribe.resync) handler();
|
|
253
354
|
}
|
|
254
355
|
|
|
255
356
|
destroy() {
|
|
@@ -267,6 +368,7 @@ export class WsClient {
|
|
|
267
368
|
this.#roomAliasMap.clear();
|
|
268
369
|
this.#ws?.close();
|
|
269
370
|
this.#ws = null;
|
|
371
|
+
this.#setState("closed");
|
|
270
372
|
}
|
|
271
373
|
|
|
272
374
|
on<Data = unknown>(key: string, callback: (data: Data) => void) {
|
|
@@ -329,7 +431,14 @@ export class WsClient {
|
|
|
329
431
|
if (!this.#ws) this.#warnUnconnected("subscribe", option.key);
|
|
330
432
|
let roomSubscribe = this.#roomSubscribeMap.get(roomId);
|
|
331
433
|
if (!roomSubscribe) {
|
|
332
|
-
roomSubscribe = {
|
|
434
|
+
roomSubscribe = {
|
|
435
|
+
key: option.key,
|
|
436
|
+
data: option.data,
|
|
437
|
+
listener: new Set(),
|
|
438
|
+
resync: new Set(),
|
|
439
|
+
cursor: { epoch: null, seq: null },
|
|
440
|
+
recovering: false,
|
|
441
|
+
};
|
|
333
442
|
this.#roomSubscribeMap.set(roomId, roomSubscribe);
|
|
334
443
|
if (this.#ws?.readyState === WebSocket.OPEN) this.#sendSubscribe(option.key, option.data, true);
|
|
335
444
|
this.logger.verbose(`Websocket subscribe pubsub for ${roomId}`);
|