Skip to content

Commit 50bbaff

Browse files
committed
fix(server): align request retirement tests with current MCP scope
2 parents 239f8d9 + 3e6b450 commit 50bbaff

266 files changed

Lines changed: 14612 additions & 3341 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/desktop/src/app/DesktopApp.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts";
1919
import * as DesktopWindow from "../window/DesktopWindow.ts";
2020
import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";
2121
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
22+
import * as DesktopLegacyLocalStorage from "./DesktopLegacyLocalStorage.ts";
2223
import * as DesktopLifecycle from "./DesktopLifecycle.ts";
2324
import * as DesktopLinuxUrlHandler from "./DesktopLinuxUrlHandler.ts";
2425
import * as DesktopObservability from "./DesktopObservability.ts";
@@ -32,6 +33,7 @@ import * as DesktopRemoteUpdates from "../updates/DesktopRemoteUpdates.ts";
3233
import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
3334
import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.ts";
3435
import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";
36+
import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts";
3537

3638
const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
3739
const MAX_TCP_PORT = 65_535;
@@ -179,6 +181,10 @@ const bootstrap = Effect.gen(function* () {
179181
});
180182
yield* installDesktopIpcHandlers();
181183
yield* logBootstrapInfo("bootstrap ipc handlers registered");
184+
// Before any window: the preload merges these items before the app reads storage.
185+
yield* (yield* DesktopLegacyLocalStorage.DesktopLegacyLocalStorage).load(
186+
yield* (yield* DesktopAppIdentity.DesktopAppIdentity).resolveUserDataPath,
187+
);
182188

183189
yield* snapShot.initialize;
184190

