@akanjs/devkit 3.0.0-beta.9 → 3.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/DEV_RUNTIME_KNOBS.md +44 -26
- package/README.ko.md +21 -7
- package/README.md +17 -8
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +6 -8
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +56 -3
- package/akanApp/akanApp.host.ts +218 -278
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +81 -35
- package/akanApp/devHostPolicy.ts +41 -102
- package/akanConfig/akanBin.test.ts +62 -0
- package/akanConfig/akanBin.ts +78 -0
- package/akanConfig/akanConfig.test.ts +439 -141
- package/akanConfig/akanConfig.ts +359 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +22 -8
- package/akanContext.ts +110 -160
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +41 -5
- package/applicationBuildRunner.ts +71 -63
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +32 -30
- package/artifact/implicitRootLayout.ts +58 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +35 -2
- package/biomeBase.ts +2 -5
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +77 -39
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +26 -5
- package/cloud/globalConfig.ts +71 -32
- package/codeAgent/agent/AkanCodeServices.ts +97 -0
- package/codeAgent/agent/AkanContextFiles.ts +94 -0
- package/codeAgent/agent/CodeAgent.ts +551 -0
- package/codeAgent/agent/CodeAgentAsks.ts +66 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
- package/codeAgent/agent/CodeAgentGate.ts +93 -0
- package/codeAgent/agent/CodeAgentProxy.ts +82 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
- package/codeAgent/agent/CodeAgentUi.ts +47 -0
- package/codeAgent/agent/CodeMailbox.ts +148 -0
- package/codeAgent/agent/CodeSessionFork.ts +56 -0
- package/codeAgent/agent/CodeSessionIndex.ts +78 -0
- package/codeAgent/agent/SubagentPool.ts +214 -0
- package/codeAgent/agent/akanCodeModel.ts +47 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanContextFiles.test.ts +114 -0
- package/codeAgent/agent/akanSystemPrompt.ts +46 -0
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +523 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/agent/codeMailbox.test.ts +76 -0
- package/codeAgent/agent/codeSessionFork.test.ts +58 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
- package/codeAgent/agent/consoleToStderr.ts +8 -0
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +82 -0
- package/codeAgent/feedback/PreviewChrome.ts +82 -0
- package/codeAgent/feedback/PreviewFeedback.ts +46 -0
- package/codeAgent/feedback/PreviewView.ts +95 -0
- package/codeAgent/feedback/TurnFeedback.ts +98 -0
- package/codeAgent/feedback/VerifyFeedback.ts +40 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +34 -0
- package/codeAgent/skills/akan-code/SKILL.md +130 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +67 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +50 -0
- package/codeAgent/tools/AkanCodePlugins.ts +136 -0
- package/codeAgent/tools/AkanEditScope.ts +48 -0
- package/codeAgent/tools/AkanEnvKeys.ts +50 -0
- package/codeAgent/tools/AkanToolPack.ts +92 -0
- package/codeAgent/tools/AkanVerifier.ts +48 -0
- package/codeAgent/tools/AskToolPack.ts +99 -0
- package/codeAgent/tools/MailToolPack.ts +76 -0
- package/codeAgent/tools/McpClient.ts +187 -0
- package/codeAgent/tools/McpOAuth.ts +233 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
- package/codeAgent/tools/McpServerConfig.ts +129 -0
- package/codeAgent/tools/McpSignIn.ts +158 -0
- package/codeAgent/tools/McpTokenStore.ts +81 -0
- package/codeAgent/tools/McpToolPack.ts +199 -0
- package/codeAgent/tools/SessionToolPack.ts +91 -0
- package/codeAgent/tools/WebToolPack.ts +147 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
- package/codeAgent/tools/askToolPack.test.ts +116 -0
- package/codeAgent/tools/mcpOAuth.test.ts +612 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +46 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +2 -10
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +68 -145
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/csrE2e/csrE2eHarness.fixture.ts +505 -0
- package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
- package/csrE2e/csrPageStack.e2e.test.ts +252 -0
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +31 -80
- package/devkitUtils.test.ts +48 -20
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +176 -106
- package/executors.ts +212 -498
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +22 -9
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +27 -38
- package/frontendBuild/autoImportSync.ts +31 -65
- package/frontendBuild/buildRouteClient.test.ts +123 -37
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +16 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
- package/frontendBuild/clientEntryDiscovery.ts +122 -110
- package/frontendBuild/csrArtifactBuilder.ts +9 -31
- package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
- package/frontendBuild/csrDevArtifactWriter.ts +307 -0
- package/frontendBuild/csrDevBundler.test.ts +66 -0
- package/frontendBuild/csrDevBundler.ts +95 -0
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
- package/frontendBuild/csrDevModuleCompiler.ts +422 -0
- package/frontendBuild/csrDevPatcher.test.ts +572 -0
- package/frontendBuild/csrDevPatcher.ts +548 -0
- package/frontendBuild/csrDevPaths.ts +145 -0
- package/frontendBuild/csrDevResolver.test.ts +239 -0
- package/frontendBuild/csrDevResolver.ts +363 -0
- package/frontendBuild/csrDevTypes.ts +61 -0
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +31 -82
- package/frontendBuild/cssImportResolver.ts +6 -19
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/devRegistryBundler.ts +192 -0
- package/frontendBuild/fontOptimizer.test.ts +6 -14
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +57 -70
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +59 -32
- package/frontendBuild/hmrWatcher.ts +31 -74
- package/frontendBuild/index.ts +4 -0
- package/frontendBuild/pagesBundleBuilder.ts +28 -30
- package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +100 -62
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
- package/frontendBuild/ssrDevBundler.ts +85 -0
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +147 -63
- package/incrementalBuilder/buildBatchProtocol.ts +13 -38
- package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
- package/incrementalBuilder/buildBatchRunner.ts +26 -38
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
- package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
- package/index.ts +5 -19
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +105 -199
- package/integration/devStabilityHarness.test.ts +13 -28
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
- package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
- package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
- package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-document-cookie.grit +29 -0
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/lint/no-web-only-api-outside-webkit.grit +44 -0
- package/lint/no-web-storage.grit +30 -0
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/desktopBin.test.ts +182 -0
- package/mobile/desktopBin.ts +144 -0
- package/mobile/desktopServerStage.test.ts +252 -0
- package/mobile/desktopServerStage.ts +178 -0
- package/mobile/index.ts +9 -0
- package/mobile/mobileTarget.test.ts +57 -8
- package/mobile/mobileTarget.ts +45 -26
- package/mobile/nativeApi.test.ts +30 -0
- package/mobile/nativeApi.ts +40 -0
- package/mobile/nativeApp.test.ts +184 -0
- package/mobile/nativeApp.ts +454 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +438 -0
- package/mobile/nativeConfig.ts +294 -0
- package/mobile/nativePluginFolders.test.ts +81 -0
- package/mobile/nativePluginFolders.ts +60 -0
- package/mobile/nativeWebDir.test.ts +80 -0
- package/mobile/nativeWebDir.ts +66 -0
- package/mobile/placeholderAppId.test.ts +13 -0
- package/mobile/placeholderAppId.ts +16 -0
- package/mobile/usageDescriptions.test.ts +27 -0
- package/mobile/usageDescriptions.ts +24 -0
- package/package.json +11 -11
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +46 -0
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +56 -0
- package/platformTest/IdleWatchedProcess.ts +73 -0
- package/platformTest/LinuxDockerTarget.ts +105 -0
- package/platformTest/PlatformTestRun.test.ts +64 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +142 -0
- package/platformTest/TestSnapshot.test.ts +72 -0
- package/platformTest/TestSnapshot.ts +103 -0
- package/platformTest/WindowsSshTarget.ts +151 -0
- package/prompter.ts +3 -13
- package/qualityScanner.test.ts +9 -26
- package/qualityScanner.ts +200 -286
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +7 -42
- package/scanInfo.ts +91 -225
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/stripAnsi.ts +3 -0
- package/subspace.test.ts +9 -25
- package/subspace.ts +48 -115
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +194 -0
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +42 -20
- package/transforms/serverEnvPlugin.test.ts +103 -0
- package/transforms/serverEnvPlugin.ts +41 -0
- package/transforms/transforms.test.ts +166 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +6 -23
- package/typeChecker.ts +7 -63
- package/types.ts +2 -0
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +101 -110
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +52 -69
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.test.ts +18 -3
- package/workspaceLayout.ts +22 -23
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/applicationReleasePackager.ts +0 -209
- package/capacitorApp.test.ts +0 -372
- package/capacitorApp.ts +0 -1648
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/guideline.ts +0 -19
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -1,30 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
4
2
|
import path from "node:path";
|
|
3
|
+
import { tempDirs } from "../testHelpers";
|
|
5
4
|
import { BuilderChannel } from "./builderChannel";
|
|
6
5
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
afterEach(async () => {
|
|
10
|
-
for (const dir of tempDirs.splice(0)) await rm(dir, { recursive: true, force: true });
|
|
11
|
-
});
|
|
6
|
+
const tempDir = tempDirs("builder-channel-");
|
|
12
7
|
|
|
13
8
|
type SendMode = "drain" | "await" | "bare";
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
* Run a child that sends what the builder sends and then exits the way the recycle drain does, and
|
|
17
|
-
* report what the parent actually received.
|
|
18
|
-
*
|
|
19
|
-
* `payload` picks the message shape, because shape decides whether a bare send survives: `manifest` is a
|
|
20
|
-
* map of many short strings (a `build-route-res` delta), `css` is one long string (a base64 stylesheet).
|
|
21
|
-
*/
|
|
10
|
+
// Shape decides whether a bare send survives: `manifest` is many short strings, `css` one long string.
|
|
22
11
|
const sendThenExit = async (
|
|
23
12
|
bytes: number,
|
|
24
13
|
{ mode, payload }: { mode: SendMode; payload: "manifest" | "css" },
|
|
25
14
|
): Promise<Array<{ type?: string }>> => {
|
|
26
|
-
const dir = await
|
|
27
|
-
tempDirs.push(dir);
|
|
15
|
+
const dir = await tempDir();
|
|
28
16
|
const entry = path.join(dir, "child.ts");
|
|
29
17
|
const modulePath = JSON.stringify(path.join(import.meta.dir, "builderChannel"));
|
|
30
18
|
const build =
|
|
@@ -63,44 +51,42 @@ const sendThenExit = async (
|
|
|
63
51
|
return received;
|
|
64
52
|
};
|
|
65
53
|
|
|
54
|
+
const withDeferredFlushes = async (fn: (flushes: Array<() => void>) => Promise<void>) => {
|
|
55
|
+
const send = process.send;
|
|
56
|
+
const flushes: Array<() => void> = [];
|
|
57
|
+
try {
|
|
58
|
+
(process as { send?: unknown }).send = (_msg: unknown, _h: unknown, _o: unknown, cb: () => void) => {
|
|
59
|
+
flushes.push(cb);
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
await fn(flushes);
|
|
63
|
+
} finally {
|
|
64
|
+
(process as { send?: unknown }).send = send;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
66
68
|
describe("BuilderChannel", () => {
|
|
67
69
|
test("delivers a reply too large for the pipe buffer before the process exits", async () => {
|
|
68
70
|
expect(await sendThenExit(1_000_000, { mode: "await", payload: "manifest" })).toMatchObject([{ id: 7 }]);
|
|
69
|
-
// A manifest delta is usually well past the buffer, but the small case must keep working too.
|
|
70
71
|
expect(await sendThenExit(0, { mode: "await", payload: "manifest" })).toMatchObject([{ id: 7 }]);
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
test("a drained event survives the exit even though nobody awaited it", async () => {
|
|
74
|
-
// The event path: `css-updated` is relayed with no caller to await it, and the recycle drain exits
|
|
75
|
-
// milliseconds later. Only `drain()` stands between the two.
|
|
76
75
|
expect(await sendThenExit(200_000, { mode: "drain", payload: "css" })).toMatchObject([{ type: "css-updated" }]);
|
|
77
76
|
});
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
* darwin only, measured: on Linux a 1MB message sent immediately before `process.exit` **arrives in
|
|
81
|
-
* full**, so the loss this control demonstrates does not reproduce there and the assertion would fail
|
|
82
|
-
* for the right reason on the wrong platform. `BuilderChannel` itself stays correct and costs nothing
|
|
83
|
-
* on Linux — the bug it prevents is one macOS developers hit and a Linux fleet does not.
|
|
84
|
-
*/
|
|
78
|
+
// darwin only: on Linux a message sent right before `process.exit` arrives in full, so the loss does not reproduce.
|
|
85
79
|
test.skipIf(process.platform !== "darwin")(
|
|
86
80
|
"without the flush wait the same messages are lost, which is why this class exists",
|
|
87
81
|
async () => {
|
|
88
82
|
// Controls, not requirements: if a future bun flushes ipc writes on exit, these fail and say so.
|
|
89
83
|
expect(await sendThenExit(1_000_000, { mode: "bare", payload: "manifest" })).toEqual([]);
|
|
90
|
-
// 20KB of css, far below the 64KB the manifest shape survives — one long string dies much earlier,
|
|
91
|
-
// so no size threshold would have been safe to special-case.
|
|
92
84
|
expect(await sendThenExit(20_000, { mode: "bare", payload: "css" })).toEqual([]);
|
|
93
85
|
},
|
|
94
86
|
);
|
|
95
87
|
|
|
96
88
|
test("drain resolves only once every tracked send has flushed", async () => {
|
|
97
|
-
|
|
98
|
-
const flushes: Array<() => void> = [];
|
|
99
|
-
try {
|
|
100
|
-
(process as { send?: unknown }).send = (_msg: unknown, _h: unknown, _o: unknown, cb: () => void) => {
|
|
101
|
-
flushes.push(cb);
|
|
102
|
-
return true;
|
|
103
|
-
};
|
|
89
|
+
await withDeferredFlushes(async (flushes) => {
|
|
104
90
|
BuilderChannel.emit({ type: "builder-ready" });
|
|
105
91
|
BuilderChannel.emit({ type: "builder-ready" });
|
|
106
92
|
let drained = false;
|
|
@@ -113,31 +99,20 @@ describe("BuilderChannel", () => {
|
|
|
113
99
|
expect(drained).toBe(false);
|
|
114
100
|
flushes[1]?.();
|
|
115
101
|
expect(await draining).toBe(2);
|
|
116
|
-
}
|
|
117
|
-
(process as { send?: unknown }).send = send;
|
|
118
|
-
}
|
|
102
|
+
});
|
|
119
103
|
});
|
|
120
104
|
|
|
121
105
|
test("a send started while draining is drained too", async () => {
|
|
122
|
-
|
|
123
|
-
const flushes: Array<() => void> = [];
|
|
124
|
-
try {
|
|
125
|
-
(process as { send?: unknown }).send = (_msg: unknown, _h: unknown, _o: unknown, cb: () => void) => {
|
|
126
|
-
flushes.push(cb);
|
|
127
|
-
return true;
|
|
128
|
-
};
|
|
106
|
+
await withDeferredFlushes(async (flushes) => {
|
|
129
107
|
BuilderChannel.emit({ type: "builder-ready" });
|
|
130
108
|
const draining = BuilderChannel.drain();
|
|
131
|
-
// What `#reportMetrics` does from a work item's `finally`, after the drain has begun.
|
|
132
109
|
BuilderChannel.emit({ type: "builder-metrics", data: { rssBytes: 1, generation: 1, workCount: 1 } });
|
|
133
110
|
for (let i = 0; i < 4 && flushes.length; i++) {
|
|
134
111
|
flushes.shift()?.();
|
|
135
112
|
await Bun.sleep(1);
|
|
136
113
|
}
|
|
137
114
|
expect(await draining).toBe(2);
|
|
138
|
-
}
|
|
139
|
-
(process as { send?: unknown }).send = send;
|
|
140
|
-
}
|
|
115
|
+
});
|
|
141
116
|
});
|
|
142
117
|
|
|
143
118
|
test("resolves instead of hanging when there is no ipc channel", async () => {
|
|
@@ -1,42 +1,18 @@
|
|
|
1
1
|
import type { BuilderMessage } from "akanjs/server";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* the process.
|
|
6
|
-
*
|
|
7
|
-
* `process.exit` discards an ipc write that has not flushed, and this process exits routinely — the
|
|
8
|
-
* recycle that keeps its RSS under the ceiling ends in `process.exit(0)`. Whether a given message
|
|
9
|
-
* survives that depends on its *shape*, not only its size. Measured on bun 1.3.14 (darwin,
|
|
10
|
-
* `serialization: "advanced"`), send-then-exit-immediately, 20 rounds each:
|
|
11
|
-
*
|
|
12
|
-
* - one long string: 8KB delivered 20/20, 16KB delivered **0/20**
|
|
13
|
-
* - many short strings: 60KB delivered 20/20, 100KB delivered **0/20**
|
|
14
|
-
*
|
|
15
|
-
* `css-updated` is the first shape (a base64 stylesheet per url) and `build-route-res` the second (a
|
|
16
|
-
* manifest delta), so neither sits safely under a threshold worth relying on. Nothing here assumes one:
|
|
17
|
-
* every send reports when it flushed, and `drain` is what `shutdown` awaits before exiting.
|
|
18
|
-
*
|
|
19
|
-
* A natural exit does *not* have this problem — a process that simply returns from `main` flushes 1MB
|
|
20
|
-
* 20/20 — which is why the disposable build worker needs none of this. Only an explicit `process.exit`
|
|
21
|
-
* cuts a write short.
|
|
22
|
-
*/
|
|
3
|
+
// `process.exit` drops an ipc write that has not flushed (a natural exit does not), and whether one survives
|
|
4
|
+
// depends on the message's shape as well as its size — so every send reports its flush and `shutdown` drains.
|
|
23
5
|
export class BuilderChannel {
|
|
24
|
-
|
|
25
|
-
* Bound so a runtime that ever stops invoking the flush callback costs one late message instead of
|
|
26
|
-
* wedging the shutdown drain until the host's kill watchdog fires.
|
|
27
|
-
*/
|
|
6
|
+
// Bounded so a runtime that never calls the flush callback costs one late message, not a wedged drain.
|
|
28
7
|
static readonly #flushTimeoutMs = 5_000;
|
|
29
|
-
/** Sends that have not flushed yet; `drain` only ever needs to await them all. */
|
|
30
8
|
static readonly #flushing = new Set<Promise<void>>();
|
|
31
9
|
|
|
32
|
-
/** Send, and resolve once the write has left this process. */
|
|
33
10
|
static send(message: BuilderMessage): Promise<void> {
|
|
34
11
|
const send = process.send;
|
|
35
12
|
if (!send) return Promise.resolve();
|
|
36
13
|
const flushed = new Promise<void>((resolve) => {
|
|
37
14
|
const timer = setTimeout(resolve, BuilderChannel.#flushTimeoutMs);
|
|
38
|
-
//
|
|
39
|
-
// process exits, and a lost event is superseded by the next generation's.
|
|
15
|
+
// No retry: the host answers an orphaned request id on exit, and a lost event is superseded next generation.
|
|
40
16
|
send.call(process, message, undefined, undefined, () => {
|
|
41
17
|
clearTimeout(timer);
|
|
42
18
|
resolve();
|
|
@@ -47,19 +23,12 @@ export class BuilderChannel {
|
|
|
47
23
|
return flushed;
|
|
48
24
|
}
|
|
49
25
|
|
|
50
|
-
|
|
51
|
-
* Send an event no caller awaits. Tracked exactly like `send`, which is the whole point: a relayed
|
|
52
|
-
* `css-updated` has nobody to await it, so `drain` is the only thing standing between it and the exit.
|
|
53
|
-
*/
|
|
26
|
+
// Tracked like `send`: for an event nobody awaits, `drain` is all that stands between it and the exit.
|
|
54
27
|
static emit(message: BuilderMessage): void {
|
|
55
28
|
void BuilderChannel.send(message);
|
|
56
29
|
}
|
|
57
30
|
|
|
58
|
-
|
|
59
|
-
* Resolve once every send handed over so far has flushed, and report how many were still in flight.
|
|
60
|
-
* Sends started *while* draining are covered too — `#reportMetrics` can fire from a work item's
|
|
61
|
-
* `finally`, after the drain has already begun.
|
|
62
|
-
*/
|
|
31
|
+
// Loops because `#reportMetrics` can send from a work item's `finally` after the drain has begun.
|
|
63
32
|
static async drain(): Promise<number> {
|
|
64
33
|
let flushed = 0;
|
|
65
34
|
while (BuilderChannel.#flushing.size) {
|
|
@@ -10,7 +10,6 @@ const routeReq = (id: number, routeId: string): BuilderReq => ({
|
|
|
10
10
|
knownEntries: [],
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
/** `routeId` is what tells one generation's answer from the other's; the rest is an empty delta. */
|
|
14
13
|
const routeRes = (id: number, routeId: string): Extract<BuilderRes, { ok: true }> => ({
|
|
15
14
|
type: "build-route-res",
|
|
16
15
|
id,
|
|
@@ -23,7 +22,6 @@ describe("BuilderRequestRouter", () => {
|
|
|
23
22
|
const router = new BuilderRequestRouter();
|
|
24
23
|
router.startGeneration();
|
|
25
24
|
|
|
26
|
-
// Everything but the id travels untouched; the builder never learns it was renumbered.
|
|
27
25
|
const first = router.issue(routeReq(1, "/home"));
|
|
28
26
|
const second = router.issue(routeReq(2, "/about"));
|
|
29
27
|
expect(first.routeId).toBe("/home");
|
|
@@ -35,11 +33,6 @@ describe("BuilderRequestRouter", () => {
|
|
|
35
33
|
expect(router.inFlightCount).toBe(0);
|
|
36
34
|
});
|
|
37
35
|
|
|
38
|
-
/**
|
|
39
|
-
* The defect this class exists for. `BuilderRpc` numbers from 1 in every backend process, so without
|
|
40
|
-
* renumbering the second generation's `id: 1` request is settled by the first generation's answer —
|
|
41
|
-
* a page rendered against another route's client manifest — and its own answer is then dropped.
|
|
42
|
-
*/
|
|
43
36
|
test("does not deliver a dead generation's answer to the backend that replaced it", () => {
|
|
44
37
|
const router = new BuilderRequestRouter();
|
|
45
38
|
router.startGeneration();
|
|
@@ -70,7 +63,6 @@ describe("BuilderRequestRouter", () => {
|
|
|
70
63
|
router.startGeneration();
|
|
71
64
|
const outgoing = router.issue(routeReq(4, "/home"));
|
|
72
65
|
|
|
73
|
-
// The caller answers the backend itself in this case, so a late builder answer must not answer it again.
|
|
74
66
|
router.withdraw(outgoing.id);
|
|
75
67
|
expect(router.inFlightCount).toBe(0);
|
|
76
68
|
expect(router.settle(routeRes(outgoing.id, "/home"))).toBeNull();
|
|
@@ -3,19 +3,8 @@ import type { BuilderCsrReq, BuilderCsrRes, BuilderReq, BuilderRes } from "akanj
|
|
|
3
3
|
type BuilderRequest = BuilderReq | BuilderCsrReq;
|
|
4
4
|
type BuilderResponse = BuilderRes | BuilderCsrRes;
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
* `BuilderRpc` numbers its requests from 1 in each backend *process*, and the host relays them to a
|
|
10
|
-
* builder that outlives the backend. So after a restart the two generations collide on id 1: the builder
|
|
11
|
-
* answers the request the *previous* backend made, the host relays it, and the new backend settles its own
|
|
12
|
-
* request of that number with another route's manifest delta — a page rendered against client modules that
|
|
13
|
-
* were never built for it. The answer it was actually waiting for then arrives to an empty pending map and
|
|
14
|
-
* is dropped, so the correct build is discarded too.
|
|
15
|
-
*
|
|
16
|
-
* A host-owned id fixes it without touching the protocol: neither the backend nor the builder learns that
|
|
17
|
-
* anything was renumbered, and a response whose generation is gone is discarded instead of misdelivered.
|
|
18
|
-
*/
|
|
6
|
+
// `BuilderRpc` numbers from 1 per backend process while the builder outlives backends, so ids collide across a
|
|
7
|
+
// restart; host-owned ids keep a dead generation's answer from settling the new backend's request.
|
|
19
8
|
export class BuilderRequestRouter {
|
|
20
9
|
#generation = 0;
|
|
21
10
|
#nextId = 1;
|
|
@@ -29,33 +18,25 @@ export class BuilderRequestRouter {
|
|
|
29
18
|
return this.#inFlight.size;
|
|
30
19
|
}
|
|
31
20
|
|
|
32
|
-
|
|
33
|
-
* Begin a new backend generation, abandoning every request the previous one owned.
|
|
34
|
-
*
|
|
35
|
-
* Nothing is answered on the way out: the process that asked is gone, and its `BuilderRpc` went with it.
|
|
36
|
-
*/
|
|
21
|
+
// Abandons the previous generation's requests unanswered: the backend that asked is gone.
|
|
37
22
|
startGeneration(): number {
|
|
38
23
|
this.#generation += 1;
|
|
39
24
|
this.#inFlight.clear();
|
|
40
25
|
return this.#generation;
|
|
41
26
|
}
|
|
42
27
|
|
|
43
|
-
/** The request to forward to the builder, carrying this host's id in place of the backend's. */
|
|
44
28
|
issue<T extends BuilderRequest>(message: T): T {
|
|
45
29
|
const id = this.#nextId++;
|
|
46
30
|
this.#inFlight.set(id, { backendId: message.id, generation: this.#generation });
|
|
47
31
|
return { ...message, id };
|
|
48
32
|
}
|
|
49
33
|
|
|
50
|
-
|
|
34
|
+
// For a failed send, after which the caller answers the backend itself.
|
|
51
35
|
withdraw(id: number): void {
|
|
52
36
|
this.#inFlight.delete(id);
|
|
53
37
|
}
|
|
54
38
|
|
|
55
|
-
|
|
56
|
-
* The response to forward to the backend with its own id restored, or `null` when no live backend is
|
|
57
|
-
* waiting for it — either it was never issued or the generation that issued it has been replaced.
|
|
58
|
-
*/
|
|
39
|
+
// null when no live backend is waiting: never issued, or its generation was replaced.
|
|
59
40
|
settle<T extends BuilderResponse>(message: T): T | null {
|
|
60
41
|
const request = this.#inFlight.get(message.id);
|
|
61
42
|
if (!request) return null;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { type BatchJob, BuilderWorkQueue } from "./builderWorkQueue";
|
|
3
|
+
|
|
4
|
+
const gate = () => {
|
|
5
|
+
const { promise: opened, resolve: open } = Promise.withResolvers<void>();
|
|
6
|
+
return { open, opened };
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
describe("BuilderWorkQueue", () => {
|
|
10
|
+
test("runs jobs one at a time, in the order they were queued", async () => {
|
|
11
|
+
const order: string[] = [];
|
|
12
|
+
const queue = new BuilderWorkQueue({ runBatch: async () => undefined });
|
|
13
|
+
const first = gate();
|
|
14
|
+
const a = queue.enqueue("a", async () => {
|
|
15
|
+
await first.opened;
|
|
16
|
+
order.push("a");
|
|
17
|
+
});
|
|
18
|
+
const b = queue.enqueue("b", async () => {
|
|
19
|
+
order.push("b");
|
|
20
|
+
return 2;
|
|
21
|
+
});
|
|
22
|
+
expect(queue.size).toBe(2);
|
|
23
|
+
first.open();
|
|
24
|
+
await a;
|
|
25
|
+
expect(await b).toBe(2);
|
|
26
|
+
expect(order).toEqual(["a", "b"]);
|
|
27
|
+
expect(queue.size).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("folds a batch queued behind another batch into it, for the newest generation", async () => {
|
|
31
|
+
const ran: BatchJob[] = [];
|
|
32
|
+
const running = gate();
|
|
33
|
+
const queue = new BuilderWorkQueue({
|
|
34
|
+
runBatch: async (batch) => {
|
|
35
|
+
ran.push(batch);
|
|
36
|
+
if (ran.length === 1) await running.opened;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const first = queue.enqueueBatch({ generation: 1, needs: ["pages", "css"], changedFiles: ["a.tsx"] });
|
|
40
|
+
const second = queue.enqueueBatch({
|
|
41
|
+
generation: 2,
|
|
42
|
+
needs: ["pages", "css"],
|
|
43
|
+
changedFiles: ["b.tsx"],
|
|
44
|
+
discovery: { files: ["b.tsx"], refresh: false, generation: 2 },
|
|
45
|
+
});
|
|
46
|
+
const third = queue.enqueueBatch({ generation: 3, needs: ["csr", "pages"], changedFiles: ["a.tsx", "c.tsx"] });
|
|
47
|
+
expect(second).toBe(third);
|
|
48
|
+
running.open();
|
|
49
|
+
await Promise.all([first, second, third]);
|
|
50
|
+
expect(ran).toHaveLength(2);
|
|
51
|
+
expect(ran[1]).toEqual({
|
|
52
|
+
generation: 3,
|
|
53
|
+
needs: ["pages", "css", "csr"],
|
|
54
|
+
changedFiles: ["b.tsx", "a.tsx", "c.tsx"],
|
|
55
|
+
discovery: { files: ["b.tsx"], refresh: false, generation: 2 },
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("keeps a batch queued after a task behind that task", async () => {
|
|
60
|
+
const order: string[] = [];
|
|
61
|
+
const running = gate();
|
|
62
|
+
const queue = new BuilderWorkQueue({
|
|
63
|
+
runBatch: async (batch) => {
|
|
64
|
+
order.push(`batch:${batch.generation}`);
|
|
65
|
+
if (batch.generation === 1) await running.opened;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const first = queue.enqueueBatch({ generation: 1, needs: ["pages"], changedFiles: [] });
|
|
69
|
+
const route = queue.enqueue("route", async () => {
|
|
70
|
+
order.push("route");
|
|
71
|
+
});
|
|
72
|
+
const second = queue.enqueueBatch({ generation: 2, needs: ["pages"], changedFiles: [] });
|
|
73
|
+
running.open();
|
|
74
|
+
await Promise.all([first, route, second]);
|
|
75
|
+
expect(order).toEqual(["batch:1", "route", "batch:2"]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("drains jobs queued while it waits, and survives a job that throws", async () => {
|
|
79
|
+
const queue = new BuilderWorkQueue({ runBatch: async () => undefined });
|
|
80
|
+
const failing = queue.enqueue("fails", async () => {
|
|
81
|
+
throw new Error("boom");
|
|
82
|
+
});
|
|
83
|
+
let late = false;
|
|
84
|
+
void queue.enqueue("queues-more", async () => {
|
|
85
|
+
void queue.enqueue("late", async () => {
|
|
86
|
+
late = true;
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
await expect(failing).rejects.toThrow("boom");
|
|
90
|
+
await queue.drain();
|
|
91
|
+
expect(late).toBe(true);
|
|
92
|
+
expect(queue.size).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { HmrTrace } from "akanjs/server";
|
|
2
|
+
import type { BuildBatchNeed } from "./buildBatchProtocol";
|
|
3
|
+
|
|
4
|
+
export interface DiscoveryJob {
|
|
5
|
+
files: string[];
|
|
6
|
+
refresh: boolean;
|
|
7
|
+
/** The newest batch these files came from: a route build that starts after has taken it in. */
|
|
8
|
+
generation: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BatchJob {
|
|
12
|
+
generation: number;
|
|
13
|
+
needs: BuildBatchNeed[];
|
|
14
|
+
changedFiles: string[];
|
|
15
|
+
trace?: HmrTrace;
|
|
16
|
+
/** Code files whose client-entry discovery goes stale before this batch builds; `refresh` rebuilds it whole. */
|
|
17
|
+
discovery?: DiscoveryJob;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface QueuedJob {
|
|
21
|
+
label: string;
|
|
22
|
+
batch: BatchJob | null;
|
|
23
|
+
run: () => Promise<void>;
|
|
24
|
+
settle: PromiseWithResolvers<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BuilderWorkQueueOptions {
|
|
28
|
+
runBatch: (batch: BatchJob) => Promise<void>;
|
|
29
|
+
onSettled?: (label: string, ms: number) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//* The builder's slow lane: build-worker batches, route builds and full CSR builds run here one at a time, so a save's
|
|
33
|
+
//* CSR patch never waits behind them. A batch queued behind another batch folds into it: a burst of saves builds pages
|
|
34
|
+
//* and css once for the newest generation instead of once per save.
|
|
35
|
+
export class BuilderWorkQueue {
|
|
36
|
+
readonly #runBatch: BuilderWorkQueueOptions["runBatch"];
|
|
37
|
+
readonly #onSettled: BuilderWorkQueueOptions["onSettled"];
|
|
38
|
+
readonly #pending: QueuedJob[] = [];
|
|
39
|
+
#running: QueuedJob | null = null;
|
|
40
|
+
#idle: Promise<void> = Promise.resolve();
|
|
41
|
+
#wakeIdle: (() => void) | null = null;
|
|
42
|
+
|
|
43
|
+
constructor({ runBatch, onSettled }: BuilderWorkQueueOptions) {
|
|
44
|
+
this.#runBatch = runBatch;
|
|
45
|
+
this.#onSettled = onSettled;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Jobs waiting or running. */
|
|
49
|
+
get size(): number {
|
|
50
|
+
return this.#pending.length + (this.#running ? 1 : 0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
enqueueBatch(batch: BatchJob): Promise<void> {
|
|
54
|
+
const tail = this.#pending.at(-1);
|
|
55
|
+
if (tail?.batch) {
|
|
56
|
+
tail.batch = BuilderWorkQueue.merge(tail.batch, batch);
|
|
57
|
+
return tail.settle.promise;
|
|
58
|
+
}
|
|
59
|
+
const job = this.#job("batch", batch, async () => undefined);
|
|
60
|
+
job.run = async () => await this.#runBatch(job.batch ?? batch);
|
|
61
|
+
return this.#push(job);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
enqueue<T>(label: string, run: () => Promise<T>): Promise<T> {
|
|
65
|
+
let result: T | undefined;
|
|
66
|
+
const job = this.#job(label, null, async () => {
|
|
67
|
+
result = await run();
|
|
68
|
+
});
|
|
69
|
+
return this.#push(job).then(() => result as T);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Resolves once nothing is waiting or running, including jobs queued while it waits. */
|
|
73
|
+
async drain(): Promise<void> {
|
|
74
|
+
while (this.size > 0) await this.#idle;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static merge(into: BatchJob, next: BatchJob): BatchJob {
|
|
78
|
+
const discovery =
|
|
79
|
+
into.discovery || next.discovery
|
|
80
|
+
? {
|
|
81
|
+
files: [...new Set([...(into.discovery?.files ?? []), ...(next.discovery?.files ?? [])])],
|
|
82
|
+
refresh: !!into.discovery?.refresh || !!next.discovery?.refresh,
|
|
83
|
+
generation: Math.max(into.discovery?.generation ?? 0, next.discovery?.generation ?? 0),
|
|
84
|
+
}
|
|
85
|
+
: undefined;
|
|
86
|
+
return {
|
|
87
|
+
generation: Math.max(into.generation, next.generation),
|
|
88
|
+
needs: [...new Set([...into.needs, ...next.needs])],
|
|
89
|
+
changedFiles: [...new Set([...into.changedFiles, ...next.changedFiles])],
|
|
90
|
+
trace: next.trace ?? into.trace,
|
|
91
|
+
...(discovery ? { discovery } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#job(label: string, batch: BatchJob | null, run: () => Promise<void>): QueuedJob {
|
|
96
|
+
return { label, batch, run, settle: Promise.withResolvers<void>() };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#push(job: QueuedJob): Promise<void> {
|
|
100
|
+
if (this.size === 0)
|
|
101
|
+
this.#idle = new Promise<void>((resolve) => {
|
|
102
|
+
this.#wakeIdle = resolve;
|
|
103
|
+
});
|
|
104
|
+
this.#pending.push(job);
|
|
105
|
+
if (!this.#running) void this.#pump();
|
|
106
|
+
return job.settle.promise;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async #pump(): Promise<void> {
|
|
110
|
+
for (let job = this.#pending.shift(); job; job = this.#pending.shift()) {
|
|
111
|
+
this.#running = job;
|
|
112
|
+
const started = Date.now();
|
|
113
|
+
try {
|
|
114
|
+
await job.run();
|
|
115
|
+
job.settle.resolve();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
job.settle.reject(error);
|
|
118
|
+
} finally {
|
|
119
|
+
this.#running = null;
|
|
120
|
+
this.#onSettled?.(job.label, Date.now() - started);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
this.#wakeIdle?.();
|
|
124
|
+
this.#wakeIdle = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DevChangePlanner, type GeneratedIndexSyncResult } from "../frontendBuild";
|
|
3
4
|
import { prepareDevWatchBatch } from "./devWatchBatch";
|
|
4
5
|
|
|
6
|
+
const prepare = (root: string, generation: number, file: string, indexSync: GeneratedIndexSyncResult) =>
|
|
7
|
+
prepareDevWatchBatch({
|
|
8
|
+
generation,
|
|
9
|
+
batch: { files: [file], kinds: new Set(["code"]) },
|
|
10
|
+
indexSync,
|
|
11
|
+
changePlanner: new DevChangePlanner({ workspaceRoot: root }),
|
|
12
|
+
});
|
|
13
|
+
|
|
5
14
|
describe("prepareDevWatchBatch", () => {
|
|
6
15
|
test("includes generated indexes in the same invalidate generation", () => {
|
|
7
|
-
const root = "/repo";
|
|
8
|
-
const changedFile =
|
|
9
|
-
const generatedIndex =
|
|
10
|
-
const prepared =
|
|
11
|
-
generation: 12,
|
|
12
|
-
batch: { files: [changedFile], kinds: new Set(["code"]) },
|
|
13
|
-
indexSync: { changedFiles: [generatedIndex], errors: [] },
|
|
14
|
-
changePlanner: new DevChangePlanner({ workspaceRoot: root }),
|
|
15
|
-
});
|
|
16
|
+
const root = path.resolve("/repo");
|
|
17
|
+
const changedFile = path.join(root, "libs/shared/common/foo.ts");
|
|
18
|
+
const generatedIndex = path.join(root, "libs/shared/common/index.ts");
|
|
19
|
+
const prepared = prepare(root, 12, changedFile, { changedFiles: [generatedIndex], errors: [] });
|
|
16
20
|
|
|
17
21
|
expect(prepared.hasSyncErrors).toBe(false);
|
|
18
22
|
expect(prepared.files).toEqual([changedFile, generatedIndex]);
|
|
@@ -25,15 +29,10 @@ describe("prepareDevWatchBatch", () => {
|
|
|
25
29
|
test.each(["common", "srvkit", "ui", "webkit"])(
|
|
26
30
|
"keeps %s facet add/delete generated index in the same generation",
|
|
27
31
|
(facet) => {
|
|
28
|
-
const root = "/repo";
|
|
29
|
-
const changedFile =
|
|
30
|
-
const generatedIndex =
|
|
31
|
-
const prepared =
|
|
32
|
-
generation: 20,
|
|
33
|
-
batch: { files: [changedFile], kinds: new Set(["code"]) },
|
|
34
|
-
indexSync: { changedFiles: [generatedIndex], errors: [] },
|
|
35
|
-
changePlanner: new DevChangePlanner({ workspaceRoot: root }),
|
|
36
|
-
});
|
|
32
|
+
const root = path.resolve("/repo");
|
|
33
|
+
const changedFile = path.join(root, "libs/shared", facet, "tmpExample.ts");
|
|
34
|
+
const generatedIndex = path.join(root, "libs/shared", facet, "index.ts");
|
|
35
|
+
const prepared = prepare(root, 20, changedFile, { changedFiles: [generatedIndex], errors: [] });
|
|
37
36
|
|
|
38
37
|
expect(new Set(prepared.files)).toEqual(new Set([changedFile, generatedIndex]));
|
|
39
38
|
expect(prepared.event.devPlan?.generatedFiles).toEqual([generatedIndex]);
|
|
@@ -44,11 +43,9 @@ describe("prepareDevWatchBatch", () => {
|
|
|
44
43
|
|
|
45
44
|
test("marks failed generated index sync as an error generation", () => {
|
|
46
45
|
const root = "/repo";
|
|
47
|
-
const prepared =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
indexSync: { changedFiles: [], errors: ["sync failed"] },
|
|
51
|
-
changePlanner: new DevChangePlanner({ workspaceRoot: root }),
|
|
46
|
+
const prepared = prepare(root, 13, `${root}/libs/shared/common/foo.ts`, {
|
|
47
|
+
changedFiles: [],
|
|
48
|
+
errors: ["sync failed"],
|
|
52
49
|
});
|
|
53
50
|
|
|
54
51
|
expect(prepared.hasSyncErrors).toBe(true);
|