Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const serverExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExp
setMode: () => Effect.die("unexpected setMode"),
setTailscaleServeEnabled: () => Effect.die("unexpected setTailscaleServeEnabled"),
getAdvertisedEndpoints: Effect.succeed([]),
} satisfies DesktopServerExposure.DesktopServerExposureShape);
} satisfies DesktopServerExposure.DesktopServerExposure["Service"]);

function makeEnvironmentLayer(
baseDir: string,
Expand Down
87 changes: 42 additions & 45 deletions apps/desktop/src/backend/DesktopBackendConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,14 @@ import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
import * as DesktopObservability from "../app/DesktopObservability.ts";
import * as DesktopServerExposure from "./DesktopServerExposure.ts";

export interface DesktopBackendConfigurationShape {
readonly resolve: Effect.Effect<
DesktopBackendManager.DesktopBackendStartConfig,
PlatformError.PlatformError
>;
}

export class DesktopBackendConfiguration extends Context.Service<
DesktopBackendConfiguration,
DesktopBackendConfigurationShape
{
readonly resolve: Effect.Effect<
DesktopBackendManager.DesktopBackendStartConfig,
PlatformError.PlatformError
>;
}
>()("@t3tools/desktop/backend/DesktopBackendConfiguration") {}

interface BackendObservabilitySettings {
Expand Down Expand Up @@ -130,40 +128,39 @@ const resolveBackendStartConfig = Effect.fn("desktop.backendConfiguration.resolv
},
);

export const layer = Layer.effect(
DesktopBackendConfiguration,
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const crypto = yield* Crypto.Crypto;
const tokenRef = yield* Ref.make(Option.none<string>());
const getOrCreateBootstrapToken = Effect.gen(function* () {
const existing = yield* Ref.get(tokenRef);
if (Option.isSome(existing)) {
return existing.value;
}

const token = Encoding.encodeHex(yield* crypto.randomBytes(24));
yield* Ref.set(tokenRef, Option.some(token));
return token;
});

return DesktopBackendConfiguration.of({
resolve: Effect.gen(function* () {
const bootstrapToken = yield* getOrCreateBootstrapToken;
const observabilitySettings = yield* readPersistedBackendObservabilitySettings.pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment),
);
return yield* resolveBackendStartConfig({
bootstrapToken,
observabilitySettings,
}).pipe(
Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment),
Effect.provideService(DesktopServerExposure.DesktopServerExposure, serverExposure),
);
}).pipe(Effect.withSpan("desktop.backendConfiguration.resolve")),
});
}),
);
export const make = Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const crypto = yield* Crypto.Crypto;
const tokenRef = yield* Ref.make(Option.none<string>());
const getOrCreateBootstrapToken = Effect.gen(function* () {
const existing = yield* Ref.get(tokenRef);
if (Option.isSome(existing)) {
return existing.value;
}

const token = Encoding.encodeHex(yield* crypto.randomBytes(24));
yield* Ref.set(tokenRef, Option.some(token));
return token;
});

return DesktopBackendConfiguration.of({
resolve: Effect.gen(function* () {
const bootstrapToken = yield* getOrCreateBootstrapToken;
const observabilitySettings = yield* readPersistedBackendObservabilitySettings.pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment),
);
return yield* resolveBackendStartConfig({
bootstrapToken,
observabilitySettings,
}).pipe(
Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment),
Effect.provideService(DesktopServerExposure.DesktopServerExposure, serverExposure),
);
}).pipe(Effect.withSpan("desktop.backendConfiguration.resolve")),
});
});

export const layer = Layer.effect(DesktopBackendConfiguration, make);
83 changes: 51 additions & 32 deletions apps/desktop/src/backend/DesktopBackendManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as Cause from "effect/Cause";
import * as Context from "effect/Context";
import * as Data from "effect/Data";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
Expand All @@ -16,8 +15,9 @@ import * as Schema from "effect/Schema";
import * as Semaphore from "effect/Semaphore";
import * as Scope from "effect/Scope";
import * as Stream from "effect/Stream";
import { HttpClient } from "effect/unstable/http";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
import * as HttpClient from "effect/unstable/http/HttpClient";
import * as ChildProcess from "effect/unstable/process/ChildProcess";
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";

