| author | |
| committer | |
| log | 60b16d1f28104c95a3ab8e673cb439124a4a8668 |
| tree | 981268d6b7fa59a6ae0c9c445034fd1174b9d919 |
| parent | ab0baaa88614905722a6638aa56ec858caf9c563 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>17 files changed, 521 insertions(+), 520 deletions(-)
apps/desktop/src/app/DesktopAppIdentity.test.ts+2-2| ... | ... | @@ -63,7 +63,7 @@ const makeElectronAppLayer = (calls: ElectronAppCalls) => |
| 63 | 63 | }), |
| 64 | 64 | appendCommandLineSwitch: () => Effect.void, |
| 65 | 65 | on: () => Effect.void, |
| 66 | } satisfies ElectronApp.ElectronAppShape); | |
| 66 | } satisfies ElectronApp.ElectronApp["Service"]); | |
| 67 | 67 | |
| 68 | 68 | const makeAssetsLayer = (png: Option.Option<string>) => |
| 69 | 69 | Layer.succeed(DesktopAssets.DesktopAssets, { |
| ... | ... | @@ -73,7 +73,7 @@ const makeAssetsLayer = (png: Option.Option<string>) => |
| 73 | 73 | png, |
| 74 | 74 | }), |
| 75 | 75 | resolveResourcePath: () => Effect.succeed(Option.none()), |
| 76 | } satisfies DesktopAssets.DesktopAssetsShape); | |
| 76 | } satisfies DesktopAssets.DesktopAssets["Service"]); | |
| 77 | 77 | |
| 78 | 78 | const makeEnvironmentLayer = (overrides: TestEnvironmentInput = {}) => { |
| 79 | 79 | const { env, ...environmentOverrides } = overrides; |
apps/desktop/src/app/DesktopAppIdentity.ts+4-6| ... | ... | @@ -18,14 +18,12 @@ const AppPackageMetadata = Schema.Struct({ |
| 18 | 18 | }); |
| 19 | 19 | const decodeAppPackageMetadata = Schema.decodeEffect(Schema.fromJsonString(AppPackageMetadata)); |
| 20 | 20 | |
| 21 | export interface DesktopAppIdentityShape { | |
| 22 | readonly resolveUserDataPath: Effect.Effect<string>; | |
| 23 | readonly configure: Effect.Effect<void>; | |
| 24 | } | |
| 25 | ||
| 26 | 21 | export class DesktopAppIdentity extends Context.Service< |
| 27 | 22 | DesktopAppIdentity, |
| 28 | DesktopAppIdentityShape | |
| 23 | { | |
| 24 | readonly resolveUserDataPath: Effect.Effect<string>; | |
| 25 | readonly configure: Effect.Effect<void>; | |
| 26 | } | |
| 29 | 27 | >()("@t3tools/desktop/app/DesktopAppIdentity") {} |
| 30 | 28 | |
| 31 | 29 | const normalizeCommitHash = (value: string): Option.Option<string> => { |
apps/desktop/src/app/DesktopAssets.ts+7-8| ... | ... | @@ -12,14 +12,13 @@ export interface DesktopIconPaths { |
| 12 | 12 | readonly png: Option.Option<string>; |
| 13 | 13 | } |
| 14 | 14 | |
| 15 | export interface DesktopAssetsShape { | |
| 16 | readonly iconPaths: Effect.Effect<DesktopIconPaths>; | |
| 17 | readonly resolveResourcePath: (fileName: string) => Effect.Effect<Option.Option<string>>; | |
| 18 | } | |
| 19 | ||
| 20 | export class DesktopAssets extends Context.Service<DesktopAssets, DesktopAssetsShape>()( | |
| 21 | "@t3tools/desktop/app/DesktopAssets", | |
| 22 | ) {} | |
| 15 | export class DesktopAssets extends Context.Service< | |
| 16 | DesktopAssets, | |
| 17 | { | |
| 18 | readonly iconPaths: Effect.Effect<DesktopIconPaths>; | |
| 19 | readonly resolveResourcePath: (fileName: string) => Effect.Effect<Option.Option<string>>; | |
| 20 | } | |
| 21 | >()("@t3tools/desktop/app/DesktopAssets") {} | |
| 23 | 22 | |
| 24 | 23 | const resolveResourcePath = Effect.fn("desktop.assets.resolveResourcePath")(function* ( |
| 25 | 24 | fileName: string, |
apps/desktop/src/app/DesktopBackendOutputLog.ts created+280| ... | ... | @@ -0,0 +1,280 @@ |
| 1 | import * as Context from "effect/Context"; | |
| 2 | import * as DateTime from "effect/DateTime"; | |
| 3 | import * as Effect from "effect/Effect"; | |
| 4 | import * as FileSystem from "effect/FileSystem"; | |
| 5 | import * as Layer from "effect/Layer"; | |
| 6 | import * as Option from "effect/Option"; | |
| 7 | import * as Path from "effect/Path"; | |
| 8 | import * as PlatformError from "effect/PlatformError"; | |
| 9 | import * as References from "effect/References"; | |
| 10 | import * as Ref from "effect/Ref"; | |
| 11 | import * as Schema from "effect/Schema"; | |
| 12 | import * as Semaphore from "effect/Semaphore"; | |
| 13 | ||
| 14 | import * as DesktopEnvironment from "./DesktopEnvironment.ts"; | |
| 15 | ||
| 16 | export const DESKTOP_LOG_FILE_MAX_BYTES = 10 * 1024 * 1024; | |
| 17 | export const DESKTOP_LOG_FILE_MAX_FILES = 10; | |
| 18 | ||
| 19 | const DESKTOP_BACKEND_CHILD_LOG_FIBER_ID = "#backend-child"; | |
| 20 | ||
| 21 | interface RotatingLogFileWriter { | |
| 22 | readonly writeBytes: (chunk: Uint8Array) => Effect.Effect<void>; | |
| 23 | readonly writeText: (chunk: string) => Effect.Effect<void>; | |
| 24 | } | |
| 25 | ||
| 26 | export class DesktopBackendOutputLog extends Context.Service< | |
| 27 | DesktopBackendOutputLog, | |
| 28 | { | |
| 29 | readonly writeSessionBoundary: (input: { | |
| 30 | readonly phase: "START" | "END"; | |
| 31 | readonly details: string; | |
| 32 | }) => Effect.Effect<void>; | |
| 33 | readonly writeOutputChunk: ( | |
| 34 | streamName: "stdout" | "stderr", | |
| 35 | chunk: Uint8Array, | |
| 36 | ) => Effect.Effect<void>; | |
| 37 | } | |
| 38 | >()("@t3tools/desktop/app/DesktopBackendOutputLog") {} | |
| 39 | ||
| 40 | class DesktopLogFileWriterConfigurationError extends Schema.TaggedErrorClass<DesktopLogFileWriterConfigurationError>()( | |
| 41 | "DesktopLogFileWriterConfigurationError", | |
| 42 | { | |
| 43 | option: Schema.Literals(["maxBytes", "maxFiles"]), | |
| 44 | value: Schema.Number, | |
| 45 | }, | |
| 46 | ) { | |
| 47 | override get message(): string { | |
| 48 | return `${this.option} must be >= 1 (received ${this.value})`; | |
| 49 | } | |
| 50 | } | |
| 51 | ||
| 52 | type DesktopLogFileWriterError = | |
| 53 | | DesktopLogFileWriterConfigurationError | |
| 54 | | PlatformError.PlatformError; | |
| 55 | ||
| 56 | const DesktopBackendChildLogRecord = Schema.Struct({ | |
| 57 | message: Schema.String, | |
| 58 | level: Schema.Literals(["INFO", "ERROR"]), | |
| 59 | timestamp: Schema.String, | |
| 60 | annotations: Schema.Record(Schema.String, Schema.Unknown), | |
| 61 | spans: Schema.Record(Schema.String, Schema.Unknown), | |
| 62 | fiberId: Schema.String, | |
| 63 | }); | |
| 64 | ||
| 65 | const encodeDesktopBackendChildLogRecord = Schema.encodeEffect( | |
| 66 | Schema.fromJsonString(DesktopBackendChildLogRecord), | |
| 67 | ); | |
| 68 | ||
| 69 | const DesktopBackendOutputLogNoop: DesktopBackendOutputLog["Service"] = { | |
| 70 | writeSessionBoundary: () => Effect.void, | |
| 71 | writeOutputChunk: () => Effect.void, | |
| 72 | }; | |
| 73 | ||
| 74 | const textEncoder = new TextEncoder(); | |
| 75 | const textDecoder = new TextDecoder(); | |
| 76 | ||
| 77 | const currentDesktopRunId = Effect.gen(function* () { | |
| 78 | const annotations = yield* References.CurrentLogAnnotations; | |
| 79 | const runId = annotations.runId; | |
| 80 | return typeof runId === "string" && runId.length > 0 ? runId : "unknown"; | |
| 81 | }); | |
| 82 | ||
| 83 | const sanitizeLogValue = (value: string): string => value.replace(/\s+/g, " ").trim(); | |
| 84 | ||
| 85 | const refreshFileSize = ( | |
| 86 | fileSystem: FileSystem.FileSystem, | |
| 87 | filePath: string, | |
| 88 | ): Effect.Effect<number, never> => | |
| 89 | fileSystem.stat(filePath).pipe( | |
| 90 | Effect.map((stat) => Number(stat.size)), | |
| 91 | Effect.orElseSucceed(() => 0), | |
| 92 | ); | |
| 93 | ||
| 94 | const makeRotatingLogFileWriter = Effect.fn("makeRotatingLogFileWriter")(function* (input: { | |
| 95 | readonly filePath: string; | |
| 96 | readonly maxBytes?: number; | |
| 97 | readonly maxFiles?: number; | |
| 98 | }): Effect.fn.Return< | |
| 99 | RotatingLogFileWriter, | |
| 100 | DesktopLogFileWriterError, | |
| 101 | FileSystem.FileSystem | Path.Path | |
| 102 | > { | |
| 103 | const fileSystem = yield* FileSystem.FileSystem; | |
| 104 | const path = yield* Path.Path; | |
| 105 | const maxBytes = input.maxBytes ?? DESKTOP_LOG_FILE_MAX_BYTES; | |
| 106 | const maxFiles = input.maxFiles ?? DESKTOP_LOG_FILE_MAX_FILES; | |
| 107 | const directory = path.dirname(input.filePath); | |
| 108 | const baseName = path.basename(input.filePath); | |
| 109 | ||
| 110 | if (maxBytes < 1) { | |
| 111 | return yield* new DesktopLogFileWriterConfigurationError({ | |
| 112 | option: "maxBytes", | |
| 113 | value: maxBytes, | |
| 114 | }); | |
| 115 | } | |
| 116 | if (maxFiles < 1) { | |
| 117 | return yield* new DesktopLogFileWriterConfigurationError({ | |
| 118 | option: "maxFiles", | |
| 119 | value: maxFiles, | |
| 120 | }); | |
| 121 | } | |
| 122 | ||
| 123 | yield* fileSystem.makeDirectory(directory, { recursive: true }); | |
| 124 | ||
| 125 | const withSuffix = (index: number) => `${input.filePath}.${index}`; | |
| 126 | const currentSize = yield* Ref.make(yield* refreshFileSize(fileSystem, input.filePath)); | |
| 127 | const mutex = yield* Semaphore.make(1); | |
| 128 | ||
| 129 | const pruneOverflowBackups = Effect.gen(function* () { | |
| 130 | const entries = yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => [])); | |
| 131 | for (const entry of entries) { | |
| 132 | if (!entry.startsWith(`${baseName}.`)) continue; | |
| 133 | const suffix = Number(entry.slice(baseName.length + 1)); | |
| 134 | if (!Number.isInteger(suffix) || suffix <= maxFiles) continue; | |
| 135 | yield* fileSystem.remove(path.join(directory, entry), { force: true }).pipe(Effect.ignore); | |
| 136 | } | |
| 137 | }); | |
| 138 | ||
| 139 | const rotate = Effect.gen(function* () { | |
| 140 | yield* fileSystem.remove(withSuffix(maxFiles), { force: true }).pipe(Effect.ignore); | |
| 141 | for (let index = maxFiles - 1; index >= 1; index -= 1) { | |
| 142 | const source = withSuffix(index); | |
| 143 | const sourceExists = yield* fileSystem.exists(source).pipe(Effect.orElseSucceed(() => false)); | |
| 144 | if (sourceExists) { | |
| 145 | yield* fileSystem.rename(source, withSuffix(index + 1)); | |
| 146 | } | |
| 147 | } | |
| 148 | const currentExists = yield* fileSystem | |
| 149 | .exists(input.filePath) | |
| 150 | .pipe(Effect.orElseSucceed(() => false)); | |
| 151 | if (currentExists) { | |
| 152 | yield* fileSystem.rename(input.filePath, withSuffix(1)); | |
| 153 | } | |
| 154 | yield* Ref.set(currentSize, 0); | |
| 155 | }).pipe( | |
| 156 | Effect.catch(() => | |
| 157 | refreshFileSize(fileSystem, input.filePath).pipe( | |
| 158 | Effect.flatMap((size) => Ref.set(currentSize, size)), | |
| 159 | ), | |
| 160 | ), | |
| 161 | ); | |
| 162 | ||
| 163 | const writeBytes = (chunk: Uint8Array): Effect.Effect<void> => { | |
| 164 | if (chunk.byteLength === 0) return Effect.void; | |
| 165 | ||
| 166 | return mutex.withPermits(1)( | |
| 167 | Effect.gen(function* () { | |
| 168 | const beforeSize = yield* Ref.get(currentSize); | |
| 169 | if (beforeSize > 0 && beforeSize + chunk.byteLength > maxBytes) { | |
| 170 | yield* rotate; | |
| 171 | } | |
| 172 | ||
| 173 | yield* fileSystem.writeFile(input.filePath, chunk, { flag: "a" }); | |
| 174 | const afterSize = (yield* Ref.get(currentSize)) + chunk.byteLength; | |
| 175 | yield* Ref.set(currentSize, afterSize); | |
| 176 | ||
| 177 | if (afterSize > maxBytes) { | |
| 178 | yield* rotate; | |
| 179 | } | |
| 180 | }).pipe( | |
| 181 | Effect.catch(() => | |
| 182 | refreshFileSize(fileSystem, input.filePath).pipe( | |
| 183 | Effect.flatMap((size) => Ref.set(currentSize, size)), | |
| 184 | ), | |
| 185 | ), | |
| 186 | ), | |
| 187 | ); | |
| 188 | }; | |
| 189 | ||
| 190 | yield* pruneOverflowBackups; | |
| 191 | ||
| 192 | return { | |
| 193 | writeBytes, | |
| 194 | writeText: (chunk) => writeBytes(textEncoder.encode(chunk)), | |
| 195 | } satisfies RotatingLogFileWriter; | |
| 196 | }); | |
| 197 | ||
| 198 | const writeDevelopmentConsoleOutput = ( | |
| 199 | streamName: "stdout" | "stderr", | |
| 200 | chunk: Uint8Array, | |
| 201 | ): Effect.Effect<void> => | |
| 202 | Effect.sync(() => { | |
| 203 | const output = streamName === "stderr" ? process.stderr : process.stdout; | |
| 204 | output.write(chunk); | |
| 205 | }).pipe(Effect.ignore); | |
| 206 | ||
| 207 | const writeBackendChildLogRecord = Effect.fn("desktop.observability.writeBackendChildLogRecord")( | |
| 208 | function* ( | |
| 209 | logFile: RotatingLogFileWriter, | |
| 210 | input: { | |
| 211 | readonly message: string; | |
| 212 | readonly level: "INFO" | "ERROR"; | |
| 213 | readonly annotations: Record<string, unknown>; | |
| 214 | }, | |
| 215 | ): Effect.fn.Return<void> { | |
| 216 | return yield* Effect.gen(function* () { | |
| 217 | const timestamp = DateTime.formatIso(yield* DateTime.now); | |
| 218 | const encoded = yield* encodeDesktopBackendChildLogRecord({ | |
| 219 | message: input.message, | |
| 220 | level: input.level, | |
| 221 | timestamp, | |
| 222 | annotations: input.annotations, | |
| 223 | spans: {}, | |
| 224 | fiberId: DESKTOP_BACKEND_CHILD_LOG_FIBER_ID, | |
| 225 | }); | |
| 226 | yield* logFile.writeText(`${encoded}\n`); | |
| 227 | }).pipe(Effect.ignore({ log: true })); | |
| 228 | }, | |
| 229 | ); | |
| 230 | ||
| 231 | const make = Effect.gen(function* () { | |
| 232 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | |
| 233 | const writer = yield* makeRotatingLogFileWriter({ | |
| 234 | filePath: environment.path.join(environment.logDir, "server-child.log"), | |
| 235 | }).pipe(Effect.option); | |
| 236 | ||
| 237 | const service = Option.match(writer, { | |
| 238 | onNone: () => DesktopBackendOutputLogNoop, | |
| 239 | onSome: (logFile) => | |
| 240 | ({ | |
| 241 | writeSessionBoundary: Effect.fn("desktop.observability.backendOutput.writeSessionBoundary")( | |
| 242 | function* ({ phase, details }) { | |
| 243 | const runId = yield* currentDesktopRunId; | |
| 244 | yield* writeBackendChildLogRecord(logFile, { | |
| 245 | message: `backend child process session ${phase.toLowerCase()}`, | |
| 246 | level: "INFO", | |
| 247 | annotations: { | |
| 248 | component: "desktop-backend-child", | |
| 249 | runId, | |
| 250 | phase, | |
| 251 | details: sanitizeLogValue(details), | |
| 252 | }, | |
| 253 | }); | |
| 254 | }, | |
| 255 | ), | |
| 256 | writeOutputChunk: Effect.fn("desktop.observability.backendOutput.writeOutputChunk")( | |
| 257 | function* (streamName, chunk) { | |
| 258 | if (environment.isDevelopment) { | |
| 259 | yield* writeDevelopmentConsoleOutput(streamName, chunk); | |
| 260 | } | |
| 261 | const runId = yield* currentDesktopRunId; | |
| 262 | yield* writeBackendChildLogRecord(logFile, { | |
| 263 | message: "backend child process output", | |
| 264 | level: streamName === "stderr" ? "ERROR" : "INFO", | |
| 265 | annotations: { | |
| 266 | component: "desktop-backend-child", | |
| 267 | runId, | |
| 268 | stream: streamName, | |
| 269 | text: textDecoder.decode(chunk), | |
| 270 | }, | |
| 271 | }); | |
| 272 | }, | |
| 273 | ), | |
| 274 | }) satisfies DesktopBackendOutputLog["Service"], | |
| 275 | }); | |
| 276 | ||
| 277 | return DesktopBackendOutputLog.of(service); | |
| 278 | }); | |
| 279 | ||
| 280 | export const layer = Layer.effect(DesktopBackendOutputLog, make); |
apps/desktop/src/app/DesktopClerk.test.ts+8-9| ... | ... | @@ -21,10 +21,6 @@ vi.mock("@clerk/electron/storage", () => ({ |
| 21 | 21 | storage: storageMock, |
| 22 | 22 | })); |
| 23 | 23 | |
| 24 | import { | |
| 25 | createDesktopClerkBridge, | |
| 26 | resolveDesktopClerkFrontendApiHostname, | |
| 27 | } from "./DesktopClerk.ts"; | |
| 28 | 24 | import * as DesktopClerk from "./DesktopClerk.ts"; |
| 29 | 25 | import * as DesktopEnvironment from "./DesktopEnvironment.ts"; |
| 30 | 26 | |
| ... | ... | @@ -32,9 +28,12 @@ describe("DesktopClerk", () => { |
| 32 | 28 | it("derives the Clerk Frontend API hostname used by the desktop CSP", () => { |
| 33 | 29 | const publishableKey = `pk_test_${btoa("clerk.t3.codes$")}`; |
| 34 | 30 | |
| 35 | assert.equal(resolveDesktopClerkFrontendApiHostname(publishableKey), "clerk.t3.codes"); | |
| 36 | assert.equal(resolveDesktopClerkFrontendApiHostname(""), undefined); | |
| 37 | assert.equal(resolveDesktopClerkFrontendApiHostname("invalid"), undefined); | |
| 31 | assert.equal( | |
| 32 | DesktopClerk.resolveDesktopClerkFrontendApiHostname(publishableKey), | |
| 33 | "clerk.t3.codes", | |
| 34 | ); | |
| 35 | assert.equal(DesktopClerk.resolveDesktopClerkFrontendApiHostname(""), undefined); | |
| 36 | assert.equal(DesktopClerk.resolveDesktopClerkFrontendApiHostname("invalid"), undefined); | |
| 38 | 37 | }); |
| 39 | 38 | |
| 40 | 39 | it.effect("acquires and releases the SDK bridge with the layer", () => { |
| ... | ... | @@ -44,7 +43,7 @@ describe("DesktopClerk", () => { |
| 44 | 43 | const environment = DesktopEnvironment.DesktopEnvironment.of({ |
| 45 | 44 | stateDir: "/tmp/t3-state", |
| 46 | 45 | isDevelopment: true, |
| 47 | } as unknown as DesktopEnvironment.DesktopEnvironmentShape); | |
| 46 | } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]); | |
| 48 | 47 | |
| 49 | 48 | return Effect.gen(function* () { |
| 50 | 49 | yield* Effect.scoped( |
| ... | ... | @@ -78,7 +77,7 @@ describe("DesktopClerk", () => { |
| 78 | 77 | storageMock.mockReturnValue(storageAdapter); |
| 79 | 78 | createClerkBridgeMock.mockReturnValue(bridge); |
| 80 | 79 | |
| 81 | assert.equal(createDesktopClerkBridge("/tmp/t3-state", isDevelopment), bridge); | |
| 80 | assert.equal(DesktopClerk.createDesktopClerkBridge("/tmp/t3-state", isDevelopment), bridge); | |
| 82 | 81 | assert.deepEqual(storageMock.mock.calls, [[{ path: "/tmp/t3-state" }]]); |
| 83 | 82 | assert.deepEqual(createClerkBridgeMock.mock.calls, [ |
| 84 | 83 | [ |
apps/desktop/src/app/DesktopClerk.ts+10-11| ... | ... | @@ -14,17 +14,16 @@ import * as DesktopEnvironment from "./DesktopEnvironment.ts"; |
| 14 | 14 | |
| 15 | 15 | declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined; |
| 16 | 16 | |
| 17 | export interface DesktopClerkShape { | |
| 18 | readonly configure: Effect.Effect< | |
| 19 | void, | |
| 20 | never, | |
| 21 | ElectronApp.ElectronApp | ElectronWindow.ElectronWindow | Scope.Scope | |
| 22 | >; | |
| 23 | } | |
| 24 | ||
| 25 | export class DesktopClerk extends Context.Service<DesktopClerk, DesktopClerkShape>()( | |
| 26 | "@t3tools/desktop/app/DesktopClerk", | |
| 27 | ) {} | |
| 17 | export class DesktopClerk extends Context.Service< | |
| 18 | DesktopClerk, | |
| 19 | { | |
| 20 | readonly configure: Effect.Effect< | |
| 21 | void, | |
| 22 | never, | |
| 23 | ElectronApp.ElectronApp | ElectronWindow.ElectronWindow | Scope.Scope | |
| 24 | >; | |
| 25 | } | |
| 26 | >()("@t3tools/desktop/app/DesktopClerk") {} | |
| 28 | 27 | |
| 29 | 28 | export function resolveDesktopClerkFrontendApiHostname( |
| 30 | 29 | publishableKey: string | undefined, |
apps/desktop/src/app/DesktopEnvironment.ts+50-55| ... | ... | @@ -11,10 +11,7 @@ import * as Layer from "effect/Layer"; |
| 11 | 11 | import * as Option from "effect/Option"; |
| 12 | 12 | import * as Path from "effect/Path"; |
| 13 | 13 | |
| 14 | import { | |
| 15 | type DesktopSettings, | |
| 16 | resolveDefaultDesktopSettings, | |
| 17 | } from "../settings/DesktopAppSettings.ts"; | |
| 14 | import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; | |
| 18 | 15 | import * as DesktopConfig from "./DesktopConfig.ts"; |
| 19 | 16 | import { isNightlyDesktopVersion } from "../updates/updateChannels.ts"; |
| 20 | 17 | |
| ... | ... | @@ -30,55 +27,53 @@ export interface MakeDesktopEnvironmentInput { |
| 30 | 27 | readonly runningUnderArm64Translation: boolean; |
| 31 | 28 | } |
| 32 | 29 | |
| 33 | export interface DesktopEnvironmentShape { | |
| 34 | readonly path: Path.Path; | |
| 35 | readonly dirname: string; | |
| 36 | readonly platform: NodeJS.Platform; | |
| 37 | readonly processArch: string; | |
| 38 | readonly isPackaged: boolean; | |
| 39 | readonly isDevelopment: boolean; | |
| 40 | readonly appVersion: string; | |
| 41 | readonly appPath: string; | |
| 42 | readonly resourcesPath: string; | |
| 43 | readonly homeDirectory: string; | |
| 44 | readonly appDataDirectory: string; | |
| 45 | readonly baseDir: string; | |
| 46 | readonly stateDir: string; | |
| 47 | readonly desktopSettingsPath: string; | |
| 48 | readonly clientSettingsPath: string; | |
| 49 | readonly savedEnvironmentRegistryPath: string; | |
| 50 | readonly serverSettingsPath: string; | |
| 51 | readonly logDir: string; | |
| 52 | readonly browserArtifactsDir: string; | |
| 53 | readonly rootDir: string; | |
| 54 | readonly appRoot: string; | |
| 55 | readonly backendEntryPath: string; | |
| 56 | readonly backendCwd: string; | |
| 57 | readonly preloadPath: string; | |
| 58 | readonly appUpdateYmlPath: string; | |
| 59 | readonly devServerUrl: Option.Option<URL>; | |
| 60 | readonly devRemoteT3ServerEntryPath: Option.Option<string>; | |
| 61 | readonly configuredBackendPort: Option.Option<number>; | |
| 62 | readonly commitHashOverride: Option.Option<string>; | |
| 63 | readonly otlpTracesUrl: Option.Option<string>; | |
| 64 | readonly otlpExportIntervalMs: number; | |
| 65 | readonly branding: DesktopAppBranding; | |
| 66 | readonly displayName: string; | |
| 67 | readonly appUserModelId: string; | |
| 68 | readonly linuxDesktopEntryName: string; | |
| 69 | readonly linuxWmClass: string; | |
| 70 | readonly userDataDirName: string; | |
| 71 | readonly legacyUserDataDirName: string; | |
| 72 | readonly defaultDesktopSettings: DesktopSettings; | |
| 73 | readonly runtimeInfo: DesktopRuntimeInfo; | |
| 74 | readonly resolvePickFolderDefaultPath: (rawOptions: unknown) => Option.Option<string>; | |
| 75 | readonly resolveResourcePathCandidates: (fileName: string) => readonly string[]; | |
| 76 | readonly developmentDockIconPath: string; | |
| 77 | } | |
| 78 | ||
| 79 | 30 | export class DesktopEnvironment extends Context.Service< |
| 80 | 31 | DesktopEnvironment, |
| 81 | DesktopEnvironmentShape | |
| 32 | { | |
| 33 | readonly path: Path.Path; | |
| 34 | readonly dirname: string; | |
| 35 | readonly platform: NodeJS.Platform; | |
| 36 | readonly processArch: string; | |
| 37 | readonly isPackaged: boolean; | |
| 38 | readonly isDevelopment: boolean; | |
| 39 | readonly appVersion: string; | |
| 40 | readonly appPath: string; | |
| 41 | readonly resourcesPath: string; | |
| 42 | readonly homeDirectory: string; | |
| 43 | readonly appDataDirectory: string; | |
| 44 | readonly baseDir: string; | |
| 45 | readonly stateDir: string; | |
| 46 | readonly desktopSettingsPath: string; | |
| 47 | readonly clientSettingsPath: string; | |
| 48 | readonly savedEnvironmentRegistryPath: string; | |
| 49 | readonly serverSettingsPath: string; | |
| 50 | readonly logDir: string; | |
| 51 | readonly browserArtifactsDir: string; | |
| 52 | readonly rootDir: string; | |
| 53 | readonly appRoot: string; | |
| 54 | readonly backendEntryPath: string; | |
| 55 | readonly backendCwd: string; | |
| 56 | readonly preloadPath: string; | |
| 57 | readonly appUpdateYmlPath: string; | |
| 58 | readonly devServerUrl: Option.Option<URL>; | |
| 59 | readonly devRemoteT3ServerEntryPath: Option.Option<string>; | |
| 60 | readonly configuredBackendPort: Option.Option<number>; | |
| 61 | readonly commitHashOverride: Option.Option<string>; | |
| 62 | readonly otlpTracesUrl: Option.Option<string>; | |
| 63 | readonly otlpExportIntervalMs: number; | |
| 64 | readonly branding: DesktopAppBranding; | |
| 65 | readonly displayName: string; | |
| 66 | readonly appUserModelId: string; | |
| 67 | readonly linuxDesktopEntryName: string; | |
| 68 | readonly linuxWmClass: string; | |
| 69 | readonly userDataDirName: string; | |
| 70 | readonly legacyUserDataDirName: string; | |
| 71 | readonly defaultDesktopSettings: DesktopAppSettings.DesktopSettings; | |
| 72 | readonly runtimeInfo: DesktopRuntimeInfo; | |
| 73 | readonly resolvePickFolderDefaultPath: (rawOptions: unknown) => Option.Option<string>; | |
| 74 | readonly resolveResourcePathCandidates: (fileName: string) => readonly string[]; | |
| 75 | readonly developmentDockIconPath: string; | |
| 76 | } | |
| 82 | 77 | >()("@t3tools/desktop/app/DesktopEnvironment") {} |
| 83 | 78 | |
| 84 | 79 | const APP_BASE_NAME = "T3 Code"; |
| ... | ... | @@ -136,9 +131,9 @@ function resolveDesktopRuntimeInfo(input: { |
| 136 | 131 | }; |
| 137 | 132 | } |
| 138 | 133 | |
| 139 | const makeDesktopEnvironment = Effect.fn("desktop.environment.make")(function* ( | |
| 134 | const make = Effect.fn("desktop.environment.make")(function* ( | |
| 140 | 135 | input: MakeDesktopEnvironmentInput, |
| 141 | ): Effect.fn.Return<DesktopEnvironmentShape, Config.ConfigError, Path.Path> { | |
| 136 | ): Effect.fn.Return<DesktopEnvironment["Service"], Config.ConfigError, Path.Path> { | |
| 142 | 137 | const path = yield* Path.Path; |
| 143 | 138 | const config = yield* DesktopConfig.DesktopConfig; |
| 144 | 139 | const homeDirectory = input.homeDirectory; |
| ... | ... | @@ -208,7 +203,7 @@ const makeDesktopEnvironment = Effect.fn("desktop.environment.make")(function* ( |
| 208 | 203 | linuxWmClass: isDevelopment ? "t3code-dev" : "t3code", |
| 209 | 204 | userDataDirName, |
| 210 | 205 | legacyUserDataDirName, |
| 211 | defaultDesktopSettings: resolveDefaultDesktopSettings(input.appVersion), | |
| 206 | defaultDesktopSettings: DesktopAppSettings.resolveDefaultDesktopSettings(input.appVersion), | |
| 212 | 207 | runtimeInfo: resolveDesktopRuntimeInfo({ |
| 213 | 208 | platform: input.platform, |
| 214 | 209 | processArch: input.processArch, |
| ... | ... | @@ -250,4 +245,4 @@ const makeDesktopEnvironment = Effect.fn("desktop.environment.make")(function* ( |
| 250 | 245 | }); |
| 251 | 246 | |
| 252 | 247 | export const layer = (input: MakeDesktopEnvironmentInput) => |
| 253 | Layer.effect(DesktopEnvironment, makeDesktopEnvironment(input)); | |
| 248 | Layer.effect(DesktopEnvironment, make(input)); |
apps/desktop/src/app/DesktopLifecycle.ts+89-120| ... | ... | @@ -1,7 +1,6 @@ |
| 1 | 1 | import * as Cause from "effect/Cause"; |
| 2 | 2 | import * as Context from "effect/Context"; |
| 3 | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Deferred from "effect/Deferred"; | |
| 5 | 4 | import * as Layer from "effect/Layer"; |
| 6 | 5 | import * as Ref from "effect/Ref"; |
| 7 | 6 | import * as Scope from "effect/Scope"; |
| ... | ... | @@ -10,63 +9,34 @@ import type * as Electron from "electron"; |
| 10 | 9 | |
| 11 | 10 | import * as DesktopEnvironment from "./DesktopEnvironment.ts"; |
| 12 | 11 | import * as DesktopObservability from "./DesktopObservability.ts"; |
| 12 | import * as DesktopShutdownModule from "./DesktopShutdown.ts"; | |
| 13 | 13 | import * as ElectronApp from "../electron/ElectronApp.ts"; |
| 14 | 14 | import * as ElectronTheme from "../electron/ElectronTheme.ts"; |
| 15 | 15 | import * as DesktopState from "./DesktopState.ts"; |
| 16 | 16 | import * as DesktopWindow from "../window/DesktopWindow.ts"; |
| 17 | 17 | |
| 18 | export interface DesktopShutdownShape { | |
| 19 | readonly request: Effect.Effect<void>; | |
| 20 | readonly awaitRequest: Effect.Effect<void>; | |
| 21 | readonly markComplete: Effect.Effect<void>; | |
| 22 | readonly awaitComplete: Effect.Effect<void>; | |
| 23 | readonly isComplete: Effect.Effect<boolean>; | |
| 24 | } | |
| 25 | ||
| 26 | export class DesktopShutdown extends Context.Service<DesktopShutdown, DesktopShutdownShape>()( | |
| 27 | "@t3tools/desktop/app/DesktopLifecycle/DesktopShutdown", | |
| 28 | ) {} | |
| 29 | ||
| 30 | const makeShutdown = Effect.gen(function* () { | |
| 31 | const requested = yield* Deferred.make<void>(); | |
| 32 | const completed = yield* Deferred.make<void>(); | |
| 33 | const completedRef = yield* Ref.make(false); | |
| 34 | ||
| 35 | return DesktopShutdown.of({ | |
| 36 | request: Deferred.succeed(requested, undefined).pipe(Effect.asVoid), | |
| 37 | awaitRequest: Deferred.await(requested), | |
| 38 | markComplete: Ref.set(completedRef, true).pipe( | |
| 39 | Effect.andThen(Deferred.succeed(completed, undefined)), | |
| 40 | Effect.asVoid, | |
| 41 | ), | |
| 42 | awaitComplete: Deferred.await(completed), | |
| 43 | isComplete: Ref.get(completedRef), | |
| 44 | }); | |
| 45 | }); | |
| 46 | ||
| 47 | export const layerShutdown = Layer.effect(DesktopShutdown, makeShutdown); | |
| 18 | export { DesktopShutdown, layer as layerShutdown } from "./DesktopShutdown.ts"; | |
| 48 | 19 | |
| 49 | 20 | export type DesktopLifecycleRuntimeServices = |
| 50 | 21 | | DesktopEnvironment.DesktopEnvironment |
| 51 | | DesktopShutdown | |
| 22 | | DesktopShutdownModule.DesktopShutdown | |
| 52 | 23 | | DesktopState.DesktopState |
| 53 | 24 | | DesktopWindow.DesktopWindow |
| 54 | 25 | | ElectronApp.ElectronApp |
| 55 | 26 | | ElectronTheme.ElectronTheme; |
| 56 | 27 | |
| 57 | export interface DesktopLifecycleShape { | |
| 58 | readonly relaunch: ( | |
| 59 | reason: string, | |
| 60 | ) => Effect.Effect<void, never, DesktopLifecycleRuntimeServices>; | |
| 61 | readonly register: Effect.Effect<void, never, Scope.Scope | DesktopLifecycleRuntimeServices>; | |
| 62 | } | |
| 63 | ||
| 64 | 28 | /** |
| 65 | 29 | * @effect-expect-leaking DesktopEnvironment | DesktopShutdown | DesktopState | DesktopWindow | ElectronApp | ElectronTheme |
| 66 | 30 | */ |
| 67 | export class DesktopLifecycle extends Context.Service<DesktopLifecycle, DesktopLifecycleShape>()( | |
| 68 | "@t3tools/desktop/app/DesktopLifecycle", | |
| 69 | ) {} | |
| 31 | export class DesktopLifecycle extends Context.Service< | |
| 32 | DesktopLifecycle, | |
| 33 | { | |
| 34 | readonly relaunch: ( | |
| 35 | reason: string, | |
| 36 | ) => Effect.Effect<void, never, DesktopLifecycleRuntimeServices>; | |
| 37 | readonly register: Effect.Effect<void, never, Scope.Scope | DesktopLifecycleRuntimeServices>; | |
| 38 | } | |
| 39 | >()("@t3tools/desktop/app/DesktopLifecycle") {} | |
| 70 | 40 | |
| 71 | 41 | const { logInfo: logLifecycleInfo, logError: logLifecycleError } = |
| 72 | 42 | DesktopObservability.makeComponentLogger("desktop-lifecycle"); |
| ... | ... | @@ -93,8 +63,8 @@ function addScopedListener<Args extends ReadonlyArray<unknown>>( |
| 93 | 63 | } |
| 94 | 64 | |
| 95 | 65 | const requestDesktopShutdownAndWait = Effect.fn("desktop.lifecycle.requestShutdownAndWait")( |
| 96 | function* (): Effect.fn.Return<void, never, DesktopShutdown> { | |
| 97 | const shutdown = yield* DesktopShutdown; | |
| 66 | function* (): Effect.fn.Return<void, never, DesktopShutdownModule.DesktopShutdown> { | |
| 67 | const shutdown = yield* DesktopShutdownModule.DesktopShutdown; | |
| 98 | 68 | yield* shutdown.request; |
| 99 | 69 | yield* shutdown.awaitComplete; |
| 100 | 70 | }, |
| ... | ... | @@ -154,83 +124,82 @@ function quitFromSignal( |
| 154 | 124 | ); |
| 155 | 125 | } |
| 156 | 126 | |
| 157 | export const layer = Layer.succeed( | |
| 158 | DesktopLifecycle, | |
| 159 | DesktopLifecycle.of({ | |
| 160 | relaunch: Effect.fn("desktop.lifecycle.relaunch")(function* (reason) { | |
| 161 | const electronApp = yield* ElectronApp.ElectronApp; | |
| 162 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | |
| 163 | const state = yield* DesktopState.DesktopState; | |
| 164 | yield* logLifecycleInfo("desktop relaunch requested", { reason }); | |
| 165 | yield* Effect.gen(function* () { | |
| 166 | yield* Effect.yieldNow; | |
| 167 | yield* Ref.set(state.quitting, true); | |
| 168 | yield* requestDesktopShutdownAndWait(); | |
| 169 | if (environment.isDevelopment) { | |
| 170 | yield* electronApp.exit(75); | |
| 171 | return; | |
| 172 | } | |
| 173 | yield* electronApp.relaunch({ | |
| 174 | execPath: process.execPath, | |
| 175 | args: process.argv.slice(1), | |
| 176 | }); | |
| 177 | yield* electronApp.exit(0); | |
| 178 | }).pipe( | |
| 179 | Effect.catchCause((cause) => | |
| 180 | logLifecycleError("desktop relaunch failed", { | |
| 181 | cause: Cause.pretty(cause), | |
| 182 | }), | |
| 183 | ), | |
| 184 | Effect.forkDetach, | |
| 185 | Effect.asVoid, | |
| 186 | ); | |
| 187 | }), | |
| 188 | register: Effect.gen(function* () { | |
| 189 | const desktopWindow = yield* DesktopWindow.DesktopWindow; | |
| 190 | const electronApp = yield* ElectronApp.ElectronApp; | |
| 191 | const electronTheme = yield* ElectronTheme.ElectronTheme; | |
| 192 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | |
| 193 | const context = yield* Effect.context<DesktopLifecycleRuntimeServices>(); | |
| 194 | const runEffect = Effect.runPromiseWith(context); | |
| 195 | let quitAllowed = false; | |
| 196 | yield* electronTheme.onUpdated(() => { | |
| 197 | void runEffect( | |
| 198 | desktopWindow.syncAppearance.pipe(Effect.withSpan("desktop.lifecycle.themeUpdated")), | |
| 199 | ); | |
| 200 | }); | |
| 201 | yield* electronApp.on("before-quit", (event: Electron.Event) => { | |
| 202 | handleBeforeQuit( | |
| 203 | event, | |
| 204 | runEffect, | |
| 205 | () => quitAllowed, | |
| 206 | () => { | |
| 207 | quitAllowed = true; | |
| 208 | }, | |
| 209 | ); | |
| 127 | const make = DesktopLifecycle.of({ | |
| 128 | relaunch: Effect.fn("desktop.lifecycle.relaunch")(function* (reason) { | |
| 129 | const electronApp = yield* ElectronApp.ElectronApp; | |
| 130 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | |
| 131 | const state = yield* DesktopState.DesktopState; | |
| 132 | yield* logLifecycleInfo("desktop relaunch requested", { reason }); | |
| 133 | yield* Effect.gen(function* () { | |
| 134 | yield* Effect.yieldNow; | |
| 135 | yield* Ref.set(state.quitting, true); | |
| 136 | yield* requestDesktopShutdownAndWait(); | |
| 137 | if (environment.isDevelopment) { | |
| 138 | yield* electronApp.exit(75); | |
| 139 | return; | |
| 140 | } | |
| 141 | yield* electronApp.relaunch({ | |
| 142 | execPath: process.execPath, | |
| 143 | args: process.argv.slice(1), | |
| 210 | 144 | }); |
| 211 | yield* electronApp.on("activate", () => { | |
| 212 | void runEffect(desktopWindow.activate.pipe(Effect.withSpan("desktop.lifecycle.activate"))); | |
| 145 | yield* electronApp.exit(0); | |
| 146 | }).pipe( | |
| 147 | Effect.catchCause((cause) => | |
| 148 | logLifecycleError("desktop relaunch failed", { | |
| 149 | cause: Cause.pretty(cause), | |
| 150 | }), | |
| 151 | ), | |
| 152 | Effect.forkDetach, | |
| 153 | Effect.asVoid, | |
| 154 | ); | |
| 155 | }), | |
| 156 | register: Effect.gen(function* () { | |
| 157 | const desktopWindow = yield* DesktopWindow.DesktopWindow; | |
| 158 | const electronApp = yield* ElectronApp.ElectronApp; | |
| 159 | const electronTheme = yield* ElectronTheme.ElectronTheme; | |
| 160 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | |
| 161 | const context = yield* Effect.context<DesktopLifecycleRuntimeServices>(); | |
| 162 | const runEffect = Effect.runPromiseWith(context); | |
| 163 | let quitAllowed = false; | |
| 164 | yield* electronTheme.onUpdated(() => { | |
| 165 | void runEffect( | |
| 166 | desktopWindow.syncAppearance.pipe(Effect.withSpan("desktop.lifecycle.themeUpdated")), | |
| 167 | ); | |
| 168 | }); | |
| 169 | yield* electronApp.on("before-quit", (event: Electron.Event) => { | |
| 170 | handleBeforeQuit( | |
| 171 | event, | |
| 172 | runEffect, | |
| 173 | () => quitAllowed, | |
| 174 | () => { | |
| 175 | quitAllowed = true; | |
| 176 | }, | |
| 177 | ); | |
| 178 | }); | |
| 179 | yield* electronApp.on("activate", () => { | |
| 180 | void runEffect(desktopWindow.activate.pipe(Effect.withSpan("desktop.lifecycle.activate"))); | |
| 181 | }); | |
| 182 | yield* electronApp.on("window-all-closed", () => { | |
| 183 | void runEffect( | |
| 184 | Effect.gen(function* () { | |
| 185 | const app = yield* ElectronApp.ElectronApp; | |
| 186 | const state = yield* DesktopState.DesktopState; | |
| 187 | if (environment.platform !== "darwin" && !(yield* Ref.get(state.quitting))) { | |
| 188 | yield* app.quit; | |
| 189 | } | |
| 190 | }).pipe(Effect.withSpan("desktop.lifecycle.windowAllClosed")), | |
| 191 | ); | |
| 192 | }); | |
| 193 | ||
| 194 | if (environment.platform !== "win32") { | |
| 195 | yield* addScopedListener(process, "SIGINT", () => { | |
| 196 | quitFromSignal("SIGINT", runEffect); | |
| 213 | 197 | }); |
| 214 | yield* electronApp.on("window-all-closed", () => { | |
| 215 | void runEffect( | |
| 216 | Effect.gen(function* () { | |
| 217 | const app = yield* ElectronApp.ElectronApp; | |
| 218 | const state = yield* DesktopState.DesktopState; | |
| 219 | if (environment.platform !== "darwin" && !(yield* Ref.get(state.quitting))) { | |
| 220 | yield* app.quit; | |
| 221 | } | |
| 222 | }).pipe(Effect.withSpan("desktop.lifecycle.windowAllClosed")), | |
| 223 | ); | |
| 198 | yield* addScopedListener(process, "SIGTERM", () => { | |
| 199 | quitFromSignal("SIGTERM", runEffect); | |
| 224 | 200 | }); |
| 201 | } | |
| 202 | }).pipe(Effect.withSpan("desktop.lifecycle.register")), | |
| 203 | }); | |
| 225 | 204 | |
| 226 | if (environment.platform !== "win32") { | |
| 227 | yield* addScopedListener(process, "SIGINT", () => { | |
| 228 | quitFromSignal("SIGINT", runEffect); | |
| 229 | }); | |
| 230 | yield* addScopedListener(process, "SIGTERM", () => { | |
| 231 | quitFromSignal("SIGTERM", runEffect); | |
| 232 | }); | |
| 233 | } | |
| 234 | }).pipe(Effect.withSpan("desktop.lifecycle.register")), | |
| 235 | }), | |
| 236 | ); | |
| 205 | export const layer = Layer.succeed(DesktopLifecycle, make); |
apps/desktop/src/app/DesktopObservability.ts+7-277| ... | ... | @@ -1,52 +1,20 @@ |
| 1 | 1 | import { makeLocalFileTracer, makeTraceSink } from "@t3tools/shared/observability"; |
| 2 | 2 | import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; |
| 3 | import * as Context from "effect/Context"; | |
| 4 | import * as Data from "effect/Data"; | |
| 5 | import * as DateTime from "effect/DateTime"; | |
| 6 | 3 | import * as Effect from "effect/Effect"; |
| 7 | 4 | import * as FileSystem from "effect/FileSystem"; |
| 8 | 5 | import * as Layer from "effect/Layer"; |
| 9 | 6 | import * as Logger from "effect/Logger"; |
| 10 | 7 | import * as Option from "effect/Option"; |
| 11 | import * as Path from "effect/Path"; | |
| 12 | import * as PlatformError from "effect/PlatformError"; | |
| 13 | 8 | import * as References from "effect/References"; |
| 14 | import * as Ref from "effect/Ref"; | |
| 15 | import * as Schema from "effect/Schema"; | |
| 16 | import * as Semaphore from "effect/Semaphore"; | |
| 17 | 9 | import * as Tracer from "effect/Tracer"; |
| 18 | 10 | import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; |
| 19 | 11 | |
| 12 | import * as DesktopBackendOutputLogModule from "./DesktopBackendOutputLog.ts"; | |
| 20 | 13 | import * as DesktopEnvironment from "./DesktopEnvironment.ts"; |
| 21 | 14 | |
| 22 | const DESKTOP_LOG_FILE_MAX_BYTES = 10 * 1024 * 1024; | |
| 23 | const DESKTOP_LOG_FILE_MAX_FILES = 10; | |
| 24 | const DESKTOP_BACKEND_CHILD_LOG_FIBER_ID = "#backend-child"; | |
| 25 | 15 | const DESKTOP_TRACE_BATCH_WINDOW_MS = 200; |
| 26 | 16 | |
| 27 | export interface RotatingLogFileWriter { | |
| 28 | readonly writeBytes: (chunk: Uint8Array) => Effect.Effect<void>; | |
| 29 | readonly writeText: (chunk: string) => Effect.Effect<void>; | |
| 30 | } | |
| 31 | ||
| 32 | export interface DesktopBackendOutputLogShape { | |
| 33 | readonly writeSessionBoundary: (input: { | |
| 34 | readonly phase: "START" | "END"; | |
| 35 | readonly details: string; | |
| 36 | }) => Effect.Effect<void>; | |
| 37 | readonly writeOutputChunk: ( | |
| 38 | streamName: "stdout" | "stderr", | |
| 39 | chunk: Uint8Array, | |
| 40 | ) => Effect.Effect<void>; | |
| 41 | } | |
| 42 | ||
| 43 | export class DesktopBackendOutputLog extends Context.Service< | |
| 44 | DesktopBackendOutputLog, | |
| 45 | DesktopBackendOutputLogShape | |
| 46 | >()("@t3tools/desktop/app/DesktopObservability/DesktopBackendOutputLog") {} | |
| 47 | ||
| 48 | const textEncoder = new TextEncoder(); | |
| 49 | const textDecoder = new TextDecoder(); | |
| 17 | export { DesktopBackendOutputLog } from "./DesktopBackendOutputLog.ts"; | |
| 50 | 18 | |
| 51 | 19 | export type DesktopLogAnnotations = Record<string, unknown>; |
| 52 | 20 | |
| ... | ... | @@ -82,160 +50,6 @@ export function makeComponentLogger(component: string): DesktopComponentLogger { |
| 82 | 50 | }; |
| 83 | 51 | } |
| 84 | 52 | |
| 85 | class DesktopLogFileWriterConfigurationError extends Data.TaggedError( | |
| 86 | "DesktopLogFileWriterConfigurationError", | |
| 87 | )<{ | |
| 88 | readonly option: "maxBytes" | "maxFiles"; | |
| 89 | readonly value: number; | |
| 90 | }> { | |
| 91 | override get message() { | |
| 92 | return `${this.option} must be >= 1 (received ${this.value})`; | |
| 93 | } | |
| 94 | } | |
| 95 | ||
| 96 | type DesktopLogFileWriterError = | |
| 97 | | DesktopLogFileWriterConfigurationError | |
| 98 | | PlatformError.PlatformError; | |
| 99 | ||
| 100 | const sanitizeLogValue = (value: string): string => value.replace(/\s+/g, " ").trim(); | |
| 101 | ||
| 102 | const DesktopBackendChildLogRecord = Schema.Struct({ | |
| 103 | message: Schema.String, | |
| 104 | level: Schema.Literals(["INFO", "ERROR"]), | |
| 105 | timestamp: Schema.String, | |
| 106 | annotations: Schema.Record(Schema.String, Schema.Unknown), | |
| 107 | spans: Schema.Record(Schema.String, Schema.Unknown), | |
| 108 | fiberId: Schema.String, | |
| 109 | }); | |
| 110 | ||
| 111 | const encodeDesktopBackendChildLogRecord = Schema.encodeEffect( | |
| 112 | Schema.fromJsonString(DesktopBackendChildLogRecord), | |
| 113 | ); | |
| 114 | ||
| 115 | const DesktopBackendOutputLogNoop: DesktopBackendOutputLogShape = { | |
| 116 | writeSessionBoundary: () => Effect.void, | |
| 117 | writeOutputChunk: () => Effect.void, | |
| 118 | }; | |
| 119 | ||
| 120 | const currentDesktopRunId = Effect.gen(function* () { | |
| 121 | const annotations = yield* References.CurrentLogAnnotations; | |
| 122 | const runId = annotations.runId; | |
| 123 | return typeof runId === "string" && runId.length > 0 ? runId : "unknown"; | |
| 124 | }); | |
| 125 | ||
| 126 | const refreshFileSize = ( | |
| 127 | fileSystem: FileSystem.FileSystem, | |
| 128 | filePath: string, | |
| 129 | ): Effect.Effect<number, never> => | |
| 130 | fileSystem.stat(filePath).pipe( | |
| 131 | Effect.map((stat) => Number(stat.size)), | |
| 132 | Effect.orElseSucceed(() => 0), | |
| 133 | ); | |
| 134 | ||
| 135 | const makeRotatingLogFileWriter = Effect.fn("makeRotatingLogFileWriter")(function* (input: { | |
| 136 | readonly filePath: string; | |
| 137 | readonly maxBytes?: number; | |
| 138 | readonly maxFiles?: number; | |
| 139 | }): Effect.fn.Return< | |
| 140 | RotatingLogFileWriter, | |
| 141 | DesktopLogFileWriterError, | |
| 142 | FileSystem.FileSystem | Path.Path | |
| 143 | > { | |
| 144 | const fileSystem = yield* FileSystem.FileSystem; | |
| 145 | const path = yield* Path.Path; | |
| 146 | const maxBytes = input.maxBytes ?? DESKTOP_LOG_FILE_MAX_BYTES; | |
| 147 | const maxFiles = input.maxFiles ?? DESKTOP_LOG_FILE_MAX_FILES; | |
| 148 | const directory = path.dirname(input.filePath); | |
| 149 | const baseName = path.basename(input.filePath); | |
| 150 | ||
| 151 | if (maxBytes < 1) { | |
| 152 | return yield* new DesktopLogFileWriterConfigurationError({ | |
| 153 | option: "maxBytes", | |
| 154 | value: maxBytes, | |
| 155 | }); | |
| 156 | } | |
| 157 | if (maxFiles < 1) { | |
| 158 | return yield* new DesktopLogFileWriterConfigurationError({ | |
| 159 | option: "maxFiles", | |
| 160 | value: maxFiles, | |
| 161 | }); | |
| 162 | } | |
| 163 | ||
| 164 | yield* fileSystem.makeDirectory(directory, { recursive: true }); | |
| 165 | ||
| 166 | const withSuffix = (index: number) => `${input.filePath}.${index}`; | |
| 167 | const currentSize = yield* Ref.make(yield* refreshFileSize(fileSystem, input.filePath)); | |
| 168 | const mutex = yield* Semaphore.make(1); | |
| 169 | ||
| 170 | const pruneOverflowBackups = Effect.gen(function* () { | |
| 171 | const entries = yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => [])); | |
| 172 | for (const entry of entries) { | |
| 173 | if (!entry.startsWith(`${baseName}.`)) continue; | |
| 174 | const suffix = Number(entry.slice(baseName.length + 1)); | |
| 175 | if (!Number.isInteger(suffix) || suffix <= maxFiles) continue; | |
| 176 | yield* fileSystem.remove(path.join(directory, entry), { force: true }).pipe(Effect.ignore); | |
| 177 | } | |
| 178 | }); | |
| 179 | ||
| 180 | const rotate = Effect.gen(function* () { | |
| 181 | yield* fileSystem.remove(withSuffix(maxFiles), { force: true }).pipe(Effect.ignore); | |
| 182 | for (let index = maxFiles - 1; index >= 1; index -= 1) { | |
| 183 | const source = withSuffix(index); | |
| 184 | const sourceExists = yield* fileSystem.exists(source).pipe(Effect.orElseSucceed(() => false)); | |
| 185 | if (sourceExists) { | |
| 186 | yield* fileSystem.rename(source, withSuffix(index + 1)); | |
| 187 | } | |
| 188 | } | |
| 189 | const currentExists = yield* fileSystem | |
| 190 | .exists(input.filePath) | |
| 191 | .pipe(Effect.orElseSucceed(() => false)); | |
| 192 | if (currentExists) { | |
| 193 | yield* fileSystem.rename(input.filePath, withSuffix(1)); | |
| 194 | } | |
| 195 | yield* Ref.set(currentSize, 0); | |
| 196 | }).pipe( | |
| 197 | Effect.catch(() => | |
| 198 | refreshFileSize(fileSystem, input.filePath).pipe( | |
| 199 | Effect.flatMap((size) => Ref.set(currentSize, size)), | |
| 200 | ), | |
| 201 | ), | |
| 202 | ); | |
| 203 | ||
| 204 | const writeBytes = (chunk: Uint8Array): Effect.Effect<void> => { | |
| 205 | if (chunk.byteLength === 0) return Effect.void; | |
| 206 | ||
| 207 | return mutex.withPermits(1)( | |
| 208 | Effect.gen(function* () { | |
| 209 | const beforeSize = yield* Ref.get(currentSize); | |
| 210 | if (beforeSize > 0 && beforeSize + chunk.byteLength > maxBytes) { | |
| 211 | yield* rotate; | |
| 212 | } | |
| 213 | ||
| 214 | yield* fileSystem.writeFile(input.filePath, chunk, { flag: "a" }); | |
| 215 | const afterSize = (yield* Ref.get(currentSize)) + chunk.byteLength; | |
| 216 | yield* Ref.set(currentSize, afterSize); | |
| 217 | ||
| 218 | if (afterSize > maxBytes) { | |
| 219 | yield* rotate; | |
| 220 | } | |
| 221 | }).pipe( | |
| 222 | Effect.catch(() => | |
| 223 | refreshFileSize(fileSystem, input.filePath).pipe( | |
| 224 | Effect.flatMap((size) => Ref.set(currentSize, size)), | |
| 225 | ), | |
| 226 | ), | |
| 227 | ), | |
| 228 | ); | |
| 229 | }; | |
| 230 | ||
| 231 | yield* pruneOverflowBackups; | |
| 232 | ||
| 233 | return { | |
| 234 | writeBytes, | |
| 235 | writeText: (chunk) => writeBytes(textEncoder.encode(chunk)), | |
| 236 | } satisfies RotatingLogFileWriter; | |
| 237 | }); | |
| 238 | ||
| 239 | 53 | const readPersistedOtlpTracesUrl: Effect.Effect< |
| 240 | 54 | Option.Option<string>, |
| 241 | 55 | never, |
| ... | ... | @@ -260,90 +74,6 @@ const resolveOtlpTracesUrl = Effect.gen(function* () { |
| 260 | 74 | return yield* readPersistedOtlpTracesUrl; |
| 261 | 75 | }); |
| 262 | 76 | |
| 263 | const writeDevelopmentConsoleOutput = ( | |
| 264 | streamName: "stdout" | "stderr", | |
| 265 | chunk: Uint8Array, | |
| 266 | ): Effect.Effect<void> => | |
| 267 | Effect.sync(() => { | |
| 268 | const output = streamName === "stderr" ? process.stderr : process.stdout; | |
| 269 | output.write(chunk); | |
| 270 | }).pipe(Effect.ignore); | |
| 271 | ||
| 272 | const writeBackendChildLogRecord = Effect.fn("desktop.observability.writeBackendChildLogRecord")( | |
| 273 | function* ( | |
| 274 | logFile: RotatingLogFileWriter, | |
| 275 | input: { | |
| 276 | readonly message: string; | |
| 277 | readonly level: "INFO" | "ERROR"; | |
| 278 | readonly annotations: Record<string, unknown>; | |
| 279 | }, | |
| 280 | ): Effect.fn.Return<void> { | |
| 281 | return yield* Effect.gen(function* () { | |
| 282 | const timestamp = DateTime.formatIso(yield* DateTime.now); | |
| 283 | const encoded = yield* encodeDesktopBackendChildLogRecord({ | |
| 284 | message: input.message, | |
| 285 | level: input.level, | |
| 286 | timestamp, | |
| 287 | annotations: input.annotations, | |
| 288 | spans: {}, | |
| 289 | fiberId: DESKTOP_BACKEND_CHILD_LOG_FIBER_ID, | |
| 290 | }); | |
| 291 | yield* logFile.writeText(`${encoded}\n`); | |
| 292 | }).pipe(Effect.ignore({ log: true })); | |
| 293 | }, | |
| 294 | ); | |
| 295 | ||
| 296 | const backendOutputLogLayer = Layer.effect( | |
| 297 | DesktopBackendOutputLog, | |
| 298 | Effect.gen(function* () { | |
| 299 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | |
| 300 | ||
| 301 | const writer = yield* makeRotatingLogFileWriter({ | |
| 302 | filePath: environment.path.join(environment.logDir, "server-child.log"), | |
| 303 | }).pipe(Effect.option); | |
| 304 | ||
| 305 | return Option.match(writer, { | |
| 306 | onNone: () => DesktopBackendOutputLogNoop, | |
| 307 | onSome: (logFile) => | |
| 308 | ({ | |
| 309 | writeSessionBoundary: Effect.fn( | |
| 310 | "desktop.observability.backendOutput.writeSessionBoundary", | |
| 311 | )(function* ({ phase, details }) { | |
| 312 | const runId = yield* currentDesktopRunId; | |
| 313 | yield* writeBackendChildLogRecord(logFile, { | |
| 314 | message: `backend child process session ${phase.toLowerCase()}`, | |
| 315 | level: "INFO", | |
| 316 | annotations: { | |
| 317 | component: "desktop-backend-child", | |
| 318 | runId, | |
| 319 | phase, | |
| 320 | details: sanitizeLogValue(details), | |
| 321 | }, | |
| 322 | }); | |
| 323 | }), | |
| 324 | writeOutputChunk: Effect.fn("desktop.observability.backendOutput.writeOutputChunk")( | |
| 325 | function* (streamName, chunk) { | |
| 326 | if (environment.isDevelopment) { | |
| 327 | yield* writeDevelopmentConsoleOutput(streamName, chunk); | |
| 328 | } | |
| 329 | const runId = yield* currentDesktopRunId; | |
| 330 | yield* writeBackendChildLogRecord(logFile, { | |
| 331 | message: "backend child process output", | |
| 332 | level: streamName === "stderr" ? "ERROR" : "INFO", | |
| 333 | annotations: { | |
| 334 | component: "desktop-backend-child", | |
| 335 | runId, | |
| 336 | stream: streamName, | |
| 337 | text: textDecoder.decode(chunk), | |
| 338 | }, | |
| 339 | }); | |
| 340 | }, | |
| 341 | ), | |
| 342 | }) satisfies DesktopBackendOutputLogShape, | |
| 343 | }); | |
| 344 | }), | |
| 345 | ); | |
| 346 | ||
| 347 | 77 | const desktopLoggerLayer = Layer.mergeAll( |
| 348 | 78 | Logger.layer([Logger.consolePretty(), Logger.tracerLogger], { mergeWithExisting: false }), |
| 349 | 79 | Layer.succeed(References.MinimumLogLevel, "Info"), |
| ... | ... | @@ -356,8 +86,8 @@ const tracerLayer = Layer.unwrap( |
| 356 | 86 | const tracePath = environment.path.join(environment.logDir, "desktop.trace.ndjson"); |
| 357 | 87 | const sink = yield* makeTraceSink({ |
| 358 | 88 | filePath: tracePath, |
| 359 | maxBytes: DESKTOP_LOG_FILE_MAX_BYTES, | |
| 360 | maxFiles: DESKTOP_LOG_FILE_MAX_FILES, | |
| 89 | maxBytes: DesktopBackendOutputLogModule.DESKTOP_LOG_FILE_MAX_BYTES, | |
| 90 | maxFiles: DesktopBackendOutputLogModule.DESKTOP_LOG_FILE_MAX_FILES, | |
| 361 | 91 | batchWindowMs: DESKTOP_TRACE_BATCH_WINDOW_MS, |
| 362 | 92 | }); |
| 363 | 93 | const delegate = Option.isNone(otlpTracesUrl) |
| ... | ... | @@ -375,8 +105,8 @@ const tracerLayer = Layer.unwrap( |
| 375 | 105 | }); |
| 376 | 106 | const tracer = yield* makeLocalFileTracer({ |
| 377 | 107 | filePath: tracePath, |
| 378 | maxBytes: DESKTOP_LOG_FILE_MAX_BYTES, | |
| 379 | maxFiles: DESKTOP_LOG_FILE_MAX_FILES, | |
| 108 | maxBytes: DesktopBackendOutputLogModule.DESKTOP_LOG_FILE_MAX_BYTES, | |
| 109 | maxFiles: DesktopBackendOutputLogModule.DESKTOP_LOG_FILE_MAX_FILES, | |
| 380 | 110 | batchWindowMs: DESKTOP_TRACE_BATCH_WINDOW_MS, |
| 381 | 111 | sink, |
| 382 | 112 | ...(delegate ? { delegate } : {}), |
| ... | ... | @@ -387,7 +117,7 @@ const tracerLayer = Layer.unwrap( |
| 387 | 117 | ).pipe(Layer.provideMerge(OtlpSerialization.layerJson)); |
| 388 | 118 | |
| 389 | 119 | export const layer = Layer.mergeAll( |
| 390 | backendOutputLogLayer, | |
| 120 | DesktopBackendOutputLogModule.layer, | |
| 391 | 121 | desktopLoggerLayer, |
| 392 | 122 | tracerLayer, |
| 393 | 123 | Layer.succeed(Tracer.MinimumTraceLevel, "Info"), |
apps/desktop/src/app/DesktopShutdown.ts created+35| ... | ... | @@ -0,0 +1,35 @@ |
| 1 | import * as Context from "effect/Context"; | |
| 2 | import * as Deferred from "effect/Deferred"; | |
| 3 | import * as Effect from "effect/Effect"; | |
| 4 | import * as Layer from "effect/Layer"; | |
| 5 | import * as Ref from "effect/Ref"; | |
| 6 | ||
| 7 | export class DesktopShutdown extends Context.Service< | |
| 8 | DesktopShutdown, | |
| 9 | { | |
| 10 | readonly request: Effect.Effect<void>; | |
| 11 | readonly awaitRequest: Effect.Effect<void>; | |
| 12 | readonly markComplete: Effect.Effect<void>; | |
| 13 | readonly awaitComplete: Effect.Effect<void>; | |
| 14 | readonly isComplete: Effect.Effect<boolean>; | |
| 15 | } | |
| 16 | >()("@t3tools/desktop/app/DesktopShutdown") {} | |
| 17 | ||
| 18 | const make = Effect.gen(function* () { | |
| 19 | const requested = yield* Deferred.make<void>(); | |
| 20 | const completed = yield* Deferred.make<void>(); | |
| 21 | const completedRef = yield* Ref.make(false); | |
| 22 | ||
| 23 | return DesktopShutdown.of({ | |
| 24 | request: Deferred.succeed(requested, undefined).pipe(Effect.asVoid), | |
| 25 | awaitRequest: Deferred.await(requested), | |
| 26 | markComplete: Ref.set(completedRef, true).pipe( | |
| 27 | Effect.andThen(Deferred.succeed(completed, undefined)), | |
| 28 | Effect.asVoid, | |
| 29 | ), | |
| 30 | awaitComplete: Deferred.await(completed), | |
| 31 | isComplete: Ref.get(completedRef), | |
| 32 | }); | |
| 33 | }); | |
| 34 | ||
| 35 | export const layer = Layer.effect(DesktopShutdown, make); |
apps/desktop/src/app/DesktopState.ts+12-14| ... | ... | @@ -3,19 +3,17 @@ import * as Effect from "effect/Effect"; |
| 3 | 3 | import * as Layer from "effect/Layer"; |
| 4 | 4 | import * as Ref from "effect/Ref"; |
| 5 | 5 | |
| 6 | export interface DesktopStateShape { | |
| 7 | readonly backendReady: Ref.Ref<boolean>; | |
| 8 | readonly quitting: Ref.Ref<boolean>; | |
| 9 | } | |
| 6 | export class DesktopState extends Context.Service< | |
| 7 | DesktopState, | |
| 8 | { | |
| 9 | readonly backendReady: Ref.Ref<boolean>; | |
| 10 | readonly quitting: Ref.Ref<boolean>; | |
| 11 | } | |
| 12 | >()("@t3tools/desktop/app/DesktopState") {} | |
| 10 | 13 | |
| 11 | export class DesktopState extends Context.Service<DesktopState, DesktopStateShape>()( | |
| 12 | "@t3tools/desktop/app/DesktopState", | |
| 13 | ) {} | |
| 14 | const make = Effect.all({ | |
| 15 | backendReady: Ref.make(false), | |
| 16 | quitting: Ref.make(false), | |
| 17 | }); | |
| 14 | 18 | |
| 15 | export const layer = Layer.effect( | |
| 16 | DesktopState, | |
| 17 | Effect.all({ | |
| 18 | backendReady: Ref.make(false), | |
| 19 | quitting: Ref.make(false), | |
| 20 | }), | |
| 21 | ); | |
| 19 | export const layer = Layer.effect(DesktopState, make); |
apps/desktop/src/backend/DesktopBackendManager.test.ts+5-5| ... | ... | @@ -104,9 +104,9 @@ function decodeBootstrap(raw: string) { |
| 104 | 104 | function makeManagerLayer(input: { |
| 105 | 105 | readonly spawnerLayer: Layer.Layer<ChildProcessSpawner.ChildProcessSpawner>; |
| 106 | 106 | readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>; |
| 107 | readonly backendOutputLog?: Partial<DesktopObservability.DesktopBackendOutputLogShape>; | |
| 108 | readonly desktopState?: DesktopState.DesktopStateShape; | |
| 109 | readonly desktopWindow?: Partial<DesktopWindow.DesktopWindowShape>; | |
| 107 | readonly backendOutputLog?: Partial<DesktopObservability.DesktopBackendOutputLog["Service"]>; | |
| 108 | readonly desktopState?: DesktopState.DesktopState["Service"]; | |
| 109 | readonly desktopWindow?: Partial<DesktopWindow.DesktopWindow["Service"]>; | |
| 110 | 110 | readonly config?: DesktopBackendManager.DesktopBackendStartConfig; |
| 111 | 111 | }) { |
| 112 | 112 | return DesktopBackendManager.layer.pipe( |
| ... | ... | @@ -127,7 +127,7 @@ function makeManagerLayer(input: { |
| 127 | 127 | writeSessionBoundary: () => Effect.void, |
| 128 | 128 | writeOutputChunk: () => Effect.void, |
| 129 | 129 | ...input.backendOutputLog, |
| 130 | } satisfies DesktopObservability.DesktopBackendOutputLogShape), | |
| 130 | } satisfies DesktopObservability.DesktopBackendOutputLog["Service"]), | |
| 131 | 131 | Layer.succeed(DesktopWindow.DesktopWindow, { |
| 132 | 132 | createMain: Effect.die("unexpected createMain"), |
| 133 | 133 | ensureMain: Effect.die("unexpected ensureMain"), |
| ... | ... | @@ -138,7 +138,7 @@ function makeManagerLayer(input: { |
| 138 | 138 | dispatchMenuAction: () => Effect.void, |
| 139 | 139 | syncAppearance: Effect.void, |
| 140 | 140 | ...input.desktopWindow, |
| 141 | } satisfies DesktopWindow.DesktopWindowShape), | |
| 141 | } satisfies DesktopWindow.DesktopWindow["Service"]), | |
| 142 | 142 | ), |
| 143 | 143 | ), |
| 144 | 144 | ); |
apps/desktop/src/main.ts+2-3| ... | ... | @@ -14,7 +14,6 @@ import { resolveRemoteT3CliPackageSpec } from "@t3tools/ssh/command"; |
| 14 | 14 | import type { RemoteT3RunnerOptions } from "@t3tools/ssh/tunnel"; |
| 15 | 15 | import serverPackageJson from "../../server/package.json" with { type: "json" }; |
| 16 | 16 | |
| 17 | import type { DesktopSettings as DesktopSettingsValue } from "./settings/DesktopAppSettings.ts"; | |
| 18 | 17 | import * as DesktopIpc from "./ipc/DesktopIpc.ts"; |
| 19 | 18 | import * as ElectronApp from "./electron/ElectronApp.ts"; |
| 20 | 19 | import * as ElectronDialog from "./electron/ElectronDialog.ts"; |
| ... | ... | @@ -68,8 +67,8 @@ const desktopEnvironmentLayer = Layer.unwrap( |
| 68 | 67 | ); |
| 69 | 68 | |
| 70 | 69 | const resolveDesktopSshCliRunner = ( |
| 71 | environment: DesktopEnvironment.DesktopEnvironmentShape, | |
| 72 | settings: DesktopSettingsValue, | |
| 70 | environment: DesktopEnvironment.DesktopEnvironment["Service"], | |
| 71 | settings: DesktopAppSettings.DesktopSettings, | |
| 73 | 72 | ): RemoteT3RunnerOptions => { |
| 74 | 73 | const devRemoteEntryPath = Option.getOrUndefined(environment.devRemoteT3ServerEntryPath); |
| 75 | 74 | if (environment.isDevelopment && devRemoteEntryPath !== undefined) { |
apps/desktop/src/preview/Manager.test.ts+2-2| ... | ... | @@ -59,7 +59,7 @@ const environmentLayer = Layer.succeed( |
| 59 | 59 | DesktopEnvironment.DesktopEnvironment, |
| 60 | 60 | DesktopEnvironment.DesktopEnvironment.of({ |
| 61 | 61 | browserArtifactsDir: "/tmp/t3/dev/browser-artifacts", |
| 62 | } as DesktopEnvironment.DesktopEnvironmentShape), | |
| 62 | } as DesktopEnvironment.DesktopEnvironment["Service"]), | |
| 63 | 63 | ); |
| 64 | 64 | |
| 65 | 65 | const fileSystemLayer = FileSystem.layerNoop({ |
| ... | ... | @@ -82,7 +82,7 @@ const layer = PreviewManager.layer.pipe( |
| 82 | 82 | |
| 83 | 83 | const withManager = <A>( |
| 84 | 84 | use: ( |
| 85 | manager: PreviewManager.PreviewManagerShape, | |
| 85 | manager: PreviewManager.PreviewManager["Service"], | |
| 86 | 86 | ) => Effect.Effect<A, PreviewManager.PreviewManagerError, Scope.Scope>, |
| 87 | 87 | ) => |
| 88 | 88 | Effect.gen(function* () { |
apps/desktop/src/shell/DesktopShellEnvironment.test.ts+1-1| ... | ... | @@ -64,7 +64,7 @@ function runShellEnvironment(input: { |
| 64 | 64 | DesktopEnvironment.DesktopEnvironment, |
| 65 | 65 | DesktopEnvironment.DesktopEnvironment.of({ |
| 66 | 66 | platform: input.platform, |
| 67 | } as DesktopEnvironment.DesktopEnvironmentShape), | |
| 67 | } as DesktopEnvironment.DesktopEnvironment["Service"]), | |
| 68 | 68 | ); |
| 69 | 69 | const spawnerLayer = Layer.succeed( |
| 70 | 70 | ChildProcessSpawner.ChildProcessSpawner, |
apps/desktop/src/updates/DesktopUpdates.ts+1-1| ... | ... | @@ -127,7 +127,7 @@ function parseAppUpdateYml(raw: string): Effect.Effect<Option.Option<AppUpdateYm |
| 127 | 127 | function createBaseUpdateState( |
| 128 | 128 | channel: DesktopUpdateChannel, |
| 129 | 129 | enabled: boolean, |
| 130 | environment: DesktopEnvironment.DesktopEnvironmentShape, | |
| 130 | environment: DesktopEnvironment.DesktopEnvironment["Service"], | |
| 131 | 131 | ): DesktopUpdateState { |
| 132 | 132 | return { |
| 133 | 133 | ...createInitialDesktopUpdateState(environment.appVersion, environment.runtimeInfo, channel), |
apps/desktop/src/window/DesktopWindow.test.ts+6-6| ... | ... | @@ -91,7 +91,7 @@ const desktopAssetsLayer = Layer.succeed(DesktopAssets.DesktopAssets, { |
| 91 | 91 | png: Option.none<string>(), |
| 92 | 92 | }), |
| 93 | 93 | resolveResourcePath: () => Effect.succeed(Option.none<string>()), |
| 94 | } satisfies DesktopAssets.DesktopAssetsShape); | |
| 94 | } satisfies DesktopAssets.DesktopAssets["Service"]); | |
| 95 | 95 | |
| 96 | 96 | const desktopServerExposureLayer = Layer.succeed(DesktopServerExposure.DesktopServerExposure, { |
| 97 | 97 | getState: Effect.die("unexpected getState"), |
| ... | ... | @@ -106,19 +106,19 @@ const desktopServerExposureLayer = Layer.succeed(DesktopServerExposure.DesktopSe |
| 106 | 106 | setMode: () => Effect.die("unexpected setMode"), |
| 107 | 107 | setTailscaleServeEnabled: () => Effect.die("unexpected setTailscaleServeEnabled"), |
| 108 | 108 | getAdvertisedEndpoints: Effect.die("unexpected getAdvertisedEndpoints"), |
| 109 | } satisfies DesktopServerExposure.DesktopServerExposureShape); | |
| 109 | } satisfies DesktopServerExposure.DesktopServerExposure["Service"]); | |
| 110 | 110 | |
| 111 | 111 | const electronMenuLayer = Layer.succeed(ElectronMenu.ElectronMenu, { |
| 112 | 112 | setApplicationMenu: () => Effect.void, |
| 113 | 113 | popupTemplate: () => Effect.void, |
| 114 | 114 | showContextMenu: () => Effect.succeed(Option.none()), |
| 115 | } satisfies ElectronMenu.ElectronMenuShape); | |
| 115 | } satisfies ElectronMenu.ElectronMenu["Service"]); | |
| 116 | 116 | |
| 117 | 117 | const electronThemeLayer = Layer.succeed(ElectronTheme.ElectronTheme, { |
| 118 | 118 | shouldUseDarkColors: Effect.succeed(false), |
| 119 | 119 | setSource: () => Effect.void, |
| 120 | 120 | onUpdated: () => Effect.void, |
| 121 | } satisfies ElectronTheme.ElectronThemeShape); | |
| 121 | } satisfies ElectronTheme.ElectronTheme["Service"]); | |
| 122 | 122 | |
| 123 | 123 | const desktopEnvironmentLayer = DesktopEnvironment.layer(environmentInput).pipe( |
| 124 | 124 | Layer.provide( |
| ... | ... | @@ -156,7 +156,7 @@ function makeTestLayer(input: { |
| 156 | 156 | sendAll: () => Effect.void, |
| 157 | 157 | destroyAll: Effect.void, |
| 158 | 158 | syncAllAppearance: (sync) => sync(input.window), |
| 159 | } satisfies ElectronWindow.ElectronWindowShape); | |
| 159 | } satisfies ElectronWindow.ElectronWindow["Service"]); | |
| 160 | 160 | |
| 161 | 161 | return DesktopWindow.layer.pipe( |
| 162 | 162 | Layer.provide( |
| ... | ... | @@ -173,7 +173,7 @@ function makeTestLayer(input: { |
| 173 | 173 | return true; |
| 174 | 174 | }), |
| 175 | 175 | copyText: () => Effect.void, |
| 176 | } satisfies ElectronShell.ElectronShellShape), | |
| 176 | } satisfies ElectronShell.ElectronShell["Service"]), | |
| 177 | 177 | electronThemeLayer, |
| 178 | 178 | electronWindowLayer, |
| 179 | 179 | Layer.mock(PreviewManager.PreviewManager)({ |