Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion packages/core/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export class EventsService {
}

// handle special events
this.ctx.fiber.assertActive()
this.ctx.fiber.assertRegistrable()
listener = this.ctx.reflect.bind(listener)
const result = this.bail(this.ctx, 'internal/listener', name, listener, options)
if (result) return result
Expand Down
15 changes: 13 additions & 2 deletions packages/core/src/fiber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,18 @@ export class Fiber {
}

assertActive() {
if (this.uid !== null) return
if (this.uid !== null && this.state !== FiberState.UNLOADING) return
throw new CordisError('INACTIVE_EFFECT')
}

/** Refuse effect/listener/plugin REGISTRATION while this fiber is UNLOADING
* (roadmap 74(a)): an undo that calls ctx.effect() during deactivation would
* be accepted by the disposed-only assertActive() check, and the resulting
* disposer leaks permanently — the unload snapshot was already taken. Unlike
* assertActive(), this is NOT used by update()/restart(), which must keep
* working through an inertial reload's UNLOADING pass. */
assertRegistrable() {
if (this.uid !== null && this.state !== FiberState.UNLOADING) return
throw new CordisError('INACTIVE_EFFECT')
}

Expand Down Expand Up @@ -275,7 +286,7 @@ export class Fiber {
effect(execute: () => SyncEffect, label?: string): Disposable<Promise<void>>
effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
effect(execute: () => Effect, label = 'anonymous'): any {
this.assertActive()
this.assertRegistrable()

const disposables: Disposable[] = []
const dispose = () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export class RegistryService {
// check if it's a valid plugin
const callback = this.resolve(plugin)
if (!callback) throw new Error('invalid plugin, expect function or object with an "apply" method, received ' + typeof plugin)
this.ctx.fiber.assertActive()
this.ctx.fiber.assertRegistrable()

let runtime = this._internal.get(callback)
if (!runtime) {
Expand Down
37 changes: 37 additions & 0 deletions packages/core/tests/fiber.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,41 @@ describe('Fiber', () => {
expect(Object.hasOwn(consumer, 'state')).to.equal(false)
expect(Object.hasOwn(consumer, 'inertia')).to.equal(false)
})
it('refuses effect registration while UNLOADING (G5 residue)', withTimers(async (root) => {
// roadmap 74(a): an undo that registers a new effect during deactivation
// must be refused (INACTIVE_EFFECT), not accepted-and-leaked. The
// deactivation path: provider.dispose() withdraws 'svc' -> Rogue
// deactivates (passes through UNLOADING) -> its generator effect's undo
// runs -> ctx.effect() inside must throw.
let leaked = false
let leakDisposed = false
const Provider = {
name: 'Provider',
apply(ctx: any) {
ctx.provide('svc', {})
},
}
const Rogue = {
name: 'Rogue',
inject: ['svc'],
apply(ctx: any) {
ctx.effect(function* () {
yield () => {
ctx.effect(() => {
leaked = true
return () => { leakDisposed = true }
})
}
})
},
}
const provider = await root.plugin(Provider)
const rogue = await root.plugin(Rogue)
await provider.dispose() // withdraw svc -> Rogue deactivates -> undo runs
expect(leaked).to.equal(false)
expect(rogue.state).to.equal(FiberState.PENDING)
expect(rogue.getEffects().length).to.equal(0)
await rogue.dispose()
expect(leakDisposed).to.equal(false)
}))
})