@lowdefy/server-dev 6.0.0 → 7.0.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/client/App.jsx +52 -24
- package/client/Inspector.jsx +73 -30
- package/client/JourneyObserver.jsx +47 -0
- package/client/Page.jsx +54 -22
- package/client/Recorder.jsx +73 -0
- package/client/Routing.jsx +37 -13
- package/client/clientBuildImports.test.mjs +63 -0
- package/client/devStreamConnections.test.mjs +40 -0
- package/client/feedback/FeedbackMount.jsx +2 -2
- package/client/feedback/FeedbackOverlay.jsx +7 -4
- package/client/feedback/feedbackStyles.js +3 -0
- package/client/journeyObserver/startJourneyObserver.js +101 -0
- package/client/journeyObserver/startJourneyObserver.test.mjs +149 -0
- package/client/openInEditor/OpenInEditorListener.jsx +2 -0
- package/client/recorder/buildEngineRecord.js +37 -0
- package/client/recorder/buildInteractionRecord.js +64 -0
- package/client/recorder/buildTraceEvent.js +57 -0
- package/client/recorder/buildTraceEvent.test.mjs +81 -0
- package/client/recorder/chunkRecords.js +69 -0
- package/client/recorder/chunkRecords.test.mjs +59 -0
- package/client/recorder/collectEventActions.js +30 -0
- package/client/recorder/collectEventEndpoints.js +40 -0
- package/client/recorder/collectEventRequests.js +39 -0
- package/client/recorder/createPairingBuffer.js +187 -0
- package/client/recorder/createPairingBuffer.test.mjs +405 -0
- package/client/recorder/createPasswordRedactor.js +84 -0
- package/client/recorder/createRecordBatcher.js +54 -0
- package/client/recorder/createRecordBatcher.test.mjs +76 -0
- package/client/recorder/createRecorder.js +216 -0
- package/client/recorder/createRecorder.test.mjs +318 -0
- package/client/recorder/createRecordingSession.js +61 -0
- package/client/recorder/createRecordingSession.test.mjs +72 -0
- package/client/recorder/diffStateWrites.js +77 -0
- package/client/recorder/diffStateWrites.test.mjs +66 -0
- package/client/recorder/isEventCall.js +30 -0
- package/client/recorder/jsonType.js +39 -0
- package/client/recorder/keyChord.js +47 -0
- package/client/recorder/keyChord.test.mjs +50 -0
- package/client/recorder/sendRecords.js +40 -0
- package/client/recorder/toJsonSafe.js +29 -0
- package/lib/client/BuildingPage.jsx +3 -1
- package/lib/client/ErrorBar.jsx +17 -13
- package/lib/client/RedirectingPage.jsx +56 -0
- package/lib/client/auth/Auth.jsx +11 -6
- package/lib/client/auth/AuthConfigured.jsx +224 -39
- package/lib/client/auth/AuthNotConfigured.js +45 -4
- package/lib/client/auth/authNotConfiguredParity.test.mjs +61 -0
- package/lib/client/auth/authTemplateSync.test.mjs +40 -0
- package/lib/client/auth/betterAuthSessionContract.test.mjs +181 -0
- package/lib/client/auth/createFetchCheckingBuild.js +45 -0
- package/lib/client/bundleBuildId.js +25 -0
- package/lib/client/collectVisibleBlockIds.js +51 -0
- package/lib/client/getCspNonce.js +25 -0
- package/lib/client/getPagePath.js +36 -0
- package/lib/client/getPagePath.test.mjs +51 -0
- package/lib/client/utils/formatErrorsForCopy.js +30 -0
- package/lib/client/utils/formatErrorsForCopy.test.mjs +39 -0
- package/lib/client/utils/getErrorBarColor.js +25 -0
- package/lib/client/utils/getErrorBarColor.test.mjs +34 -0
- package/lib/client/utils/request.js +8 -1
- package/lib/client/utils/usePageConfig.js +89 -25
- package/lib/client/utils/usePageConfig.test.mjs +198 -13
- package/lib/configTree/buildConfigTree.mjs +58 -0
- package/lib/configTree/buildConfigTree.test.mjs +77 -0
- package/lib/configTree/collectConfigText.mjs +101 -0
- package/lib/configTree/collectConfigText.test.mjs +116 -0
- package/lib/configTree/prepareScratchServer.mjs +46 -0
- package/lib/configTree/runConfigTreeBuild.mjs +88 -0
- package/lib/configTree/runConfigTreeBuild.skipPluginChecks.test.mjs +74 -0
- package/lib/configTree/runConfigTreeBuild.test.mjs +202 -0
- package/lib/docs/JourneyStepError.js +30 -0
- package/lib/docs/acquireBrowserSlot.js +30 -0
- package/lib/docs/addNestedEndpoints.js +54 -0
- package/lib/docs/addNestedEndpoints.test.mjs +185 -0
- package/lib/docs/addWriteFlags.js +77 -0
- package/lib/docs/buildEditedPages.js +50 -0
- package/lib/docs/buildWaitHeader.js +21 -0
- package/lib/docs/captureAnnotatedScreenshot.js +23 -12
- package/lib/docs/checkAgentFiles.js +148 -0
- package/lib/docs/checkAgentFiles.test.mjs +225 -0
- package/lib/docs/checkStepReferences.js +239 -0
- package/lib/docs/checkStepReferences.test.mjs +173 -0
- package/lib/docs/checkpointStore.js +3 -0
- package/lib/docs/clientErrorStore.js +18 -1
- package/lib/docs/clientErrorStore.test.mjs +46 -0
- package/lib/docs/collectExercised.js +52 -0
- package/lib/docs/createBrowserLifecycle.js +102 -0
- package/lib/docs/createBrowserLifecycle.test.mjs +212 -0
- package/lib/docs/createDocsMcpServer.js +345 -388
- package/lib/docs/createDocsMcpServer.test.mjs +644 -4
- package/lib/docs/createDocsMcpServerRunJourney.test.mjs +148 -0
- package/lib/docs/createJourneyActors.chromium.test.mjs +227 -0
- package/lib/docs/createJourneyActors.js +213 -0
- package/lib/docs/createJourneyActors.test.mjs +79 -0
- package/lib/docs/createJourneyObservations.js +80 -0
- package/lib/docs/createLeftOriginError.js +26 -0
- package/lib/docs/createNetworkCounter.js +101 -0
- package/lib/docs/dataSets/checkSharedCollectionIds.js +67 -0
- package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
- package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +679 -0
- package/lib/docs/dataSets/describeDataSetResult.js +25 -0
- package/lib/docs/dataSets/describeDatabaseTarget.js +46 -0
- package/lib/docs/dataSets/deserializeFixture.js +34 -0
- package/lib/docs/dataSets/drainSessionWork.js +37 -0
- package/lib/docs/dataSets/drainSessionWork.test.mjs +64 -0
- package/lib/docs/dataSets/duplicateIndexName.js +24 -0
- package/lib/docs/dataSets/findCollectionCollisions.js +52 -0
- package/lib/docs/dataSets/getDataStore.js +106 -0
- package/lib/docs/dataSets/getDataStore.mongodb.test.mjs +63 -0
- package/lib/docs/dataSets/getDataStore.test.mjs +195 -0
- package/lib/docs/dataSets/groupDataSetByCollection.js +44 -0
- package/lib/docs/dataSets/insertFixture.js +57 -0
- package/lib/docs/dataSets/insertGeneratedDocuments.js +41 -0
- package/lib/docs/dataSets/isDataStoreGone.js +29 -0
- package/lib/docs/dataSets/loadDataSetCollection.js +42 -0
- package/lib/docs/dataSets/mergeCollectionIndexes.js +52 -0
- package/lib/docs/dataSets/mergeCollectionIndexes.test.mjs +59 -0
- package/lib/docs/dataSets/openDataSession.js +119 -0
- package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +343 -0
- package/lib/docs/dataSets/pickDataStorePort.js +58 -0
- package/lib/docs/dataSets/pickDataStorePort.test.mjs +63 -0
- package/lib/docs/dataSets/readConnectionArtifacts.js +46 -0
- package/lib/docs/dataSets/readDataSession.js +41 -0
- package/lib/docs/dataSets/readDataSession.test.mjs +57 -0
- package/lib/docs/dataSets/readDataSet.js +68 -0
- package/lib/docs/dataSets/readDataSet.test.mjs +220 -0
- package/lib/docs/dataSets/registerDataStoreShutdown.js +47 -0
- package/lib/docs/dataSets/removeDataStoreFiles.js +32 -0
- package/lib/docs/dataSets/resolveDataSetCollection.js +46 -0
- package/lib/docs/dataSets/resolveJourneyDataSet.js +106 -0
- package/lib/docs/dataSets/resolveJourneyDataSet.test.mjs +130 -0
- package/lib/docs/dataSets/sweepOrphanDatabases.js +33 -0
- package/lib/docs/dataSets/test/app/lowdefy.yaml +203 -0
- package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
- package/lib/docs/dataSets/test/app/tests/data/shop.yaml +8 -0
- package/lib/docs/dataSets/test/buildTestApp.mjs +43 -0
- package/lib/docs/devEventBus.js +136 -0
- package/lib/docs/devEventBus.test.mjs +269 -0
- package/lib/docs/devToolDefinitions.js +529 -0
- package/lib/docs/docSources.js +21 -0
- package/lib/docs/docs.test.mjs +171 -15
- package/lib/docs/docsIndex.test.mjs +377 -0
- package/lib/docs/evalOperator.js +5 -3
- package/lib/docs/evalOperatorHeadless.js +84 -12
- package/lib/docs/evalOperatorHeadless.test.mjs +7 -1
- package/lib/docs/evalOperatorInTab.js +2 -1
- package/lib/docs/findConfig.js +95 -49
- package/lib/docs/fitResponse.js +81 -0
- package/lib/docs/fitResponse.test.mjs +128 -0
- package/lib/docs/formatFeedback.js +26 -5
- package/lib/docs/formatFeedback.test.mjs +48 -0
- package/lib/docs/getAppMap.js +7 -1
- package/lib/docs/getBrowser.js +150 -80
- package/lib/docs/getBrowser.test.mjs +407 -16
- package/lib/docs/getBuildId.js +61 -0
- package/lib/docs/getBuildId.test.mjs +84 -0
- package/lib/docs/getBuildStatus.js +54 -6
- package/lib/docs/getBuildStatus.test.mjs +133 -0
- package/lib/docs/getBuildStatusAfterEdits.js +35 -0
- package/lib/docs/getBuildStatusAfterEdits.test.mjs +76 -0
- package/lib/docs/getCoreDoc.js +9 -1
- package/lib/docs/getDoc.js +100 -0
- package/lib/docs/getDocsIndex.js +152 -0
- package/lib/docs/getHazards.js +115 -0
- package/lib/docs/getHazards.test.mjs +175 -0
- package/lib/docs/getMailOutboxDirectory.js +26 -0
- package/lib/docs/getOverview.js +79 -13
- package/lib/docs/getOverviewBasePath.test.mjs +37 -0
- package/lib/docs/getPageBuildStatus.js +46 -0
- package/lib/docs/getPluginDoc.js +21 -23
- package/lib/docs/getSchema.js +33 -1
- package/lib/docs/getStaleStatus.js +36 -0
- package/lib/docs/getStaleStatus.test.mjs +74 -0
- package/lib/docs/guardJourneyOrigin.js +39 -0
- package/lib/docs/inspectState.js +5 -3
- package/lib/docs/inspectStateFromTab.js +2 -2
- package/lib/docs/inspectStateHeadless.js +89 -27
- package/lib/docs/inspectStateHeadless.test.mjs +20 -1
- package/lib/docs/installHeadlessShell.failure.test.mjs +45 -0
- package/lib/docs/installHeadlessShell.js +123 -0
- package/lib/docs/installHeadlessShell.test.mjs +70 -0
- package/lib/docs/installHeadlessShell.timeout.test.mjs +72 -0
- package/lib/docs/isHeadlessShellIncomplete.js +34 -0
- package/lib/docs/isHeadlessShellIncomplete.test.mjs +63 -0
- package/lib/docs/isPageReady.js +77 -0
- package/lib/docs/isPageReady.test.mjs +235 -0
- package/lib/docs/isRunErrorEntry.js +25 -0
- package/lib/docs/journeyMutantCookie.chromium.test.mjs +150 -0
- package/lib/docs/journeyRunRecording.js +35 -0
- package/lib/docs/journeyRunRecording.test.mjs +39 -0
- package/lib/docs/keepEventStreamAlive.js +68 -0
- package/lib/docs/keepEventStreamAlive.test.mjs +77 -0
- package/lib/docs/launchBrowser.js +83 -0
- package/lib/docs/launchBrowser.test.mjs +175 -0
- package/lib/docs/listModuleDocEntries.js +69 -0
- package/lib/docs/listPackageDocFiles.js +67 -0
- package/lib/docs/listPluginDocEntries.js +111 -0
- package/lib/docs/listPlugins.js +9 -58
- package/lib/docs/listTypes.js +14 -6
- package/lib/docs/loadFrameworkHazards.js +40 -0
- package/lib/docs/loadFrameworkHazards.test.mjs +43 -0
- package/lib/docs/loadState.js +103 -35
- package/lib/docs/makeModuleManifestDoc.js +174 -0
- package/lib/docs/matchExercisedPages.js +35 -0
- package/lib/docs/matchJourneyRoute.js +69 -0
- package/lib/docs/matchJourneyRoute.test.mjs +178 -0
- package/lib/docs/mergeNetworkSnapshots.js +49 -0
- package/lib/docs/nextJourneySequence.js +30 -0
- package/lib/docs/noBrowserError.js +22 -0
- package/lib/docs/observe/TRANSIENT_SELECTOR.js +21 -0
- package/lib/docs/observe/claimExpectedErrors.js +67 -0
- package/lib/docs/observe/createJourneyAppErrors.js +236 -0
- package/lib/docs/observe/createJourneyAppErrors.test.mjs +385 -0
- package/lib/docs/observe/evaluateInvariants.js +183 -0
- package/lib/docs/observe/evaluateInvariants.test.mjs +299 -0
- package/lib/docs/observe/findingKey.js +29 -0
- package/lib/docs/observe/hasNoEffect.js +31 -0
- package/lib/docs/observe/hasNoEffect.test.mjs +37 -0
- package/lib/docs/observe/installStepObserver.js +72 -0
- package/lib/docs/observe/isAppApiUrl.js +33 -0
- package/lib/docs/observe/parseAppApiUrl.js +37 -0
- package/lib/docs/observe/readStepObserver.js +42 -0
- package/lib/docs/observe/relativeSource.js +29 -0
- package/lib/docs/observe/resolveConfigKeySource.js +33 -0
- package/lib/docs/observe/takeJourneyEvents.js +36 -0
- package/lib/docs/observe/toAppErrorFailure.js +61 -0
- package/lib/docs/observe/usesSearchStage.js +45 -0
- package/lib/docs/observe/waitForClientErrorReports.js +31 -0
- package/lib/docs/observe/watchJourneyContext.js +75 -0
- package/lib/docs/observe/withAppErrors.js +31 -0
- package/lib/docs/observe/withAppErrors.test.mjs +101 -0
- package/lib/docs/openJourney.js +94 -0
- package/lib/docs/openJourneyEmail.js +41 -0
- package/lib/docs/openJourneyEmail.test.mjs +187 -0
- package/lib/docs/pageBlocks.js +47 -0
- package/lib/docs/pageBuildStatus.test.mjs +103 -0
- package/lib/{client/setPageId.js → docs/parseModuleSource.js} +11 -12
- package/lib/docs/rails/authEngineActions.js +53 -0
- package/lib/docs/rails/authEngineActions.test.mjs +69 -0
- package/lib/docs/rails/authEngineMethods.js +52 -0
- package/lib/docs/rails/authEngineMethods.test.mjs +36 -0
- package/lib/docs/rails/checkAuthActionsRan.js +66 -0
- package/lib/docs/rails/checkAuthActionsRan.test.mjs +149 -0
- package/lib/docs/rails/checkClickRails.js +56 -0
- package/lib/docs/rails/createDataSetRails.js +82 -0
- package/lib/docs/rails/createDataSetRails.test.mjs +172 -0
- package/lib/docs/rails/findExternalBlocks.js +147 -0
- package/lib/docs/rails/findExternalBlocks.test.mjs +254 -0
- package/lib/docs/rails/installAuthActionRecorder.js +56 -0
- package/lib/docs/rails/readAuthActionRecorder.js +32 -0
- package/lib/docs/rails/readBuiltArtifact.js +30 -0
- package/lib/docs/readConnectionPlugins.js +26 -0
- package/lib/docs/readDevAuthMode.js +32 -0
- package/lib/docs/readDocEntry.js +40 -0
- package/lib/docs/readDocTitle.js +26 -0
- package/lib/docs/readJourneyEmailMatch.js +57 -0
- package/lib/docs/readJourneyEmailMatch.test.mjs +110 -0
- package/lib/docs/readJourneySession.js +56 -0
- package/lib/docs/readMailOutbox.js +37 -0
- package/lib/docs/readPagePath.js +27 -0
- package/lib/docs/readPluginPackages.js +99 -0
- package/lib/docs/readProxyBuildWait.js +38 -0
- package/lib/docs/readRoutineSteps.js +45 -0
- package/lib/docs/registerShownTextEngine.js +118 -0
- package/lib/docs/requestRestart.js +30 -0
- package/lib/docs/requestRestart.test.mjs +49 -0
- package/lib/docs/resolvePageInstance.js +46 -0
- package/lib/docs/resolvePageInstance.test.mjs +81 -0
- package/lib/docs/resolveSource.js +11 -7
- package/lib/docs/resolveSource.test.mjs +1 -1
- package/lib/docs/resolveToolCaller.js +62 -0
- package/lib/docs/reviewPage.js +51 -0
- package/lib/docs/reviewPageBuilds.js +56 -0
- package/lib/docs/reviewPageBuilds.test.mjs +207 -0
- package/lib/docs/runCheck.js +75 -0
- package/lib/docs/runCheck.test.mjs +91 -0
- package/lib/docs/runCheckChild.js +49 -0
- package/lib/docs/runEndpoint.js +185 -0
- package/lib/docs/runEndpoint.test.mjs +509 -0
- package/lib/docs/runErrorBuffers.js +99 -0
- package/lib/docs/runErrorBuffers.test.mjs +95 -0
- package/lib/docs/runJourney.js +347 -0
- package/lib/docs/runJourney.test.mjs +2383 -0
- package/lib/docs/runJourneyAsUsers.js +44 -0
- package/lib/docs/runJourneyDataSet.test.mjs +274 -0
- package/lib/docs/runJourneySteps.js +1320 -0
- package/lib/docs/runRequest.js +100 -59
- package/lib/docs/runRequest.test.mjs +190 -0
- package/lib/docs/saveAnnotatedScreenshot.js +1 -1
- package/lib/docs/scaffoldPage.js +3 -1
- package/lib/docs/screenshotPage.js +163 -11
- package/lib/docs/screenshotPage.test.mjs +263 -1
- package/lib/docs/screenshotPageClip.test.mjs +4 -0
- package/lib/docs/searchDocs.js +82 -30
- package/lib/docs/searchDocs.test.mjs +100 -0
- package/lib/docs/searchIcons.js +90 -0
- package/lib/docs/searchIcons.test.mjs +127 -0
- package/lib/docs/selectExercised.js +39 -0
- package/lib/docs/selectFinalState.js +66 -0
- package/lib/docs/selectFinalState.test.mjs +51 -0
- package/lib/docs/serverErrorStore.js +54 -0
- package/lib/docs/serverErrorStore.test.mjs +50 -0
- package/lib/docs/setupTestFixtures.mjs +133 -39
- package/lib/docs/shownTextSelector.js +28 -0
- package/lib/docs/snapshotState.js +13 -5
- package/lib/docs/splitSearchTerms.js +62 -0
- package/lib/docs/splitSearchTerms.test.mjs +50 -0
- package/lib/docs/stateCheckpoints.test.mjs +77 -0
- package/lib/docs/tabAvailable.js +6 -5
- package/lib/docs/tabChannel.js +127 -21
- package/lib/docs/tabChannel.test.mjs +187 -29
- package/lib/docs/unsettledPageNote.js +24 -0
- package/lib/docs/validateJourneyMail.js +36 -0
- package/lib/docs/validateJourneyTimeout.js +38 -0
- package/lib/docs/validateJourneyTimeout.test.mjs +29 -0
- package/lib/docs/validateRecordingParam.js +42 -0
- package/lib/docs/validateStateSelection.js +33 -0
- package/lib/docs/validateStateSelection.test.mjs +33 -0
- package/lib/docs/validateViewport.js +51 -0
- package/lib/docs/validateViewport.test.mjs +54 -0
- package/lib/docs/waitForBuild.js +50 -0
- package/lib/docs/waitForBuild.test.mjs +75 -0
- package/lib/docs/waitForJourneyEmail.js +77 -0
- package/lib/docs/withBrowserSlot.js +45 -0
- package/lib/docs/withBrowserSlot.test.mjs +75 -0
- package/lib/docs/withDataSession.js +53 -0
- package/lib/server/applyDataSetRedirect.js +83 -0
- package/lib/server/applyDataSetRedirect.test.mjs +340 -0
- package/lib/server/applyDataSetRedirectTenantReach.test.mjs +204 -0
- package/lib/server/applyDataSetRedirectWalled.test.mjs +131 -0
- package/lib/server/auth/createSystemContext.js +73 -0
- package/lib/server/auth/getAuth.js +46 -0
- package/lib/server/auth/getHeadlessUser.js +9 -8
- package/lib/server/auth/getHeadlessUser.test.mjs +41 -0
- package/lib/server/auth/getMockUser.js +18 -14
- package/lib/server/auth/{getAuthConfig.js → getStrategies.js} +12 -10
- package/lib/server/auth/headlessUser.js +3 -2
- package/lib/server/auth/journeyActor.js +35 -0
- package/lib/server/auth/resolveHeadlessUser.test.mjs +0 -4
- package/lib/server/checkPageRecord.js +44 -0
- package/lib/server/contextMapBudget.js +24 -0
- package/lib/server/contextMapBudget.test.mjs +98 -0
- package/lib/server/continueJitKeys.js +28 -0
- package/lib/server/continueJitKeys.test.mjs +43 -0
- package/lib/server/createDataSessionEndedError.js +33 -0
- package/lib/server/createDataSetRefusal.js +26 -0
- package/lib/server/createLowdefyContext.js +174 -41
- package/lib/server/createLowdefyContext.test.mjs +437 -0
- package/lib/server/getClientAddress.js +51 -0
- package/lib/server/getClientAddress.test.mjs +56 -0
- package/lib/server/getPageJitEnrichment.test.mjs +29 -11
- package/lib/server/hashConfigContent.js +26 -0
- package/lib/server/importAppCode.test.mjs +57 -0
- package/lib/server/isRoutineStep.js +27 -0
- package/lib/server/jitPageBuilder.js +309 -126
- package/lib/server/jitPageBuilder.skipPluginChecks.test.mjs +78 -0
- package/lib/server/jitPageBuilder.test.mjs +218 -0
- package/lib/server/journeyCookies/JOURNEY_COOKIES.js +37 -0
- package/lib/server/journeyCookies/forwardJourneyCookies.js +46 -0
- package/lib/server/journeyCookies/readJourneyCookie.js +52 -0
- package/lib/server/journeyCookies/writeJourneyCookie.js +46 -0
- package/lib/server/journeyCookies.js +22 -0
- package/lib/server/journeyCookies.test.mjs +145 -0
- package/lib/server/loadDynamicJsMap.js +47 -0
- package/lib/server/log/createHandleError.js +38 -2
- package/lib/server/log/createHandleError.test.mjs +125 -2
- package/lib/server/log/createLogger.js +3 -0
- package/lib/server/log/createLogger.test.mjs +77 -0
- package/lib/server/log/logRequest.js +2 -3
- package/lib/server/mutantDataSetWrappers.test.mjs +112 -0
- package/lib/server/mutants/applyMutant.js +39 -0
- package/lib/server/mutants/artifactPatterns.js +25 -0
- package/lib/server/mutants/buildAnchor.js +45 -0
- package/lib/server/mutants/collectSharedIds.js +41 -0
- package/lib/server/mutants/configPathSegments.js +38 -0
- package/lib/server/mutants/createMutantReadConfigFile.js +42 -0
- package/lib/server/mutants/describeEvent.js +22 -0
- package/lib/server/mutants/enumerateArtifact.js +56 -0
- package/lib/server/mutants/findNodeByKey.js +45 -0
- package/lib/server/mutants/findNodeByPointer.js +49 -0
- package/lib/server/mutants/getArtifactKind.js +32 -0
- package/lib/server/mutants/groupMutantCopies.js +56 -0
- package/lib/server/mutants/hashArtifact.js +25 -0
- package/lib/server/mutants/hashMutantTarget.js +34 -0
- package/lib/server/mutants/listMutants.js +122 -0
- package/lib/server/mutants/listMutants.test.mjs +502 -0
- package/lib/server/mutants/mutantId.js +30 -0
- package/lib/server/mutants/mutantRuns.js +57 -0
- package/lib/server/mutants/mutantRuns.test.mjs +136 -0
- package/lib/server/mutants/operators/dropAction.js +47 -0
- package/lib/server/mutants/operators/dropBlock.js +44 -0
- package/lib/server/mutants/operators/dropPayload.js +52 -0
- package/lib/server/mutants/operators/dropStep.js +44 -0
- package/lib/server/mutants/operators/flipVisible.js +53 -0
- package/lib/server/mutants/operators/index.js +37 -0
- package/lib/server/mutants/operators/retargetLink.js +72 -0
- package/lib/server/mutants/operators/skipValidate.js +77 -0
- package/lib/server/mutants/operators/swapIf.js +112 -0
- package/lib/server/mutants/operators.test.mjs +396 -0
- package/lib/server/mutants/positionFreePath.js +38 -0
- package/lib/server/mutants/removeFromParent.js +28 -0
- package/lib/server/mutants/test/fixtures.mjs +173 -0
- package/lib/server/mutants/test/keyArtifact.mjs +63 -0
- package/lib/server/mutants/validateMutantParam.js +69 -0
- package/lib/server/mutants/validateMutantsBody.js +88 -0
- package/lib/server/mutants/walkArtifact.js +179 -0
- package/lib/server/pageBuildRecords.js +131 -0
- package/lib/server/pageBuildRecords.test.mjs +258 -0
- package/lib/server/pageCache.mjs +22 -7
- package/lib/server/pageCache.test.mjs +22 -7
- package/lib/server/pageRebuilds.test.mjs +543 -0
- package/lib/server/pruneJitMaps.js +36 -0
- package/lib/server/pruneJitMaps.test.mjs +55 -0
- package/lib/server/readInstanceRecord.js +33 -0
- package/lib/server/readInstanceRecord.test.mjs +99 -0
- package/lib/server/readMergedMaps.js +86 -0
- package/lib/server/readMergedMaps.test.mjs +79 -0
- package/lib/server/recording/appendRecords.js +33 -0
- package/lib/server/recording/errorRecordingStamp.js +34 -0
- package/lib/server/recording/errorRecordingStamp.test.mjs +31 -0
- package/lib/server/recording/isRecordingEnabled.js +31 -0
- package/lib/server/recording/readRecordingCookie.js +71 -0
- package/lib/server/recording/readRecordingCookie.test.mjs +73 -0
- package/lib/server/recording/recordingCookiePayload.js +33 -0
- package/lib/server/recording/servedBuilds.js +46 -0
- package/lib/server/recording/servedBuilds.test.mjs +42 -0
- package/lib/server/scrubSecrets.js +23 -0
- package/lib/server/sinkExternalConnections.js +95 -0
- package/lib/server/sinkExternalConnections.test.mjs +135 -0
- package/lib/server/skipStaleMapWrites.js +62 -0
- package/lib/server/skipStaleMapWrites.test.mjs +113 -0
- package/lib/server/startIdleGc.js +67 -0
- package/lib/server/startIdleGc.test.mjs +138 -0
- package/lib/server/testFixtures/importAppCodeTwice.mjs +70 -0
- package/lib/server/trackSessionWork.js +30 -0
- package/lib/vite/createDevServerExclude.js +40 -0
- package/lib/vite/createDevServerExclude.test.mjs +79 -0
- package/manager/getContext.mjs +24 -2
- package/manager/processes/initialBuild.mjs +14 -0
- package/manager/processes/initialBuild.test.mjs +101 -0
- package/manager/processes/lowdefyBuild.mjs +30 -7
- package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +62 -0
- package/manager/processes/lowdefyBuild.test.mjs +136 -1
- package/manager/processes/optimizeDependencies.mjs +59 -0
- package/manager/processes/restartServer.mjs +34 -6
- package/manager/processes/restartServer.test.mjs +162 -0
- package/manager/processes/shutdownServer.mjs +11 -0
- package/manager/processes/shutdownServer.test.mjs +45 -0
- package/manager/processes/startFirstServer.mjs +46 -0
- package/manager/processes/startFirstServer.test.mjs +326 -0
- package/manager/processes/startMailSink.mjs +135 -0
- package/manager/processes/startMailSink.test.mjs +146 -0
- package/manager/processes/startProxy.mjs +129 -12
- package/manager/processes/startProxy.test.mjs +366 -0
- package/manager/processes/startServer.mjs +47 -6
- package/manager/processes/startServer.test.mjs +155 -0
- package/manager/processes/startWatchers.mjs +6 -4
- package/manager/processes/syncServer.mjs +69 -0
- package/manager/processes/syncServer.test.mjs +141 -0
- package/manager/processes/waitForDevServer.mjs +48 -0
- package/manager/processes/waitForDevServer.test.mjs +112 -0
- package/manager/run.mjs +128 -43
- package/manager/utils/BatchChanges.mjs +21 -5
- package/manager/utils/BatchChanges.test.mjs +77 -0
- package/manager/utils/acquireDevInstance.mjs +99 -0
- package/manager/utils/acquireDevInstance.test.mjs +160 -0
- package/manager/utils/createBuildActivity.mjs +67 -0
- package/manager/utils/createBuildActivity.test.mjs +58 -0
- package/manager/utils/createChangeSignal.mjs +39 -0
- package/manager/utils/createChangeSignal.test.mjs +62 -0
- package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
- package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
- package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
- package/manager/utils/createDotPathIgnore.mjs +44 -0
- package/manager/utils/createDotPathIgnore.test.mjs +65 -0
- package/manager/utils/createIdleGc.mjs +51 -0
- package/manager/utils/createIdleGc.test.mjs +77 -0
- package/manager/utils/createRequestActivity.mjs +100 -0
- package/manager/utils/createRequestActivity.test.mjs +121 -0
- package/manager/utils/createServerArtifactTracker.mjs +83 -0
- package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
- package/manager/utils/createServerEnv.mjs +39 -0
- package/manager/utils/filesHaveSameBytes.mjs +41 -0
- package/manager/utils/findBuildFilesOutsideWatch.mjs +59 -0
- package/manager/utils/findBuildFilesOutsideWatch.test.mjs +111 -0
- package/manager/utils/importFresh.mjs +46 -0
- package/manager/utils/importFresh.test.mjs +72 -0
- package/manager/utils/importFreshWorker.mjs +20 -0
- package/manager/utils/killTaggedBrowser.mjs +35 -0
- package/manager/utils/pagePathChanged.mjs +56 -0
- package/manager/utils/pruneRecordings.mjs +85 -0
- package/manager/utils/pruneRecordings.test.mjs +117 -0
- package/manager/utils/publishBuildDirectory.mjs +121 -0
- package/manager/utils/publishBuildDirectory.order.test.mjs +74 -0
- package/manager/utils/publishBuildDirectory.test.mjs +262 -0
- package/manager/utils/readBasePath.mjs +27 -0
- package/manager/utils/readBasePath.test.mjs +43 -0
- package/manager/utils/readBuildStatusWait.mjs +67 -0
- package/manager/utils/readManagerVersion.mjs +30 -0
- package/manager/utils/readManagerVersion.test.mjs +35 -0
- package/manager/utils/readPluginDefinitions.mjs +36 -0
- package/manager/utils/resolveDevAuthUrl.mjs +58 -0
- package/manager/utils/resolveDevAuthUrl.test.mjs +107 -0
- package/manager/utils/resolvePorts.mjs +49 -0
- package/manager/utils/setupWatcher.mjs +19 -11
- package/manager/utils/startRecordingPruner.mjs +45 -0
- package/manager/utils/startRecordingPruner.test.mjs +66 -0
- package/manager/utils/waitForChildExit.mjs +53 -0
- package/manager/utils/waitForChildExit.test.mjs +70 -0
- package/manager/utils/waitForServer.mjs +44 -0
- package/manager/utils/writeBuildStatus.mjs +1 -1
- package/manager/watchers/createPluginSourceIgnore.mjs +77 -0
- package/manager/watchers/envWatcher.mjs +9 -2
- package/manager/watchers/envWatcher.test.mjs +87 -0
- package/manager/watchers/listServerSidePackages.mjs +47 -0
- package/manager/watchers/listServerSidePackages.test.mjs +38 -0
- package/manager/watchers/lowdefyBuildWatcher.mjs +106 -22
- package/manager/watchers/lowdefyBuildWatcher.test.mjs +309 -0
- package/manager/watchers/pluginSourceWatcher.mjs +79 -0
- package/manager/watchers/pluginSourceWatcher.test.mjs +222 -0
- package/manager/watchers/restartRequestWatcher.mjs +71 -0
- package/manager/watchers/restartRequestWatcher.test.mjs +111 -0
- package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
- package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
- package/manager/watchers/serverPackageWatcher.mjs +38 -0
- package/package.json +61 -49
- package/package.original.json +61 -49
- package/src/app.js +131 -20
- package/src/html/renderDevPage.js +63 -11
- package/src/html/renderDevPage.test.mjs +138 -0
- package/src/lib/getRequestPath.js +29 -0
- package/src/middleware/createSameOriginGuard.js +37 -0
- package/src/middleware/createSameOriginGuard.test.mjs +102 -0
- package/src/middleware/errorHandler.js +51 -1
- package/src/middleware/errorHandler.test.mjs +196 -14
- package/src/middleware/isRebindingSafeHost.js +44 -0
- package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
- package/src/middleware/localDevToolsOnly.js +38 -0
- package/src/middleware/localDevToolsOnly.test.mjs +61 -0
- package/src/middleware/noStore.js +27 -0
- package/src/middleware/noStore.test.mjs +48 -0
- package/src/middleware/requestTimeout.js +38 -0
- package/src/middleware/staleFlag.js +76 -0
- package/src/middleware/staleFlag.test.mjs +127 -0
- package/src/middleware/stampCaller.js +38 -0
- package/src/middleware/stampCaller.test.mjs +64 -0
- package/src/routes/agent.js +15 -3
- package/src/routes/auth.js +29 -20
- package/src/routes/auth.test.mjs +30 -56
- package/src/routes/clientError.js +13 -13
- package/src/routes/cron.js +3 -1
- package/src/routes/cronForward.js +4 -2
- package/src/routes/detached.js +22 -9
- package/src/routes/devInspect.js +17 -8
- package/src/routes/devInspect.test.mjs +44 -7
- package/src/routes/devRecording.js +113 -0
- package/src/routes/devRecording.test.mjs +253 -0
- package/src/routes/docs/buildStatus.js +10 -2
- package/src/routes/docs/buildStatus.test.mjs +46 -0
- package/src/routes/docs/content.js +5 -3
- package/src/routes/docs/evalOperator.js +15 -1
- package/src/routes/docs/events.js +50 -0
- package/src/routes/docs/events.test.mjs +117 -0
- package/src/routes/docs/find.js +9 -1
- package/src/routes/docs/icons.js +30 -0
- package/src/routes/docs/inspectState.js +16 -1
- package/src/routes/docs/journey.js +194 -0
- package/src/routes/docs/journey.test.mjs +401 -0
- package/src/routes/docs/journeyRefusals.test.mjs +71 -0
- package/src/routes/docs/loadState.js +4 -2
- package/src/routes/docs/mcp.js +34 -3
- package/src/routes/docs/mcp.test.mjs +130 -0
- package/src/routes/docs/mutants.js +78 -0
- package/src/routes/docs/mutants.test.mjs +180 -0
- package/src/routes/docs/parsePathParamsParam.js +41 -0
- package/src/routes/docs/parsePathParamsParam.test.mjs +42 -0
- package/src/routes/docs/parseUserParam.js +27 -21
- package/src/routes/docs/parseUserParam.test.mjs +17 -5
- package/src/routes/docs/restart.js +35 -0
- package/src/routes/docs/restart.test.mjs +64 -0
- package/src/routes/docs/runEndpoint.js +54 -0
- package/src/routes/docs/runEndpoint.test.mjs +165 -0
- package/src/routes/docs/runRequest.js +25 -8
- package/src/routes/docs/runRequest.test.mjs +92 -0
- package/src/routes/docs/screenshot.js +50 -2
- package/src/routes/docs/screenshot.test.mjs +117 -0
- package/src/routes/docs/search.js +15 -2
- package/src/routes/docs/snapshotState.js +17 -3
- package/src/routes/endpoints.js +27 -26
- package/src/routes/endpointsTextAnswer.test.mjs +77 -0
- package/src/routes/feedback.js +10 -13
- package/src/routes/jitPage.js +99 -35
- package/src/routes/jitPage.test.mjs +248 -0
- package/src/routes/mcp.js +82 -5
- package/src/routes/mcp.test.mjs +215 -0
- package/src/routes/mcpMemberToken.test.mjs +125 -0
- package/src/routes/mcpProtectedResourceMetadata.js +34 -0
- package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
- package/src/routes/mountDevPageRoutes.js +25 -0
- package/src/routes/mountDevPageRoutes.test.mjs +30 -0
- package/src/routes/mountOauthDiscovery.js +44 -0
- package/src/routes/mountOauthDiscovery.test.mjs +79 -0
- package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
- package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
- package/src/routes/reload.js +16 -0
- package/src/routes/request.js +6 -4
- package/src/routes/request.test.mjs +9 -5
- package/src/routes/requestBody.test.mjs +159 -0
- package/src/routes/usage.js +6 -2
- package/src/routes/user.js +27 -0
- package/src/routes/websocket.js +29 -8
- package/src/routes/websocket.test.mjs +65 -0
- package/src/routes/wellKnownFallback.js +30 -0
- package/src/routes/wellKnownFallback.test.mjs +32 -0
- package/src/websocket/devWebSocket.js +41 -26
- package/src/websocket/devWebSocket.test.mjs +157 -0
- package/vite.config.js +53 -23
- package/lib/server/auth/getDevSession.js +0 -70
- package/lib/server/auth/getDevSession.test.mjs +0 -138
- package/lib/server/auth/session.js +0 -39
- package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
- package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
- package/manager/watchers/moduleBuildWatcher.mjs +0 -79
- package/manager/watchers/serverArtifactWatcher.mjs +0 -120
- package/src/routes/apiPage.js +0 -37
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
import http from 'node:http';
|
|
18
18
|
import net from 'node:net';
|
|
19
19
|
|
|
20
|
+
import { devPassiveHeader } from '@lowdefy/node-utils';
|
|
21
|
+
|
|
22
|
+
import buildWaitHeader from '../../lib/docs/buildWaitHeader.js';
|
|
23
|
+
import readBuildStatusWait from '../utils/readBuildStatusWait.mjs';
|
|
24
|
+
|
|
20
25
|
/*
|
|
21
26
|
The manager owns the public port; the Vite child listens on an internal
|
|
22
27
|
loopback port and every restart replaces only the child. Before this proxy the
|
|
@@ -32,6 +37,19 @@ While the child is down, requests and upgrades wait for it to come back
|
|
|
32
37
|
covers a Vite respawn with headroom; a child that stays down longer than that
|
|
33
38
|
answers 503 so callers are not held forever. In-flight streams cannot survive
|
|
34
39
|
a child exit — those sockets close and the client reconnects into the hold.
|
|
40
|
+
|
|
41
|
+
A child is probed until it answers once; after that, requests go straight to
|
|
42
|
+
it for as long as that child process lives. Probing every request opened (and
|
|
43
|
+
closed) one extra TCP connection per request, and a Vite page load is hundreds
|
|
44
|
+
of module requests: each closed probe sits in TIME_WAIT, and a few dozen page
|
|
45
|
+
loads within half a minute ran the machine out of ephemeral ports, failing
|
|
46
|
+
unrelated connections. A proxied request that cannot reach the child, a child
|
|
47
|
+
that exits, or a new child after a restart sends requests back through the
|
|
48
|
+
probe and its hold. A restart first waits for the stopped child to exit, since
|
|
49
|
+
until then it still answers the probe. A GET or HEAD that never reached the
|
|
50
|
+
child (a refused connect, or a keep-alive socket the dying child had closed)
|
|
51
|
+
is replayed through the hold rather than answered 502: a child can close its
|
|
52
|
+
sockets before the manager sees it exit.
|
|
35
53
|
*/
|
|
36
54
|
|
|
37
55
|
const RETRY_MS = 250;
|
|
@@ -51,8 +69,7 @@ function probeChild(port) {
|
|
|
51
69
|
});
|
|
52
70
|
}
|
|
53
71
|
|
|
54
|
-
async function waitForChild({ port }) {
|
|
55
|
-
const deadline = Date.now() + HOLD_MS;
|
|
72
|
+
async function waitForChild({ port, deadline }) {
|
|
56
73
|
for (;;) {
|
|
57
74
|
if (await probeChild(port)) return true;
|
|
58
75
|
if (Date.now() > deadline) return false;
|
|
@@ -60,14 +77,57 @@ async function waitForChild({ port }) {
|
|
|
60
77
|
}
|
|
61
78
|
}
|
|
62
79
|
|
|
63
|
-
function
|
|
80
|
+
function isConfirmedChild({ context, proxyState }) {
|
|
81
|
+
const child = context.devServer;
|
|
82
|
+
return (
|
|
83
|
+
Boolean(child) &&
|
|
84
|
+
child === proxyState.confirmedChild &&
|
|
85
|
+
child.exitCode === null &&
|
|
86
|
+
child.signalCode === null
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function waitUntil({ promise, deadline }) {
|
|
91
|
+
return Promise.race([
|
|
92
|
+
promise,
|
|
93
|
+
new Promise((resolve) => setTimeout(resolve, Math.max(0, deadline - Date.now()))),
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function waitForConfirmedChild({ context, proxyState, deadline }) {
|
|
98
|
+
if (isConfirmedChild({ context, proxyState })) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
await waitUntil({ promise: context.devServerExited, deadline });
|
|
102
|
+
const child = context.devServer;
|
|
103
|
+
const up = await waitForChild({ port: context.internalPort, deadline });
|
|
104
|
+
if (up) {
|
|
105
|
+
proxyState.confirmedChild = child;
|
|
106
|
+
}
|
|
107
|
+
return up;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const REPLAYABLE_METHODS = ['GET', 'HEAD'];
|
|
111
|
+
|
|
112
|
+
// Nothing reached the child: the connect was refused, or the keep-alive socket
|
|
113
|
+
// the agent reused had already been closed by a child that is going away.
|
|
114
|
+
function isUnsentError({ error, proxyReq }) {
|
|
115
|
+
return (
|
|
116
|
+
error.code === 'ECONNREFUSED' || (proxyReq.reusedSocket === true && error.code === 'ECONNRESET')
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function forwardRequest({ body, context, proxyState }, req, res, deadline = Date.now() + HOLD_MS) {
|
|
64
121
|
// Wait for a live child BEFORE piping the request body — the body stream can
|
|
65
122
|
// only be consumed once, so retrying after a failed proxy request would need
|
|
66
|
-
// full-body buffering.
|
|
67
|
-
//
|
|
68
|
-
// attempt resolves through the
|
|
69
|
-
|
|
70
|
-
|
|
123
|
+
// full-body buffering. Only a bodiless GET or HEAD that never reached the
|
|
124
|
+
// child is replayed; any other request that loses the child mid-forward
|
|
125
|
+
// surfaces as one 502, which the client's next attempt resolves through the
|
|
126
|
+
// hold.
|
|
127
|
+
waitForConfirmedChild({ context, proxyState, deadline }).then((up) => {
|
|
128
|
+
// The response, not the request: an incoming request reads as destroyed
|
|
129
|
+
// once its body has been read, which a replayed GET's has.
|
|
130
|
+
if (res.destroyed) return;
|
|
71
131
|
if (!up) {
|
|
72
132
|
res.writeHead(503, { 'content-type': 'application/json' });
|
|
73
133
|
res.end(JSON.stringify({ message: 'Lowdefy dev server is restarting.' }));
|
|
@@ -87,7 +147,16 @@ function forwardRequest(context, req, res) {
|
|
|
87
147
|
res.flushHeaders?.();
|
|
88
148
|
proxyRes.pipe(res);
|
|
89
149
|
});
|
|
90
|
-
proxyReq.on('error', () => {
|
|
150
|
+
proxyReq.on('error', (error) => {
|
|
151
|
+
proxyState.confirmedChild = null;
|
|
152
|
+
if (
|
|
153
|
+
!res.headersSent &&
|
|
154
|
+
REPLAYABLE_METHODS.includes(req.method) &&
|
|
155
|
+
isUnsentError({ error, proxyReq })
|
|
156
|
+
) {
|
|
157
|
+
forwardRequest({ body, context, proxyState }, req, res, deadline);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
91
160
|
if (res.headersSent) {
|
|
92
161
|
res.destroy();
|
|
93
162
|
return;
|
|
@@ -95,7 +164,16 @@ function forwardRequest(context, req, res) {
|
|
|
95
164
|
res.writeHead(502, { 'content-type': 'application/json' });
|
|
96
165
|
res.end(JSON.stringify({ message: 'Lowdefy dev server connection dropped.' }));
|
|
97
166
|
});
|
|
98
|
-
|
|
167
|
+
// A body read to recognise a build-status wait is sent as read. A
|
|
168
|
+
// replayed GET's request stream has already ended, so there is nothing
|
|
169
|
+
// left to pipe.
|
|
170
|
+
if (body !== undefined) {
|
|
171
|
+
proxyReq.end(body);
|
|
172
|
+
} else if (req.readableEnded) {
|
|
173
|
+
proxyReq.end();
|
|
174
|
+
} else {
|
|
175
|
+
req.pipe(proxyReq);
|
|
176
|
+
}
|
|
99
177
|
req.on('error', () => proxyReq.destroy());
|
|
100
178
|
// A client that goes away mid-response (a closed browser tab holding the
|
|
101
179
|
// reload SSE stream, an agent dropping its MCP stream) must reach the
|
|
@@ -107,8 +185,43 @@ function forwardRequest(context, req, res) {
|
|
|
107
185
|
});
|
|
108
186
|
}
|
|
109
187
|
|
|
188
|
+
// Activity is use of the server. Not counted: a passive look (the hub's tab
|
|
189
|
+
// poll, an agent's status read), and long-lived streams - an event stream a
|
|
190
|
+
// client opens with a GET (the agent shim's MCP push stream, a tab's reload
|
|
191
|
+
// stream) or a websocket - which live as long as the server and reconnect on
|
|
192
|
+
// their own, so counting them would keep every server they touch alive. An
|
|
193
|
+
// MCP tool call is a POST that may be answered as an event stream; it counts.
|
|
194
|
+
function isActivity(req) {
|
|
195
|
+
if (req.headers[devPassiveHeader] === '1') {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
const accept = req.headers.accept ?? '';
|
|
199
|
+
return !(req.method === 'GET' && accept.includes('text/event-stream'));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// A build-status wait is held here until the manager has processed the
|
|
203
|
+
// latest edits, restarts included, and only then forwarded: a restart ends
|
|
204
|
+
// the dev server process, and any wait running in it. The header tells the
|
|
205
|
+
// dev server what this wait saw, so it does not wait again.
|
|
206
|
+
async function handleRequest({ context, proxyState }, req, res) {
|
|
207
|
+
if (isActivity(req)) {
|
|
208
|
+
context.requestActivity.trackResponse(res);
|
|
209
|
+
}
|
|
210
|
+
delete req.headers[buildWaitHeader];
|
|
211
|
+
const { wait, body } = await readBuildStatusWait({ basePath: context.basePath, req });
|
|
212
|
+
if (wait) {
|
|
213
|
+
const waited = await context.buildActivity.waitForIdle();
|
|
214
|
+
req.headers[buildWaitHeader] = new URLSearchParams({
|
|
215
|
+
settled: String(waited.settled),
|
|
216
|
+
sawBuild: String(waited.sawBuild),
|
|
217
|
+
waitedMs: String(waited.waitedMs),
|
|
218
|
+
}).toString();
|
|
219
|
+
}
|
|
220
|
+
forwardRequest({ body, context, proxyState }, req, res);
|
|
221
|
+
}
|
|
222
|
+
|
|
110
223
|
function forwardUpgrade(context, req, socket, head) {
|
|
111
|
-
waitForChild({ port: context.internalPort }).then((up) => {
|
|
224
|
+
waitForChild({ port: context.internalPort, deadline: Date.now() + HOLD_MS }).then((up) => {
|
|
112
225
|
if (socket.destroyed) return;
|
|
113
226
|
if (!up) {
|
|
114
227
|
socket.end('HTTP/1.1 503 Service Unavailable\r\nconnection: close\r\n\r\n');
|
|
@@ -136,7 +249,11 @@ function forwardUpgrade(context, req, socket, head) {
|
|
|
136
249
|
|
|
137
250
|
function startProxy(context) {
|
|
138
251
|
if (context.proxyServer) return Promise.resolve();
|
|
139
|
-
const
|
|
252
|
+
const proxyState = { confirmedChild: null };
|
|
253
|
+
const proxy = http.createServer((req, res) => {
|
|
254
|
+
// A client that drops an MCP request while its body is read.
|
|
255
|
+
handleRequest({ context, proxyState }, req, res).catch(() => res.destroy());
|
|
256
|
+
});
|
|
140
257
|
proxy.on('upgrade', (req, socket, head) => forwardUpgrade(context, req, socket, head));
|
|
141
258
|
// Long-lived streams (SSE, MCP) must not be reaped by the default 5-minute
|
|
142
259
|
// request timeout; keep the proxy transparent.
|
|
@@ -20,6 +20,8 @@ import net from 'node:net';
|
|
|
20
20
|
import { jest } from '@jest/globals';
|
|
21
21
|
|
|
22
22
|
const { default: startProxy } = await import('./startProxy.mjs');
|
|
23
|
+
const { default: createBuildActivity } = await import('../utils/createBuildActivity.mjs');
|
|
24
|
+
const { default: createRequestActivity } = await import('../utils/createRequestActivity.mjs');
|
|
23
25
|
|
|
24
26
|
function listen(server) {
|
|
25
27
|
return new Promise((resolve) => {
|
|
@@ -46,6 +48,7 @@ function getFreePort() {
|
|
|
46
48
|
|
|
47
49
|
let child;
|
|
48
50
|
let context;
|
|
51
|
+
let activityRecords;
|
|
49
52
|
|
|
50
53
|
afterEach(async () => {
|
|
51
54
|
if (context?.proxyServer) await close(context.proxyServer);
|
|
@@ -57,10 +60,15 @@ afterEach(async () => {
|
|
|
57
60
|
async function startChildAndProxy(handler) {
|
|
58
61
|
child = http.createServer(handler);
|
|
59
62
|
const internalPort = await listen(child);
|
|
63
|
+
activityRecords = [];
|
|
60
64
|
context = {
|
|
61
65
|
internalPort,
|
|
62
66
|
options: { port: await getFreePort() },
|
|
63
67
|
logger: { debug: jest.fn() },
|
|
68
|
+
requestActivity: createRequestActivity({
|
|
69
|
+
onChange: (fields) => activityRecords.push(fields),
|
|
70
|
+
throttleMs: 0,
|
|
71
|
+
}),
|
|
64
72
|
};
|
|
65
73
|
await startProxy(context);
|
|
66
74
|
return context.options.port;
|
|
@@ -76,6 +84,101 @@ test('startProxy forwards a request to the child and relays its response', async
|
|
|
76
84
|
expect(await response.json()).toEqual({ path: '/api/ping?x=1' });
|
|
77
85
|
});
|
|
78
86
|
|
|
87
|
+
test('startProxy probes a running child once instead of opening a connection per request', async () => {
|
|
88
|
+
let childConnections = 0;
|
|
89
|
+
const port = await startChildAndProxy((req, res) => {
|
|
90
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
91
|
+
res.end('ok');
|
|
92
|
+
});
|
|
93
|
+
child.on('connection', () => {
|
|
94
|
+
childConnections += 1;
|
|
95
|
+
});
|
|
96
|
+
context.devServer = { exitCode: null, signalCode: null };
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < 20; i += 1) {
|
|
99
|
+
const response = await fetch(`http://localhost:${port}/module-${i}.js`);
|
|
100
|
+
expect(await response.text()).toBe('ok');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// One probe, then the keep-alive agent's socket - not a probe per request.
|
|
104
|
+
expect(childConnections).toBeLessThanOrEqual(2);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('startProxy probes a new child after a restart before forwarding to it', async () => {
|
|
108
|
+
let childConnections = 0;
|
|
109
|
+
const port = await startChildAndProxy((req, res) => {
|
|
110
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
111
|
+
res.end('ok');
|
|
112
|
+
});
|
|
113
|
+
child.on('connection', () => {
|
|
114
|
+
childConnections += 1;
|
|
115
|
+
});
|
|
116
|
+
context.devServer = { exitCode: null, signalCode: null };
|
|
117
|
+
await fetch(`http://localhost:${port}/a.js`).then((response) => response.text());
|
|
118
|
+
const afterFirstChild = childConnections;
|
|
119
|
+
|
|
120
|
+
context.devServer = { exitCode: null, signalCode: null };
|
|
121
|
+
await fetch(`http://localhost:${port}/b.js`).then((response) => response.text());
|
|
122
|
+
|
|
123
|
+
expect(childConnections).toBe(afterFirstChild + 1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('startProxy sends requests back through the hold once a confirmed child goes away', async () => {
|
|
127
|
+
function handler(req, res) {
|
|
128
|
+
res.end('ok');
|
|
129
|
+
}
|
|
130
|
+
const port = await startChildAndProxy(handler);
|
|
131
|
+
context.devServer = { exitCode: null, signalCode: null };
|
|
132
|
+
expect(await (await fetch(`http://localhost:${port}/a.js`)).text()).toBe('ok');
|
|
133
|
+
|
|
134
|
+
async function stopChildAndRestartSoon() {
|
|
135
|
+
await close(child);
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
child = http.createServer(handler);
|
|
138
|
+
child.listen(context.internalPort, '127.0.0.1');
|
|
139
|
+
}, 300);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Gone before the manager has seen it exit: a GET never reached the child,
|
|
143
|
+
// so it is replayed through the hold and answered by the next child.
|
|
144
|
+
await stopChildAndRestartSoon();
|
|
145
|
+
expect(await (await fetch(`http://localhost:${port}/b.js`)).text()).toBe('ok');
|
|
146
|
+
|
|
147
|
+
// A POST's body was consumed by the failed forward, so it fails once, and
|
|
148
|
+
// the next request waits for the child instead of failing too.
|
|
149
|
+
await stopChildAndRestartSoon();
|
|
150
|
+
expect(
|
|
151
|
+
(await fetch(`http://localhost:${port}/save`, { method: 'POST', body: '{}' })).status
|
|
152
|
+
).toBe(502);
|
|
153
|
+
expect(await (await fetch(`http://localhost:${port}/c.js`)).text()).toBe('ok');
|
|
154
|
+
|
|
155
|
+
// An exit the manager has seen sends the very next request through the hold.
|
|
156
|
+
await stopChildAndRestartSoon();
|
|
157
|
+
context.devServer.exitCode = 1;
|
|
158
|
+
expect(await (await fetch(`http://localhost:${port}/d.js`)).text()).toBe('ok');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('startProxy waits for a stopped child to exit before probing its replacement', async () => {
|
|
162
|
+
const port = await startChildAndProxy((req, res) => res.end('old'));
|
|
163
|
+
context.devServer = { exitCode: null, signalCode: null };
|
|
164
|
+
expect(await (await fetch(`http://localhost:${port}/a.js`)).text()).toBe('old');
|
|
165
|
+
|
|
166
|
+
// A restart: the old child is signalled but answers until it exits.
|
|
167
|
+
let markExited;
|
|
168
|
+
context.devServerExited = new Promise((resolve) => {
|
|
169
|
+
markExited = resolve;
|
|
170
|
+
});
|
|
171
|
+
context.devServer = { exitCode: null, signalCode: null };
|
|
172
|
+
const pending = fetch(`http://localhost:${port}/b.js`).then((response) => response.text());
|
|
173
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
174
|
+
await close(child);
|
|
175
|
+
child = http.createServer((req, res) => res.end('new'));
|
|
176
|
+
await new Promise((resolve) => child.listen(context.internalPort, '127.0.0.1', resolve));
|
|
177
|
+
markExited();
|
|
178
|
+
|
|
179
|
+
expect(await pending).toBe('new');
|
|
180
|
+
});
|
|
181
|
+
|
|
79
182
|
test('startProxy aborts the child request when the client drops a streaming response', async () => {
|
|
80
183
|
let upstreamClosed;
|
|
81
184
|
const upstreamClosedPromise = new Promise((resolve) => {
|
|
@@ -100,3 +203,266 @@ test('startProxy aborts the child request when the client drops a streaming resp
|
|
|
100
203
|
]);
|
|
101
204
|
expect(outcome).toBe('closed');
|
|
102
205
|
});
|
|
206
|
+
|
|
207
|
+
function echoRequest(label) {
|
|
208
|
+
return async (req, res) => {
|
|
209
|
+
const chunks = [];
|
|
210
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
211
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
212
|
+
res.end(
|
|
213
|
+
JSON.stringify({
|
|
214
|
+
label,
|
|
215
|
+
body: Buffer.concat(chunks).toString('utf8'),
|
|
216
|
+
buildWait: req.headers['x-lowdefy-build-wait'] ?? null,
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function mcpCall(name, args) {
|
|
223
|
+
return JSON.stringify({
|
|
224
|
+
jsonrpc: '2.0',
|
|
225
|
+
id: 1,
|
|
226
|
+
method: 'tools/call',
|
|
227
|
+
params: { name, arguments: args },
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
test.each([
|
|
232
|
+
['GET build-status?wait=true', () => ({ path: '/lowdefy-docs/build-status?wait=true' })],
|
|
233
|
+
[
|
|
234
|
+
'the lowdefy_build_status MCP tool with wait: true',
|
|
235
|
+
() => ({
|
|
236
|
+
path: '/lowdefy-docs/mcp',
|
|
237
|
+
init: { method: 'POST', body: mcpCall('lowdefy_build_status', { wait: true }) },
|
|
238
|
+
}),
|
|
239
|
+
],
|
|
240
|
+
])(
|
|
241
|
+
'startProxy holds %s through a restart and forwards it to the new server',
|
|
242
|
+
async (_, makeRequest) => {
|
|
243
|
+
const port = await startChildAndProxy(echoRequest('old'));
|
|
244
|
+
context.basePath = '';
|
|
245
|
+
context.buildActivity = createBuildActivity({ onChange: () => {} });
|
|
246
|
+
context.devServer = { exitCode: null, signalCode: null };
|
|
247
|
+
context.buildActivity.setBusy(true);
|
|
248
|
+
// waitedMs is timed from when the proxy starts waiting, a moment after the request is sent.
|
|
249
|
+
// Holding for 200ms from that moment, not from the fetch call, keeps the waitedMs assertion
|
|
250
|
+
// below from racing the request's arrival.
|
|
251
|
+
const { waitForIdle } = context.buildActivity;
|
|
252
|
+
const waitStarted = new Promise((resolve) => {
|
|
253
|
+
context.buildActivity.waitForIdle = (options) => {
|
|
254
|
+
const waiting = waitForIdle(options);
|
|
255
|
+
resolve();
|
|
256
|
+
return waiting;
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const { path: requestPath, init } = makeRequest();
|
|
261
|
+
const pending = fetch(`http://localhost:${port}${requestPath}`, {
|
|
262
|
+
...init,
|
|
263
|
+
headers: { 'x-lowdefy-build-wait': 'settled=true&sawBuild=false&waitedMs=0' },
|
|
264
|
+
}).then((response) => response.json());
|
|
265
|
+
await waitStarted;
|
|
266
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
267
|
+
|
|
268
|
+
// The restart the build needed: the old server stops, a new one answers.
|
|
269
|
+
context.devServerExited = Promise.resolve();
|
|
270
|
+
context.devServer = { exitCode: null, signalCode: null };
|
|
271
|
+
await close(child);
|
|
272
|
+
child = http.createServer(echoRequest('new'));
|
|
273
|
+
await new Promise((resolve) => child.listen(context.internalPort, '127.0.0.1', resolve));
|
|
274
|
+
context.buildActivity.setBusy(false);
|
|
275
|
+
|
|
276
|
+
const result = await pending;
|
|
277
|
+
expect(result.label).toBe('new');
|
|
278
|
+
expect(result.body).toBe(init?.body ?? '');
|
|
279
|
+
const wait = new URLSearchParams(result.buildWait);
|
|
280
|
+
expect(wait.get('settled')).toBe('true');
|
|
281
|
+
expect(wait.get('sawBuild')).toBe('true');
|
|
282
|
+
expect(Number(wait.get('waitedMs'))).toBeGreaterThanOrEqual(200);
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
test('startProxy forwards other MCP calls and build-status reads at once, with their body', async () => {
|
|
287
|
+
const port = await startChildAndProxy(echoRequest('child'));
|
|
288
|
+
context.basePath = '';
|
|
289
|
+
context.buildActivity = createBuildActivity({ onChange: () => {} });
|
|
290
|
+
context.buildActivity.setBusy(true);
|
|
291
|
+
const body = mcpCall('lowdefy_build_status', {});
|
|
292
|
+
|
|
293
|
+
const [call, read] = await Promise.all([
|
|
294
|
+
fetch(`http://localhost:${port}/lowdefy-docs/mcp`, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
body,
|
|
297
|
+
headers: { 'x-lowdefy-build-wait': 'settled=true' },
|
|
298
|
+
}).then((response) => response.json()),
|
|
299
|
+
fetch(`http://localhost:${port}/lowdefy-docs/build-status`).then((response) => response.json()),
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
expect(call).toEqual({ label: 'child', body, buildWait: null });
|
|
303
|
+
expect(read).toEqual({ label: 'child', body: '', buildWait: null });
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const paddedWaitCall = mcpCall('lowdefy_build_status', { wait: true, pad: 'x'.repeat(70 * 1024) });
|
|
307
|
+
const waitCall = mcpCall('lowdefy_build_status', { wait: true });
|
|
308
|
+
|
|
309
|
+
test.each([
|
|
310
|
+
['a body larger than 64 KiB', { body: paddedWaitCall }, paddedWaitCall],
|
|
311
|
+
[
|
|
312
|
+
'a streamed body without a length',
|
|
313
|
+
{ body: new Blob([waitCall]).stream(), duplex: 'half' },
|
|
314
|
+
waitCall,
|
|
315
|
+
],
|
|
316
|
+
])('startProxy forwards an MCP POST with %s unread, without holding it', async (_, init, sent) => {
|
|
317
|
+
const port = await startChildAndProxy(echoRequest('child'));
|
|
318
|
+
context.basePath = '';
|
|
319
|
+
context.buildActivity = createBuildActivity({ onChange: () => {} });
|
|
320
|
+
context.buildActivity.setBusy(true);
|
|
321
|
+
|
|
322
|
+
const call = await fetch(`http://localhost:${port}/lowdefy-docs/mcp`, {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
...init,
|
|
325
|
+
}).then((response) => response.json());
|
|
326
|
+
|
|
327
|
+
expect(call).toEqual({ label: 'child', body: sent, buildWait: null });
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
function activeRequests() {
|
|
331
|
+
return activityRecords.at(-1).activeRequests;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function waitFor(check) {
|
|
335
|
+
const deadline = Date.now() + 2000;
|
|
336
|
+
while (!check()) {
|
|
337
|
+
if (Date.now() > deadline) throw new Error('Condition not met within 2 seconds.');
|
|
338
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// A child that holds every response open until release() is called.
|
|
343
|
+
function heldHandler({ contentType = 'text/plain' } = {}) {
|
|
344
|
+
const held = [];
|
|
345
|
+
function handler(req, res) {
|
|
346
|
+
res.writeHead(200, { 'content-type': contentType });
|
|
347
|
+
res.flushHeaders();
|
|
348
|
+
if (contentType === 'text/event-stream') res.write('event: message\ndata: {}\n\n');
|
|
349
|
+
held.push(res);
|
|
350
|
+
}
|
|
351
|
+
function release() {
|
|
352
|
+
held.splice(0).forEach((res) => res.end('done'));
|
|
353
|
+
}
|
|
354
|
+
return { handler, held, release };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
test('startProxy counts a request as activity for as long as it is in flight', async () => {
|
|
358
|
+
const child$ = heldHandler();
|
|
359
|
+
const port = await startChildAndProxy(child$.handler);
|
|
360
|
+
context.basePath = '';
|
|
361
|
+
expect(activeRequests()).toBe(0);
|
|
362
|
+
|
|
363
|
+
const pending = fetch(`http://localhost:${port}/api/request/page/get`).then((response) =>
|
|
364
|
+
response.text()
|
|
365
|
+
);
|
|
366
|
+
await waitFor(() => child$.held.length === 1);
|
|
367
|
+
expect(activeRequests()).toBe(1);
|
|
368
|
+
|
|
369
|
+
child$.release();
|
|
370
|
+
expect(await pending).toBe('done');
|
|
371
|
+
await waitFor(() => activeRequests() === 0);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test('startProxy counts an MCP POST answered as an event stream until it ends', async () => {
|
|
375
|
+
const child$ = heldHandler({ contentType: 'text/event-stream' });
|
|
376
|
+
const port = await startChildAndProxy(child$.handler);
|
|
377
|
+
context.basePath = '';
|
|
378
|
+
|
|
379
|
+
const pending = fetch(`http://localhost:${port}/lowdefy-docs/mcp`, {
|
|
380
|
+
method: 'POST',
|
|
381
|
+
headers: { accept: 'application/json, text/event-stream' },
|
|
382
|
+
body: mcpCall('lowdefy_screenshot', {}),
|
|
383
|
+
}).then((response) => response.text());
|
|
384
|
+
await waitFor(() => child$.held.length === 1);
|
|
385
|
+
expect(activeRequests()).toBe(1);
|
|
386
|
+
|
|
387
|
+
child$.release();
|
|
388
|
+
await pending;
|
|
389
|
+
await waitFor(() => activeRequests() === 0);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test.each([
|
|
393
|
+
['a GET that opens an event stream', { accept: 'text/event-stream' }],
|
|
394
|
+
['a passive request', { 'x-lowdefy-passive': '1' }],
|
|
395
|
+
])('startProxy does not count %s as activity', async (_, headers) => {
|
|
396
|
+
const child$ = heldHandler({ contentType: 'text/event-stream' });
|
|
397
|
+
const port = await startChildAndProxy(child$.handler);
|
|
398
|
+
context.basePath = '';
|
|
399
|
+
const before = activityRecords.length;
|
|
400
|
+
|
|
401
|
+
const clientRequest = http.get(`http://localhost:${port}/lowdefy-docs/mcp`, { headers });
|
|
402
|
+
await new Promise((resolve) => clientRequest.on('response', resolve));
|
|
403
|
+
await waitFor(() => child$.held.length === 1);
|
|
404
|
+
clientRequest.destroy();
|
|
405
|
+
child$.release();
|
|
406
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
407
|
+
|
|
408
|
+
expect(activityRecords.length).toBe(before);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('startProxy ends the activity of a request whose client goes away', async () => {
|
|
412
|
+
const child$ = heldHandler();
|
|
413
|
+
const port = await startChildAndProxy(child$.handler);
|
|
414
|
+
context.basePath = '';
|
|
415
|
+
|
|
416
|
+
const clientRequest = http.get(`http://localhost:${port}/slow`);
|
|
417
|
+
clientRequest.on('error', () => {});
|
|
418
|
+
await waitFor(() => child$.held.length === 1);
|
|
419
|
+
expect(activeRequests()).toBe(1);
|
|
420
|
+
|
|
421
|
+
clientRequest.destroy();
|
|
422
|
+
await waitFor(() => activeRequests() === 0);
|
|
423
|
+
child$.release();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test('startProxy does not count a websocket upgrade as activity', async () => {
|
|
427
|
+
const port = await startChildAndProxy((req, res) => res.end('ok'));
|
|
428
|
+
const childSockets = [];
|
|
429
|
+
child.on('upgrade', (req, socket) => {
|
|
430
|
+
childSockets.push(socket);
|
|
431
|
+
socket.write(
|
|
432
|
+
'HTTP/1.1 101 Switching Protocols\r\nupgrade: websocket\r\nconnection: upgrade\r\n\r\n'
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
const before = activityRecords.length;
|
|
436
|
+
|
|
437
|
+
const clientRequest = http.get(`http://localhost:${port}/hmr`, {
|
|
438
|
+
headers: { connection: 'upgrade', upgrade: 'websocket' },
|
|
439
|
+
});
|
|
440
|
+
const socket = await new Promise((resolve) =>
|
|
441
|
+
clientRequest.on('upgrade', (res, upgraded) => resolve(upgraded))
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
expect(activityRecords.length).toBe(before);
|
|
445
|
+
socket.destroy();
|
|
446
|
+
childSockets.forEach((childSocket) => childSocket.destroy());
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('startProxy keeps a request held through a child restart counted', async () => {
|
|
450
|
+
const port = await startChildAndProxy((req, res) => res.end('old'));
|
|
451
|
+
context.basePath = '';
|
|
452
|
+
context.devServer = { exitCode: null, signalCode: null };
|
|
453
|
+
expect(await (await fetch(`http://localhost:${port}/a.js`)).text()).toBe('old');
|
|
454
|
+
await waitFor(() => activeRequests() === 0);
|
|
455
|
+
|
|
456
|
+
await close(child);
|
|
457
|
+
context.devServer.exitCode = 1;
|
|
458
|
+
const pending = fetch(`http://localhost:${port}/b.js`).then((response) => response.text());
|
|
459
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
460
|
+
expect(activeRequests()).toBe(1);
|
|
461
|
+
|
|
462
|
+
context.devServer = { exitCode: null, signalCode: null };
|
|
463
|
+
child = http.createServer((req, res) => res.end('new'));
|
|
464
|
+
await new Promise((resolve) => child.listen(context.internalPort, '127.0.0.1', resolve));
|
|
465
|
+
|
|
466
|
+
expect(await pending).toBe('new');
|
|
467
|
+
await waitFor(() => activeRequests() === 0);
|
|
468
|
+
});
|
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { spawn } from 'child_process';
|
|
18
|
+
import crypto from 'crypto';
|
|
18
19
|
|
|
19
|
-
import
|
|
20
|
+
import createServerEnv from '../utils/createServerEnv.mjs';
|
|
21
|
+
import killTaggedBrowser from '../utils/killTaggedBrowser.mjs';
|
|
22
|
+
import readBasePath from '../utils/readBasePath.mjs';
|
|
23
|
+
import resolveDevAuthUrl from '../utils/resolveDevAuthUrl.mjs';
|
|
20
24
|
|
|
21
25
|
function createStdErrLineHandler({ context }) {
|
|
22
26
|
const port = context.internalPort;
|
|
@@ -33,7 +37,31 @@ function createStdErrLineHandler({ context }) {
|
|
|
33
37
|
|
|
34
38
|
function startServer(context) {
|
|
35
39
|
context.shutdownServer();
|
|
36
|
-
|
|
40
|
+
|
|
41
|
+
// The app, its API and the /lowdefy-docs tools all live under basePath, and
|
|
42
|
+
// a config change that edits basePath restarts the child - so the URL is
|
|
43
|
+
// recorded on every start. Everything that finds this server through
|
|
44
|
+
// .lowdefy/instance.json (lowdefy mcp, the hub, lowdefy test) appends its
|
|
45
|
+
// paths to this URL.
|
|
46
|
+
context.basePath = readBasePath(context);
|
|
47
|
+
context.url = `http://localhost:${context.options.port}${context.basePath}`;
|
|
48
|
+
context.instance.update({ url: context.url });
|
|
49
|
+
|
|
50
|
+
const env = createServerEnv(context);
|
|
51
|
+
const configuredAuthUrl = process.env.BETTER_AUTH_URL;
|
|
52
|
+
const { authUrl, rewritten } = resolveDevAuthUrl({
|
|
53
|
+
configured: configuredAuthUrl,
|
|
54
|
+
port: context.options.port,
|
|
55
|
+
});
|
|
56
|
+
if (rewritten && context.loggedAuthUrl !== authUrl) {
|
|
57
|
+
context.logger.info(
|
|
58
|
+
`BETTER_AUTH_URL ${configuredAuthUrl} names another port; this dev server uses ${authUrl}.`
|
|
59
|
+
);
|
|
60
|
+
context.loggedAuthUrl = authUrl;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// New per child start, so killing one child's browser can match nothing else.
|
|
64
|
+
const browserTag = crypto.randomUUID();
|
|
37
65
|
|
|
38
66
|
// The child binds context.internalPort on loopback; the manager's proxy owns
|
|
39
67
|
// the public context.options.port (see startProxy.mjs) so a restart never
|
|
@@ -41,6 +69,8 @@ function startServer(context) {
|
|
|
41
69
|
const devServer = spawn(
|
|
42
70
|
'node',
|
|
43
71
|
[
|
|
72
|
+
// For the child's idle GC (lib/server/startIdleGc.js).
|
|
73
|
+
'--expose-gc',
|
|
44
74
|
context.bin.vite,
|
|
45
75
|
'--host',
|
|
46
76
|
'127.0.0.1',
|
|
@@ -49,11 +79,21 @@ function startServer(context) {
|
|
|
49
79
|
'--strictPort',
|
|
50
80
|
],
|
|
51
81
|
{
|
|
52
|
-
|
|
82
|
+
// The manager holds the child's stdin and never writes to it: the pipe
|
|
83
|
+
// closes when the manager dies, however it dies (SIGKILL, out of
|
|
84
|
+
// memory), and the child exits on that (see vite.config.js).
|
|
85
|
+
stdio: ['pipe', 'inherit', 'pipe'],
|
|
53
86
|
env: {
|
|
54
|
-
...
|
|
55
|
-
|
|
56
|
-
|
|
87
|
+
...env,
|
|
88
|
+
LOWDEFY_BROWSER_TAG: browserTag,
|
|
89
|
+
LOWDEFY_EXIT_ON_STDIN_CLOSE: '1',
|
|
90
|
+
// The manager's owner and registry record cover the child: it does
|
|
91
|
+
// not register, and it lives and dies with the manager.
|
|
92
|
+
LOWDEFY_EXIT_WITH_PID: undefined,
|
|
93
|
+
LOWDEFY_SERVER_REGISTRY_DIR: undefined,
|
|
94
|
+
// Reported as the MCP serverInfo version: lowdefy mcp takes each
|
|
95
|
+
// tool's definition from the newest Lowdefy version it meets.
|
|
96
|
+
LOWDEFY_SERVER_DEV_VERSION: context.version,
|
|
57
97
|
},
|
|
58
98
|
}
|
|
59
99
|
);
|
|
@@ -71,6 +111,7 @@ function startServer(context) {
|
|
|
71
111
|
context.logger.debug(`Started dev server with pid ${devServer.pid}.`);
|
|
72
112
|
devServer.on('exit', (code, signal) => {
|
|
73
113
|
context.logger.debug(`devServer exit ${devServer.pid}, signal: ${signal}, code: ${code}`);
|
|
114
|
+
killTaggedBrowser({ tag: browserTag });
|
|
74
115
|
});
|
|
75
116
|
devServer.on('error', (error) => {
|
|
76
117
|
context.logger.error(error);
|