-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[core] feat(interpreter): allow to provide errorListeners #2841
Changes from 6 commits
123ae97
f5027b0
c0a2a55
fbc8f50
cf32294
baa772f
5b1372e
a49c586
77f034b
b5b4dd0
42a307f
f4d1d1b
610686a
52037b0
6d90543
243eebe
865f2ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'xstate': minor | ||
--- | ||
|
||
allow to provide errorListeners into interpreter |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -160,6 +160,7 @@ export class Interpreter< | |
private contextListeners: Set<ContextListener<TContext>> = new Set(); | ||
private stopListeners: Set<Listener> = new Set(); | ||
private doneListeners: Set<EventListener> = new Set(); | ||
private errorListeners: Set<EventListener> = new Set(); | ||
private eventListeners: Set<EventListener> = new Set(); | ||
private sendListeners: Set<EventListener> = new Set(); | ||
private logger: (...args: any[]) => void; | ||
|
@@ -313,6 +314,15 @@ export class Interpreter< | |
this.stop(); | ||
} | ||
} | ||
|
||
private sendError(errorEvent: Event<TEvent> | SCXML.Event<TEvent>): void { | ||
if (this.errorListeners.size) { | ||
for (const listener of this.errorListeners) { | ||
listener(errorEvent as EventObject); | ||
} | ||
} | ||
} | ||
|
||
/* | ||
* Adds a listener that is notified whenever a state transition happens. The listener is called with | ||
* the next state and the event object that caused the state transition. | ||
|
@@ -343,7 +353,7 @@ export class Interpreter< | |
nextListenerOrObserver?: | ||
| ((state: State<TContext, TEvent, any, TTypestate>) => void) | ||
| Observer<State<TContext, TEvent, any, TTypestate>>, | ||
_?: (error: any) => void, // TODO: error listener | ||
errorListener?: (error: any) => void, | ||
completeListener?: () => void | ||
): Subscription { | ||
if (!nextListenerOrObserver) { | ||
|
@@ -352,6 +362,7 @@ export class Interpreter< | |
|
||
let listener: (state: State<TContext, TEvent, any, TTypestate>) => void; | ||
let resolvedCompleteListener = completeListener; | ||
let resolvedErrorListener = errorListener; | ||
|
||
if (typeof nextListenerOrObserver === 'function') { | ||
listener = nextListenerOrObserver; | ||
|
@@ -373,11 +384,17 @@ export class Interpreter< | |
this.onDone(resolvedCompleteListener); | ||
} | ||
|
||
if (resolvedErrorListener) { | ||
this.onError(resolvedErrorListener); | ||
} | ||
|
||
return { | ||
unsubscribe: () => { | ||
listener && this.listeners.delete(listener); | ||
resolvedCompleteListener && | ||
this.doneListeners.delete(resolvedCompleteListener); | ||
resolvedErrorListener && | ||
this.errorListeners.delete(resolvedErrorListener); | ||
} | ||
}; | ||
} | ||
|
@@ -432,6 +449,16 @@ export class Interpreter< | |
this.doneListeners.add(listener); | ||
return this; | ||
} | ||
/** | ||
* Adds a state listener that is notified when the statechart has reached an error. | ||
* @param listener The state listener | ||
*/ | ||
public onError( | ||
listener: EventListener<DoneEvent> | ||
): Interpreter<TContext, TStateSchema, TEvent, TTypestate> { | ||
this.errorListeners.add(listener); | ||
return this; | ||
} | ||
/** | ||
* Removes a listener. | ||
* @param listener The listener to remove | ||
|
@@ -444,6 +471,7 @@ export class Interpreter< | |
this.sendListeners.delete(listener); | ||
this.stopListeners.delete(listener); | ||
this.doneListeners.delete(listener); | ||
this.errorListeners.delete(listener); | ||
this.contextListeners.delete(listener); | ||
return this; | ||
} | ||
|
@@ -510,6 +538,9 @@ export class Interpreter< | |
for (const listener of this.doneListeners) { | ||
this.doneListeners.delete(listener); | ||
} | ||
for (const listener of this.errorListeners) { | ||
this.errorListeners.delete(listener); | ||
} | ||
|
||
if (!this.initialized) { | ||
// Interpreter already stopped; do nothing | ||
|
@@ -552,13 +583,22 @@ export class Interpreter< | |
*/ | ||
public send = ( | ||
event: SingleOrArray<Event<TEvent>> | SCXML.Event<TEvent>, | ||
payload?: EventData | ||
payload?: EventData, | ||
sendError = false | ||
): State<TContext, TEvent, TStateSchema, TTypestate> => { | ||
if (isArray(event)) { | ||
this.batch(event); | ||
return this.state; | ||
} | ||
|
||
if (sendError) { | ||
if (this.parent) { | ||
this.parent.sendError(event as any); | ||
} else { | ||
this.sendError(event as any); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one sounds like it's going to send this event somewhere (like some kind of an actor or something), but it's more like notifying current subscribers. I also do not (overall) liking how this might make more errors to be swallowed. In general, the error story in XState is currently under-specified so it's somewhat hard to answer "how this should behave now?". I would love to hear out your thoughts about this RFC: statelyai/rfcs#4 . Going through that could also show you my perspective better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks pretty nice, could you please guide me on the proper changes when you get some chance? |
||
} | ||
} | ||
|
||
const _event = toSCXMLEvent(toEventObject(event as Event<TEvent>, payload)); | ||
|
||
if (this.status === InterpreterStatus.Stopped) { | ||
|
@@ -788,10 +828,14 @@ export class Interpreter< | |
}); | ||
} catch (err) { | ||
if (this.parent) { | ||
this.parent.send({ | ||
type: 'xstate.error', | ||
data: err | ||
} as EventObject); | ||
this.parent.send( | ||
{ | ||
type: 'xstate.error', | ||
data: err | ||
} as EventObject, | ||
undefined, | ||
true | ||
); | ||
} | ||
|
||
throw err; | ||
|
@@ -1053,10 +1097,24 @@ export class Interpreter< | |
// Send "error.platform.id" to this (parent). | ||
this.send(toSCXMLEvent(errorEvent as any, { origin: id })); | ||
} catch (error) { | ||
reportUnhandledExceptionOnInvocation(errorData, error, id); | ||
if (this.parent) { | ||
this.parent.send( | ||
{ | ||
type: 'xstate.error', | ||
data: error | ||
} as EventObject, | ||
undefined, | ||
true | ||
); | ||
} else if (!this.errorListeners.size) { | ||
reportUnhandledExceptionOnInvocation(errorData, error, id); | ||
} | ||
if (this.devTools) { | ||
this.devTools.send(errorEvent, this.state); | ||
this.devTools.send(errorEvent, this.state, true); | ||
} | ||
|
||
this.sendError(errorEvent); | ||
|
||
if (this.machine.strict) { | ||
// it would be better to always stop the state machine if unhandled | ||
// exception/promise rejection happens but because we don't want to | ||
|
@@ -1134,7 +1192,7 @@ export class Interpreter< | |
receivers.add(newListener); | ||
}); | ||
} catch (err) { | ||
this.send(error(id, err) as any); | ||
this.send(error(id, err) as any, undefined, true); | ||
} | ||
|
||
if (isPromiseLike(callbackStop)) { | ||
|
@@ -1184,7 +1242,11 @@ export class Interpreter< | |
}, | ||
(err) => { | ||
this.removeChild(id); | ||
this.send(toSCXMLEvent(error(id, err) as any, { origin: id })); | ||
this.send( | ||
toSCXMLEvent(error(id, err) as any, { origin: id }), | ||
undefined, | ||
true | ||
); | ||
}, | ||
() => { | ||
this.removeChild(id); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's remove this; errors should either be unhandled
error.*
events or "natural" errors thrown from execution of the machine.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank you @davidkpiano I just see your message, in my last commit I was replacing this by checking the type in the event, is that ok or should I do in a different way?