refactor(dispatcher): use enum instead of symbols for propagation

This commit is contained in:
teidesu 2021-06-14 18:58:07 +03:00
parent 707e317e16
commit 257f5392ea
4 changed files with 229 additions and 103 deletions

View file

@ -15,7 +15,6 @@
"@mtcute/tl": "~1.129.0", "@mtcute/tl": "~1.129.0",
"@mtcute/core": "^1.0.0", "@mtcute/core": "^1.0.0",
"@mtcute/client": "^1.0.0", "@mtcute/client": "^1.0.0",
"es6-symbol": "^3.1.3",
"debug": "^4.3.1" "debug": "^4.3.1"
} }
} }

View file

@ -10,12 +10,6 @@ import {
UsersIndex, UsersIndex,
} from '@mtcute/client' } from '@mtcute/client'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import {
ContinuePropagation,
PropagationSymbol,
StopChildrenPropagation,
StopPropagation,
} from './propagation'
// begin-codegen-imports // begin-codegen-imports
import { import {
UpdateHandler, UpdateHandler,
@ -45,6 +39,7 @@ import { UserTypingUpdate } from './updates/user-typing-update'
import { DeleteMessageUpdate } from './updates/delete-message-update' import { DeleteMessageUpdate } from './updates/delete-message-update'
import { IStateStorage, UpdateState, StateKeyDelegate } from './state' import { IStateStorage, UpdateState, StateKeyDelegate } from './state'
import { defaultStateKeyDelegate } from './state/key' import { defaultStateKeyDelegate } from './state/key'
import { PropagationAction } from './propagation'
const noop = () => {} const noop = () => {}
@ -299,9 +294,9 @@ export class Dispatcher<State = never, SceneName extends string = string> {
} }
private async _dispatchUpdateNowImpl( private async _dispatchUpdateNowImpl(
update: tl.TypeUpdate | tl.TypeMessage, update: tl.TypeUpdate | tl.TypeMessage | null,
users: UsersIndex, users: UsersIndex | null,
chats: ChatsIndex, chats: ChatsIndex | null,
// this is getting a bit crazy lol // this is getting a bit crazy lol
parsed?: any, parsed?: any,
parsedType?: Exclude<UpdateHandler['type'], 'raw'> | null, parsedType?: Exclude<UpdateHandler['type'], 'raw'> | null,
@ -311,12 +306,12 @@ export class Dispatcher<State = never, SceneName extends string = string> {
): Promise<void> { ): Promise<void> {
if (!this._client) return if (!this._client) return
const isRawMessage = tl.isAnyMessage(update) const isRawMessage = update && tl.isAnyMessage(update)
if (parsed === undefined && this._handlersCount[update._]) { if (parsed === undefined && this._handlersCount[update!._]) {
const pair = PARSERS[update._] const pair = PARSERS[update!._]
if (pair) { if (pair) {
parsed = pair[1](this._client, update, users, chats) parsed = pair[1](this._client, update!, users!, chats!)
parsedType = pair[0] parsedType = pair[0]
} else { } else {
parsed = parsedType = null parsed = parsedType = null
@ -400,7 +395,41 @@ export class Dispatcher<State = never, SceneName extends string = string> {
outer: for (const grp of this._groupsOrder) { outer: for (const grp of this._groupsOrder) {
const group = this._groups[grp] const group = this._groups[grp]
let tryRaw = !isRawMessage if (update && !isRawMessage && 'raw' in group) {
const handlers = group['raw'] as RawUpdateHandler[]
for (const h of handlers) {
let result: void | PropagationAction
if (
!h.check ||
(await h.check(
this._client,
update as any,
users!,
chats!
))
) {
result = await h.callback(
this._client,
update as any,
users!,
chats!
)
} else continue
switch (result) {
case 'continue':
continue
case 'stop':
break outer
case 'stop-children':
return
}
break
}
}
if (parsedType && parsedType in group) { if (parsedType && parsedType in group) {
// raw is not handled here, so we can safely assume this // raw is not handled here, so we can safely assume this
@ -411,7 +440,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
try { try {
for (const h of handlers) { for (const h of handlers) {
let result: void | PropagationSymbol let result: void | PropagationAction
if ( if (
!h.check || !h.check ||
@ -423,11 +452,35 @@ export class Dispatcher<State = never, SceneName extends string = string> {
) )
} else continue } else continue
if (result === ContinuePropagation) continue switch (result) {
if (result === StopPropagation) break outer case 'continue':
if (result === StopChildrenPropagation) return continue
case 'stop':
break outer
case 'stop-children':
return
case 'scene': {
if (!parsedState)
throw new MtCuteArgumentError('Cannot use ToScene without state')
const scene = parsedState['_scene']
if (!scene)
throw new MtCuteArgumentError('Cannot use ToScene without entering a scene')
return this._scenes[scene]._dispatchUpdateNowImpl(
update,
users,
chats,
parsed,
parsedType,
undefined,
scene,
true
)
}
}
tryRaw = false
break break
} }
} catch (e) { } catch (e) {
@ -442,37 +495,6 @@ export class Dispatcher<State = never, SceneName extends string = string> {
} }
} }
} }
if (tryRaw && 'raw' in group) {
const handlers = group['raw'] as RawUpdateHandler[]
for (const h of handlers) {
let result: void | PropagationSymbol
if (
!h.check ||
(await h.check(
this._client,
update as any,
users,
chats
))
) {
result = await h.callback(
this._client,
update as any,
users,
chats
)
} else continue
if (result === ContinuePropagation) continue
if (result === StopPropagation) break outer
if (result === StopChildrenPropagation) return
break
}
}
} }
for (const child of this._children) { for (const child of this._children) {
@ -595,6 +617,25 @@ export class Dispatcher<State = never, SceneName extends string = string> {
else this._errorHandler = undefined else this._errorHandler = undefined
} }
/**
* Set error handler that will propagate
* the error to the parent dispatcher
*/
propagateErrorToParent<T extends Exclude<UpdateHandler, RawUpdateHandler>>(
err: Error,
update: UpdateInfoForError<T>,
state?: UpdateState<State, SceneName>
): MaybeAsync<void> {
if (!this.parent)
throw new MtCuteArgumentError('This dispatcher is not a child')
if (this.parent._errorHandler) {
return this.parent._errorHandler(err, update, state)
} else {
throw err
}
}
// children // // children //
/** /**
@ -721,26 +762,36 @@ export class Dispatcher<State = never, SceneName extends string = string> {
removeChild(child: Dispatcher): void { removeChild(child: Dispatcher): void {
const idx = this._children.indexOf(child) const idx = this._children.indexOf(child)
if (idx > -1) { if (idx > -1) {
child._parent = child._client = undefined child._unparent()
;(child as any)._stateKeyDelegate = undefined
;(child as any)._storage = undefined
this._children.splice(idx, 1) this._children.splice(idx, 1)
} }
} }
private _unparent(): void {
this._parent = this._client = undefined
;(this as any)._stateKeyDelegate = undefined
;(this as any)._storage = undefined
}
/** /**
* Extend current dispatcher by copying other dispatcher's * Extend current dispatcher by copying other dispatcher's
* handlers and children to the current. * handlers and children to the current.
* *
* This might be more efficient for simple cases, but do note that the handler * This might be more efficient for simple cases, but do note that the handler
* groups will get merged (unlike {@link addChild}, where they * groups, children and scenes will get merged (unlike {@link addChild},
* are independent). Also note that unlike with children, * where they are independent). Also note that unlike with children,
* when adding handlers to `other` *after* you extended * when adding handlers to `other` *after* you extended
* the current dispatcher, changes will not be applied. * the current dispatcher, changes will not be applied.
* *
* @param other Other dispatcher * @param other Other dispatcher
*/ */
extend(other: Dispatcher<State, SceneName>): void { extend(other: Dispatcher<State, SceneName>): void {
if (other._customStorage || other._customStateKeyDelegate) {
throw new MtCuteArgumentError(
'Provided dispatcher has custom storage and cannot be extended from.'
)
}
other._groupsOrder.forEach((group) => { other._groupsOrder.forEach((group) => {
if (!(group in this._groups)) { if (!(group in this._groups)) {
this._groups[group] = other._groups[group] this._groups[group] = other._groups[group]
@ -758,27 +809,87 @@ export class Dispatcher<State = never, SceneName extends string = string> {
} }
}) })
Object.keys(other._handlersCount).forEach((typ) => {
this._handlersCount[typ] += other._handlersCount[typ]
})
other._children.forEach((it) => {
it._unparent()
this.addChild(it as any)
})
if (other._scenes) {
Object.keys(other._scenes).forEach((key) => {
other._scenes[key]._unparent()
if (key in this._scenes) {
// will be overwritten
delete this._scenes[key]
}
this.addScene(
key as any,
other._scenes[key] as any,
other._scenes[key]._sceneScoped as any
)
})
}
this._groupsOrder.sort((a, b) => a - b) this._groupsOrder.sort((a, b) => a - b)
} }
/** /**
* Create a filter based on state predicate * Create a clone of this dispatcher, that has the same handlers,
* but is not bound to a client or to a parent dispatcher.
* *
* If state exists and matches `predicate`, update passes * Custom Storage and key delegate are copied too.
* this filter, otherwise it doesn't
* *
* Essentially just a wrapper over {@link filters.state} * By default, child dispatchers (and scenes) are ignored, since
* that requires cloning every single one of them recursively
* and then binding them back.
* *
* @param predicate State predicate * @param children Whether to also clone children and scenes
*/ */
state( clone(children = false): Dispatcher<State, SceneName> {
predicate: (state: State) => MaybeAsync<boolean> const dp = new Dispatcher<State, SceneName>()
): UpdateFilter<Message, {}, State> {
if (!this._storage) // copy handlers.
throw new MtCuteArgumentError( Object.keys(this._groups).forEach((key) => {
'Cannot use state() filter without state storage' const idx = (key as any) as number
dp._groups[idx] = {} as any
Object.keys(this._groups[idx]).forEach(
(type: UpdateHandler['type']) => {
dp._groups[idx][type] = [...this._groups[idx][type]]
}
) )
return filters.state(predicate) })
dp._groupsOrder = [...this._groupsOrder]
dp._handlersCount = { ...this._handlersCount }
dp._errorHandler = this._errorHandler
dp._customStateKeyDelegate = this._customStateKeyDelegate
dp._customStorage = this._customStorage
if (children) {
this._children.forEach((it) => {
const child = it.clone(true)
dp.addChild(child as any)
})
if (this._scenes) {
Object.keys(this._scenes).forEach((key) => {
const scene = this._scenes[key].clone(true)
dp.addScene(
key as any,
scene as any,
this._scenes[key]._sceneScoped as any
)
})
}
}
return dp
} }
/** /**
@ -861,6 +972,33 @@ export class Dispatcher<State = never, SceneName extends string = string> {
}) })
} }
/**
* Get global state.
*
* This will load the state for the given object
* ignoring local custom storage, key delegate and scene scope.
*/
getGlobalState<T>(object: Parameters<StateKeyDelegate>[0]): Promise<UpdateState<T, SceneName>> {
if (!this._parent) {
throw new MtCuteArgumentError('This dispatcher does not have a parent')
}
return Promise.resolve(this._stateKeyDelegate!(object)).then((key) => {
if (!key) {
throw new MtCuteArgumentError(
'Cannot derive key from given object'
)
}
return new UpdateState(
this._storage!,
key,
this._scene ?? null,
false
)
})
}
// addUpdateHandler convenience wrappers // // addUpdateHandler convenience wrappers //
private _addKnownHandler( private _addKnownHandler(
@ -930,10 +1068,12 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onNewMessage<Mod>( onNewMessage<Mod>(
filter: UpdateFilter<Message, Mod>, filter: UpdateFilter<Message, Mod, State>,
handler: NewMessageHandler< handler: NewMessageHandler<
filters.Modify<Message, Mod>, filters.Modify<Message, Mod>,
State extends never ? never : UpdateState<State, SceneName> State extends never
? never
: UpdateState<State, SceneName>
>['callback'], >['callback'],
group?: number group?: number
): void ): void

View file

@ -8,7 +8,7 @@ import {
ChatsIndex, ChatsIndex,
} from '@mtcute/client' } from '@mtcute/client'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { PropagationSymbol } from './propagation' import { PropagationAction } from './propagation'
import { ChatMemberUpdate } from './updates' import { ChatMemberUpdate } from './updates'
import { ChosenInlineResult } from './updates/chosen-inline-result' import { ChosenInlineResult } from './updates/chosen-inline-result'
import { PollUpdate } from './updates/poll-update' import { PollUpdate } from './updates/poll-update'
@ -26,7 +26,7 @@ interface BaseUpdateHandler<Type, Handler, Checker> {
type ParsedUpdateHandler<Type, Update, State = never> = BaseUpdateHandler< type ParsedUpdateHandler<Type, Update, State = never> = BaseUpdateHandler<
Type, Type,
(update: Update, state: State) => MaybeAsync<void | PropagationSymbol>, (update: Update, state: State) => MaybeAsync<void | PropagationAction>,
(update: Update, state: State) => MaybeAsync<boolean> (update: Update, state: State) => MaybeAsync<boolean>
> >
@ -47,7 +47,7 @@ export type RawUpdateHandler = BaseUpdateHandler<
update: tl.TypeUpdate, update: tl.TypeUpdate,
users: UsersIndex, users: UsersIndex,
chats: ChatsIndex chats: ChatsIndex
) => MaybeAsync<void | PropagationSymbol>, ) => MaybeAsync<void | PropagationAction>,
( (
client: TelegramClient, client: TelegramClient,
update: tl.TypeUpdate, update: tl.TypeUpdate,

View file

@ -1,34 +1,21 @@
const _sym = require('es6-symbol')
/** /**
* Stop the propagation of the event through any handler groups * Propagation action.
* on the current dispatcher.
* *
* However, returning this will still execute children * `Stop`: Stop the propagation of the event through any handler groups
*/ * in the current dispatcher. Does not prevent child dispatchers from
export const StopPropagation: unique symbol = _sym.for('mtcute:StopPropagation') * being executed.
/**
* Stop the propagation of the event through any handler groups
* on the current dispatcher, and any of its children.
* *
* Note that if current dispatcher is a child, * `StopChildren`: Stop the propagation of the event through any handler groups
* this will not prevent from propagating the event * in the current dispatcher, and any of its children. If current dispatcher
* to other children of current's parent. * is a child, does not prevent from propagating to its siblings.
*
* `Continue`: Continue propagating the event inside the same handler group.
*
* `ToScene`: Used after using `state.enter()` to dispatch the update to the scene
*/ */
export const StopChildrenPropagation: unique symbol = _sym.for( export enum PropagationAction {
'mtcute:StopChildrenPropagation' Stop = 'stop',
) StopChildren = 'stop-children',
Continue = 'continue',
/** ToScene = 'scene'
* Continue propagating the event inside the same handler group. }
*/
export const ContinuePropagation: unique symbol = _sym.for(
'mtcute:ContinuePropagation'
)
export type PropagationSymbol = symbol
// this seems to cause issues after publishing
// | typeof StopPropagation
// | typeof ContinuePropagation
// | typeof StopChildrenPropagation