Răsfoiți Sursa

Probe invariant readiness on raw fibers

Hypatia May 1 lună în urmă
părinte
comite
a4747f43bd
2 a modificat fișierele cu 69 adăugiri și 10 ștergeri
  1. 61 6
      scripts/test-invariants.spec.ts
  2. 8 4
      scripts/test-invariants.ts

+ 61 - 6
scripts/test-invariants.spec.ts

@@ -39,6 +39,18 @@ function requiredConfig() {
   })
 }
 
+function queuedReadinessConfig(
+  ctx: Context,
+  onPublished: (dispose: () => void) => void,
+) {
+  return z.transform(z.any(), () => {
+    queueMicrotask(() => {
+      onPublished(ctx.provide(TEST_INVARIANT_READY_SERVICE, true))
+    })
+    return {}
+  }, true)
+}
+
 function invalidConfigApply(): never {
   throw new Error('invalid plugin apply executed')
 }
@@ -186,12 +198,9 @@ describe('global test invariant host', () => {
         const plugin = {
           apply,
           Config: z.intersect([
-            z.transform(z.any(), () => {
-              queueMicrotask(() => {
-                disposeQueuedReadiness = ctx.provide(TEST_INVARIANT_READY_SERVICE, true)
-              })
-              return {}
-            }, true),
+            queuedReadinessConfig(ctx, (dispose) => {
+              disposeQueuedReadiness = dispose
+            }),
             requiredConfig(),
           ]),
         }
@@ -216,6 +225,52 @@ describe('global test invariant host', () => {
     )
   })
 
+  it('retains a valid plugin failure when readiness wins the initial-probe race', async () => {
+    await withDelayedFirstCompanion(
+      async ({ started, release }) => {
+        const ctx = new Context()
+        const failure = new Error('valid plugin apply failed')
+        const applied = deferred()
+        const apply = vi.fn(function validConfigApply() {
+          applied.resolve()
+          throw failure
+        })
+        let disposeQueuedReadiness: (() => void) | undefined
+        const plugin = {
+          apply,
+          Config: queuedReadinessConfig(ctx, (dispose) => {
+            disposeQueuedReadiness = dispose
+          }),
+        }
+
+        const fiber = ctx.plugin(plugin, {})
+        const returnedError = rejectionOf(fiber)
+        try {
+          await Promise.all([started, applied.promise])
+          expect(fiber.state).toBe(FiberState.FAILED)
+          expect(apply).toHaveBeenCalledOnce()
+          expect(ctx.registry.has(plugin)).toBe(true)
+          expect(ctx.registry.get(plugin)?.fibers).toHaveLength(1)
+
+          if (disposeQueuedReadiness === undefined) throw new Error('queued readiness was not published')
+          Reflect.deleteProperty(fiber.inject, TEST_INVARIANT_READY_SERVICE)
+          disposeQueuedReadiness()
+          release()
+
+          expect(await returnedError).toBe(failure)
+          expect(fiber.state).toBe(FiberState.FAILED)
+          expect(apply).toHaveBeenCalledOnce()
+          expect(ctx.registry.has(plugin)).toBe(true)
+          expect(ctx.registry.get(plugin)?.fibers).toHaveLength(1)
+        } finally {
+          Reflect.deleteProperty(fiber.inject, TEST_INVARIANT_READY_SERVICE)
+          disposeQueuedReadiness?.()
+          release()
+        }
+      },
+    )
+  })
+
   it('holds a root plugin until every lazy companion is active, then permits nested startup', async () => {
     const delayedStarted = deferred()
     const releaseDelayed = deferred()

+ 8 - 4
scripts/test-invariants.ts

@@ -85,7 +85,7 @@ RegistryService.prototype.plugin = function(plugin: Plugin, config?: unknown, ge
     config,
     getOuterStack,
   )
-  const initiallyPending = fiber.state === FiberState.PENDING
+  const initiallyPending = fiber.ctx.fiber.state === FiberState.PENDING
   host.barrierOwners.add(fiber.ctx.fiber)
   return joinInvariantStartup(fiber, host.ready, initiallyPending)
 }
@@ -212,16 +212,20 @@ function joinInvariantStartup(
   invariantReady: Promise<void>,
   disposeInitialFailure = false,
 ): PluginFiber {
+  // RegistryService returns a thenable wrapper whose context still points to
+  // the raw Fiber. Calling inherited await() on the wrapper would return and
+  // assimilate that thenable, accidentally following later plugin startup.
+  const rawFiber = fiber.ctx.fiber
   const initialized = disposeInitialFailure
-    ? fiber.await().catch(async (error: unknown) => {
+    ? rawFiber.await().catch(async (error: unknown) => {
       // Config validation is the only failure recorded while a gated fiber
       // is initially PENDING. Dispose it even if queued readiness publication
       // changes its state before this rejection handler runs.
-      await fiber.dispose()
+      await rawFiber.dispose()
       throw error
     })
     : Promise.resolve()
-  const readiness = initialized.then(() => invariantReady).then(() => fiber.await())
+  const readiness = initialized.then(() => invariantReady).then(() => rawFiber.await())
   const joined = Object.create(fiber) as PluginFiber
   joined.then = readiness.then.bind(readiness)
   return joined