diff --git a/packages/dispatcher/package.json b/packages/dispatcher/package.json index 51f859de..fc30ec14 100644 --- a/packages/dispatcher/package.json +++ b/packages/dispatcher/package.json @@ -15,7 +15,6 @@ "@mtcute/tl": "~1.129.0", "@mtcute/core": "^1.0.0", "@mtcute/client": "^1.0.0", - "es6-symbol": "^3.1.3", "debug": "^4.3.1" } } diff --git a/packages/dispatcher/src/dispatcher.ts b/packages/dispatcher/src/dispatcher.ts index 9a4da773..6c28c0bd 100644 --- a/packages/dispatcher/src/dispatcher.ts +++ b/packages/dispatcher/src/dispatcher.ts @@ -10,12 +10,6 @@ import { UsersIndex, } from '@mtcute/client' import { tl } from '@mtcute/tl' -import { - ContinuePropagation, - PropagationSymbol, - StopChildrenPropagation, - StopPropagation, -} from './propagation' // begin-codegen-imports import { UpdateHandler, @@ -45,6 +39,7 @@ import { UserTypingUpdate } from './updates/user-typing-update' import { DeleteMessageUpdate } from './updates/delete-message-update' import { IStateStorage, UpdateState, StateKeyDelegate } from './state' import { defaultStateKeyDelegate } from './state/key' +import { PropagationAction } from './propagation' const noop = () => {} @@ -299,9 +294,9 @@ export class Dispatcher { } private async _dispatchUpdateNowImpl( - update: tl.TypeUpdate | tl.TypeMessage, - users: UsersIndex, - chats: ChatsIndex, + update: tl.TypeUpdate | tl.TypeMessage | null, + users: UsersIndex | null, + chats: ChatsIndex | null, // this is getting a bit crazy lol parsed?: any, parsedType?: Exclude | null, @@ -311,12 +306,12 @@ export class Dispatcher { ): Promise { if (!this._client) return - const isRawMessage = tl.isAnyMessage(update) + const isRawMessage = update && tl.isAnyMessage(update) - if (parsed === undefined && this._handlersCount[update._]) { - const pair = PARSERS[update._] + if (parsed === undefined && this._handlersCount[update!._]) { + const pair = PARSERS[update!._] if (pair) { - parsed = pair[1](this._client, update, users, chats) + parsed = pair[1](this._client, update!, users!, chats!) parsedType = pair[0] } else { parsed = parsedType = null @@ -400,7 +395,41 @@ export class Dispatcher { outer: for (const grp of this._groupsOrder) { 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) { // raw is not handled here, so we can safely assume this @@ -411,7 +440,7 @@ export class Dispatcher { try { for (const h of handlers) { - let result: void | PropagationSymbol + let result: void | PropagationAction if ( !h.check || @@ -423,11 +452,35 @@ export class Dispatcher { ) } else continue - if (result === ContinuePropagation) continue - if (result === StopPropagation) break outer - if (result === StopChildrenPropagation) return + switch (result) { + case 'continue': + 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 } } catch (e) { @@ -442,37 +495,6 @@ export class Dispatcher { } } } - - 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) { @@ -595,6 +617,25 @@ export class Dispatcher { else this._errorHandler = undefined } + /** + * Set error handler that will propagate + * the error to the parent dispatcher + */ + propagateErrorToParent>( + err: Error, + update: UpdateInfoForError, + state?: UpdateState + ): MaybeAsync { + 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 // /** @@ -721,26 +762,36 @@ export class Dispatcher { removeChild(child: Dispatcher): void { const idx = this._children.indexOf(child) if (idx > -1) { - child._parent = child._client = undefined - ;(child as any)._stateKeyDelegate = undefined - ;(child as any)._storage = undefined + child._unparent() 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 * handlers and children to the current. * * This might be more efficient for simple cases, but do note that the handler - * groups will get merged (unlike {@link addChild}, where they - * are independent). Also note that unlike with children, + * groups, children and scenes will get merged (unlike {@link addChild}, + * where they are independent). Also note that unlike with children, * when adding handlers to `other` *after* you extended * the current dispatcher, changes will not be applied. * * @param other Other dispatcher */ extend(other: Dispatcher): void { + if (other._customStorage || other._customStateKeyDelegate) { + throw new MtCuteArgumentError( + 'Provided dispatcher has custom storage and cannot be extended from.' + ) + } + other._groupsOrder.forEach((group) => { if (!(group in this._groups)) { this._groups[group] = other._groups[group] @@ -758,27 +809,87 @@ export class Dispatcher { } }) + 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) } /** - * 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 - * this filter, otherwise it doesn't + * Custom Storage and key delegate are copied too. * - * 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( - predicate: (state: State) => MaybeAsync - ): UpdateFilter { - if (!this._storage) - throw new MtCuteArgumentError( - 'Cannot use state() filter without state storage' + clone(children = false): Dispatcher { + const dp = new Dispatcher() + + // copy handlers. + Object.keys(this._groups).forEach((key) => { + 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 { }) } + /** + * Get global state. + * + * This will load the state for the given object + * ignoring local custom storage, key delegate and scene scope. + */ + getGlobalState(object: Parameters[0]): Promise> { + 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 // private _addKnownHandler( @@ -930,10 +1068,12 @@ export class Dispatcher { * @param group Handler group index */ onNewMessage( - filter: UpdateFilter, + filter: UpdateFilter, handler: NewMessageHandler< filters.Modify, - State extends never ? never : UpdateState + State extends never + ? never + : UpdateState >['callback'], group?: number ): void diff --git a/packages/dispatcher/src/handler.ts b/packages/dispatcher/src/handler.ts index 76df4097..595864a7 100644 --- a/packages/dispatcher/src/handler.ts +++ b/packages/dispatcher/src/handler.ts @@ -8,7 +8,7 @@ import { ChatsIndex, } from '@mtcute/client' import { tl } from '@mtcute/tl' -import { PropagationSymbol } from './propagation' +import { PropagationAction } from './propagation' import { ChatMemberUpdate } from './updates' import { ChosenInlineResult } from './updates/chosen-inline-result' import { PollUpdate } from './updates/poll-update' @@ -26,7 +26,7 @@ interface BaseUpdateHandler { type ParsedUpdateHandler = BaseUpdateHandler< Type, - (update: Update, state: State) => MaybeAsync, + (update: Update, state: State) => MaybeAsync, (update: Update, state: State) => MaybeAsync > @@ -47,7 +47,7 @@ export type RawUpdateHandler = BaseUpdateHandler< update: tl.TypeUpdate, users: UsersIndex, chats: ChatsIndex - ) => MaybeAsync, + ) => MaybeAsync, ( client: TelegramClient, update: tl.TypeUpdate, diff --git a/packages/dispatcher/src/propagation.ts b/packages/dispatcher/src/propagation.ts index a7622cce..9e55d55f 100644 --- a/packages/dispatcher/src/propagation.ts +++ b/packages/dispatcher/src/propagation.ts @@ -1,34 +1,21 @@ -const _sym = require('es6-symbol') - /** - * Stop the propagation of the event through any handler groups - * on the current dispatcher. + * Propagation action. * - * However, returning this will still execute children - */ -export const StopPropagation: unique symbol = _sym.for('mtcute:StopPropagation') - -/** - * Stop the propagation of the event through any handler groups - * on the current dispatcher, and any of its children. + * `Stop`: Stop the propagation of the event through any handler groups + * in the current dispatcher. Does not prevent child dispatchers from + * being executed. * - * Note that if current dispatcher is a child, - * this will not prevent from propagating the event - * to other children of current's parent. + * `StopChildren`: Stop the propagation of the event through any handler groups + * in the current dispatcher, and any of its children. If current dispatcher + * 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( - 'mtcute:StopChildrenPropagation' -) - -/** - * 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 +export enum PropagationAction { + Stop = 'stop', + StopChildren = 'stop-children', + Continue = 'continue', + ToScene = 'scene' +}