|
| 1 | +// @effect-diagnostics nodeBuiltinImport:off - userData must resolve synchronously before Electron is ready (the Clerk bridge registers privileged schemes). |
| 2 | +import * as NodeFs from "node:fs"; |
| 3 | + |
1 | 4 | import { createClerkBridge } from "@clerk/electron"; |
2 | 5 | import { storage } from "@clerk/electron/storage"; |
3 | 6 | import * as Context from "effect/Context"; |
4 | 7 | import * as Effect from "effect/Effect"; |
| 8 | +import * as FileSystem from "effect/FileSystem"; |
5 | 9 | import * as Layer from "effect/Layer"; |
6 | 10 | import * as Option from "effect/Option"; |
| 11 | +import * as PlatformError from "effect/PlatformError"; |
7 | 12 | import * as Schema from "effect/Schema"; |
8 | 13 | import * as Scope from "effect/Scope"; |
9 | 14 |
|
@@ -83,13 +88,64 @@ function createDesktopClerkBridge(stateDir: string, isDevelopment: boolean) { |
83 | 88 | }); |
84 | 89 | } |
85 | 90 |
|
| 91 | +const syncSystemErrorTag = (cause: unknown): PlatformError.SystemErrorTag => { |
| 92 | + switch ((cause as NodeJS.ErrnoException | undefined)?.code) { |
| 93 | + case "EEXIST": |
| 94 | + return "AlreadyExists"; |
| 95 | + case "ENOENT": |
| 96 | + return "NotFound"; |
| 97 | + case "EACCES": |
| 98 | + case "EPERM": |
| 99 | + return "PermissionDenied"; |
| 100 | + case "EBUSY": |
| 101 | + return "Busy"; |
| 102 | + default: |
| 103 | + return "Unknown"; |
| 104 | + } |
| 105 | +}; |
| 106 | + |
| 107 | +const syncFsCall = <A>(method: string, path: string, run: () => A) => |
| 108 | + Effect.try({ |
| 109 | + try: run, |
| 110 | + catch: (cause) => |
| 111 | + PlatformError.systemError({ |
| 112 | + _tag: syncSystemErrorTag(cause), |
| 113 | + module: "FileSystem", |
| 114 | + method, |
| 115 | + pathOrDescriptor: path, |
| 116 | + cause, |
| 117 | + }), |
| 118 | + }); |
| 119 | + |
| 120 | +/** |
| 121 | + * createClerkBridge calls protocol.registerSchemesAsPrivileged, which throws once Electron |
| 122 | + * is ready. The Windows userData resolution touches the disk; through the async Node |
| 123 | + * FileSystem it yields to the event loop and Electron emits ready first, so every Windows |
| 124 | + * launch exits with DesktopClerkBridgeInitializationError. Resolve through synchronous fs. |
| 125 | + */ |
| 126 | +const preReadySyncFileSystem = FileSystem.makeNoop({ |
| 127 | + exists: (path) => syncFsCall("exists", path, () => NodeFs.existsSync(path)), |
| 128 | + readFileString: (path) => |
| 129 | + syncFsCall("readFileString", path, () => NodeFs.readFileSync(path, "utf8")), |
| 130 | + makeDirectory: (path, options) => |
| 131 | + syncFsCall("makeDirectory", path, () => { |
| 132 | + NodeFs.mkdirSync(path, { recursive: options?.recursive ?? false }); |
| 133 | + }), |
| 134 | + writeFileString: (path, data, options) => |
| 135 | + syncFsCall("writeFileString", path, () => |
| 136 | + NodeFs.writeFileSync(path, data, { flag: options?.flag ?? "w" }), |
| 137 | + ), |
| 138 | +}); |
| 139 | + |
86 | 140 | /** @public Service construction is part of the canonical Effect module API. */ |
87 | 141 | export const make = Effect.gen(function* () { |
88 | 142 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
89 | 143 | const electronApp = yield* ElectronApp.ElectronApp; |
90 | 144 |
|
91 | 145 | // The SDK bridge acquires Electron's profile-scoped single-instance lock. |
92 | | - const userDataPath = yield* DesktopUserData.resolveUserDataPath(environment); |
| 146 | + const userDataPath = yield* DesktopUserData.resolveUserDataPath(environment).pipe( |
| 147 | + Effect.provideService(FileSystem.FileSystem, preReadySyncFileSystem), |
| 148 | + ); |
93 | 149 | yield* electronApp.setPath("userData", userDataPath); |
94 | 150 |
|
95 | 151 | const bridge = yield* Effect.acquireRelease( |
|
0 commit comments