Просмотр исходного кода

Merge remote-tracking branch 'origin/feat/directory-picker' into feat/workspace-directory-browser

creatixchu 2 месяцев назад
Родитель
Сommit
38b5cb9b25

+ 13 - 6
packages/host/directory-picker-native/src/client/index.ts

@@ -62,12 +62,19 @@ export const inject = ['slots', 'workspaces']
 export function apply(ctx: ClientContext): void {
   const injected = (): NativeFlowInjected => ({ pick: () => ctx.workspaces.pickDirectory() })
   ctx.effect(() => {
-    const deferred = [
-      deferRegistration(ctx.slots, 'conversation.hero.workspace.directoryFlow', NativeDirectoryFlow, () =>
-        ctx.slots.register({ name: 'conversation.hero.workspace.directoryFlow', inject: injected }, NativeDirectoryFlow)),
-      deferRegistration(ctx.slots, 'sidebar.workspaces.directoryFlow', NativeDirectoryFlow, () =>
-        ctx.slots.register({ name: 'sidebar.workspaces.directoryFlow', inject: injected }, NativeDirectoryFlow)),
-    ]
+    // Constructing the pair can throw halfway (a declared hole already
+    // occupied registers synchronously): roll the earlier deferral back so
+    // no live subscription outlives the failed fiber.
+    const deferred: ReturnType<typeof deferRegistration>[] = []
+    try {
+      deferred.push(deferRegistration(ctx.slots, 'conversation.hero.workspace.directoryFlow', NativeDirectoryFlow, () =>
+        ctx.slots.register({ name: 'conversation.hero.workspace.directoryFlow', inject: injected }, NativeDirectoryFlow)))
+      deferred.push(deferRegistration(ctx.slots, 'sidebar.workspaces.directoryFlow', NativeDirectoryFlow, () =>
+        ctx.slots.register({ name: 'sidebar.workspaces.directoryFlow', inject: injected }, NativeDirectoryFlow)))
+    } catch (error) {
+      for (const entry of deferred) entry.dispose()
+      throw error
+    }
     return () => { for (const entry of deferred) entry.dispose() }
   }, 'directory-picker-native: flow registrations')
 }

+ 25 - 0
packages/host/directory-picker-native/tests/client-flow.spec.tsx

@@ -55,6 +55,31 @@ describe('directory-picker-native client half', () => {
     for (const hole of HOLES) expect(after.slots.entries(hole)).toHaveLength(1)
   })
 
+  it('rolls back the first deferral when the second hole is already occupied', async () => {
+    const b = await bench()
+    b.declare()
+    // Foreign occupant in the SECOND registered hole: the pair construction
+    // throws after the first deferral installed its subscription.
+    b.slots.register({ name: HOLES[1] } as never, () => null)
+    const rejections: unknown[] = []
+    const onUnhandled = (reason: unknown): void => { rejections.push(reason) }
+    process.on('unhandledRejection', onUnhandled)
+    try {
+      const fiber = b.ctx.plugin({ inject: [...inject], apply })
+      await expect(fiber.await()).rejects.toThrow(/already has a registration/)
+      // A leaked first deferral would now race this probe registration and
+      // throw from its orphaned subscription against the HERO hole; the
+      // rollback leaves only the activation failure itself (cordis re-raises
+      // the apply throw as a late rejection — installFailLoud's contract).
+      const disposeProbe = b.slots.register({ name: HOLES[0] } as never, () => null)
+      await new Promise(resolve => setTimeout(resolve, 20))
+      expect(rejections.map(String).filter(text => text.includes(HOLES[0]))).toEqual([])
+      disposeProbe()
+    } finally {
+      process.off('unhandledRejection', onUnhandled)
+    }
+  })
+
   it('rejects a second flow occupant at load (single-kind hole)', async () => {
     const b = await bench()
     b.declare()