Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
157 commits
Select commit Hold shift + click to select a range
623ad18
perf(server): start the Claude CLI while the baseline checkpoint is c…
maria-rcks Oct 9, 2026
e9f2861
perf(server): fewer sequential git processes per checkpoint
maria-rcks Oct 9, 2026
240ebab
perf(server): refresh VCS status after a turn without holding the thr…
maria-rcks Oct 9, 2026
d8fefcf
perf(server): show an item's first streamed text without the 50 ms hold
maria-rcks Oct 9, 2026
6ad355d
perf(server): write a fraction of the trace bytes per minute
maria-rcks Oct 9, 2026
aa31929
perf(server): send each checkpoint's file list once in bounded snapshots
maria-rcks Oct 9, 2026
f5067fa
perf(server): resolve checkpoint metadata in one git process
maria-rcks Oct 9, 2026
8903e82
perf(desktop): load the renderer while the local backend starts
maria-rcks Oct 9, 2026
9c0d6f4
Merge remote-tracking branch 'origin/main' into t3/app-startup-perfor…
maria-rcks Oct 9, 2026
7ef5da7
perf(web): reduce repeated chat and panel work
maria-rcks Oct 9, 2026
31967f8
fix(client-runtime): preserve encoded thread urls during snapshot com…
maria-rcks Oct 9, 2026
1623911
perf(server): defer startup maintenance and parallelize editor discovery
maria-rcks Oct 9, 2026
777b5e9
perf(server): compact thread sync events and index background sweep f…
maria-rcks Oct 9, 2026
14dbe70
fix(startup): preserve snapshot opt-ins and serialize main window cre…
maria-rcks Oct 9, 2026
5662ff5
perf(web): trim and parallelize application startup
maria-rcks Oct 9, 2026
ef932ea
fix(web): preserve boot module boundaries across platforms
maria-rcks Oct 9, 2026
0dda283
fix(server): preserve activity stamps when replaying legacy pull requ…
maria-rcks Oct 9, 2026
d52027d
test(provider): cover deferred startup probes using current settings
maria-rcks Oct 9, 2026
a6c2e9d
test(provider): return void from deferred probe enrichment
maria-rcks Oct 9, 2026
7f09da6
fix(web): use the runtime clock for descriptor freshness
maria-rcks Oct 9, 2026
5035247
fix(server): close abandoned claude turn preparation
maria-rcks Oct 9, 2026
44cb054
fix(startup): follow node namespace imports in build hooks
maria-rcks Oct 9, 2026
f471559
test(server): isolate sweep migration upgrade state
maria-rcks Oct 9, 2026
c18aead
test(markdown): compare rendered whitespace and boolean attributes
maria-rcks Oct 9, 2026
f7990e8
perf(web): group static startup graphs explicitly
maria-rcks Oct 9, 2026
417dfe4
test(markdown): normalize adjacent rendered text
maria-rcks Oct 9, 2026
807cea8
test(web): await raw markdown support in timeline rendering
maria-rcks Oct 9, 2026
95a752a
fix(web): preserve startup initialization and scoped palette identity
maria-rcks Oct 9, 2026
281c487
perf(web): avoid sidebar work on the first keystroke
maria-rcks Oct 9, 2026
d848a58
test(server): include sweep migration in preview upgrades
maria-rcks Oct 9, 2026
5953737
chore: merge upstream relay URL compatibility
maria-rcks Oct 9, 2026
856af15
fix(server): preserve newline paths in checkpoint probes
maria-rcks Oct 9, 2026
2c5fe99
style(server): format existing performance regression checks
maria-rcks Oct 9, 2026
7b2e984
fix(server): preserve structured causes in performance warnings
maria-rcks Oct 9, 2026
98d4945
fix(web): size the custom snooze date picker on its first open
maria-rcks Oct 10, 2026
168f7cb
chore: merge origin/main into t3/app-startup-performance-audit
maria-rcks Oct 10, 2026
a84b9e9
fix(server): parked startup requests reach the handler in arrival order
maria-rcks Oct 10, 2026
02602d6
fix(server): a new item's first text skips another item's pending flu…
maria-rcks Oct 10, 2026
d967660
fix(orchestration): pull request link syncs no longer count as thread…
maria-rcks Oct 10, 2026
ce256b9
fix(server): keep git stderr out of the checkpoint probe error detail
maria-rcks Oct 10, 2026
7f37690
fix(provider-core): settings changes wait for the startup probe gate
maria-rcks Oct 10, 2026
eafa8bd
fix(web): a held timeline no longer changes the active thread's follo…
maria-rcks Oct 10, 2026
16f9e9f
fix(web): recover from failed lazy chunk loads
maria-rcks Oct 10, 2026
ba867d5
fix(web): a failed highlighter load stays on the plain-text fallback
maria-rcks Oct 10, 2026
904a4b7
fix(web): custom snooze falls back to a typed date when the calendar …
maria-rcks Oct 10, 2026
2f88b24
fix(web): a chunk that fails every boot no longer reload-loops
maria-rcks Oct 10, 2026
a6624c2
fix(server): capture the baseline checkpoint before the Claude CLI st…
maria-rcks Oct 10, 2026
105597c
fix(web): a sign-in during the onboarding chunk load still opens the …
maria-rcks Oct 10, 2026
900652d
fix(server): sweep index follows payloads written by any build
maria-rcks Oct 10, 2026
d4e7adf
fix(web): a second deploy within a minute still gets its chunk reload
maria-rcks Oct 10, 2026
a2cef49
fix(desktop): a cold local backend no longer fails the window's sign-in
maria-rcks Oct 10, 2026
d06ae6d
fix(server): sweep index migration takes a slot no other build has used
maria-rcks Oct 10, 2026
c0a5859
fix(server): sweep index migration runs as 61 so main keeps no gap
maria-rcks Oct 10, 2026
6f750e9
fix(server): retry the post-turn workspace refresh
maria-rcks Oct 10, 2026
7c663d7
fix(server): a link sync retries sibling links that never synced
maria-rcks Oct 10, 2026
209eaf9
fix(desktop): a window closed during a cold start stays closed at bac…
maria-rcks Oct 10, 2026
bb6d69a
fix(desktop): a failed early window open no longer quits the app
maria-rcks Oct 10, 2026
aaf4ac7
fix(desktop): quitting during a cold backend start no longer shows a …
maria-rcks Oct 10, 2026
6c028a4
fix(web): keys typed while the palette loads go to its query
maria-rcks Oct 10, 2026
121afb0
fix(web): lazy sidebars keep their header while loading or after a fa…
maria-rcks Oct 10, 2026
1a4faf0
perf(web): settings, usage and welcome cold loads skip chat-only code
maria-rcks Oct 10, 2026
d1373eb
fix(web): leaving an anchored reply remembers the reading position
maria-rcks Oct 10, 2026
c7fee2a
fix(web): a failed rehype-raw load stays on the sanitized fallback
maria-rcks Oct 10, 2026
76c7d1d
fix(web): each new build gets its own chunk reloads
maria-rcks Oct 10, 2026
2f8e108
fix(web): a failed primary discovery shows the empty workspace again
maria-rcks Oct 10, 2026
3451c68
fix(web): a second theme change within two frames also paints without…
maria-rcks Oct 10, 2026
956638a
chore: merge main into t3/app-startup-performance-audit
maria-rcks Oct 11, 2026
42e2b35
fix(web): snooze date fallback uses the Input's own size
maria-rcks Oct 11, 2026
d779416
chore: merge main into t3/app-startup-performance-audit
maria-rcks Oct 11, 2026
37f8c26
chore: merge main into t3/app-startup-performance-audit
maria-rcks Oct 11, 2026
e4ecba5
chore(perf): merge current main into startup stack
maria-rcks Oct 11, 2026
a85efd3
chore(perf): merge current main into startup stack
maria-rcks Oct 12, 2026
77b5b76
chore(perf): merge current main into startup stack
maria-rcks Oct 14, 2026
c8b6c2f
fix(server): move the sweep index migration after main's migrations
maria-rcks Oct 14, 2026
7fba2c3
test(server): align compact PR replay coverage after main merge
maria-rcks Oct 14, 2026
91bffa0
refactor(server): infer enrichment method types
maria-rcks Oct 14, 2026
c2f0520
refactor(server): name the execution failure scenarios
maria-rcks Oct 14, 2026
37fb3a5
refactor(web): trim startup performance commentary
maria-rcks Oct 14, 2026
1da5e92
refactor(web): simplify composer send dispatch
maria-rcks Oct 14, 2026
50762a5
refactor(web): simplify thread details loading
maria-rcks Oct 14, 2026
56aecdc
refactor(web): drop precompressed asset sidecars
maria-rcks Oct 14, 2026
243ad96
refactor(web): simplify snooze calendar loading
maria-rcks Oct 14, 2026
5b6d40d
refactor(web): drop speculative primary discovery
maria-rcks Oct 14, 2026
d6ec511
refactor(web): keep the mobile transition gate private
maria-rcks Oct 14, 2026
2533090
refactor(web): retain descriptor prefetch for startup
maria-rcks Oct 14, 2026
97ee9be
refactor(web): retain lazy details for startup
maria-rcks Oct 14, 2026
2b641c2
refactor(web): retain asset sidecars for startup
maria-rcks Oct 14, 2026
c3e6a94
refactor(web): drop speculative descriptor discovery
maria-rcks Oct 14, 2026
de797a6
refactor(web): retain lazy calendar for startup
maria-rcks Oct 14, 2026
fcc9fda
revert(web): keep the speculative primary descriptor request
maria-rcks Oct 14, 2026
23f6180
refactor(web): drop lazy palette boundary
maria-rcks Oct 15, 2026
be10606
refactor(web): drop lazy thread details boundary
maria-rcks Oct 15, 2026
0a683cc
refactor(web): drop lazy snooze calendar boundary
maria-rcks Oct 15, 2026
afa537d
refactor(web): drop precompressed asset builds
maria-rcks Oct 15, 2026
9d919c4
refactor(web): drop unmeasured palette caches
maria-rcks Oct 15, 2026
25f5bac
refactor(web): drop effective shortcut cache
maria-rcks Oct 15, 2026
1b49644
refactor(web): drop lazy onboarding split
maria-rcks Oct 15, 2026
daea839
refactor(web): drop route family repartition
maria-rcks Oct 15, 2026
9db4a5f
refactor(web): retain onboarding opt out state
maria-rcks Oct 15, 2026
2d2840d
refactor(web): drop speculative descriptor discovery
maria-rcks Oct 15, 2026
39282e2
refactor(web): drop composer document cache
maria-rcks Oct 15, 2026
dc3b58e
refactor(web): drop media query cache
maria-rcks Oct 15, 2026
29c7bb5
refactor(web): drop changed file tree cache
maria-rcks Oct 15, 2026
84024af
refactor(web): drop asynchronous favicon encoding
maria-rcks Oct 15, 2026
d6672a9
refactor(web): drop monogram segmenter fast path
maria-rcks Oct 15, 2026
08d40b0
refactor(web): drop empty prompt measurement fast path
maria-rcks Oct 15, 2026
e428526
refactor(web): drop resize observer first paint shortcut
maria-rcks Oct 15, 2026
25e0b30
refactor(web): drop client trace interval change
maria-rcks Oct 15, 2026
b293001
refactor(web): drop parallel terminal initialization
maria-rcks Oct 15, 2026
db8b925
refactor(web): drop font preview visibility gate
maria-rcks Oct 15, 2026
6f1a700
refactor(web): drop lazy proof signing
maria-rcks Oct 15, 2026
ac7a47c
refactor(web): drop sidebar route preloads
maria-rcks Oct 15, 2026
94ddec5
refactor(web): drop lazy sidebar navigation
maria-rcks Oct 15, 2026
409449f
refactor(web): drop draft motion subscription optimization
maria-rcks Oct 15, 2026
e402f41
test(web): trim chunk reload regression cases
maria-rcks Oct 15, 2026
ed536a1
test(web): simplify scoped palette and reload regressions
maria-rcks Oct 15, 2026
398a1bd
refactor(web): drop draft transition gate
maria-rcks Oct 15, 2026
24e9224
refactor(web): drop details identity memo
maria-rcks Oct 15, 2026
0079480
refactor(web): drop git control subscription gating
maria-rcks Oct 15, 2026
7ecb8f3
refactor(web): simplify measured asset precompression
maria-rcks Oct 15, 2026
29ef4aa
refactor(snapshot): drop checkpoint file-list compaction
maria-rcks Oct 15, 2026
89720fa
refactor(git): drop batched checkpoint and repository probes
maria-rcks Oct 15, 2026
d256f55
test(markdown): trim raw HTML equivalence fixtures
maria-rcks Oct 15, 2026
8d725de
test(db): trim sweep migration fixtures
maria-rcks Oct 15, 2026
e0eac01
test(startup): trim first-text and backend readiness fixtures
maria-rcks Oct 15, 2026
ebb5751
test(orchestration): reuse baseline startup coverage
maria-rcks Oct 15, 2026
e74e1ed
refactor(http): drop precompressed asset selection
maria-rcks Oct 15, 2026
77cca0f
refactor(startup): simplify background work gate
maria-rcks Oct 15, 2026
5dc0561
refactor(tracing): drop query normalization and span suppression
maria-rcks Oct 15, 2026
28620d9
test(sync): trim duplicate compact event fixtures
maria-rcks Oct 15, 2026
50b4776
test(perf): correct and simplify trimmed regression cases
maria-rcks Oct 15, 2026
c1f27a0
refactor(sync): reuse thread mutation for compact PR events
maria-rcks Oct 15, 2026
c789798
test(checkpoint): reuse workspace refresh retry fixture
maria-rcks Oct 15, 2026
aa10df7
test(startup): trim parking setup and retain unavailable backend checks
maria-rcks Oct 15, 2026
a15ba43
refactor(auth): drop prepared connection equality cache
maria-rcks Oct 15, 2026
b7e7969
refactor(telemetry): drop startup SQL count shortcut
maria-rcks Oct 15, 2026
249ac7b
refactor(startup): drop maintenance staggering
maria-rcks Oct 15, 2026
19fd619
refactor(contracts): simplify private compact event schemas
maria-rcks Oct 15, 2026
6c6925b
test(orchestration): fold baseline ordering into failure coverage
maria-rcks Oct 15, 2026
e72e517
test(startup): correct reduced gate and retry assertions
maria-rcks Oct 15, 2026
04b6ed1
refactor(server): drop unmeasured parallel editor discovery
maria-rcks Oct 15, 2026
b88df2f
refactor(orchestration): drop unmeasured background workspace refresh
maria-rcks Oct 15, 2026
29c8898
test(startup): trim duplicate rendering and sync assertions
maria-rcks Oct 15, 2026
ff2ed61
test(startup): trim redundant effect scopes
maria-rcks Oct 15, 2026
e66d785
test(markdown): compare rendered nodes without parser metadata
maria-rcks Oct 15, 2026
04f35fc
perf(http): retain measured precompressed asset serving
maria-rcks Oct 15, 2026
b18cfce
perf(server): restore concurrent editor discovery probes
maria-rcks Oct 15, 2026
f7e81b6
chore(perf): merge current main into startup stack
maria-rcks Oct 15, 2026
9610d4f
chore(perf): merge current main into startup stack
maria-rcks Oct 16, 2026
e63af0a
fix(web): drop removed mobile beta notice import
maria-rcks Oct 16, 2026
c75f25c
fix(server): compact single-link sync and visit events
maria-rcks Oct 16, 2026
488b121
chore(perf): merge current main into startup stack
maria-rcks Oct 17, 2026
cf876e9
chore(perf): merge current main into startup stack
maria-rcks Oct 19, 2026
09d098c
fix(server): move perf migrations after main's 063 and 064
maria-rcks Oct 19, 2026
fa395d0
test(server): isolate published perf migration upgrade cases
maria-rcks Oct 19, 2026
2cb9f14
test(web): await lazy markdown in browser rendering checks
maria-rcks Oct 19, 2026
72cee3c
chore(perf): merge latest main telemetry into startup stack
maria-rcks Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,20 @@ const bootstrap = Effect.gen(function* () {
}
yield* primaryBackend.start;
yield* logBootstrapInfo("bootstrap backend start requested");
// The renderer loads bundled assets and can connect while the backend
// boots. Starting first makes its endpoint and bootstrap credential
// available before the preload reads them. WSL-only retains its splash
// until the primary has a usable endpoint. A failed open is not fatal:
// backend readiness tries again.
if (!(settings.wslOnly === true && settings.wslBackendEnabled === true)) {
yield* desktopWindow.ensureMain.pipe(
Effect.catch((error) =>
logBootstrapWarning("failed to open main window while the backend starts", {
error: error.message,
}),
),
);
}
yield* appActivation.start.pipe(
Effect.tap(() => logBootstrapInfo("desktop app control socket ready")),
Effect.catch((error) => logStartupError("desktop app control socket unavailable", { error })),
Expand Down
32 changes: 32 additions & 0 deletions apps/desktop/src/backend/DesktopBackendManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
type DesktopBackendBootstrap as DesktopBackendBootstrapValue,
DesktopTelemetryControlMessage,
} from "@t3tools/contracts";
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, describe, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Duration from "effect/Duration";
Expand Down Expand Up @@ -568,6 +570,36 @@ describe("DesktopBackendManager", () => {
),
);

// POSIX socketpairs reset when the reader exits with unread data; Windows uses named pipes.
it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")(
"survives a backend that exits with unread bootstrap input",
() =>
Effect.scoped(
Effect.gen(function* () {
const uncaught: Array<unknown> = [];
const onUncaught = (error: unknown) => {
uncaught.push(error);
};
yield* Effect.acquireRelease(
Effect.sync(() => process.on("uncaughtException", onUncaught)),
() => Effect.sync(() => process.off("uncaughtException", onUncaught)),
);

// Reads one byte of the bootstrap line and exits, so its pipe resets.
const exit = yield* DesktopBackendManager.runBackendProcess({
...baseConfig,
executablePath: process.execPath,
args: ["-e", 'require("node:fs").readSync(3, Buffer.alloc(1)); process.exit(0)'],
cwd: process.cwd(),
desktopTelemetryStream: Stream.empty,
}).pipe(Effect.provide(Layer.merge(NodeServices.layer, layerHealthyHttpClient)));

assert.equal(exit.code.pipe(Option.getOrUndefined), 0);
assert.deepEqual(uncaught, []);
}),
),
);