import {
DesktopBackendBootstrap,
Expand Down Expand Up @@ -59,29 +59,38 @@ interface BackendProcessExit {
readonly result: Result.Result<ChildProcessSpawner.ExitCode, PlatformError.PlatformError>;
}

export class BackendTimeoutError extends Data.TaggedError("BackendTimeoutError")<{
readonly url: URL;
}> {
override get message() {
export class BackendTimeoutError extends Schema.TaggedErrorClass<BackendTimeoutError>()(
"BackendTimeoutError",
{
url: Schema.URL,
},
) {
override get message(): string {
return `Timed out waiting for backend readiness at ${this.url.href}.`;
}
}

class BackendProcessBootstrapEncodeError extends Data.TaggedError(
class BackendProcessBootstrapEncodeError extends Schema.TaggedErrorClass<BackendProcessBootstrapEncodeError>()(
"BackendProcessBootstrapEncodeError",
)<{
readonly cause: Schema.SchemaError;
}> {
override get message() {
return `Failed to encode desktop backend bootstrap payload: ${this.cause.message}`;
{
detail: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to encode desktop backend bootstrap payload: ${this.detail}`;
}
}

class BackendProcessSpawnError extends Data.TaggedError("BackendProcessSpawnError")<{
readonly cause: PlatformError.PlatformError;
}> {
override get message() {
return `Failed to spawn desktop backend process: ${this.cause.message}`;
class BackendProcessSpawnError extends Schema.TaggedErrorClass<BackendProcessSpawnError>()(
"BackendProcessSpawnError",
{
detail: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to spawn desktop backend process: ${this.detail}`;
}
}

Expand All @@ -106,16 +115,14 @@ export interface DesktopBackendSnapshot {
readonly restartScheduled: boolean;
}

export interface DesktopBackendManagerShape {
readonly start: Effect.Effect<void>;
readonly stop: (options?: { readonly timeout?: Duration.Duration }) => Effect.Effect<void>;
readonly currentConfig: Effect.Effect<Option.Option<DesktopBackendStartConfig>>;
readonly snapshot: Effect.Effect<DesktopBackendSnapshot>;
}

export class DesktopBackendManager extends Context.Service<
DesktopBackendManager,
DesktopBackendManagerShape
{
readonly start: Effect.Effect<void>;
readonly stop: (options?: { readonly timeout?: Duration.Duration }) => Effect.Effect<void>;
readonly currentConfig: Effect.Effect<Option.Option<DesktopBackendStartConfig>>;
readonly snapshot: Effect.Effect<DesktopBackendSnapshot>;
}
>()("@t3tools/desktop/backend/DesktopBackendManager") {}

const { logWarning: logBackendManagerWarning, logError: logBackendManagerError } =
Expand Down Expand Up @@ -230,7 +237,13 @@ const runBackendProcess = Effect.fn("runBackendProcess")(function* (
): Effect.fn.Return<BackendProcessExit, BackendProcessError, BackendProcessRunRequirements> {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const bootstrapJson = yield* encodeBootstrapJson(options.bootstrap).pipe(
Effect.mapError((cause) => new BackendProcessBootstrapEncodeError({ cause })),
Effect.mapError(
(cause) =>
new BackendProcessBootstrapEncodeError({
detail: cause.message,
cause,
}),
),
);
const onOutput = options.onOutput ?? (() => Effect.void);
const command = ChildProcess.make(
Expand All @@ -256,9 +269,15 @@ const runBackendProcess = Effect.fn("runBackendProcess")(function* (
},
);

const handle = yield* spawner
.spawn(command)
.pipe(Effect.mapError((cause) => new BackendProcessSpawnError({ cause })));
const handle = yield* spawner.spawn(command).pipe(
Effect.mapError(
(cause) =>
new BackendProcessSpawnError({
detail: cause.message,
cause,
}),
),
);

yield* options.onStarted?.(handle.pid) ?? Effect.void;
if (options.captureOutput) {
Expand All @@ -277,7 +296,7 @@ const runBackendProcess = Effect.fn("runBackendProcess")(function* (
return describeProcessExit(yield* Effect.result(handle.exitCode));
});

const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(function* () {
export const make = Effect.gen(function* () {
const parentScope = yield* Scope.Scope;
const fileSystem = yield* FileSystem.FileSystem;
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
Expand Down Expand Up @@ -603,4 +622,4 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio
});
});

export const layer = Layer.effect(DesktopBackendManager, makeDesktopBackendManager());
export const layer = Layer.effect(DesktopBackendManager, make);
3 changes: 2 additions & 1 deletion apps/desktop/src/backend/DesktopLocalEnvironmentAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import { HttpClient, HttpClientResponse } from "effect/unstable/http";
import * as HttpClient from "effect/unstable/http/HttpClient";
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";

import * as DesktopBackendManager from "./DesktopBackendManager.ts";
import * as DesktopLocalEnvironmentAuth from "./DesktopLocalEnvironmentAuth.ts";
Expand Down
Loading
Loading