@@ -337,14 +343,18 @@ const scopedProgram = Effect.scoped(
337343
yield* Effect.annotateCurrentSpan({ scope: "desktop", runId });
338344

339345
const shutdown = yield* DesktopShutdown.DesktopShutdown;
346+
const rendererHistory = yield* DesktopRendererHistory.DesktopRendererHistory;
340347

341348
yield* Effect.addFinalizer(() =>
342349
// Stop every backend in the pool, not just the primary. The
343350
// electronApp.quit() path can race ahead of the layer-scope
344351
// cascade, so leaving the WSL instance for its parent scope
345352
// finalizer means it gets hard-killed by the OS instead of
346353
// receiving SIGTERM + grace.
347-
stopAllPoolInstances().pipe(Effect.ensuring(shutdown.markComplete)),
354+
stopAllPoolInstances().pipe(
355+
Effect.ensuring(rendererHistory.shutdown),
356+
Effect.ensuring(shutdown.markComplete),
357+
),
348358
);
349359

350360
yield* startup;
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import * as Context from "effect/Context";
2+
import * as Effect from "effect/Effect";
3+
import * as FileSystem from "effect/FileSystem";
4+
import * as Layer from "effect/Layer";
5+
import * as Option from "effect/Option";
6+
import * as Path from "effect/Path";
7+
import * as Ref from "effect/Ref";
8+
9+
import * as ElectronProtocol from "../electron/ElectronProtocol.ts";
10+
import { readChromiumLocalStorage } from "./chromiumLocalStorage.ts";
11+
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
12+
13+
/**
14+
* Carries the renderer's localStorage (prompt stash, unsent drafts, layout,
15+
* theme) over from the V1 desktop profile, which V2 replaced with its own
16+
* `t3code-v2` profile. The V1 profile is only read, never opened by Chromium,
17+
* so this works while V1 is still running.
18+
*
19+
* `load` runs before the window opens; the preload takes the items once,
20+
* merges them into the new profile, and calls `complete`, which writes a
21+
* marker so later launches skip the read. A failed read or merge, or a window
22+
* that never completes, leaves no marker and retries next launch.
23+
*/
24+
export class DesktopLegacyLocalStorage extends Context.Service<
25+
DesktopLegacyLocalStorage,
26+
{
27+
readonly load: (userDataPath: string) => Effect.Effect<void>;
28+
readonly take: Effect.Effect<Option.Option<Readonly<Record<string, string>>>>;
29+
readonly complete: Effect.Effect<void>;
30+
}
31+
>()("@t3tools/desktop/app/DesktopLegacyLocalStorage") {}
32+
33+
const MARKER_FILE_NAME = "v1-local-storage-imported";
34+
// V1 used "T3 Code (Alpha)" when that folder existed and "t3code" otherwise.
35+
const V1_PROFILE_NAMES = ["T3 Code (Alpha)", "t3code"];
36+
37+
const make = Effect.gen(function* () {
38+
const environment = yield* DesktopEnvironment.DesktopEnvironment;
39+
const fs = yield* FileSystem.FileSystem;
40+
const path = yield* Path.Path;
41+
const items = yield* Ref.make(Option.none<Readonly<Record<string, string>>>());
42+
const markerPath = yield* Ref.make(Option.none<string>());
43+
44+
/** Newest file mtime in a directory; appending to a log leaves the directory's own mtime alone. */
45+
const newestFileMtime = (directory: string) =>
46+
Effect.gen(function* () {
47+
let newest = 0;
48+
for (const name of yield* fs.readDirectory(directory)) {
49+
const info = yield* fs.stat(path.join(directory, name)).pipe(Effect.option);
50+
if (Option.isNone(info)) continue;
51+
const mtime = Option.match(info.value.mtime, {
52+
onNone: () => 0,
53+
onSome: (date) => date.getTime(),
54+
});
55+
newest = Math.max(newest, mtime);
56+
}
57+
return newest;
58+
}).pipe(Effect.option);
59+
60+
/** The V1 profile whose Local Storage was written most recently, if any. */
61+
const findV1LocalStorage = Effect.gen(function* () {
62+
let newest: { readonly directory: string; readonly mtime: number } | null = null;
63+
for (const name of V1_PROFILE_NAMES) {
64+
const directory = path.join(environment.appDataDirectory, name, "Local Storage", "leveldb");
65+
const mtime = yield* newestFileMtime(directory);
66+
if (Option.isNone(mtime)) continue;
67+
if (newest === null || mtime.value > newest.mtime) newest = { directory, mtime: mtime.value };
68+
}
69+
return newest?.directory ?? null;
70+
});
71+
72+
const writeMarker = Effect.gen(function* () {
73+
const marker = yield* Ref.get(markerPath);
74+
if (Option.isNone(marker)) return;
75+
yield* fs
76+
.writeFileString(marker.value, "")
77+
.pipe(
78+
Effect.catch((error) =>
79+
Effect.logWarning("Could not record the V1 Local Storage import", error),
80+
),
81+
);
82+
});
83+
84+
const load = Effect.fn("desktop.legacyLocalStorage.load")(function* (userDataPath: string) {
85+
// Development already shares its profile between versions.
86+
if (environment.isDevelopment) return;
87+
const marker = path.join(userDataPath, MARKER_FILE_NAME);
88+
if (yield* fs.exists(marker).pipe(Effect.orElseSucceed(() => true))) return;
89+
yield* Ref.set(markerPath, Option.some(marker));
90+
const directory = yield* findV1LocalStorage;
91+
if (directory === null) return yield* writeMarker;
92+
const origin = ElectronProtocol.getDesktopUrl(false).replace(/\/$/, "");
93+
const read = yield* readChromiumLocalStorage(directory, origin).pipe(
94+
Effect.provideService(FileSystem.FileSystem, fs),
95+
Effect.provideService(Path.Path, path),
96+
Effect.tapError((error) =>
97+
Effect.logWarning("Could not read V1 Local Storage; will retry next launch", error),
98+
),
99+
Effect.option,
100+
);
101+
if (Option.isNone(read)) return;
102+
yield* Effect.logInfo("V1 Local Storage ready to import", {
103+
directory,
104+
keys: read.value.size,
105+
});
106+
yield* Ref.set(items, Option.some(Object.fromEntries(read.value)));
107+
});
108+
109+
return DesktopLegacyLocalStorage.of({
110+
load,
111+
// Taking clears the items, so a reload before `complete` cannot merge twice.
112+
take: Ref.getAndSet(items, Option.none()),
113+
complete: writeMarker,
114+
});
115+
});
116+
117+
export const layer = Layer.effect(DesktopLegacyLocalStorage, make);

‎apps/desktop/src/app/DesktopObservability.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ const refreshFileSize = (
227227
Effect.orElseSucceed(() => 0),
228228
);
229229

230-
const makeRotatingLogFileWriter = Effect.fn("makeRotatingLogFileWriter")(function* (input: {
230+
export const makeRotatingLogFileWriter = Effect.fn("makeRotatingLogFileWriter")(function* (input: {
231231
readonly filePath: string;
232232
readonly maxBytes?: number;
233233
readonly maxFiles?: number;

0 commit comments

Comments
 (0)