@@ -10,15 +10,17 @@ import * as Effect from "effect/Effect";
1010import * as Layer from "effect/Layer" ;
1111import { beforeEach , vi } from "vite-plus/test" ;
1212
13- const { createClerkBridgeMock, storageAdapter, storageMock } = vi . hoisted ( ( ) => ( {
14- createClerkBridgeMock : vi . fn ( ) ,
15- storageAdapter : {
16- getItem : vi . fn ( ) ,
17- setItem : vi . fn ( ) ,
18- removeItem : vi . fn ( ) ,
19- } ,
20- storageMock : vi . fn ( ) ,
21- } ) ) ;
13+ const { createClerkBridgeMock, requestSingleInstanceLockMock, storageAdapter, storageMock } =
14+ vi . hoisted ( ( ) => ( {
15+ createClerkBridgeMock : vi . fn ( ) ,
16+ requestSingleInstanceLockMock : vi . fn ( ( ) => true ) ,
17+ storageAdapter : {
18+ getItem : vi . fn ( ) ,
19+ setItem : vi . fn ( ) ,
20+ removeItem : vi . fn ( ) ,
21+ } ,
22+ storageMock : vi . fn ( ) ,
23+ } ) ) ;
2224
2325vi . mock ( "@clerk/electron" , ( ) => ( {
2426 createClerkBridge : createClerkBridgeMock ,
@@ -56,6 +58,7 @@ const layerDesktopClerk = (
5658 openSystemSettings : ( ) => Effect . succeed ( false ) ,
5759 copyText : ( ) => Effect . void ,
5860 } ,
61+ listeners = new Map < string , ( ...args : unknown [ ] ) => void > ( ) ,
5962) => {
6063 const environment = DesktopEnvironment . DesktopEnvironment . of ( {
6164 stateDir : "/tmp/t3-state" ,
@@ -69,6 +72,24 @@ const layerDesktopClerk = (
6972 Effect . sync ( ( ) => {
7073 events . push ( `setPath:${ name } :${ value } ` ) ;
7174 } ) ,
75+ requestSingleInstanceLock : Effect . sync ( ( ) => {
76+ events . push ( "requestSingleInstanceLock" ) ;
77+ return requestSingleInstanceLockMock ( ) ;
78+ } ) ,
79+ quit : Effect . sync ( ( ) => {
80+ events . push ( "quit" ) ;
81+ } ) ,
82+ on : ( name : string , listener : ( ...args : unknown [ ] ) => void ) =>
83+ Effect . acquireRelease (
84+ Effect . sync ( ( ) => {
85+ events . push ( `on:${ name } ` ) ;
86+ listeners . set ( name , listener ) ;
87+ } ) ,
88+ ( ) =>
89+ Effect . sync ( ( ) => {
90+ listeners . delete ( name ) ;
91+ } ) ,
92+ ) . pipe ( Effect . asVoid ) ,
7293 } as unknown as ElectronApp . ElectronApp [ "Service" ] ;
7394
7495 return DesktopClerk . layer . pipe (
@@ -87,6 +108,7 @@ const layerDesktopClerk = (
87108describe ( "DesktopClerk" , ( ) => {
88109 beforeEach ( ( ) => {
89110 createClerkBridgeMock . mockReset ( ) ;
111+ requestSingleInstanceLockMock . mockReset ( ) . mockReturnValue ( true ) ;
90112 storageMock . mockReset ( ) ;
91113 } ) ;
92114
@@ -106,22 +128,37 @@ describe("DesktopClerk", () => {
106128 [
107129 {
108130 storage : storageAdapter ,
131+ manageSingleInstanceLock : false ,
109132 passkeys : true ,
110133 renderer : { scheme : "t3code-dev" , host : "app" } ,
111134 } ,
112135 ] ,
113136 ] ) ;
114137 assert . equal ( cleanup . mock . calls . length , 1 ) ;
115- // The bridge acquires Electron's single-instance lock at creation, and
116- // the lock both lives in and creates the userData directory — so the
117- // real path must be set before the bridge exists.
118- assert . deepEqual ( events , [ "setPath:userData:/tmp/app-data/t3code-dev" , "createClerkBridge" ] ) ;
138+ assert . deepEqual ( events , [
139+ "setPath:userData:/tmp/app-data/t3code-dev" ,
140+ "requestSingleInstanceLock" ,
141+ "on:second-instance" ,
142+ "createClerkBridge" ,
143+ ] ) ;
119144 storageMock . mockClear ( ) ;
120145 createClerkBridgeMock . mockClear ( ) ;
121146 } ) ;
122147 } ) ;
123148
124149 it . each ( [
150+ {
151+ name : "packaged macOS" ,
152+ isDevelopment : false ,
153+ platform : "darwin" as const ,
154+ userData : "/tmp/app-data/t3code-v2" ,
155+ } ,
156+ {
157+ name : "packaged Linux" ,
158+ isDevelopment : false ,
159+ platform : "linux" as const ,
160+ userData : "/tmp/app-data/t3code-v2" ,
161+ } ,
125162 {
126163 name : "packaged Windows" ,
127164 isDevelopment : false ,
@@ -152,7 +189,12 @@ describe("DesktopClerk", () => {
152189 ) ,
153190 ) ;
154191
155- assert . deepEqual ( events , [ `setPath:userData:${ userData } ` , "createClerkBridge" ] ) ;
192+ assert . deepEqual ( events , [
193+ `setPath:userData:${ userData } ` ,
194+ "requestSingleInstanceLock" ,
195+ "on:second-instance" ,
196+ "createClerkBridge" ,
197+ ] ) ;
156198 } ,
157199 ) ;
158200
@@ -204,63 +246,122 @@ describe("DesktopClerk", () => {
204246 } ) ;
205247 } ) ;
206248
207- it . effect ( "registers the second-instance handler in the primary instance" , ( ) => {
208- storageMock . mockReturnValue ( storageAdapter ) ;
209- createClerkBridgeMock . mockReturnValue ( { cleanup : vi . fn ( ) , isPrimaryInstance : true } ) ;
210- const quit = vi . fn ( ) ;
211- const registeredEvents : string [ ] = [ ] ;
212- const electronApp = {
213- quit : Effect . sync ( quit ) ,
214- on : ( eventName : string ) =>
215- Effect . sync ( ( ) => {
216- registeredEvents . push ( eventName ) ;
217- } ) ,
218- } as unknown as ElectronApp . ElectronApp [ "Service" ] ;
219- const electronWindow = { } as ElectronWindow . ElectronWindow [ "Service" ] ;
249+ it . effect . each ( [ "before configure" , "after configure" ] as const ) (
250+ "reveals the running window when a second instance launches %s" ,
251+ ( entry ) => {
252+ storageMock . mockReturnValue ( storageAdapter ) ;
253+ createClerkBridgeMock . mockReturnValue ( { cleanup : vi . fn ( ) , isPrimaryInstance : true } ) ;
254+ const quit = vi . fn ( ) ;
255+ const listeners = new Map < string , ( ...args : unknown [ ] ) => void > ( ) ;
256+ const revealed = Promise . withResolvers < unknown > ( ) ;
257+ const window = { id : 1 } ;
258+ const electronApp = {
259+ quit : Effect . sync ( quit ) ,
260+ on : ( eventName : string , listener : ( ...args : unknown [ ] ) => void ) =>
261+ Effect . sync ( ( ) => {
262+ listeners . set ( eventName , listener ) ;
263+ } ) ,
264+ } as unknown as ElectronApp . ElectronApp [ "Service" ] ;
265+ const electronWindow = {
266+ currentMainOrFirst : Effect . succeedSome ( window ) ,
267+ reveal : ( value : unknown ) => Effect . sync ( ( ) => revealed . resolve ( value ) ) ,
268+ } as unknown as ElectronWindow . ElectronWindow [ "Service" ] ;
220269
221- return Effect . gen ( function * ( ) {
222- const clerk = yield * DesktopClerk . DesktopClerk ;
223- const exit = yield * Effect . exit ( Effect . scoped ( clerk . configure ) ) ;
270+ return Effect . gen ( function * ( ) {
271+ const clerk = yield * DesktopClerk . DesktopClerk ;
272+ if ( entry === "before configure" ) listeners . get ( "second-instance" ) ?.( { } , [ "t3" ] ) ;
273+ yield * clerk . configure ;
224274
225- assert . isTrue ( Exit . isSuccess ( exit ) ) ;
226- assert . equal ( quit . mock . calls . length , 0 ) ;
227- assert . deepEqual ( registeredEvents , [ "open-url" , "open-file" , "second-instance" ] ) ;
228- } ) . pipe (
229- Effect . provide ( layerDesktopClerk ( ) ) ,
230- Effect . provideService ( ElectronApp . ElectronApp , electronApp ) ,
231- Effect . provideService ( ElectronWindow . ElectronWindow , electronWindow ) ,
232- Effect . provideService ( DesktopWebLinks . DesktopWebLinks , ignoreWebLinks ) ,
233- ) ;
234- } ) ;
275+ assert . equal ( quit . mock . calls . length , 0 ) ;
276+ if ( entry === "after configure" ) listeners . get ( "second-instance" ) ! ( { } , [ "t3" ] ) ;
277+ assert . strictEqual ( yield * Effect . promise ( ( ) => revealed . promise ) , window ) ;
278+ } ) . pipe (
279+ Effect . scoped ,
280+ Effect . provide ( layerDesktopClerk ( true , [ ] , "darwin" , undefined , undefined , listeners ) ) ,
281+ Effect . provideService ( ElectronApp . ElectronApp , electronApp ) ,
282+ Effect . provideService ( ElectronWindow . ElectronWindow , electronWindow ) ,
283+ Effect . provideService ( DesktopWebLinks . DesktopWebLinks , ignoreWebLinks ) ,
284+ ) ;
285+ } ,
286+ ) ;
235287
236- it . effect ( "quits and interrupts startup in a secondary instance " , ( ) => {
288+ it . effect ( "delivers queued startup links and later links once in arrival order " , ( ) => {
237289 storageMock . mockReturnValue ( storageAdapter ) ;
238- createClerkBridgeMock . mockReturnValue ( { cleanup : vi . fn ( ) , isPrimaryInstance : false } ) ;
239- const quit = vi . fn ( ) ;
240- const registeredEvents : string [ ] = [ ] ;
290+ createClerkBridgeMock . mockReturnValue ( { cleanup : vi . fn ( ) , isPrimaryInstance : true } ) ;
291+ const listeners = new Map < string , ( ...args : unknown [ ] ) => void > ( ) ;
292+ const received : string [ ] = [ ] ;
293+ const first = "t3code://new?prompt=first" ;
294+ const second = "t3code://threads/env-1/thread-1" ;
295+ const later = "t3code://new?prompt=later" ;
296+ const delivered = Promise . withResolvers < void > ( ) ;
241297 const electronApp = {
242- quit : Effect . sync ( quit ) ,
243- on : ( eventName : string ) =>
298+ on : ( name : string , listener : ( ...args : unknown [ ] ) => void ) =>
244299 Effect . sync ( ( ) => {
245- registeredEvents . push ( eventName ) ;
300+ listeners . set ( name , listener ) ;
246301 } ) ,
247302 } as unknown as ElectronApp . ElectronApp [ "Service" ] ;
248- const electronWindow = { } as ElectronWindow . ElectronWindow [ "Service" ] ;
249303
250304 return Effect . gen ( function * ( ) {
251305 const clerk = yield * DesktopClerk . DesktopClerk ;
252- const exit = yield * Effect . exit ( Effect . scoped ( clerk . configure ) ) ;
306+ listeners . get ( "second-instance" ) ?.( { } , [ "t3" , first ] ) ;
307+ listeners . get ( "second-instance" ) ?.( { } , [ "t3" , second ] ) ;
308+ assert . deepEqual ( received , [ ] ) ;
253309
254- assert . isTrue ( Exit . hasInterrupts ( exit ) ) ;
255- assert . equal ( quit . mock . calls . length , 1 ) ;
256- assert . deepEqual ( registeredEvents , [ ] ) ;
310+ yield * clerk . configure ;
311+ listeners . get ( "second-instance" ) ! ( { } , [ "t3" , later ] ) ;
312+ yield * Effect . promise ( ( ) => delivered . promise ) ;
313+
314+ assert . deepEqual ( received , [ first , second , later ] ) ;
257315 } ) . pipe (
258- Effect . provide ( layerDesktopClerk ( ) ) ,
316+ Effect . scoped ,
317+ Effect . provide ( layerDesktopClerk ( true , [ ] , "darwin" , undefined , undefined , listeners ) ) ,
259318 Effect . provideService ( ElectronApp . ElectronApp , electronApp ) ,
260- Effect . provideService ( ElectronWindow . ElectronWindow , electronWindow ) ,
261- Effect . provideService ( DesktopWebLinks . DesktopWebLinks , ignoreWebLinks ) ,
319+ Effect . provideService (
320+ ElectronWindow . ElectronWindow ,
321+ { } as ElectronWindow . ElectronWindow [ "Service" ] ,
322+ ) ,
323+ Effect . provideService (
324+ DesktopWebLinks . DesktopWebLinks ,
325+ DesktopWebLinks . DesktopWebLinks . of ( {
326+ receive : ( url ) =>
327+ Effect . sync ( ( ) => {
328+ received . push ( url ) ;
329+ if ( url === later ) delivered . resolve ( ) ;
330+ } ) ,
331+ setRendererReady : ( ) => Effect . void ,
332+ } ) ,
333+ ) ,
262334 ) ;
263335 } ) ;
336+
337+ it . effect . each ( [ "darwin" , "win32" , "linux" ] as const ) (
338+ "quits a second launch before initializing Clerk or the application runtime on %s" ,
339+ ( platform ) =>
340+ Effect . gen ( function * ( ) {
341+ requestSingleInstanceLockMock . mockReturnValueOnce ( false ) ;
342+ createClerkBridgeMock . mockReturnValue ( { cleanup : vi . fn ( ) , isPrimaryInstance : true } ) ;
343+ const events : string [ ] = [ ] ;
344+ const runtime = layerDesktopClerk ( false , events , platform ) . pipe (
345+ Layer . flatMap ( ( ) =>
346+ Layer . effectDiscard (
347+ Effect . sync ( ( ) => {
348+ events . push ( "initializeApplicationRuntime" ) ;
349+ } ) ,
350+ ) ,
351+ ) ,
352+ ) ;
353+ const exit = yield * Effect . exit ( Effect . scoped ( Layer . build ( runtime ) ) ) ;
354+
355+ assert . isTrue ( Exit . hasInterrupts ( exit ) ) ;
356+ assert . deepEqual ( events , [
357+ "setPath:userData:/tmp/app-data/t3code-v2" ,
358+ "requestSingleInstanceLock" ,
359+ "quit" ,
360+ ] ) ;
361+ assert . equal ( createClerkBridgeMock . mock . calls . length , 0 ) ;
362+ assert . equal ( storageMock . mock . calls . length , 0 ) ;
363+ } ) ,
364+ ) ;
264365} ) ;
265366
266367it . effect (
@@ -302,15 +403,15 @@ it.effect(
302403 assert . equal ( event . preventDefault . mock . calls . length , 1 ) ;
303404 } ) . pipe (
304405 Effect . scoped ,
305- Effect . provide ( layerDesktopClerk ( ) ) ,
406+ Effect . provide ( layerDesktopClerk ( true , [ ] , "darwin" , undefined , undefined , listeners ) ) ,
306407 Effect . provideService ( ElectronApp . ElectronApp , electronApp ) ,
307408 Effect . provideService ( ElectronWindow . ElectronWindow , electronWindow ) ,
308409 Effect . provideService ( DesktopWebLinks . DesktopWebLinks , ignoreWebLinks ) ,
309410 ) ;
310411 } ,
311412) ;
312413
313- it . effect . each ( [ "startup" , "open-url" ] as const ) (
414+ it . effect . each ( [ "startup" , "open-url" , "early-second-instance" ] as const ) (
314415 "receives hosted web sign-in through the desktop %s handler" ,
315416 ( entry ) =>
316417 Effect . gen ( function * ( ) {
@@ -369,6 +470,9 @@ it.effect.each(["startup", "open-url"] as const)(
369470 } as unknown as ElectronApp . ElectronApp [ "Service" ] ;
370471 yield * Effect . gen ( function * ( ) {
371472 const clerk = yield * DesktopClerk . DesktopClerk ;
473+ if ( entry === "early-second-instance" ) {
474+ listeners . get ( "second-instance" ) ?.( { } , [ "t3" , link ] ) ;
475+ }
372476 yield * clerk . configure ;
373477 if ( entry === "open-url" ) {
374478 const event = { preventDefault : vi . fn ( ) } ;
@@ -381,7 +485,7 @@ it.effect.each(["startup", "open-url"] as const)(
381485 assert . strictEqual ( delivery ?. flowId , request . flowId ) ;
382486 assert . strictEqual ( delivery ?. returnUrl , request . returnUrl ) ;
383487 } ) . pipe (
384- Effect . provide ( layerDesktopClerk ( true , [ ] , "darwin" , undefined , shell ) ) ,
488+ Effect . provide ( layerDesktopClerk ( true , [ ] , "darwin" , undefined , shell , listeners ) ) ,
385489 Effect . provideService ( HostProcess . Arguments , entry === "startup" ? [ "t3" , link ] : [ "t3" ] ) ,
386490 Effect . provideService ( ElectronApp . ElectronApp , electronApp ) ,
387491 Effect . provideService (
@@ -428,7 +532,7 @@ it.effect("hands web and app links to the renderer and leaves other links alone"
428532 "t3code://threads/env-1/thread-1" ,
429533 ] ) ;
430534 } ) . pipe (
431- Effect . provide ( layerDesktopClerk ( true , [ ] , "darwin" ) ) ,
535+ Effect . provide ( layerDesktopClerk ( true , [ ] , "darwin" , undefined , undefined , listeners ) ) ,
432536 Effect . provideService ( HostProcess . Arguments , [ "t3" ] ) ,
433537 Effect . provideService ( ElectronApp . ElectronApp , electronApp ) ,
434538 Effect . provideService (
0 commit comments