it.effect("continues routing desktop telemetry control messages after an invalid line", () =>
Effect.scoped(
Effect.gen(function* () {
Expand Down
48 changes: 48 additions & 0 deletions apps/desktop/src/backend/DesktopLocalEnvironmentAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ describe("DesktopLocalEnvironmentAuth", () => {
{
id: PRIMARY_LOCAL_ENVIRONMENT_ID,
label: Effect.succeed("Windows"),
waitForReady: () => Effect.succeed(true),
currentConfig: Effect.succeedSome(config),
},
]),
Expand Down Expand Up @@ -120,6 +121,7 @@ describe("DesktopLocalEnvironmentAuth", () => {
{
id: PRIMARY_LOCAL_ENVIRONMENT_ID,
label: Effect.succeed("Windows"),
waitForReady: () => Effect.succeed(true),
currentConfig: Effect.succeedSome({
...config,
bootstrap: { ...config.bootstrap, desktopBootstrapSecret: "desktop-secret" },
Expand Down Expand Up @@ -166,6 +168,7 @@ describe("DesktopLocalEnvironmentAuth", () => {
{
id: PRIMARY_LOCAL_ENVIRONMENT_ID,
label: Effect.succeed("Windows"),
waitForReady: () => Effect.succeed(true),
currentConfig: Effect.succeedSome(config),
},
]),
Expand Down Expand Up @@ -232,4 +235,49 @@ describe("DesktopLocalEnvironmentAuth", () => {
assert.strictEqual(yield* Ref.get(requestCount), 1);
}).pipe(Effect.provide(TestClock.layer())),
);

it.effect.each([true, false])("waits for backend readiness before exchange: %s", (ready) =>
Effect.gen(function* () {
let requests = 0;
const pool = Layer.succeed(DesktopBackendPool.DesktopBackendPool, {
list: Effect.succeed([
{
id: PRIMARY_LOCAL_ENVIRONMENT_ID,
label: Effect.succeed("Local"),
waitForReady: () =>
Effect.sleep(ready ? "40 seconds" : "2 minutes").pipe(Effect.as(ready)),
currentConfig: Effect.succeedSome(config),
},
]),
} as unknown as DesktopBackendPool.DesktopBackendPool["Service"]);
const client = Layer.succeed(
HttpClient.HttpClient,
HttpClient.make((request) =>
Effect.sync(() => {
requests++;
return ready
? tokenResponse(request)
: HttpClientResponse.fromWeb(request, new Response("", { status: 503 }));
}),
),
);
const result = yield* DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth.pipe(
Effect.flatMap((auth) => auth.getBearerToken),
Effect.match({ onSuccess: (token) => token, onFailure: (error) => error._tag }),
Effect.forkChild,
Effect.provide(
DesktopLocalEnvironmentAuth.layer.pipe(Layer.provide(Layer.merge(pool, client))),
),
);
assert.equal(requests, 0);
yield* TestClock.adjust("3 minutes");
const token = yield* Fiber.join(result);
assert.equal(
token,
ready ? "desktop-bearer-token" : "DesktopLocalEnvironmentAuthSessionBootstrapError",
);
if (ready) assert.equal(requests, 1);
else assert.isAbove(requests, 0);
}).pipe(Effect.provide(TestClock.layer())),
);
});
8 changes: 8 additions & 0 deletions apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import * as DesktopBackendPool from "./DesktopBackendPool.ts";
const BOOTSTRAP_TRANSIENT_RETRY_TIMEOUT = Duration.seconds(15);
const BOOTSTRAP_TRANSIENT_RETRY_INTERVAL = Duration.millis(500);
const TRANSIENT_BOOTSTRAP_STATUS_CODES = new Set([502, 503, 504]);
// The window opens while the backend starts, and a cold start can outlast the
// retry window above. The exchange waits for readiness first, up to this
// bound, so a backend that never becomes ready still fails it.
const BACKEND_START_TIMEOUT = Duration.minutes(2);

const isTransientBearerBootstrapError = (error: RemoteEnvironmentRequestError): boolean => {
switch (error._tag) {
Expand Down Expand Up @@ -80,6 +84,10 @@ export const make = Effect.gen(function* () {

const instances = yield* pool.list;
const primary = instances.find((instance) => instance.id === PRIMARY_LOCAL_ENVIRONMENT_ID);
// Returns at once for a ready backend or one that is not running.
if (primary !== undefined) {
yield* primary.waitForReady(BACKEND_START_TIMEOUT);
}
const configOption = primary === undefined ? Option.none() : yield* primary.currentConfig;
if (Option.isNone(configOption)) {
return yield* new DesktopLocalEnvironmentAuthBackendNotConfiguredError();
Expand Down
57 changes: 55 additions & 2 deletions apps/desktop/src/window/DesktopWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ describe("DesktopWindow", () => {
);
});

it.effect("does not open a development window until the backend is ready", () =>
it.effect("creates one window when bootstrap and backend readiness overlap", () =>
Effect.gen(function* () {
const fakeWindow = makeFakeBrowserWindow();
const createCount = yield* Ref.make(0);
Expand All @@ -649,7 +649,13 @@ describe("DesktopWindow", () => {
yield* desktopWindow.activate;
assert.equal(yield* Ref.get(createCount), 0);

yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
yield* Effect.all(
[
desktopWindow.ensureMain,
desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")),
],
{ concurrency: "unbounded" },
);
assert.equal(yield* Ref.get(createCount), 1);
assert.equal(createdWindowOptions[0]?.width, 1100);
assert.equal(createdWindowOptions[0]?.height, 780);
Expand All @@ -664,6 +670,53 @@ describe("DesktopWindow", () => {
}),
);

it.effect("keeps a window closed during a cold start closed until activation", () =>
Effect.gen(function* () {
const fakeWindow = makeFakeBrowserWindow();
const createCount = yield* Ref.make(0);
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const layer = layerTest({ window: fakeWindow.window, createCount, mainWindow });
const readyUrl = new URL("http://127.0.0.1:3773");

yield* Effect.gen(function* () {
const desktopWindow = yield* DesktopWindow.DesktopWindow;
const closeMain = Effect.sync(() => fakeWindow.windowListeners.get("closed")!());

yield* desktopWindow.ensureMain;
yield* closeMain;
yield* desktopWindow.handleBackendReady(readyUrl);
assert.equal(yield* Ref.get(createCount), 1);
yield* desktopWindow.activate;
assert.equal(yield* Ref.get(createCount), 2);

// A dock click while the backend restarts reopens the window at readiness.
yield* desktopWindow.handleBackendNotReady;
yield* closeMain;
yield* desktopWindow.activate;
assert.equal(yield* Ref.get(createCount), 2);
yield* desktopWindow.handleBackendReady(readyUrl);
assert.equal(yield* Ref.get(createCount), 3);
}).pipe(Effect.provide(layer));
}),
);

it.effect("opens the main window at readiness when the early open failed", () =>
Effect.gen(function* () {
const main = makeFakeBrowserWindow();
const scenario = yield* makeSplashScenario([null, main.window]);

yield* Effect.gen(function* () {
const desktopWindow = yield* DesktopWindow.DesktopWindow;
const earlyExit = yield* Effect.exit(desktopWindow.ensureMain);
assert.equal(earlyExit._tag, "Failure");

yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
assert.equal(yield* Ref.get(scenario.createCalls), 2);
assert.equal(Option.getOrThrow(yield* Ref.get(scenario.mainWindow)), main.window);
}).pipe(Effect.provide(scenario.layer));
}),
);

it.effect(
"opens and reopens the window without backend readiness when local execution is disabled",
() =>
Expand Down
23 changes: 18 additions & 5 deletions apps/desktop/src/window/DesktopWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import * as Semaphore from "effect/Semaphore";

import * as Electron from "electron";

Expand Down Expand Up @@ -299,6 +300,10 @@ export const make = Effect.gen(function* () {
const electronApp = yield* ElectronApp.ElectronApp;
const rendererHistory = yield* DesktopRendererHistory.DesktopRendererHistory;
const backendReadyRef = yield* Ref.make(false);
const mainWindowCreation = yield* Semaphore.make(1);
// Set when the main window closes while the backend starts, so readiness
// leaves it closed. macOS keeps the app running; activation reopens it.
const mainClosedBeforeReadyRef = yield* Ref.make(false);
const splashWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const context = yield* Effect.context<DesktopWindowRuntimeServices>();
const runFork = Effect.runForkWith(context);
Expand Down Expand Up @@ -832,7 +837,14 @@ export const make = Effect.gen(function* () {
window.on("closed", () => {
clearDevelopmentLoadRetry();
clearBoundsPersist();
void runPromise(electronWindow.clearMain(Option.some(window)));
void runPromise(
electronWindow.clearMain(Option.some(window)).pipe(
Effect.andThen(waitingForBackend),
Effect.flatMap((waiting) =>
waiting ? Ref.set(mainClosedBeforeReadyRef, true) : Effect.void,
),
),
);
});

return window;
Expand All @@ -851,7 +863,7 @@ export const make = Effect.gen(function* () {
return existingWindow.value;
}
return yield* createMain;
}).pipe(Effect.withSpan("desktop.window.ensureMain"));
}).pipe(mainWindowCreation.withPermit, Effect.withSpan("desktop.window.ensureMain"));

const revealOrCreateMain = Effect.gen(function* () {
const window = yield* ensureMain;
Expand All @@ -866,9 +878,7 @@ export const make = Effect.gen(function* () {

const createMainIfBackendReady = Effect.gen(function* () {
if (yield* waitingForBackend) return;
const existingWindow = yield* currentMainWindow;
if (Option.isSome(existingWindow)) return;
yield* createMain;
yield* ensureMain;
}).pipe(Effect.withSpan("desktop.window.createMainIfBackendReady"));

const showConnectingSplash = Effect.gen(function* () {
Expand Down Expand Up @@ -956,6 +966,8 @@ export const make = Effect.gen(function* () {
yield* electronWindow.reveal(existingWindow.value);
return;
}
// Asking for the window also undoes an earlier close, so readiness opens it.
yield* Ref.set(mainClosedBeforeReadyRef, false);
const backendReady = yield* Ref.get(backendReadyRef);
if (!backendReady) {
const splash = yield* Ref.get(splashWindowRef);
Expand All @@ -971,6 +983,7 @@ export const make = Effect.gen(function* () {
handleBackendReady: Effect.fn("desktop.window.handleBackendReady")(function* (httpBaseUrl) {
yield* Ref.set(backendReadyRef, true);
yield* logWindowInfo("backend ready", { source: "http", url: httpBaseUrl.href });
if (yield* Ref.getAndSet(mainClosedBeforeReadyRef, false)) return;
yield* createMainIfBackendReady;
}),
handleBackendNotReady: Ref.set(backendReadyRef, false).pipe(
Expand Down
69 changes: 69 additions & 0 deletions apps/server/src/http.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, it } from "@effect/vitest";
import { describe, vi } from "vite-plus/test";
import * as NodeZlib from "node:zlib";
import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform";
import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
import * as NodeServices from "@effect/platform-node/NodeServices";
Expand Down Expand Up @@ -214,6 +215,74 @@ it.layer(
Layer.provideMerge(NodeServices.layer),
),
)("static HTTP responses", (it) => {
it.effect("serves precompressed manifest assets and revalidates other files", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const staticDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-static-compressed-" });
yield* fs.makeDirectory(path.join(staticDir, "assets"));
const body = 'export const build = "precompressed";\n'.repeat(100);
const asset = "assets/app-1234abcd.js";
const br = NodeZlib.brotliCompressSync(body);
const gz = NodeZlib.gzipSync(body);
yield* fs.writeFileString(path.join(staticDir, asset), body);
yield* fs.writeFile(path.join(staticDir, `${asset}.br`), br);
yield* fs.writeFile(path.join(staticDir, `${asset}.gz`), gz);
yield* fs.writeFileString(
path.join(staticDir, "vite-manifest.json"),
JSON.stringify({ "src/app.ts": { file: asset, isEntry: true } }),
);
// A hash-like filename and a compressed copy alone cannot make a custom file immutable.
const custom = "assets/custom-1234abcd.js";
yield* fs.writeFileString(path.join(staticDir, custom), body);
yield* fs.writeFile(path.join(staticDir, `${custom}.br`), br);
const request = yield* makeStaticRequest(staticDir);

for (const [acceptEncoding, encoding, length] of [
["br, gzip", "br", br.byteLength],
["br;q=0, gzip", "gzip", gz.byteLength],
["identity", undefined, Buffer.byteLength(body)],
] as const) {
const response = yield* request(`/${asset}`, {
headers: { "accept-encoding": acceptEncoding },
});
expect(response.status).toBe(200);
expect(response.headers["content-encoding"]).toBe(encoding);
expect(response.headers["content-length"]).toBe(String(length));
expect(response.headers["content-type"]).toContain("javascript");
expect(response.headers["cache-control"]).toBe("public, max-age=31536000, immutable");
expect(response.headers["vary"]).toBe("Accept-Encoding");
expect(yield* response.text).toBe(body);
const head = yield* request(`/${asset}`, {
method: "HEAD",
headers: { "accept-encoding": acceptEncoding },
});
expect(head.headers["content-encoding"]).toBe(encoding);
expect(head.headers["content-length"]).toBe(String(length));
expect(yield* head.text).toBe("");
const unchanged = yield* request(`/${asset}`, {
headers: {
"accept-encoding": acceptEncoding,
"if-none-match": response.headers["etag"]!,
},
});
expect(unchanged.status).toBe(304);
expect(unchanged.headers["vary"]).toBe("Accept-Encoding");
expect(yield* unchanged.text).toBe("");
}
const unlisted = yield* request(`/${custom}`, { headers: { "accept-encoding": "br" } });
expect(unlisted.headers["cache-control"]).toBe("no-cache");
expect(unlisted.headers["content-length"]).toBeUndefined();
expect(yield* unlisted.text).toBe(body);

yield* fs.remove(path.join(staticDir, `${asset}.br`));
const fallback = yield* request(`/${asset}`, { headers: { "accept-encoding": "br, gzip" } });
expect(fallback.headers["content-encoding"]).toBe("gzip");
expect(fallback.headers["content-length"]).toBe(String(gz.byteLength));
expect(yield* fallback.text).toBe(body);
}),
);

it.effect("revalidates non-HTML files and returns changed contents", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand Down
Loading
Loading