enforce convo-present invariant in snapshot instead of guarding in isConvoActive
the active convo states declare `convo` as non-optional, but a lifecycle event (background/suspend) can move the machine into an active status before setup() populates this.convo. generateSnapshot papered over this with `this.convo!`, so isConvoActive trusted the status alone and downstream consumers crashed dereferencing an undefined convo. instead of guarding at the isConvoActive call site, make the invalid state unrepresentable: generateSnapshot now reports Initializing whenever an active status has no convo yet, which lets us drop all four non-null assertions. the compiler now enforces that an active snapshot always carries a convo, so the isConvoActive guard added earlier is reverted as redundant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -237,22 +237,40 @@ export class Convo {
|
|||||||
removeReaction: undefined,
|
removeReaction: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this.status) {
|
/*
|
||||||
case ConvoStatus.Initializing: {
|
* Captured as a local so the `if (convo)` narrowing below survives the
|
||||||
return {
|
* `this.getItems()` call - TS discards narrowing on mutable `this` members
|
||||||
|
* after a method call, but not on a const.
|
||||||
|
*/
|
||||||
|
const convo = this.convo
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A lifecycle event (e.g. `Background` or `Suspend`) can move us into an
|
||||||
|
* active status before `setup()` has resolved and populated `convo`. The
|
||||||
|
* active states declare `convo` as non-optional, so we can't build one
|
||||||
|
* without it - fall back to reporting `Initializing` until the convo lands.
|
||||||
|
* This keeps the snapshot's type and runtime in agreement, so consumers can
|
||||||
|
* trust that an active status always has a `convo`.
|
||||||
|
*/
|
||||||
|
const stillInitializing = (): ConvoState => ({
|
||||||
status: ConvoStatus.Initializing,
|
status: ConvoStatus.Initializing,
|
||||||
items: [],
|
items: [],
|
||||||
convo: this.convo,
|
convo,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
...shared,
|
...shared,
|
||||||
...emptyMethods,
|
...emptyMethods,
|
||||||
}
|
})
|
||||||
|
|
||||||
|
switch (this.status) {
|
||||||
|
case ConvoStatus.Initializing: {
|
||||||
|
return stillInitializing()
|
||||||
}
|
}
|
||||||
case ConvoStatus.Disabled: {
|
case ConvoStatus.Disabled: {
|
||||||
|
if (!convo) return stillInitializing()
|
||||||
return {
|
return {
|
||||||
status: this.status,
|
status: ConvoStatus.Disabled,
|
||||||
items: this.getItems(),
|
items: this.getItems(),
|
||||||
convo: this.convo!,
|
convo,
|
||||||
relatedProfiles: this.relatedProfiles,
|
relatedProfiles: this.relatedProfiles,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
...shared,
|
...shared,
|
||||||
@@ -260,10 +278,11 @@ export class Convo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case ConvoStatus.Suspended: {
|
case ConvoStatus.Suspended: {
|
||||||
|
if (!convo) return stillInitializing()
|
||||||
return {
|
return {
|
||||||
status: this.status,
|
status: ConvoStatus.Suspended,
|
||||||
items: this.getItems(),
|
items: this.getItems(),
|
||||||
convo: this.convo!,
|
convo,
|
||||||
relatedProfiles: this.relatedProfiles,
|
relatedProfiles: this.relatedProfiles,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
...shared,
|
...shared,
|
||||||
@@ -271,10 +290,11 @@ export class Convo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case ConvoStatus.Backgrounded: {
|
case ConvoStatus.Backgrounded: {
|
||||||
|
if (!convo) return stillInitializing()
|
||||||
return {
|
return {
|
||||||
status: this.status,
|
status: ConvoStatus.Backgrounded,
|
||||||
items: this.getItems(),
|
items: this.getItems(),
|
||||||
convo: this.convo!,
|
convo,
|
||||||
relatedProfiles: this.relatedProfiles,
|
relatedProfiles: this.relatedProfiles,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
...shared,
|
...shared,
|
||||||
@@ -282,10 +302,11 @@ export class Convo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case ConvoStatus.Ready: {
|
case ConvoStatus.Ready: {
|
||||||
|
if (!convo) return stillInitializing()
|
||||||
return {
|
return {
|
||||||
status: this.status,
|
status: ConvoStatus.Ready,
|
||||||
items: this.getItems(),
|
items: this.getItems(),
|
||||||
convo: this.convo!,
|
convo,
|
||||||
relatedProfiles: this.relatedProfiles,
|
relatedProfiles: this.relatedProfiles,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
...shared,
|
...shared,
|
||||||
|
|||||||
@@ -21,19 +21,12 @@ export type ActiveConvoStates =
|
|||||||
* Checks if a `Convo` has a `status` that is "active", meaning the chat is
|
* Checks if a `Convo` has a `status` that is "active", meaning the chat is
|
||||||
* loaded and ready to be used, or its in a suspended or background state, and
|
* loaded and ready to be used, or its in a suspended or background state, and
|
||||||
* ready for resumption.
|
* ready for resumption.
|
||||||
*
|
|
||||||
* The `convo` object must also be present. The status can transition into an
|
|
||||||
* active state before the convo has finished loading (e.g. `Initializing`
|
|
||||||
* receives a `Background` event before `setup()` resolves), and every
|
|
||||||
* `ActiveConvoStates` member declares `convo` as non-optional, so we guard
|
|
||||||
* against that race here rather than crashing downstream consumers.
|
|
||||||
*/
|
*/
|
||||||
export function isConvoActive(convo: ConvoState): convo is ActiveConvoStates {
|
export function isConvoActive(convo: ConvoState): convo is ActiveConvoStates {
|
||||||
return (
|
return (
|
||||||
convo.convo !== undefined &&
|
convo.status === ConvoStatus.Ready ||
|
||||||
(convo.status === ConvoStatus.Ready ||
|
|
||||||
convo.status === ConvoStatus.Backgrounded ||
|
convo.status === ConvoStatus.Backgrounded ||
|
||||||
convo.status === ConvoStatus.Suspended ||
|
convo.status === ConvoStatus.Suspended ||
|
||||||
convo.status === ConvoStatus.Disabled)
|
convo.status === ConvoStatus.Disabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user