feat(dispatcher): scene transition hooks + ToScene when exiting

This commit is contained in:
alina 🌸 2024-06-02 17:02:36 +03:00
parent 8e04c13b60
commit 3968a35654
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
3 changed files with 135 additions and 13 deletions

View file

@ -0,0 +1,59 @@
import { MtTypeAssertionError } from '@mtcute/core'
import { makeInspectable } from '@mtcute/core/utils.js'
import { BusinessMessageContext } from './business-message.js'
import { CallbackQueryContext, InlineCallbackQueryContext } from './callback-query.js'
import { MessageContext } from './message.js'
import { UpdateContextType } from './parse.js'
/** Update which is dispatched whenever scene is entered or exited */
export class SceneTransitionContext {
constructor(
/** Name of the previous scene, if any */
readonly previousScene: string | null,
/** Update, handler for which triggered the transition */
readonly update: UpdateContextType,
) {}
/** Get {@link update}, asserting it is a message-related update */
get message(): MessageContext {
if (this.update instanceof MessageContext) {
return this.update
}
throw new MtTypeAssertionError('SceneTransitionContext.message', 'message', this.update._name)
}
/** Get {@link update}, asserting it is a business message-related update */
get businessMessage(): BusinessMessageContext {
if (this.update instanceof BusinessMessageContext) {
return this.update
}
throw new MtTypeAssertionError('SceneTransitionContext.businessMessage', 'business message', this.update._name)
}
/** Get {@link update}, asserting it is a callback query update */
get callbackQuery(): CallbackQueryContext {
if (this.update instanceof CallbackQueryContext) {
return this.update
}
throw new MtTypeAssertionError('SceneTransitionContext.callbackQuery', 'callback query', this.update._name)
}
/** Get {@link update}, asserting it is an inline callback query update */
get inlineCallbackQuery(): InlineCallbackQueryContext {
if (this.update instanceof InlineCallbackQueryContext) {
return this.update
}
throw new MtTypeAssertionError(
'SceneTransitionContext.inlineCallbackQuery',
'inline callback query',
this.update._name,
)
}
}
makeInspectable(SceneTransitionContext)

View file

@ -39,6 +39,7 @@ import {
PreCheckoutQueryContext,
} from './context/index.js'
import { _parsedUpdateToContext, UpdateContextType } from './context/parse.js'
import { SceneTransitionContext } from './context/scene-transition.js'
import { filters, UpdateFilter } from './filters/index.js'
// begin-codegen-imports
import {
@ -143,6 +144,11 @@ export class Dispatcher<State extends object = never> {
state?: UpdateState<State>,
) => MaybePromise<void>
private _sceneTransitionHandler?: (
update: SceneTransitionContext,
state: UpdateState<State>,
) => MaybePromise<PropagationAction | void>
protected constructor(client?: TelegramClient, params?: DispatcherParams) {
this.dispatchRawUpdate = this.dispatchRawUpdate.bind(this)
this.dispatchUpdate = this.dispatchUpdate.bind(this)
@ -462,7 +468,10 @@ export class Dispatcher<State extends object = never> {
(update.name === 'new_message' ||
update.name === 'edit_message' ||
update.name === 'callback_query' ||
update.name === 'message_group')
update.name === 'message_group' ||
update.name === 'new_business_message' ||
update.name === 'edit_business_message' ||
update.name === 'business_message_group')
) {
if (!parsedContext) parsedContext = _parsedUpdateToContext(this._client, update)
const key = await this._stateKeyDelegate!(parsedContext as any)
@ -521,6 +530,40 @@ export class Dispatcher<State extends object = never> {
handled = true
} else continue
if (parsedState && this._scenes) {
// check if scene transition was made
const newScene = parsedState.scene
if (parsedScene !== newScene) {
const nextDp = newScene ? this._scenes.get(newScene) : this._parent
if (!nextDp) {
throw new MtArgumentError(`Scene ${newScene} not found`)
}
if (nextDp._sceneTransitionHandler) {
const transition = new SceneTransitionContext(parsedScene, parsedContext)
const transitionResult = await nextDp._sceneTransitionHandler?.(
transition,
parsedState,
)
switch (transitionResult) {
case 'stop':
return true
case 'continue':
continue
case 'scene': {
const scene = parsedState.scene
const dp = scene ? nextDp._scenes!.get(scene)! : nextDp._parent!
return dp._dispatchUpdateNowImpl(update, undefined, scene, true)
}
}
}
}
}
switch (result) {
case 'continue':
continue
@ -535,18 +578,10 @@ export class Dispatcher<State extends object = never> {
throw new MtArgumentError('Cannot use ToScene without state')
}
const scene = parsedState['_scene']
const scene = parsedState.scene
const dp = scene ? this._scenes!.get(scene)! : this._parent!
if (!scene) {
throw new MtArgumentError('Cannot use ToScene without entering a scene')
}
return this._scenes!.get(scene)!._dispatchUpdateNowImpl(
update,
undefined,
scene,
true,
)
return dp._dispatchUpdateNowImpl(update, undefined, scene, true)
}
}
@ -739,6 +774,7 @@ export class Dispatcher<State extends object = never> {
child._client = this._client
child._storage = this._storage
child._deps = this._deps
child._scenes = this._scenes
child._stateKeyDelegate = this._stateKeyDelegate
child._customStorage ??= this._customStorage
child._customStateKeyDelegate ??= this._customStateKeyDelegate
@ -1071,6 +1107,32 @@ export class Dispatcher<State extends object = never> {
this._addKnownHandler('raw', filter, handler, group)
}
/**
* Register a scene transition handler
*
* This handler is called whenever a scene transition occurs
* in the context of the scene that is being entered,
* and before any of the its own handlers are called,
* and can be used to customize the transition behavior:
* - `Stop` to prevent dispatching the update any further **even if ToScene/ToRoot was used**
* - `Continue` same as Stop, but still dispatch the update to children
* - `ToScene` to prevent the transition and dispatch the update to the scene entered in the transition handler
*
* > **Note**: if multiple `state.enter()` calls were made within the same update,
* > this handler will only be called for the last one.
*
* @param handler Raw update handler
* @param group Handler group index
*/
onSceneTransition(
handler:
| ((ctx: SceneTransitionContext, state: UpdateState<State>) => MaybePromise<PropagationAction | void>)
| null,
): void {
if (handler) this._sceneTransitionHandler = handler
else this._sceneTransitionHandler = undefined
}
// begin-codegen
/**

View file

@ -11,7 +11,8 @@
*
* `Continue`: Continue propagating the event inside the same handler group.
*
* `ToScene`: Used after using `state.enter()` to dispatch the update to the scene
* `ToScene`: Used after using `state.enter()` to dispatch the update to the scene,
* or after `state.exit()` to dispatch the update to the root dispatcher.
*/
export enum PropagationAction {
Stop = 'stop',