Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 0 additions & 1 deletion apps/desktop/src/app/DesktopAppIdentity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ const makeElectronAppLayer = (calls: ElectronAppCalls) =>
calls.setAboutPanelOptions.push(options);
}),
setAppUserModelId: () => Effect.void,
requestSingleInstanceLock: Effect.succeed(true),
getAppMetrics: Effect.succeed([]),
isDefaultProtocolClient: () => Effect.succeed(false),
setAsDefaultProtocolClient: () => Effect.succeed(true),
Expand Down
63 changes: 61 additions & 2 deletions apps/desktop/src/app/DesktopClerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ vi.mock("@clerk/electron/storage", () => ({
storage: storageMock,
}));

import * as Exit from "effect/Exit";
import * as ElectronApp from "../electron/ElectronApp.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
import * as DesktopClerk from "./DesktopClerk.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";

Expand Down Expand Up @@ -56,7 +59,7 @@ describe("DesktopClerk", () => {
it.effect("acquires and releases the SDK bridge with the layer", () => {
const cleanup = vi.fn();
storageMock.mockReturnValue(storageAdapter);
createClerkBridgeMock.mockReturnValue({ cleanup });
createClerkBridgeMock.mockReturnValue({ cleanup, isPrimaryInstance: true });

return Effect.gen(function* () {
yield* Effect.scoped(Layer.build(makeDesktopClerkLayer()));
Expand Down Expand Up @@ -124,11 +127,67 @@ describe("DesktopClerk", () => {
});
});

it.effect("registers the second-instance handler in the primary instance", () => {
storageMock.mockReturnValue(storageAdapter);
createClerkBridgeMock.mockReturnValue({ cleanup: vi.fn(), isPrimaryInstance: true });
const quit = vi.fn();
const registeredEvents: string[] = [];
const electronApp = {
quit: Effect.sync(quit),
on: (eventName: string) =>
Effect.sync(() => {
registeredEvents.push(eventName);
}),
} as unknown as ElectronApp.ElectronApp["Service"];
const electronWindow = {} as ElectronWindow.ElectronWindow["Service"];

return Effect.gen(function* () {
const clerk = yield* DesktopClerk.DesktopClerk;
const exit = yield* Effect.exit(Effect.scoped(clerk.configure));

assert.isTrue(Exit.isSuccess(exit));
assert.equal(quit.mock.calls.length, 0);
assert.deepEqual(registeredEvents, ["second-instance"]);
}).pipe(
Effect.provide(makeDesktopClerkLayer()),
Effect.provideService(ElectronApp.ElectronApp, electronApp),
Effect.provideService(ElectronWindow.ElectronWindow, electronWindow),
);
});

it.effect("quits and interrupts startup in a secondary instance", () => {
storageMock.mockReturnValue(storageAdapter);
createClerkBridgeMock.mockReturnValue({ cleanup: vi.fn(), isPrimaryInstance: false });
const quit = vi.fn();
const registeredEvents: string[] = [];
const electronApp = {
quit: Effect.sync(quit),
on: (eventName: string) =>
Effect.sync(() => {
registeredEvents.push(eventName);
}),
} as unknown as ElectronApp.ElectronApp["Service"];
const electronWindow = {} as ElectronWindow.ElectronWindow["Service"];

return Effect.gen(function* () {
const clerk = yield* DesktopClerk.DesktopClerk;
const exit = yield* Effect.exit(Effect.scoped(clerk.configure));

assert.isTrue(Exit.hasInterrupts(exit));
assert.equal(quit.mock.calls.length, 1);
assert.deepEqual(registeredEvents, []);
}).pipe(
Effect.provide(makeDesktopClerkLayer()),
Effect.provideService(ElectronApp.ElectronApp, electronApp),
Effect.provideService(ElectronWindow.ElectronWindow, electronWindow),
);
});

it.each([
{ isDevelopment: true, scheme: "t3code-dev" },
{ isDevelopment: false, scheme: "t3code" },
])("configures the SDK with the $scheme renderer origin", ({ isDevelopment, scheme }) => {
const bridge = { cleanup: vi.fn() };
const bridge = { cleanup: vi.fn(), isPrimaryInstance: true };
storageMock.mockReturnValue(storageAdapter);
createClerkBridgeMock.mockReturnValue(bridge);

Expand Down
9 changes: 7 additions & 2 deletions apps/desktop/src/app/DesktopClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function createDesktopClerkBridge(stateDir: string, isDevelopment: boolea

export const make = Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
yield* Effect.acquireRelease(
const bridge = yield* Effect.acquireRelease(
Effect.try({
try: () => createDesktopClerkBridge(environment.stateDir, environment.isDevelopment),
catch: (cause) =>
Expand Down Expand Up @@ -113,7 +113,12 @@ export const make = Effect.gen(function* () {
const context = yield* Effect.context<ElectronWindow.ElectronWindow>();
const runPromise = Effect.runPromiseWith(context);

if (!(yield* electronApp.requestSingleInstanceLock)) {
// The SDK bridge holds Electron's single-instance lock (acquired at
// bridge creation) so OAuth deep-link callbacks on Windows/Linux are
// forwarded to the running app. In a secondary instance the bridge has
// already begun quitting the app; app.quit() is asynchronous, so stop
// bootstrap here before whenReady can fire.
if (!bridge.isPrimaryInstance) {
Comment thread
cursor[bot] marked this conversation as resolved.
yield* electronApp.quit;
return yield* Effect.interrupt;
}
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/app/DesktopLifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ describe("DesktopLifecycle", () => {
setName: () => Effect.void,
setAboutPanelOptions: () => Effect.void,
setAppUserModelId: () => Effect.void,
requestSingleInstanceLock: Effect.succeed(true),
getAppMetrics: Effect.succeed([]),
isDefaultProtocolClient: () => Effect.succeed(false),
setAsDefaultProtocolClient: () => Effect.succeed(true),
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/electron/ElectronApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const {
quitMock,
relaunchMock,
removeListenerMock,
requestSingleInstanceLockMock,
setAboutPanelOptionsMock,
setAppUserModelIdMock,
setAsDefaultProtocolClientMock,
Expand All @@ -35,7 +34,6 @@ const {
quitMock: vi.fn(),
relaunchMock: vi.fn(),
removeListenerMock: vi.fn(),
requestSingleInstanceLockMock: vi.fn(() => true),
setAboutPanelOptionsMock: vi.fn(),
setAppUserModelIdMock: vi.fn(),
setAsDefaultProtocolClientMock: vi.fn(() => true),
Expand Down Expand Up @@ -67,7 +65,6 @@ vi.mock("electron", () => ({
quit: quitMock,
relaunch: relaunchMock,
removeListener: removeListenerMock,
requestSingleInstanceLock: requestSingleInstanceLockMock,
runningUnderARM64Translation: false,
setAboutPanelOptions: setAboutPanelOptionsMock,
setAsDefaultProtocolClient: setAsDefaultProtocolClientMock,
Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src/electron/ElectronApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ export class ElectronApp extends Context.Service<
options: Electron.AboutPanelOptionsOptions,
) => Effect.Effect<void>;
readonly setAppUserModelId: (id: string) => Effect.Effect<void>;
readonly requestSingleInstanceLock: Effect.Effect<boolean>;
readonly getAppMetrics: Effect.Effect<ReadonlyArray<Electron.ProcessMetric>>;
readonly isDefaultProtocolClient: (protocol: string) => Effect.Effect<boolean>;
readonly setAsDefaultProtocolClient: (
Expand Down Expand Up @@ -153,7 +152,6 @@ export const make = ElectronApp.of({
Effect.sync(() => {
Electron.app.setAppUserModelId(id);
}),
requestSingleInstanceLock: Effect.sync(() => Electron.app.requestSingleInstanceLock()),
getAppMetrics: Effect.sync(() => Electron.app.getAppMetrics()),
isDefaultProtocolClient: (protocol) =>
Effect.sync(() => Electron.app.isDefaultProtocolClient(protocol)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ function makeElectronAppLayer(
setName: () => Effect.void,
setAboutPanelOptions: () => Effect.void,
setAppUserModelId: () => Effect.void,
requestSingleInstanceLock: Effect.succeed(true),
getAppMetrics: Effect.sync(() => {
onMetricsRead();
return metrics;
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/window/DesktopApplicationMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, {
setName: () => Effect.void,
setAboutPanelOptions: () => Effect.void,
setAppUserModelId: () => Effect.void,
requestSingleInstanceLock: Effect.succeed(true),
getAppMetrics: Effect.succeed([]),
isDefaultProtocolClient: () => Effect.succeed(false),
setAsDefaultProtocolClient: () => Effect.succeed(true),
Expand Down
Loading
Loading