/**
   * Find the previous instance of the term, then scroll to and select it. If it
   * doesn't exist, do nothing.
   * @param term The search term.
   * @param searchOptions Search options.
   * @return Whether a result was found.
   */
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
    if (!term || term.length === 0) {
      return false;
    }

    let result: ISearchResult;

    let startRow = this._terminal._core.buffer.ydisp;
    if (this._terminal._core.selectionManager.selectionStart) {
      // Start from the selection end if there is a selection
      if (this._terminal.getSelection().length !== 0) {
        startRow = this._terminal._core.selectionManager.selectionStart[1];
      }
    }

    // Search from ydisp + 1 to end
    for (let y = startRow - 1; y >= 0; y--) {
      result = this._findInLine(term, y, searchOptions);
      if (result) {
        break;
      }
    }

    // Search from the top to the current ydisp
    if (!result) {
      for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
        result = this._findInLine(term, y, searchOptions);
        if (result) {
          break;
        }
      }
    }

    // Set selection and scroll if a result was found
    return this._selectResult(result);
  }


/**
   * A found substring is a whole word if it doesn't have an alphanumeric character directly adjacent to it.
   * @param searchIndex starting indext of the potential whole word substring
   * @param line entire string in which the potential whole word was found
   * @param term the substring that starts at searchIndex
   */
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
    return (((searchIndex === 0) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) &&
        (((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1)));
  }


/**
   * Searches a line for a search term. Takes the provided terminal line and searches the text line, which may contain
   * subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that
   * started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
   * text starts on is searched.
   * @param term The search term.
   * @param y The line to search.
   * @param searchOptions Search options.
   * @return The search result if it was found.
   */
protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult {
    if (this._terminal._core.buffer.lines.get(y).isWrapped) {
      return;
    }

    const stringLine = this.translateBufferLineToStringWithWrap(y, true);
    const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
    const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
    let searchIndex = -1;

    if (searchOptions.regex) {
      const searchRegex = RegExp(searchTerm, 'g');
      const foundTerm = searchRegex.exec(searchStringLine);
      if (foundTerm && foundTerm[0].length > 0) {
        searchIndex = searchRegex.lastIndex - foundTerm[0].length;
        term = foundTerm[0];
      }
    } else {
      searchIndex = searchStringLine.indexOf(searchTerm);
    }

    if (searchIndex >= 0) {
      // Adjust the row number and search index if needed since a "line" of text can span multiple rows
      if (searchIndex >= this._terminal.cols) {
        y += Math.floor(searchIndex / this._terminal.cols);
        searchIndex = searchIndex % this._terminal.cols;
      }
      if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) {
        return;
      }

      const line = this._terminal._core.buffer.lines.get(y);

      for (let i = 0; i < searchIndex; i++) {
        const charData = line.get(i);
        // Adjust the searchIndex to normalize emoji into single chars
        const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
        if (char.length > 1) {
          searchIndex -= char.length - 1;
        }
        // Adjust the searchIndex for empty characters following wide unicode
        // chars (eg. CJK)
        const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/];
        if (charWidth === 0) {
          searchIndex++;
        }
      }
      return {
        term,
        col: searchIndex,
        row: y
      };
    }
  }


/**
   * Translates a buffer line to a string, including subsequent lines if they are wraps.
   * Wide characters will count as two columns in the resulting string. This
   * function is useful for getting the actual text underneath the raw selection
   * position.
   * @param line The line being translated.
   * @param trimRight Whether to trim whitespace to the right.
   */
public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string {
    let lineString = '';
    let lineWrapsToNext: boolean;

    do {
      const nextLine = this._terminal._core.buffer.lines.get(lineIndex + 1);
      lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
      lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight).substring(0, this._terminal.cols);
      lineIndex++;
    } while (lineWrapsToNext);

    return lineString;
  }


/**
   * Selects and scrolls to a result.
   * @param result The result to select.
   * @return Whethera result was selected.
   */
private _selectResult(result: ISearchResult): boolean {
    if (!result) {
      this._terminal.clearSelection();
      return false;
    }
    this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);
    this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp);
    return true;
  }


/**
 * Api Operation details.
 */
export function getApiOperation(args: GetApiOperationArgs, opts?: pulumi.InvokeOptions): Promise<GetApiOperationResult> {
    if (!opts) {
        opts = {}
    }

    if (!opts.version) {
        opts.version = utilities.getVersion();
    }
    return pulumi.runtime.invoke("azure-nextgen:apimanagement/v20180101:getApiOperation", {
        "apiId": args.apiId,
        "operationId": args.operationId,
        "resourceGroupName": args.resourceGroupName,
        "serviceName": args.serviceName,
    }, opts);
}


export interface GetApiOperationArgs {
    /**
     * API revision identifier. Must be unique in the current API Management service instance. Non-current revision has ;rev=n as a suffix where n is the revision number.
     */
    readonly apiId: string;
    /**
     * Operation identifier within an API. Must be unique in the current API Management service instance.
     */
    readonly operationId: string;
    /**
     * The name of the resource group.
     */
    readonly resourceGroupName: string;
    /**
     * The name of the API Management service.
     */
    readonly serviceName: string;
}


/**
 * Api Operation details.
 */
export interface GetApiOperationResult {
    /**
     * Description of the operation. May include HTML formatting tags.
     */
    readonly description?: string;
    /**
     * Operation Name.
     */
    readonly displayName: string;
    /**
     * Resource ID.
     */
    readonly id: string;
    /**
     * A Valid HTTP Operation Method. Typical Http Methods like GET, PUT, POST but not limited by only them.
     */
    readonly method: string;
    /**
     * Resource name.
     */
    readonly name: string;
    /**
     * Operation Policies
     */
    readonly policies?: string;
    /**
     * An entity containing request details.
     */
    readonly request?: outputs.apimanagement.v20180101.RequestContractResponse;
    /**
     * Array of Operation responses.
     */
    readonly responses?: outputs.apimanagement.v20180101.ResponseContractResponse[];
    /**
     * Collection of URL template parameters.
     */
    readonly templateParameters?: outputs.apimanagement.v20180101.ParameterContractResponse[];
    /**
     * Resource type for API Management resource.
     */
    readonly type: string;
    /**
     * Relative URL template identifying the target resource for this operation. May include parameters. Example: /customers/{cid}/orders/{oid}/?date={date}
     */
    readonly urlTemplate: string;
}


() => {
			const { isAuthenticated } = this.props;
			this.setHeader();
			if (this.rid) {
				this.sub?.subscribe?.();
				if (isAuthenticated) {
					this.init();
				} else {
					EventEmitter.addEventListener('connected', this.handleConnected);
				}
			}
			if (this.jumpToMessageId) {
				this.jumpToMessage(this.jumpToMessageId);
			}
			if (this.jumpToThreadId && !this.jumpToMessageId) {
				this.navToThread({ tmid: this.jumpToThreadId });
			}
			if (isIOS && this.rid) {
				this.updateUnreadCount();
			}
		}


key => nextState[key] !== state[key]


key => !dequal(nextState.roomUpdate[key], roomUpdate[key])


async () => {
						// FIXME: why do I need to tell ts this is non null if we have that if condition above?
						await obj!.update(r => {
							r.draftMessage = text;
						});
					}


r => {
							r.draftMessage = text;
						}


() => {
		const { room, unreadsCount, roomUserId, joined } = this.state;
		const { navigation, isMasterDetail, theme, baseUrl, user, insets, route } = this.props;
		const { rid, tmid } = this;
		if (!room.rid) {
			return;
		}

		const prid = room?.prid;
		const isGroupChat = RocketChat.isGroupChat(room as ISubscription);
		let title = route.params?.name;
		let parentTitle = '';
		// TODO: I think it's safe to remove this, but we need to test tablet without rooms
		if (!tmid) {
			title = RocketChat.getRoomTitle(room);
		}
		if (tmid) {
			parentTitle = RocketChat.getRoomTitle(room);
		}
		let subtitle: string | undefined;
		let t: string;
		let teamMain: boolean | undefined;
		let teamId: string | undefined;
		let encrypted: boolean | undefined;
		let userId: string | undefined;
		let token: string | undefined;
		let avatar: string | undefined;
		let visitor: IVisitor | undefined;
		let sourceType: IOmnichannelSource | undefined;
		if ('id' in room) {
			subtitle = room.topic;
			t = room.t;
			teamMain = room.teamMain;
			teamId = room.teamId;
			encrypted = room.encrypted;
			({ id: userId, token } = user);
			avatar = room.name;
			visitor = room.visitor;
		}

		if ('source' in room) {
			t = room.t;
			sourceType = room.source;
			visitor = room.visitor;
		}

		let numIconsRight = 2;
		if (tmid) {
			numIconsRight = 1;
		} else if (teamId && isTeamRoom({ teamId, joined })) {
			numIconsRight = 3;
		}
		const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight });

		navigation.setOptions({
			headerShown: true,
			headerTitleAlign: 'left',
			headerTitleContainerStyle: {
				left: headerTitlePosition.left,
				right: headerTitlePosition.right
			},
			headerLeft: () => (
				<LeftButtons
					tmid={tmid}
					unreadsCount={unreadsCount}
					navigation={navigation}
					baseUrl={baseUrl}
					userId={userId}
					token={token}
					title={avatar}
					theme={theme}
					t={t}
					goRoomActionsView={this.goRoomActionsView}
					isMasterDetail={isMasterDetail}
				/>
			),
			headerTitle: () => (
				<RoomHeader
					rid={rid}
					prid={prid}
					tmid={tmid}
					title={title}
					teamMain={teamMain}
					parentTitle={parentTitle}
					subtitle={subtitle}
					type={t}
					roomUserId={roomUserId}
					visitor={visitor}
					isGroupChat={isGroupChat}
					onPress={this.goRoomActionsView}
					testID={`room-view-title-${title}`}


() => (
				<LeftButtons
					tmid={tmid}


() => (
				<RoomHeader
					rid={rid}


(screen?: string) => {
		logEvent(events.ROOM_GO_RA);
		const { room, member, joined } = this.state;
		const { navigation, isMasterDetail } = this.props;
		if (isMasterDetail) {
			// @ts-ignore TODO: find a way to make it work
			navigation.navigate('ModalStackNavigator', {
				// @ts-ignore
				screen: screen ?? 'RoomActionsView',
				params: {
					rid: this.rid as string,
					t: this.t as SubscriptionType,
					// @ts-ignore
					room,
					member,
					showCloseModal: !!screen,
					joined
				}
			});
		} else if (this.rid && this.t) {
			navigation.push('RoomActionsView', {
				rid: this.rid,
				t: this.t as SubscriptionType,
				room: room as TSubscriptionModel,
				member,
				joined
			});
		}
	}


async () => {
		const { room } = this.state;
		const { user } = this.props;
		const readOnly = await isReadOnly(room as ISubscription, user.username as string);
		this.setState({ readOnly });
	}


async () => {
		try {
			this.setState({ loading: true });
			const { room, joined } = this.state;
			if (!this.rid) {
				return;
			}
			if (this.tmid) {
				await RoomServices.getThreadMessages(this.tmid, this.rid);
			} else {
				const newLastOpen = new Date();
				await RoomServices.getMessages(room);

				// if room is joined
				if (joined && 'id' in room) {
					if (room.alert || room.unread || room.userMentions) {
						this.setLastOpen(room.ls);
					} else {
						this.setLastOpen(null);
					}
					RoomServices.readMessages(room.rid, newLastOpen).catch(e => console.log(e));
				}
			}

			const canAutoTranslate = RocketChat.canAutoTranslate();
			const member = await this.getRoomMember();

			this.setState({ canAutoTranslate, member, loading: false });
		} catch (e) {
			this.setState({ loading: false });
			this.retryInit += 1;
			if (this.retryInit <= 1) {
				this.retryInitTimeout = setTimeout(() => {
					this.init();
				}, 300);
			}
		}
	}


() => {
					this.init();
				}


async () => {
		const { room } = this.state;
		const { t } = room;

		if ('id' in room && t === 'd' && !RocketChat.isGroupChat(room)) {
			try {
				const roomUserId = RocketChat.getUidDirectMessage(room);
				this.setState({ roomUserId }, () => this.setHeader());

				const result = await RocketChat.getUserInfo(roomUserId);
				if (result.success) {
					return result.user;
				}
			} catch (e) {
				log(e);
			}
		}

		return {};
	}


() => this.setHeader()


async (rid: string) => {
		try {
			const db = database.active;
			const subCollection = await db.get('subscriptions');
			const room = await subCollection.find(rid);
			this.setState({ room });
			if (!this.tmid) {
				this.setHeader();
			}
			this.observeRoom(room);
		} catch (error) {
			if (this.t !== 'd') {
				console.log('Room not found');
				this.internalSetState({ joined: false });
			}
			if (this.rid) {
				// We navigate to RoomView before the Room is inserted to the local db
				// So we retry just to make sure we have the right content
				this.retryFindCount = this.retryFindCount + 1 || 1;
				if (this.retryFindCount <= 3) {
					this.retryFindTimeout = setTimeout(() => {
						this.findAndObserveRoom(rid);
						this.init();
					}, 300);
				}
			}
		}
	}


() => {
						this.findAndObserveRoom(rid);
						this.init();
					}


async () => {
		if (this.sub && this.sub.unsubscribe) {
			await this.sub.unsubscribe();
		}
		delete this.sub;
	}


(room: TSubscriptionModel) => {
		const observable = room.observe();
		this.subSubscription = observable.subscribe(changes => {
			const roomUpdate = roomAttrsUpdate.reduce((ret: any, attr) => {
				ret[attr] = changes[attr];
				return ret;
			}, {});
			if (this.mounted) {
				this.internalSetState({ room: changes, roomUpdate });
			} else {
				// @ts-ignore
				this.state.room = changes;
				// @ts-ignore
				this.state.roomUpdate = roomUpdate;
			}
		});
	}


changes => {
			const roomUpdate = roomAttrsUpdate.reduce((ret: any, attr) => {
				ret[attr] = changes[attr];
				return ret;
			}, {});
			if (this.mounted) {
				this.internalSetState({ room: changes, roomUpdate });
			} else {
				// @ts-ignore
				this.state.room = changes;
				// @ts-ignore
				this.state.roomUpdate = roomUpdate;
			}
		}


(ret: any, attr) => {
				ret[attr] = changes[attr];
				return ret;
			}


(message: TAnyMessageModel) => {
		// @ts-ignore
		this.messageErrorActions?.showMessageErrorActions(message);
	}


(message: TAnyMessageModel) => {
		const newMessage = {
			id: message.id,
			subscription: {
				// @ts-ignore TODO: we can remove this after we merge a PR separating IMessage vs IMessageFromServer
				id: message.subscription.id
			},
			msg: message?.attachments?.[0]?.description || message.msg
		} as TMessageModel;
		this.setState({ selectedMessage: newMessage, editing: true });
	}


() => {
		this.setState({ selectedMessage: undefined, editing: false });
	}


async (message: TAnyMessageModel) => {
		this.setState({ selectedMessage: undefined, editing: false });
		try {
			await RocketChat.editMessage(message);
		} catch (e) {
			log(e);
		}
	}


(message: TAnyMessageModel, mention: boolean) => {
		this.setState({
			selectedMessage: message,
			replying: true,
			replyWithMention: mention
		});
	}


() => {
		this.setState({ selectedMessage: undefined, replying: false, replyWithMention: false });
	}


(message: TAnyMessageModel) => {
		this.setState({ selectedMessage: message, reacting: true });
	}


() => {
		this.setState({ selectedMessage: undefined, reacting: false });
	}


(message: TAnyMessageModel) => {
		// @ts-ignore
		this.messageActions?.showMessageActions(message);
	}


(attachment: IAttachment) => {
		const { navigation } = this.props;
		// @ts-ignore
		navigation.navigate('AttachmentView', { attachment });
	}


async (shortname: string, messageId: string) => {
		try {
			await RocketChat.setReaction(shortname, messageId);
			this.onReactionClose();
			Review.pushPositiveEvent();
		} catch (e) {
			log(e);
		}
	}


(message: TAnyMessageModel) => {
		this.setState({ selectedMessage: message, reactionsModalVisible: true });
		Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
	}


() => {
		this.setState({ selectedMessage: undefined, reactionsModalVisible: false });
	}


() => {
		logEvent(events.ROOM_ENCRYPTED_PRESS);
		const { navigation, isMasterDetail } = this.props;

		const screen = { screen: 'E2EHowItWorksView', params: { showCloseModal: true } };

		if (isMasterDetail) {
			// @ts-ignore
			return navigation.navigate('ModalStackNavigator', screen);
		}
		// @ts-ignore
		navigation.navigate('E2ESaveYourPasswordStackNavigator', screen);
	}


(item: TAnyMessageModel) => {
			const { navigation } = this.props;
			navigation.push('RoomView', {
				rid: item.drid as string,
				prid: item.rid,
				name: item.msg,
				t: 'p' as SubscriptionType
			});
		}


async () => {
		if (!this.rid) {
			return;
		}
		const db = database.active;
		const observable = await db
			.get('subscriptions')
			.query(Q.where('archived', false), Q.where('open', true), Q.where('rid', Q.notEq(this.rid)))
			.observeWithColumns(['unread']);

		this.queryUnreads = observable.subscribe(data => {
			const { unreadsCount } = this.state;
			const newUnreadsCount = data.filter(s => s.unread > 0).reduce((a, b) => a + (b.unread || 0), 0);
			if (unreadsCount !== newUnreadsCount) {
				this.setState({ unreadsCount: newUnreadsCount }, () => this.setHeader());
			}
		});
	}


data => {
			const { unreadsCount } = this.state;
			const newUnreadsCount = data.filter(s => s.unread > 0).reduce((a, b) => a + (b.unread || 0), 0);
			if (unreadsCount !== newUnreadsCount) {
				this.setState({ unreadsCount: newUnreadsCount }, () => this.setHeader());
			}
		}


s => s.unread > 0


(a, b) => a + (b.unread || 0)


(item: TAnyMessageModel) => this.navToThread(item)


(message: IMessage) => {
		if (message.tmid && message.tmid === this.tmid) {
			return false;
		}
		if (!message.tmid && message.rid === this.rid) {
			return false;
		}
		return true;
	}


async (messageUrl?: string) => {
		if (!messageUrl) {
			return;
		}
		try {
			this.setState({ showingBlockingLoader: true });
			const parsedUrl = parse(messageUrl, true);
			const messageId = parsedUrl.query.msg;
			if (messageId) {
				await this.jumpToMessage(messageId);
			}
			this.setState({ showingBlockingLoader: false });
		} catch (e) {
			this.setState({ showingBlockingLoader: false });
			log(e);
		}
	}


async (messageId: string) => {
		try {
			this.setState({ showingBlockingLoader: true });
			const message = await RoomServices.getMessageInfo(messageId);

			if (!message) {
				return;
			}

			if (this.shouldNavigateToRoom(message)) {
				if (message.rid !== this.rid) {
					this.navToRoom(message);
				} else {
					this.navToThread(message);
				}
			} else {
				/**
				 * if it's from server, we don't have it saved locally and so we fetch surroundings
				 * we test if it's not from threads because we're fetching from threads currently with `getThreadMessages`
				 */
				if (message.fromServer && !message.tmid && this.rid) {
					await RocketChat.loadSurroundingMessages({ messageId, rid: this.rid });
				}
				// @ts-ignore
				await Promise.race([this.list.current.jumpToMessage(message.id), new Promise(res => setTimeout(res, 5000))]);
				// @ts-ignore
				this.list.current.cancelJumpToMessage();
			}
		} catch (e) {
			log(e);
		} finally {
			this.setState({ showingBlockingLoader: false });
		}
	}


res => setTimeout(res, 5000)


(message: IMessage) => {
		const { dispatch } = this.props;
		dispatch(replyBroadcast(message));
	}


() => {
		this.init();
		EventEmitter.removeListener('connected', this.handleConnected);
	}


({ rid }: { rid: string }) => {
		const { room } = this.state;
		if (rid === this.rid) {
			Navigation.navigate('RoomsListView');
			!this.isOmnichannel &&
				showErrorAlert(I18n.t('You_were_removed_from_channel', { channel: RocketChat.getRoomTitle(room) }), I18n.t('Oops'));
		}
	}


(...args: any[]) => {
		if (!this.mounted) {
			return;
		}
		// @ts-ignore TODO: TS is complaining about this, but I don't feel like changing rn since it should be working
		this.setState(...args);
	}


(message: string, tmid?: string, tshow?: boolean) => {
		logEvent(events.ROOM_SEND_MESSAGE);
		const { rid } = this.state.room;
		const { user } = this.props;
		RocketChat.sendMessage(rid, message, this.tmid || tmid, user, tshow).then(() => {
			if (this.list && this.list.current) {
				// @ts-ignore
				this.list.current.update();
			}
			this.setLastOpen(null);
			Review.pushPositiveEvent();
		});
	}


() => {
			if (this.list && this.list.current) {
				// @ts-ignore
				this.list.current.update();
			}
			this.setLastOpen(null);
			Review.pushPositiveEvent();
		}


(name: string): IReduxEmoji | null => {
		const { customEmojis } = this.props;
		const emoji = customEmojis[name];
		if (emoji) {
			return emoji;
		}
		return null;
	}


(lastOpen: Date | null) => this.setState({ lastOpen })


() => {
		this.internalSetState({
			joined: true
		});
	}


async () => {
		logEvent(events.ROOM_JOIN);
		try {
			const { room } = this.state;

			if (this.isOmnichannel) {
				if ('_id' in room) {
					await takeInquiry(room._id);
				}
				this.onJoin();
			} else {
				const { joinCodeRequired, rid } = room;
				if (joinCodeRequired) {
					// @ts-ignore
					this.joinCode.current?.show();
				} else {
					await RocketChat.joinRoom(rid, null, this.t as any);
					this.onJoin();
				}
			}
		} catch (e) {
			log(e);
		}
	}


async () => {
		logEvent(events.ROOM_RESUME);
		try {
			const { room } = this.state;

			if (this.isOmnichannel) {
				if ('rid' in room) {
					await takeResume(room.rid);
				}
				this.onJoin();
			}
		} catch (e) {
			log(e);
		}
	}


(tmid: string, messageId: string) => {
		const { rid } = this.state.room;
		return getThreadName(rid, tmid, messageId);
	}


async (isFollowingThread: boolean, tmid?: string) => {
		try {
			const threadMessageId = tmid ?? this.tmid;
			if (!threadMessageId) {
				return;
			}
			await RocketChat.toggleFollowMessage(threadMessageId, !isFollowingThread);
			EventEmitter.emit(LISTENER, { message: isFollowingThread ? I18n.t('Unfollowed_thread') : I18n.t('Following_thread') });
		} catch (e) {
			log(e);
		}
	}


(messageId: string) => {
		const { room } = this.state;
		const { theme } = this.props;
		return getBadgeColor({ subscription: room, theme, messageId });
	}


(navParam: any) => {
		const { navigation, user, isMasterDetail } = this.props;
		logEvent(events[`ROOM_GO_${navParam.t === 'd' ? 'USER' : 'ROOM'}_INFO`]);
		if (navParam.rid === user.id) {
			return;
		}
		if (isMasterDetail) {
			navParam.showCloseModal = true;
			// @ts-ignore
			navigation.navigate('ModalStackNavigator', { screen: 'RoomInfoView', params: navParam });
		} else {
			navigation.navigate('RoomInfoView', navParam);
		}
	}


async (item: TAnyMessageModel | { tmid: string }) => {
		const { roomUserId } = this.state;
		const { navigation } = this.props;

		if (!this.rid) {
			return;
		}

		if (item.tmid) {
			let name = '';
			let jumpToMessageId = '';
			if ('id' in item) {
				name = item.tmsg ?? '';
				jumpToMessageId = item.id;
			}
			if (!name) {
				const result = await this.getThreadName(item.tmid, jumpToMessageId);
				// test if there isn't a thread
				if (!result) {
					return;
				}
				name = result;
			}
			if ('id' in item && item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE) {
				name = I18n.t('Encrypted_message');
			}
			return navigation.push('RoomView', {
				rid: this.rid,
				tmid: item.tmid,
				name,
				t: SubscriptionType.THREAD,
				roomUserId,
				jumpToMessageId
			});
		}

		if ('tlm' in item) {
			return navigation.push('RoomView', {
				rid: this.rid,
				tmid: item.id,
				name: makeThreadName(item),
				t: SubscriptionType.THREAD,
				roomUserId
			});
		}
	}


async (message: TAnyMessageModel) => {
		const { navigation, isMasterDetail } = this.props;
		const roomInfo = await getRoomInfo(message.rid);
		return goRoom({
			item: roomInfo as TGoRoomItem,
			isMasterDetail,
			navigationMethod: navigation.push,
			jumpToMessageId: message.id
		});
	}


() => {
		const { room } = this.state;
		if ('id' in room) {
			const { jitsiTimeout } = room;
			if (jitsiTimeout && jitsiTimeout < new Date()) {
				showErrorAlert(I18n.t('Call_already_ended'));
			} else {
				RocketChat.callJitsi(room);
			}
		}
	}


({ event }: { event: IKeyCommandEvent }) => {
		if (this.rid) {
			const { input } = event;
			if (handleCommandScroll(event)) {
				const offset = input === 'UIKeyInputUpArrow' ? 100 : -100;
				this.offset += offset;
				// @ts-ignore
				this.flatList?.scrollToOffset({ offset: this.offset });
			} else if (handleCommandRoomActions(event)) {
				this.goRoomActionsView();
			} else if (handleCommandSearchMessages(event)) {
				this.goRoomActionsView('SearchMessagesView');
			} else if (handleCommandReplyLatest(event)) {
				if (this.list && this.list.current) {
					// @ts-ignore
					const message = this.list.current.getLastMessage();
					this.onReplyInit(message, false);
				}
			}
		}
	}


({
		actionId,
		appId,
		value,
		blockId,
		rid,
		mid
	}: {
		actionId: string;
		appId: string;
		value: any;
		blockId: string;
		rid: string;
		mid: string;
	}) =>
		RocketChat.triggerBlockAction({
			blockId,
			actionId,
			value,
			mid,
			rid,
			appId,
			container: {
				type: ContainerTypes.MESSAGE,
				id: mid
			}
		})


async () => {
		const { room } = this.state;
		if ('id' in room) {
			try {
				const db = database.active;
				await db.write(async () => {
					await room.update(r => {
						r.bannerClosed = true;
					});
				});
			} catch {
				// do nothing
			}
		}
	}


async () => {
					await room.update(r => {
						r.bannerClosed = true;
					});
				}


r => {
						r.bannerClosed = true;
					}


(message: TAnyMessageModel): boolean => {
		const { room } = this.state;
		if ('id' in room) {
			return room?.ignored?.includes?.(message?.u?._id) ?? false;
		}
		return false;
	}


(loaderItem: TAnyMessageModel) => {
		const { room } = this.state;
		return RoomServices.getMoreMessages({
			rid: room.rid,
			tmid: this.tmid,
			t: room.t as any,
			loaderItem
		});
	}


(item: TAnyMessageModel, previousItem: TAnyMessageModel, highlightedMessage?: string) => {
		const { room, lastOpen, canAutoTranslate } = this.state;
		const { user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled, theme } =
			this.props;
		let dateSeparator = null;
		let showUnreadSeparator = false;

		if (!previousItem) {
			dateSeparator = item.ts;
			showUnreadSeparator = moment(item.ts).isAfter(lastOpen);
		} else {
			showUnreadSeparator =
				(lastOpen && moment(item.ts).isSameOrAfter(lastOpen) && moment(previousItem.ts).isBefore(lastOpen)) ?? false;
			if (!moment(item.ts).isSame(previousItem.ts, 'day')) {
				dateSeparator = item.ts;
			}
		}

		let content = null;
		if (item.t && MESSAGE_TYPE_ANY_LOAD.includes(item.t as MessageTypeLoad)) {
			content = (
				<LoadMore
					load={() => this.onLoadMoreMessages(item)}
					type={item.t}
					runOnRender={item.t === MessageTypeLoad.MORE && !previousItem}
				/>
			);
		}


() => {
		const { joined, room, selectedMessage, editing, replying, replyWithMention, readOnly, loading } = this.state;
		const { navigation, theme, route } = this.props;

		const usedCannedResponse = route?.params?.usedCannedResponse;

		if (!this.rid) {
			return null;
		}
		if ('onHold' in room && room.onHold) {
			return (
				<View style={styles.joinRoomContainer} key='room-view-chat-on-hold' testID='room-view-chat-on-hold'>
					<Text accessibilityLabel={I18n.t('Chat_is_on_hold')} style={[styles.previewMode, { color: themes[theme].titleText }]}>
						{I18n.t('Chat_is_on_hold')}
					</Text>
					<Touch
						onPress={this.resumeRoom}
						style={[styles.joinRoomButton, { backgroundColor: themes[theme].actionTintColor }]}
						enabled={!loading}
						theme={theme}>
						<Text style={[styles.joinRoomText, { color: themes[theme].buttonText }]} testID='room-view-chat-on-hold-button'>
							{I18n.t('Resume')}
						</Text>
					</Touch>
				</View>
			);
		}


() => {
		const { room, readOnly } = this.state;
		const { user } = this.props;
		if (!('id' in room)) {
			return null;
		}
		return (
			<>
				<MessageActions
					// @ts-ignore
					ref={ref => (this.messageActions = ref)}


(state: IApplicationState) => ({
	user: getUserSelector(state),
	isMasterDetail: state.app.isMasterDetail,
	appState: state.app.ready && state.app.foreground ? 'foreground' : 'background',
	useRealName: state.settings.UI_Use_Real_Name as boolean,
	isAuthenticated: state.login.isAuthenticated,
	Message_GroupingPeriod: state.settings.Message_GroupingPeriod as number,
	Message_TimeFormat: state.settings.Message_TimeFormat as string,
	customEmojis: state.customEmojis,
	baseUrl: state.server.server,
	serverVersion: state.server.version,
	Message_Read_Receipt_Enabled: state.settings.Message_Read_Receipt_Enabled as boolean,
	Hide_System_Messages: state.settings.Hide_System_Messages as string[]
})


interface IRoomViewProps extends IBaseScreen<ChatsStackParamList, 'RoomView'> {
	user: Pick<ILoggedUser, 'id' | 'username' | 'token' | 'showMessageInMainThread'>;
	appState: string;
	useRealName?: boolean;
	isAuthenticated: boolean;
	Message_GroupingPeriod?: number;
	Message_TimeFormat?: string;
	Message_Read_Receipt_Enabled?: boolean;
	Hide_System_Messages?: string[];
	baseUrl: string;
	serverVersion: string | null;
	customEmojis: ICustomEmojis;
	isMasterDetail: boolean;
	replyBroadcast: Function;
	width: number;
	height: number;
	insets: EdgeInsets;
}


interface IRoomViewState {
	[key: string]: any;
	joined: boolean;
	room: TSubscriptionModel | { rid: string; t: string; name?: string; fname?: string; prid?: string; joinCodeRequired?: boolean };
	roomUpdate: {
		[K in TRoomUpdate]?: any; // TODO: get type from TSubscriptionModel
	};
	member: any;
	lastOpen: Date | null;
	reactionsModalVisible: boolean;
	selectedMessage?: TAnyMessageModel;
	canAutoTranslate: boolean;
	loading: boolean;
	showingBlockingLoader: boolean;
	editing: boolean;
	replying: boolean;
	replyWithMention: boolean;
	reacting: boolean;
	readOnly: boolean;
	unreadsCount: number | null;
	roomUserId?: string | null;
}


type TRoomUpdate = typeof roomAttrsUpdate[number];


componentDidMount() {
		this.mounted = true;
		this.didMountInteraction = InteractionManager.runAfterInteractions(() => {
			const { isAuthenticated } = this.props;
			this.setHeader();
			if (this.rid) {
				this.sub?.subscribe?.();
				if (isAuthenticated) {
					this.init();
				} else {
					EventEmitter.addEventListener('connected', this.handleConnected);
				}
			}
			if (this.jumpToMessageId) {
				this.jumpToMessage(this.jumpToMessageId);
			}
			if (this.jumpToThreadId && !this.jumpToMessageId) {
				this.navToThread({ tmid: this.jumpToThreadId });
			}
			if (isIOS && this.rid) {
				this.updateUnreadCount();
			}
		});
		if (isTablet) {
			EventEmitter.addEventListener(KEY_COMMAND, this.handleCommands);
		}
		EventEmitter.addEventListener('ROOM_REMOVED', this.handleRoomRemoved);
		console.timeEnd(`${this.constructor.name} mount`);
	}


shouldComponentUpdate(nextProps: IRoomViewProps, nextState: IRoomViewState) {
		const { state } = this;
		const { roomUpdate, member } = state;
		const { appState, theme, insets, route } = this.props;
		if (theme !== nextProps.theme) {
			return true;
		}
		if (appState !== nextProps.appState) {
			return true;
		}
		if (member.statusText !== nextState.member.statusText) {
			return true;
		}
		const stateUpdated = stateAttrsUpdate.some(key => nextState[key] !== state[key]);
		if (stateUpdated) {
			return true;
		}
		if (!dequal(nextProps.insets, insets)) {
			return true;
		}
		if (!dequal(nextProps.route?.params, route?.params)) {
			return true;
		}
		return roomAttrsUpdate.some(key => !dequal(nextState.roomUpdate[key], roomUpdate[key]));
	}


componentDidUpdate(prevProps: IRoomViewProps, prevState: IRoomViewState) {
		const { roomUpdate } = this.state;
		const { appState, insets, route } = this.props;

		if (route?.params?.jumpToMessageId && route?.params?.jumpToMessageId !== prevProps.route?.params?.jumpToMessageId) {
			this.jumpToMessage(route?.params?.jumpToMessageId);
		}

		if (route?.params?.jumpToThreadId && route?.params?.jumpToThreadId !== prevProps.route?.params?.jumpToThreadId) {
			this.navToThread({ tmid: route?.params?.jumpToThreadId });
		}

		if (appState === 'foreground' && appState !== prevProps.appState && this.rid) {
			// Fire List.query() just to keep observables working
			if (this.list && this.list.current) {
				// @ts-ignore TODO: is this working?
				this.list.current?.query?.();
			}
		}
		// If it's not direct message
		if (this.t !== 'd') {
			if (roomUpdate.topic !== prevState.roomUpdate.topic) {
				this.setHeader();
			}
		}
		// If it's a livechat room
		if (this.t === 'l') {
			if (!dequal(prevState.roomUpdate.visitor, roomUpdate.visitor)) {
				this.setHeader();
			}
		}
		if (roomUpdate.teamMain !== prevState.roomUpdate.teamMain || roomUpdate.teamId !== prevState.roomUpdate.teamId) {
			this.setHeader();
		}
		if (
			(roomUpdate.fname !== prevState.roomUpdate.fname ||
				roomUpdate.name !== prevState.roomUpdate.name ||
				roomUpdate.teamMain !== prevState.roomUpdate.teamMain ||
				roomUpdate.teamId !== prevState.roomUpdate.teamId) &&
			!this.tmid
		) {
			this.setHeader();
		}
		if (insets.left !== prevProps.insets.left || insets.right !== prevProps.insets.right) {
			this.setHeader();
		}
		this.setReadOnly();
	}


async componentWillUnmount() {
		const { editing, room } = this.state;
		const db = database.active;
		this.mounted = false;
		if (!editing && this.messagebox && this.messagebox.current) {
			// @ts-ignore
			const { text } = this.messagebox.current;
			let obj: TSubscriptionModel | TThreadModel | null = null;
			if (this.tmid) {
				try {
					const threadsCollection = db.get('threads');
					obj = await threadsCollection.find(this.tmid);
				} catch (e) {
					// Do nothing
				}
			} else {
				obj = room as TSubscriptionModel;
			}
			if (obj) {
				try {
					await db.write(async () => {
						// FIXME: why do I need to tell ts this is non null if we have that if condition above?
						await obj!.update(r => {
							r.draftMessage = text;
						});
					});
				} catch (error) {
					// Do nothing
				}
			}
		}
		this.unsubscribe();
		if (this.didMountInteraction && this.didMountInteraction.cancel) {
			this.didMountInteraction.cancel();
		}
		if (this.subSubscription && this.subSubscription.unsubscribe) {
			this.subSubscription.unsubscribe();
		}
		if (this.queryUnreads && this.queryUnreads.unsubscribe) {
			this.queryUnreads.unsubscribe();
		}
		if (this.retryInitTimeout) {
			clearTimeout(this.retryInitTimeout);
		}
		if (this.retryFindTimeout) {
			clearTimeout(this.retryFindTimeout);
		}
		EventEmitter.removeListener('connected', this.handleConnected);
		if (isTablet) {
			EventEmitter.removeListener(KEY_COMMAND, this.handleCommands);
		}
		EventEmitter.removeListener('ROOM_REMOVED', this.handleRoomRemoved);
		console.countReset(`${this.constructor.name}.render calls`);
	}


export interface IProcess {
  on(event: 'starting' | 'running' | 'stopping' | 'stopped', listener: () => void): this;
  start(): void;
  run(): Promise<void>;
  stop(): Promise<void>;
  getProcessHandler(): IProcessHandler;
}


/**
 * Enables you to manage DNS A Records within Azure DNS.
 */
export class ARecord extends pulumi.CustomResource {
    /**
     * Get an existing ARecord resource's state with the given name, ID, and optional extra
     * properties used to qualify the lookup.
     *
     * @param name The _unique_ name of the resulting resource.
     * @param id The _unique_ provider ID of the resource to lookup.
     * @param state Any extra arguments used during the lookup.
     */
    public static get(name: string, id: pulumi.Input<pulumi.ID>, state?: ARecordState, opts?: pulumi.CustomResourceOptions): ARecord {
        return new ARecord(name, <any>state, { ...opts, id: id });
    }

    /**
     * The name of the DNS A Record.
     */
    public readonly name: pulumi.Output<string>;
    /**
     * List of IPv4 Addresses.
     */
    public readonly records: pulumi.Output<string[]>;
    /**
     * Specifies the resource group where the resource exists. Changing this forces a new resource to be created.
     */
    public readonly resourceGroupName: pulumi.Output<string>;
    /**
     * A mapping of tags to assign to the resource.
     */
    public readonly tags: pulumi.Output<{[key: string]: any}>;
    public readonly ttl: pulumi.Output<number>;
    /**
     * Specifies the DNS Zone where the resource exists. Changing this forces a new resource to be created.
     */
    public readonly zoneName: pulumi.Output<string>;

    /**
     * Create a ARecord resource with the given unique name, arguments, and options.
     *
     * @param name The _unique_ name of the resource.
     * @param args The arguments to use to populate this resource's properties.
     * @param opts A bag of options that control this resource's behavior.
     */
    constructor(name: string, args: ARecordArgs, opts?: pulumi.CustomResourceOptions)
    constructor(name: string, argsOrState?: ARecordArgs | ARecordState, opts?: pulumi.CustomResourceOptions) {
        let inputs: pulumi.Inputs = {};
        if (opts && opts.id) {
            const state: ARecordState = argsOrState as ARecordState | undefined;
            inputs["name"] = state ? state.name : undefined;
            inputs["records"] = state ? state.records : undefined;
            inputs["resourceGroupName"] = state ? state.resourceGroupName : undefined;
            inputs["tags"] = state ? state.tags : undefined;
            inputs["ttl"] = state ? state.ttl : undefined;
            inputs["zoneName"] = state ? state.zoneName : undefined;
        } else {
            const args = argsOrState as ARecordArgs | undefined;
            if (!args || args.records === undefined) {
                throw new Error("Missing required property 'records'");
            }
            if (!args || args.resourceGroupName === undefined) {
                throw new Error("Missing required property 'resourceGroupName'");
            }
            if (!args || args.ttl === undefined) {
                throw new Error("Missing required property 'ttl'");
            }
            if (!args || args.zoneName === undefined) {
                throw new Error("Missing required property 'zoneName'");
            }
            inputs["name"] = args ? args.name : undefined;
            inputs["records"] = args ? args.records : undefined;
            inputs["resourceGroupName"] = args ? args.resourceGroupName : undefined;
            inputs["tags"] = args ? args.tags : undefined;
            inputs["ttl"] = args ? args.ttl : undefined;
            inputs["zoneName"] = args ? args.zoneName : undefined;
        }
        super("azure:dns/aRecord:ARecord", name, inputs, opts);
    }
}


/**
 * Input properties used for looking up and filtering ARecord resources.
 */
export interface ARecordState {
    /**
     * The name of the DNS A Record.
     */
    readonly name?: pulumi.Input<string>;
    /**
     * List of IPv4 Addresses.
     */
    readonly records?: pulumi.Input<pulumi.Input<string>[]>;
    /**
     * Specifies the resource group where the resource exists. Changing this forces a new resource to be created.
     */
    readonly resourceGroupName?: pulumi.Input<string>;
    /**
     * A mapping of tags to assign to the resource.
     */
    readonly tags?: pulumi.Input<{[key: string]: any}>;
    readonly ttl?: pulumi.Input<number>;
    /**
     * Specifies the DNS Zone where the resource exists. Changing this forces a new resource to be created.
     */
    readonly zoneName?: pulumi.Input<string>;
}


/**
 * The set of arguments for constructing a ARecord resource.
 */
export interface ARecordArgs {
    /**
     * The name of the DNS A Record.
     */
    readonly name?: pulumi.Input<string>;
    /**
     * List of IPv4 Addresses.
     */
    readonly records: pulumi.Input<pulumi.Input<string>[]>;
    /**
     * Specifies the resource group where the resource exists. Changing this forces a new resource to be created.
     */
    readonly resourceGroupName: pulumi.Input<string>;
    /**
     * A mapping of tags to assign to the resource.
     */
    readonly tags?: pulumi.Input<{[key: string]: any}>;
    readonly ttl: pulumi.Input<number>;
    /**
     * Specifies the DNS Zone where the resource exists. Changing this forces a new resource to be created.
     */
    readonly zoneName: pulumi.Input<string>;
}


/**
     * Get an existing ARecord resource's state with the given name, ID, and optional extra
     * properties used to qualify the lookup.
     *
     * @param name The _unique_ name of the resulting resource.
     * @param id The _unique_ provider ID of the resource to lookup.
     * @param state Any extra arguments used during the lookup.
     */
public static get(name: string, id: pulumi.Input<pulumi.ID>, state?: ARecordState, opts?: pulumi.CustomResourceOptions): ARecord {
        return new ARecord(name, <any>state, { ...opts, id: id });
    }


(req: EchoRequest) => this.request<EchoRequest, EchoResponse>("demo.DemoService/Echo", req)


res => res.json().then(body => ({ res, body }))


body => ({ res, body })


({ res, body }) => {
            if (!res.ok) {
                const err = new Error(body.msg) as TwirpError;
                err.status = res.status;
                err.code = body.code;
                err.meta = body.meta;
                throw err;
            }
            return body as Res;
        }


export class DemoService {
  prefix: string;
  fetcher: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
  options: TwirpOptions;
  constructor(prefix: string, options: TwirpOptions = {}) {
      this.prefix = prefix;
      this.options = options;

      if (typeof options.fetcher == "function") {
        this.fetcher = options.fetcher
      } else if (typeof window != "undefined" && typeof window.fetch == "function") {
        this.fetcher = window.fetch
      } else if (typeof fetch == "function") {
        this.fetcher = fetch
      } else {
        throw new Error("missing fetcher")
      }

      if (!prefix.trim()) {
        throw new Error("missing prefix")
      }
  }

/**
 * Echo responds with the message passed into the
 *  request. Useful for testing and
 * as a minimal
 *  example.
 */
  Echo = (req: EchoRequest) => this.request<EchoRequest, EchoResponse>("demo.DemoService/Echo", req)

  request<Req,Res>(method: string, variables: Req, options: TwirpOptions = {}): Promise<Res> {
    return this.fetcher(this.prefix + method, {
        method: "POST",
        headers: {
            ...this.options.headers,
            ...options.headers,
            Accept: "application/json",
            "Content-Type": "application/json"
        },
        body: JSON.stringify(variables)
    })
        .then(res => res.json().then(body => ({ res, body })))
        .then(({ res, body }) => {
            if (!res.ok) {
                const err = new Error(body.msg) as TwirpError;
                err.status = res.status;
                err.code = body.code;
                err.meta = body.meta;
                throw err;
            }
            return body as Res;
        });
  }
}


// generated by protoc-gen-gotemplate. DO NOT EDIT.
// source: demo/service.proto
export interface EchoRequest {

  message: string;
}


export interface EchoResponse {

  message: string;
}


/**
 * DemoService shows a very simple service with only
 *  an Echo method.
 */
interface TwirpOptions {
  headers?: object
  fetcher?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
}


interface TwirpError extends Error {
  status: number
  code: string
  meta: object
}


request<Req,Res>(method: string, variables: Req, options: TwirpOptions = {}): Promise<Res> {
    return this.fetcher(this.prefix + method, {
        method: "POST",
        headers: {
            ...this.options.headers,
            ...options.headers,
            Accept: "application/json",
            "Content-Type": "application/json"
        },
        body: JSON.stringify(variables)
    })
        .then(res => res.json().then(body => ({ res, body })))
        .then(({ res, body }) => {
            if (!res.ok) {
                const err = new Error(body.msg) as TwirpError;
                err.status = res.status;
                err.code = body.code;
                err.meta = body.meta;
                throw err;
            }
            return body as Res;
        });
  }


function MovieCard({ movie }: Props) {

    return (

{movie.title}

                <MovieScore count={movie.count} score={movie.score} />
                <Link to={`/form/${movie.id}`}>

Avaliar

                </Link>

    );

}


type Props = {
    movie: Movie;
}


function registerPostProcessor(
  name: string,
  middleware: PostProcessMiddleware,
  condition?: (options: postProcessOptions) => boolean
) {
  middlewareRegistry.push({ name, middleware, condition: condition || null })
}


async function processHTML(
  html: string,
  data: renderOptions,
  options: postProcessOptions
): Promise<string> {
  // Don't parse unless there's at least one processor middleware
  if (!middlewareRegistry[0]) {
    return html
  }
  const postProcessData: postProcessData = {
    preloads: {
      images: [],
    },
  }
  const root: HTMLElement = parse(html)
  let document = html
  // Calls the middleware, with some instrumentation and logging
  async function callMiddleWare(
    middleware: PostProcessMiddleware,
    name: string
  ) {
    let timer = Date.now()
    middleware.inspect(root, postProcessData, data)
    const inspectTime = Date.now() - timer
    document = await middleware.mutate(document, postProcessData, data)
    timer = Date.now() - timer
    if (timer > MIDDLEWARE_TIME_BUDGET) {
      console.warn(
        `The postprocess middleware "${name}" took ${timer}ms(${inspectTime}, ${
          timer - inspectTime
        }) to complete. This is longer than the ${MIDDLEWARE_TIME_BUDGET} limit.`
      )
    }
    return
  }

  for (let i = 0; i < middlewareRegistry.length; i++) {
    let middleware = middlewareRegistry[i]
    if (!middleware.condition || middleware.condition(options)) {
      await callMiddleWare(
        middlewareRegistry[i].middleware,
        middlewareRegistry[i].name
      )
    }
  }

  return document
}


// Calls the middleware, with some instrumentation and logging
async function callMiddleWare(
    middleware: PostProcessMiddleware,
    name: string
  ) {
    let timer = Date.now()
    middleware.inspect(root, postProcessData, data)
    const inspectTime = Date.now() - timer
    document = await middleware.mutate(document, postProcessData, data)
    timer = Date.now() - timer
    if (timer > MIDDLEWARE_TIME_BUDGET) {
      console.warn(
        `The postprocess middleware "${name}" took ${timer}ms(${inspectTime}, ${
          timer - inspectTime
        }) to complete. This is longer than the ${MIDDLEWARE_TIME_BUDGET} limit.`
      )
    }
    return
  }


function isImgEligible(imgElement: HTMLElement): boolean {
  return (
    imgElement.hasAttribute('src') &&
    imageIsNotTooSmall(imgElement) &&
    imageIsNotHidden(imgElement)
  )
}


function preloadTagAlreadyExists(html: string, href: string) {
  const regex = new RegExp(`<link[^>]*href[^>]*${href}`)
  return html.match(regex)
}


function imageIsNotTooSmall(imgElement: HTMLElement): boolean {
  // Skip images without both height and width--we don't know enough to say if
  // they are too small
  if (
    !(imgElement.hasAttribute('height') && imgElement.hasAttribute('width'))
  ) {
    return true
  }
  try {
    if (
      parseInt(imgElement.getAttribute('height')) *
        parseInt(imgElement.getAttribute('width')) <=
      IMAGE_PRELOAD_SIZE_THRESHOLD
    ) {
      return false
    }
  } catch (err) {
    return true
  }
  return true
}


// Traverse up the dom from each image to see if it or any of it's
// ancestors have the hidden attribute.
function imageIsNotHidden(imgElement: HTMLElement): boolean {
  let activeElement = imgElement
  while (activeElement.parentNode) {
    if (activeElement.hasAttribute('hidden')) {
      return false
    }
    activeElement = activeElement.parentNode as HTMLElement
  }
  return true
}


(tag: HTMLElement) =>
          tag.getAttribute('rel') === 'stylesheet' &&
          tag.hasAttribute('data-href') &&
          OPTIMIZED_FONT_PROVIDERS.some((url) =>
            tag.getAttribute('data-href').startsWith(url)
          )


(url) =>
            tag.getAttribute('data-href').startsWith(url)


(element: HTMLElement) => {
        const url = element.getAttribute('data-href')
        this.fontDefinitions.push(url)
      }


async (
    markup: string,
    _data: postProcessData,
    options: renderOptions
  ) => {
    let result = markup
    if (!options.getFontDefinition) {
      return markup
    }
    for (const key in this.fontDefinitions) {
      const url = this.fontDefinitions[key]
      if (result.indexOf(`<style data-href="${url}">`) > -1) {
        // The font is already optimized and probably the response is cached
        continue
      }
      const fontContent = options.getFontDefinition(url)
      result = result.replace(
        '',
        `</head>`
      )
    }
    return result
  }


(el) => el.getAttribute('src')


async (markup: string, _data: postProcessData) => {
    let result = markup
    let imagePreloadTags = _data.preloads.images
      .filter((imgHref) => !preloadTagAlreadyExists(markup, imgHref))
      .reduce(
        (acc, imgHref) => acc + `<link rel="preload" href="${imgHref}"/>`,
        ''
      )
    return result.replace(
      /<link rel="preload"/,
      `${imagePreloadTags}<link rel="preload"`
    )
  }


(imgHref) => !preloadTagAlreadyExists(markup, imgHref)


(acc, imgHref) => acc + `<link rel="preload" href="${imgHref}"/>`


// Using process.env because passing Experimental flag through loader is not possible.
// @ts-ignore
(options) => options.optimizeFonts || process.env.__NEXT_OPTIMIZE_FONTS


// @ts-ignore
(options) => options.optimizeImages || process.env.__NEXT_OPTIMIZE_IMAGES


class FontOptimizerMiddleware implements PostProcessMiddleware {
  fontDefinitions: Array<string> = []
  inspect(
    originalDom: HTMLElement,
    _data: postProcessData,
    options: renderOptions
  ) {
    if (!options.getFontDefinition) {
      return
    }
    // collecting all the requested font definitions
    originalDom
      .querySelectorAll('link')
      .filter(
        (tag: HTMLElement) =>
          tag.getAttribute('rel') === 'stylesheet' &&
          tag.hasAttribute('data-href') &&
          OPTIMIZED_FONT_PROVIDERS.some((url) =>
            tag.getAttribute('data-href').startsWith(url)
          )
      )
      .forEach((element: HTMLElement) => {
        const url = element.getAttribute('data-href')
        this.fontDefinitions.push(url)
      })
  }
  mutate = async (
    markup: string,
    _data: postProcessData,
    options: renderOptions
  ) => {
    let result = markup
    if (!options.getFontDefinition) {
      return markup
    }
    for (const key in this.fontDefinitions) {
      const url = this.fontDefinitions[key]
      if (result.indexOf(`<style data-href="${url}">`) > -1) {
        // The font is already optimized and probably the response is cached
        continue
      }
      const fontContent = options.getFontDefinition(url)
      result = result.replace(
        '',
        `</head>`
      )
    }
    return result
  }
}


class ImageOptimizerMiddleware implements PostProcessMiddleware {
  inspect(originalDom: HTMLElement, _data: postProcessData) {
    const imgElements = originalDom.querySelectorAll('img')
    let eligibleImages: Array<HTMLElement> = []
    for (let i = 0; i < imgElements.length; i++) {
      if (isImgEligible(imgElements[i])) {
        eligibleImages.push(imgElements[i])
      }
      if (eligibleImages.length >= MAXIMUM_IMAGE_PRELOADS) {
        break
      }
    }
    _data.preloads.images = eligibleImages.map((el) => el.getAttribute('src'))
  }
  mutate = async (markup: string, _data: postProcessData) => {
    let result = markup
    let imagePreloadTags = _data.preloads.images
      .filter((imgHref) => !preloadTagAlreadyExists(markup, imgHref))
      .reduce(
        (acc, imgHref) => acc + `<link rel="preload" href="${imgHref}"/>`,
        ''
      )
    return result.replace(
      /<link rel="preload"/,
      `${imagePreloadTags}<link rel="preload"`
    )
  }
}


interface PostProcessMiddleware {
  inspect: (
    originalDom: HTMLElement,
    data: postProcessData,
    options: renderOptions
  ) => void
  mutate: (
    markup: string,
    data: postProcessData,
    options: renderOptions
  ) => Promise<string>
}


type postProcessOptions = {
  optimizeFonts: boolean
  optimizeImages: boolean
}


type renderOptions = {
  getFontDefinition?: (url: string) => string
}


type postProcessData = {
  preloads: {
    images: Array<string>
  }
}


type middlewareSignature = {
  name: string
  middleware: PostProcessMiddleware
  condition: ((options: postProcessOptions) => boolean) | null
}


inspect(
    originalDom: HTMLElement,
    _data: postProcessData,
    options: renderOptions
  ) {
    if (!options.getFontDefinition) {
      return
    }
    // collecting all the requested font definitions
    originalDom
      .querySelectorAll('link')
      .filter(
        (tag: HTMLElement) =>
          tag.getAttribute('rel') === 'stylesheet' &&
          tag.hasAttribute('data-href') &&
          OPTIMIZED_FONT_PROVIDERS.some((url) =>
            tag.getAttribute('data-href').startsWith(url)
          )
      )
      .forEach((element: HTMLElement) => {
        const url = element.getAttribute('data-href')
        this.fontDefinitions.push(url)
      })
  }


inspect(originalDom: HTMLElement, _data: postProcessData) {
    const imgElements = originalDom.querySelectorAll('img')
    let eligibleImages: Array<HTMLElement> = []
    for (let i = 0; i < imgElements.length; i++) {
      if (isImgEligible(imgElements[i])) {
        eligibleImages.push(imgElements[i])
      }
      if (eligibleImages.length >= MAXIMUM_IMAGE_PRELOADS) {
        break
      }
    }
    _data.preloads.images = eligibleImages.map((el) => el.getAttribute('src'))
  }


() => {
  const testString = 'ab/c-d=='
  const encoded = base64.urlEncode(testString)
  expect(base64.urlDecode(encoded)).toEqual(testString)
}


subscription => subscription as SubscriptionResponseDto


obj => obj.topic


@Injectable()
export class SubscribersService {
  constructor(
    @InjectRepository(Subscriber)
    private subscriberRepository: Repository<Subscriber>,
  ) {}

  async findAll(): Promise<SubscriptionResponseDto[]> {
    const subscriptions = await this.subscriberRepository.find();
    return subscriptions.map(
      subscription => subscription as SubscriptionResponseDto,
    );
  }

  async findAllSubscribed(): Promise<SubscriptionResponseDto[]> {
    const subscriptions = await this.subscriberRepository
      .createQueryBuilder('subscriber')
      .where('subscriber.subscribed = :subscribed', { subscribed: true })
      .getMany();
    return subscriptions.map(
      subscription => subscription as SubscriptionResponseDto,
    );
  }

  async save(
    username: string,
    token: string,
    topic: string,
    subscribed: boolean,
  ): Promise<boolean> {
    const subscriber = new Subscriber();
    subscriber.username = `${topic}-${username}`;
    subscriber.token = token;
    subscriber.topic = topic;
    subscriber.subscribed = subscribed;
    try {
      await this.subscriberRepository.save(subscriber);
      return true;
    } catch (error) {
      console.log(error);
      throw new ConflictException(error);
    }
  }

  async findAllTopics(): Promise<TopicsResponseDto> {
    try {
      const topics = await this.subscriberRepository
        .createQueryBuilder('subscriber')
        .select('topic')
        .distinct(true)
        .getRawMany();
      const dto = new TopicsResponseDto();
      dto.topics = topics.map(obj => obj.topic);
      return dto;
    } catch (error) {
      throw new ConflictException(error);
    }
  }

  async update(token: string, subscribed: boolean): Promise<boolean> {
    try {
      const subscribers = await this.subscriberRepository
      .createQueryBuilder('subscriber')
      .where('subscriber.token = :token', { token })
      .getMany();
      for (const subscriber of subscribers) {
        subscriber.subscribed = subscribed;
        await this.subscriberRepository.save(subscriber);
      }
      return true;
    } catch (error) {
      throw new ConflictException(error);
    }
  }

  async findByUsername(
    username: string,
  ): Promise<SubscriptionResponseDto | null> {
    if (!username) {
      throw new BadRequestException('Format is incorrect');
    }
    try {
      const subscriber = await this.subscriberRepository.findOne({ username });
      if (subscriber) {
        return subscriber as SubscriptionResponseDto;
      }
      return null;
    } catch (error) {
      throw new ConflictException(error);
    }
  }
}


async findAll(): Promise<SubscriptionResponseDto[]> {
    const subscriptions = await this.subscriberRepository.find();
    return subscriptions.map(
      subscription => subscription as SubscriptionResponseDto,
    );
  }


async findAllSubscribed(): Promise<SubscriptionResponseDto[]> {
    const subscriptions = await this.subscriberRepository
      .createQueryBuilder('subscriber')
      .where('subscriber.subscribed = :subscribed', { subscribed: true })
      .getMany();
    return subscriptions.map(
      subscription => subscription as SubscriptionResponseDto,
    );
  }


async save(
    username: string,
    token: string,
    topic: string,
    subscribed: boolean,
  ): Promise<boolean> {
    const subscriber = new Subscriber();
    subscriber.username = `${topic}-${username}`;
    subscriber.token = token;
    subscriber.topic = topic;
    subscriber.subscribed = subscribed;
    try {
      await this.subscriberRepository.save(subscriber);
      return true;
    } catch (error) {
      console.log(error);
      throw new ConflictException(error);
    }
  }


async findAllTopics(): Promise<TopicsResponseDto> {
    try {
      const topics = await this.subscriberRepository
        .createQueryBuilder('subscriber')
        .select('topic')
        .distinct(true)
        .getRawMany();
      const dto = new TopicsResponseDto();
      dto.topics = topics.map(obj => obj.topic);
      return dto;
    } catch (error) {
      throw new ConflictException(error);
    }
  }


async update(token: string, subscribed: boolean): Promise<boolean> {
    try {
      const subscribers = await this.subscriberRepository
      .createQueryBuilder('subscriber')
      .where('subscriber.token = :token', { token })
      .getMany();
      for (const subscriber of subscribers) {
        subscriber.subscribed = subscribed;
        await this.subscriberRepository.save(subscriber);
      }
      return true;
    } catch (error) {
      throw new ConflictException(error);
    }
  }


async findByUsername(
    username: string,
  ): Promise<SubscriptionResponseDto | null> {
    if (!username) {
      throw new BadRequestException('Format is incorrect');
    }
    try {
      const subscriber = await this.subscriberRepository.findOne({ username });
      if (subscriber) {
        return subscriber as SubscriptionResponseDto;
      }
      return null;
    } catch (error) {
      throw new ConflictException(error);
    }
  }


export enum TextWeight {
  Light = 'light',
  Medium = 'medium',
  Heavy = 'heavy',
}


<T>(yargObject: YargObject<T> | boolean): void => {
  const index = Object.keys(yargObject);
  if (index.length === 0) {
    return;
  }

  const isIndexSequential = index
    .sort((a: string, b: string) => (parseInt(a) > parseInt(b) ? 1 : -1))
    .every((suppliedIndex: string, actualIndex: number) => parseInt(suppliedIndex) == actualIndex);

  if (!isIndexSequential) {
    throw new Error('Targeted array indexes are unsupported, please provide a full array index starting at 0');
  }
}


(a: string, b: string) => (parseInt(a) > parseInt(b) ? 1 : -1)


(suppliedIndex: string, actualIndex: number) => parseInt(suppliedIndex) == actualIndex


<T>(yargObject: YargObject<T> | boolean): T[] => {
  validateArrayIndex(yargObject);

  return Object.entries(yargObject).map(entry => entry[1]);
}


entry => entry[1]


export interface YargObject<T> {
  [key: string]: T;
}


export class DistributionList {
    id: number;
    createdDate: Date;
    distributionList: string;
    senderId: number;
    userSize: number;
    distributionListUsers: DistributionListUser[];
}


export class DistributionListUser {
    id: number;
    createdDate: Date;
    lastModifiedDate: Date;
    listId: number;
    userId: number;
}


() => {
  // Automatically delete memory of missing creeps
  if(Game.time%1500 === 0) {
    for (const name in Memory.creeps) {
      if (!(name in Game.creeps)) {
        delete Memory.creeps[name];
      }
    }
  }
}


/**
 * The possible databases to test against.
 */
export type TestDatabaseId =
  | 'POSTGRES_13'
  | 'POSTGRES_9'
  | 'MYSQL_8'
  | 'SQLITE_3';


export type TestDatabaseProperties = {
  name: string;
  driver: string;
  dockerImageName?: string;
  connectionStringEnvironmentVariableName?: string;
};


export type Instance = {
  stopContainer?: () => Promise<void>;
  databaseManager: SingleConnectionDatabaseManager;
  connections: Array<Knex>;
};


export class WufWebMessagesComponentView {
    util = new UtilsComponentView();

    get_viewTitle() {
        return element(by.css('app-root h1')).getText();
    }

    get_viewWufWebMessageElement() {
        return element(by.css('wuf-web-message'));
    }

    get_errorMessageLink() {
        return element(by.id('errorMessageLink'));
    }

    get_infoMessageLink() {
        return element(by.id('infoMessageLink'));
    }

    get_successMessageLink() {
        return element(by.id('successMessageLink'));
    }

    get_warningMessageLink() {
        return element(by.id('warningMessageLink'));
    }

    /**
     * Return the children
     * @returns {WebElement[]}
     */
    get_kgWebMessageShadowChildren() {
        // browser.pause();
        const deferred = protractor.promise.defer();
        this.util.get_Element_ShadowRoot(this.get_viewWufWebMessageElement()).then(
            function (kgWebMessageElementShadowRoot) {
                deferred.fulfill(kgWebMessageElementShadowRoot.childNodes);
            }
        );
        return deferred.promise;
    }

    /**
     * Return the style element
     * @returns {WebElement[]}
     */
    get_kgWebMessageShadowStyles() {
        // browser.pause();
        const deferred = protractor.promise.defer();
        this.util.get_Element_ShadowRoot(this.get_viewWufWebMessageElement()).then(
            function (kgWebMessageElements_shadowRoot) {
                deferred.fulfill(kgWebMessageElements_shadowRoot.findElements(by.css('style')));
            }
        );
        return deferred.promise;
    }

    /**
     * Return all div elements
     * @returns {WebElement[]}
     */
    get_kgWebMessageShadowDivs() {
        // browser.pause();
        const deferred = protractor.promise.defer();
        this.util.get_Element_ShadowRoot(this.get_viewWufWebMessageElement()).then(
            function (kgWebMessageElements_shadowRoot) {
                deferred.fulfill(kgWebMessageElements_shadowRoot.findElements(by.css('div.message')));
            }
        );
        return deferred.promise;
    }

    /**
     * Poke into a message to extract its color
     * @param {WebElement} kbWebMessageElement
     * @returns {promise.Promise<string>}}
     */
    get_kgWebMessageColor(kbWebMessageElement: WebElement, messageType: string) {
        return kbWebMessageElement.getCssValue('color');
    }

    /**
     * Poke into a message to to extract its background color
     * @param {WebElement} kbWebMessageElement
     * @returns {promise.Promise<string>}
     */
    get_kgWebMessageBackgroundColor(kbWebMessageElement: WebElement) {
        return kbWebMessageElement.getCssValue('background-color');
    }

    /**
     * Poke into a message to to extract its text
     * @param {WebElement} kbWebMessageElement
     * @returns {promise.Promise<string>}
     */
    get_kgWebMessageText(kbWebMessageElement: WebElement) {
        const divEl = kbWebMessageElement.findElement(by.css('div'));
        return divEl.getText();
    }

    /**
     * Poke into a message and click its dismissal button
     * @param {WebElement} kbWebMessageElement
     */
    click_kgWebMessageElement_dismissButton(kbWebMessageElement: WebElement) {
        const deferred = protractor.promise.defer();
        kbWebMessageElement.findElement(by.css('button')).click().then(function () {
            deferred.fulfill(true);
        });
        return deferred.promise;
    }
}


get_viewTitle() {
        return element(by.css('app-root h1')).getText();
    }


get_viewWufWebMessageElement() {
        return element(by.css('wuf-web-message'));
    }


get_errorMessageLink() {
        return element(by.id('errorMessageLink'));
    }


get_infoMessageLink() {
        return element(by.id('infoMessageLink'));
    }


get_successMessageLink() {
        return element(by.id('successMessageLink'));
    }


get_warningMessageLink() {
        return element(by.id('warningMessageLink'));
    }


/**
     * Return the children
     * @returns {WebElement[]}
     */
get_kgWebMessageShadowChildren() {
        // browser.pause();
        const deferred = protractor.promise.defer();
        this.util.get_Element_ShadowRoot(this.get_viewWufWebMessageElement()).then(
            function (kgWebMessageElementShadowRoot) {
                deferred.fulfill(kgWebMessageElementShadowRoot.childNodes);
            }
        );
        return deferred.promise;
    }


/**
     * Return the style element
     * @returns {WebElement[]}
     */
get_kgWebMessageShadowStyles() {
        // browser.pause();
        const deferred = protractor.promise.defer();
        this.util.get_Element_ShadowRoot(this.get_viewWufWebMessageElement()).then(
            function (kgWebMessageElements_shadowRoot) {
                deferred.fulfill(kgWebMessageElements_shadowRoot.findElements(by.css('style')));
            }
        );
        return deferred.promise;
    }


/**
     * Return all div elements
     * @returns {WebElement[]}
     */
get_kgWebMessageShadowDivs() {
        // browser.pause();
        const deferred = protractor.promise.defer();
        this.util.get_Element_ShadowRoot(this.get_viewWufWebMessageElement()).then(
            function (kgWebMessageElements_shadowRoot) {
                deferred.fulfill(kgWebMessageElements_shadowRoot.findElements(by.css('div.message')));
            }
        );
        return deferred.promise;
    }


/**
     * Poke into a message to extract its color
     * @param {WebElement} kbWebMessageElement
     * @returns {promise.Promise<string>}}
     */
get_kgWebMessageColor(kbWebMessageElement: WebElement, messageType: string) {
        return kbWebMessageElement.getCssValue('color');
    }


/**
     * Poke into a message to to extract its background color
     * @param {WebElement} kbWebMessageElement
     * @returns {promise.Promise<string>}
     */
get_kgWebMessageBackgroundColor(kbWebMessageElement: WebElement) {
        return kbWebMessageElement.getCssValue('background-color');
    }


/**
     * Poke into a message to to extract its text
     * @param {WebElement} kbWebMessageElement
     * @returns {promise.Promise<string>}
     */
get_kgWebMessageText(kbWebMessageElement: WebElement) {
        const divEl = kbWebMessageElement.findElement(by.css('div'));
        return divEl.getText();
    }


/**
     * Poke into a message and click its dismissal button
     * @param {WebElement} kbWebMessageElement
     */
click_kgWebMessageElement_dismissButton(kbWebMessageElement: WebElement) {
        const deferred = protractor.promise.defer();
        kbWebMessageElement.findElement(by.css('button')).click().then(function () {
            deferred.fulfill(true);
        });
        return deferred.promise;
    }


/**
 * The FAST Text Area Custom Element. Implements {@link @microsoft/fast-foundation#TextArea},
 * {@link @microsoft/fast-foundation#TextAreaTemplate}
 *
 *
 * @public
 * @remarks
 * HTML Element: \<fast-text-area\>
 *
 * {@link https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/delegatesFocus | delegatesFocus}
 */
@customElement({
    name: "fast-text-area",
    template,
    styles,
})
export class FASTTextArea extends TextArea {}


elem => this.visualize(elem, depth + 1)


(a, b) => this.compare(callback(a), callback(b))


/**
 * Utils library
 *
 * Miscellaneous utility functions that don't really have a better place.
 *
 * It'll always be a judgment call whether or not a function goes into a
 * "catch-all" library like this, so here are some guidelines:
 *
 * - It must not have any dependencies
 *
 * - It must conceivably have a use in a wide variety of projects, not just
 *   Pokémon (if it's Pokémon-specific, Dex is probably a good place for it)
 *
 * - A lot of Chat functions are kind of iffy, but I'm going to say for now
 *   that if it's English-specific, it should be left out of here.
 */
type Comparable = number | string | boolean | Comparable[] | {reverse: Comparable};


escapeRegex(str: string) {
		return str.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
	}


/**
	 * Escapes HTML in a string.
	 */
escapeHTML(str: string) {
		if (!str) return '';
		return ('' + str)
			.replace(/&/g, '&')
			.replace(/</g, '<')
			.replace(/>/g, '>')
			.replace(/"/g, '"')
			.replace(/'/g, ''')
			.replace(/\//g, '/');
	}


/**
	 * Strips HTML from a string.
	 */
stripHTML(htmlContent: string) {
		if (!htmlContent) return '';
		return htmlContent.replace(/<[^>]*>/g, '');
	}


/**
	 * Visualizes eval output in a slightly more readable form
	 */
visualize(value: any, depth = 0): string {
		if (value === undefined) return `undefined`;
		if (value === null) return `null`;
		if (typeof value === 'number' || typeof value === 'boolean') {
			return `${value}`;
		}
		if (typeof value === 'string') {
			return `"${value}"`; // NOT ESCAPED
		}
		if (typeof value === 'symbol') {
			return value.toString();
		}
		if (Array.isArray(value)) {
			if (depth > 10) return `[array]`;
			return `[` + value.map(elem => this.visualize(elem, depth + 1)).join(`, `) + `]`;
		}
		if (value instanceof RegExp || value instanceof Date || value instanceof Function) {
			if (depth && value instanceof Function) return `Function`;
			return `${value}`;
		}
		let constructor = '';
		if (value.constructor && value.constructor.name && typeof value.constructor.name === 'string') {
			constructor = value.constructor.name;
			if (constructor === 'Object') constructor = '';
		} else {
			constructor = 'null';
		}
		if (value.toString) {
			try {
				const stringValue = value.toString();
				if (typeof stringValue === 'string' &&
						stringValue !== '[object Object]' &&
						stringValue !== `[object ${constructor}]`) {
					return `${constructor}(${stringValue})`;
				}
			} catch (e) {}
		}
		let buf = '';
		for (const key in value) {
			if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
			if (depth > 2 || (depth && constructor)) {
				buf = '...';
				break;
			}
			if (buf) buf += `, `;
			let displayedKey = key;
			if (!/^[A-Za-z0-9_$]+$/.test(key)) displayedKey = JSON.stringify(key);
			buf += `${displayedKey}: ` + this.visualize(value[key], depth + 1);
		}
		if (constructor && !buf && constructor !== 'null') return constructor;
		return `${constructor}{${buf}}`;
	}


/**
	 * Compares two variables; intended to be used as a smarter comparator.
	 * The two variables must be the same type (TypeScript will not check this).
	 *
	 * - Numbers are sorted low-to-high, use `-val` to reverse
	 * - Strings are sorted A to Z case-semi-insensitively, use `{reverse: val}` to reverse
	 * - Booleans are sorted true-first (REVERSE of casting to numbers), use `!val` to reverse
	 * - Arrays are sorted lexically in the order of their elements
	 *
	 * In other words: `[num, str]` will be sorted A to Z, `[num, {reverse: str}]` will be sorted Z to A.
	 */
compare(a: Comparable, b: Comparable): number {
		if (typeof a === 'number') {
			return a - (b as number);
		}
		if (typeof a === 'string') {
			return a.localeCompare(b as string);
		}
		if (typeof a === 'boolean') {
			return (a ? 1 : 2) - (b ? 1 : 2);
		}
		if (Array.isArray(a)) {
			for (let i = 0; i < a.length; i++) {
				const comparison = this.compare(a[i], (b as Comparable[])[i]);
				if (comparison) return comparison;
			}
			return 0;
		}
		if (a.reverse) {
			return this.compare((b as {reverse: string}).reverse, a.reverse);
		}
		throw new Error(`Passed value ${a} is not comparable`);
	}


/**
	 * Sorts an array according to the callback's output on its elements.
	 *
	 * The callback's output is compared according to `PSUtils.compare` (in
	 * particular, it supports arrays so you can sort by multiple things).
	 */
sortBy<T>(array: T[], callback: (a: T) => Comparable): T[];


/**
	 * Sorts an array according to `PSUtils.compare`. (Correctly sorts numbers,
	 * unlike `array.sort`)
	 */
sortBy<T extends Comparable>(array: T[]): T[];


sortBy<T>(array: T[], callback?: (a: T) => Comparable) {
		if (!callback) return (array as any[]).sort(this.compare);
		return array.sort((a, b) => this.compare(callback(a), callback(b)));
	}


/**
	 * Like string.split(delimiter), but only recognizes the first `limit`
	 * delimiters (default 1).
	 *
	 * `"1 2 3 4".split(" ", 2) => ["1", "2"]`
	 *
	 * `Utils.splitFirst("1 2 3 4", " ", 1) => ["1", "2 3 4"]`
	 *
	 * Returns an array of length exactly limit + 1.
	 *
	 */
splitFirst(str: string, delimiter: string, limit = 1) {
		const splitStr: string[] = [];
		while (splitStr.length < limit) {
			const delimiterIndex = str.indexOf(delimiter);
			if (delimiterIndex >= 0) {
				splitStr.push(str.slice(0, delimiterIndex));
				str = str.slice(delimiterIndex + delimiter.length);
			} else {
				splitStr.push(str);
				str = '';
			}
		}
		splitStr.push(str);
		return splitStr;
	}


/**
	* Template string tag function for escaping HTML
	*/
html(strings: TemplateStringsArray, ...args: any) {
		let buf = strings[0];
		let i = 0;
		while (i < args.length) {
			buf += this.escapeHTML(args[i]);
			buf += strings[++i];
		}
		return buf;
	}


shuffle<T>(arr: T[]): T[] {
		// In-place shuffle by Fisher-Yates algorithm
		for (let i = arr.length - 1; i > 0; i--) {
			const j = Math.floor(Math.random() * (i + 1));
			[arr[i], arr[j]] = [arr[j], arr[i]];
		}
		return arr;
	}


/** Forces num to be an integer (between min and max). */
clampIntRange(num: any, min?: number, max?: number): number {
		if (typeof num !== 'number') num = 0;
		num = Math.floor(num);
		if (min !== undefined && num < min) num = min;
		if (max !== undefined && num > max) num = max;
		return num;
	}


levenshtein(s: string, t: string, l: number): number {
		// Original levenshtein distance function by James Westgate, turned out to be the fastest
		const d: number[][] = [];

		// Step 1
		const n = s.length;
		const m = t.length;

		if (n === 0) return m;
		if (m === 0) return n;
		if (l && Math.abs(m - n) > l) return Math.abs(m - n);

		// Create an array of arrays in javascript (a descending loop is quicker)
		for (let i = n; i >= 0; i--) d[i] = [];

		// Step 2
		for (let i = n; i >= 0; i--) d[i][0] = i;
		for (let j = m; j >= 0; j--) d[0][j] = j;

		// Step 3
		for (let i = 1; i <= n; i++) {
			const si = s.charAt(i - 1);

			// Step 4
			for (let j = 1; j <= m; j++) {
				// Check the jagged ld total so far
				if (i === j && d[i][j] > 4) return n;

				const tj = t.charAt(j - 1);
				const cost = (si === tj) ? 0 : 1; // Step 5

				// Calculate the minimum
				let mi = d[i - 1][j] + 1;
				const b = d[i][j - 1] + 1;
				const c = d[i - 1][j - 1] + cost;

				if (b < mi) mi = b;
				if (c < mi) mi = c;

				d[i][j] = mi; // Step 6
			}
		}

		// Step 7
		return d[n][m];
	}


// apis
function getCompletionsAtPosition(fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions | undefined): ReturnType<ts.LanguageService['getCompletionsAtPosition']> {
		const finalResult = context.getTsLs().getCompletionsAtPosition(fileName, position, options);
		if (finalResult) {
			finalResult.entries = finalResult.entries.filter(entry => entry.name.indexOf('__VLS_') === -1);
		}
		return finalResult;
	}


function getReferencesAtPosition(fileName: string, position: number): ReturnType<ts.LanguageService['getReferencesAtPosition']> {
		return findLocations(fileName, position, 'references') as ts.ReferenceEntry[];
	}


function getDefinitionAtPosition(fileName: string, position: number): ReturnType<ts.LanguageService['getDefinitionAtPosition']> {
		return findLocations(fileName, position, 'definition') as ts.DefinitionInfo[];
	}


function getTypeDefinitionAtPosition(fileName: string, position: number): ReturnType<ts.LanguageService['getDefinitionAtPosition']> {
		return findLocations(fileName, position, 'typeDefinition') as ts.DefinitionInfo[];
	}


function getImplementationAtPosition(fileName: string, position: number): ReturnType<ts.LanguageService['getImplementationAtPosition']> {
		return findLocations(fileName, position, 'implementation') as ts.ImplementationLocation[];
	}


function findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): ReturnType<ts.LanguageService['findRenameLocations']> {
		return findLocations(fileName, position, 'rename', findInStrings, findInComments, providePrefixAndSuffixTextForRename) as ts.RenameLocation[];
	}


function findLocations(
		fileName: string,
		position: number,
		mode: 'definition' | 'typeDefinition' | 'references' | 'implementation' | 'rename',
		findInStrings = false,
		findInComments = false,
		providePrefixAndSuffixTextForRename?: boolean
	) {

		const tsLs = context.getTsLs();
		const loopChecker = new Set<string>();
		let symbols: (ts.DefinitionInfo | ts.ReferenceEntry | ts.ImplementationLocation | ts.RenameLocation)[] = [];

		if (tsLs)
			withTeleports(fileName, position, tsLs);

		return symbols.map(s => transformDocumentSpanLike(s)).filter(notEmpty);

		function withTeleports(fileName: string, position: number, tsLs: ts.LanguageService) {
			if (loopChecker.has(fileName + ':' + position))
				return;
			loopChecker.add(fileName + ':' + position);
			const _symbols = mode === 'definition' ? tsLs.getDefinitionAtPosition(fileName, position)
				: mode === 'typeDefinition' ? tsLs.getTypeDefinitionAtPosition(fileName, position)
					: mode === 'references' ? tsLs.getReferencesAtPosition(fileName, position)
						: mode === 'implementation' ? tsLs.getImplementationAtPosition(fileName, position)
							: mode === 'rename' ? tsLs.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename)
								: undefined;
			if (!_symbols) return;
			symbols = symbols.concat(_symbols);
			for (const ref of _symbols) {
				loopChecker.add(ref.fileName + ':' + ref.textSpan.start);
				const teleport = context.vueFiles.getTeleport(ref.fileName);

				if (!teleport)
					continue;

				for (const [teleRange] of teleport.findTeleports(
					ref.textSpan.start,
					ref.textSpan.start + ref.textSpan.length,
					sideData => {
						if ((mode === 'definition' || mode === 'typeDefinition' || mode === 'implementation') && !sideData.capabilities.definitions)
							return false;
						if ((mode === 'references') && !sideData.capabilities.references)
							return false;
						if ((mode === 'rename') && !sideData.capabilities.rename)
							return false;
						return true;
					},
				)) {
					if (loopChecker.has(ref.fileName + ':' + teleRange.start))
						continue;
					withTeleports(ref.fileName, teleRange.start, tsLs);
				}
			}
		}
	}


function withTeleports(fileName: string, position: number, tsLs: ts.LanguageService) {
			if (loopChecker.has(fileName + ':' + position))
				return;
			loopChecker.add(fileName + ':' + position);
			const _symbols = mode === 'definition' ? tsLs.getDefinitionAtPosition(fileName, position)
				: mode === 'typeDefinition' ? tsLs.getTypeDefinitionAtPosition(fileName, position)
					: mode === 'references' ? tsLs.getReferencesAtPosition(fileName, position)
						: mode === 'implementation' ? tsLs.getImplementationAtPosition(fileName, position)
							: mode === 'rename' ? tsLs.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename)
								: undefined;
			if (!_symbols) return;
			symbols = symbols.concat(_symbols);
			for (const ref of _symbols) {
				loopChecker.add(ref.fileName + ':' + ref.textSpan.start);
				const teleport = context.vueFiles.getTeleport(ref.fileName);

				if (!teleport)
					continue;

				for (const [teleRange] of teleport.findTeleports(
					ref.textSpan.start,
					ref.textSpan.start + ref.textSpan.length,
					sideData => {
						if ((mode === 'definition' || mode === 'typeDefinition' || mode === 'implementation') && !sideData.capabilities.definitions)
							return false;
						if ((mode === 'references') && !sideData.capabilities.references)
							return false;
						if ((mode === 'rename') && !sideData.capabilities.rename)
							return false;
						return true;
					},
				)) {
					if (loopChecker.has(ref.fileName + ':' + teleRange.start))
						continue;
					withTeleports(ref.fileName, teleRange.start, tsLs);
				}
			}
		}


function getDefinitionAndBoundSpan(fileName: string, position: number): ReturnType<ts.LanguageService['getDefinitionAndBoundSpan']> {

		const tsLs = context.getTsLs();
		const loopChecker = new Set<string>();
		let textSpan: ts.TextSpan | undefined;
		let symbols: ts.DefinitionInfo[] = [];

		if (tsLs)
			withTeleports(fileName, position, tsLs);

		if (!textSpan) return;
		return {
			textSpan: textSpan,
			definitions: symbols?.map(s => transformDocumentSpanLike(s)).filter(notEmpty),
		};

		function withTeleports(fileName: string, position: number, tsLs: ts.LanguageService) {
			if (loopChecker.has(fileName + ':' + position))
				return;
			loopChecker.add(fileName + ':' + position);
			const _symbols = tsLs.getDefinitionAndBoundSpan(fileName, position);
			if (!_symbols) return;
			if (!textSpan) {
				textSpan = _symbols.textSpan;
			}
			if (!_symbols.definitions) return;
			symbols = symbols.concat(_symbols.definitions);
			for (const ref of _symbols.definitions) {

				loopChecker.add(ref.fileName + ':' + ref.textSpan.start);

				const teleport = context.vueFiles.getTeleport(ref.fileName);
				if (!teleport)
					continue;

				for (const [teleRange] of teleport.findTeleports(
					ref.textSpan.start,
					ref.textSpan.start + ref.textSpan.length,
					sideData => !!sideData.capabilities.definitions,
				)) {
					if (loopChecker.has(ref.fileName + ':' + teleRange.start))
						continue;
					withTeleports(ref.fileName, teleRange.start, tsLs);
				}
			}
		}
	}


function withTeleports(fileName: string, position: number, tsLs: ts.LanguageService) {
			if (loopChecker.has(fileName + ':' + position))
				return;
			loopChecker.add(fileName + ':' + position);
			const _symbols = tsLs.getDefinitionAndBoundSpan(fileName, position);
			if (!_symbols) return;
			if (!textSpan) {
				textSpan = _symbols.textSpan;
			}
			if (!_symbols.definitions) return;
			symbols = symbols.concat(_symbols.definitions);
			for (const ref of _symbols.definitions) {

				loopChecker.add(ref.fileName + ':' + ref.textSpan.start);

				const teleport = context.vueFiles.getTeleport(ref.fileName);
				if (!teleport)
					continue;

				for (const [teleRange] of teleport.findTeleports(
					ref.textSpan.start,
					ref.textSpan.start + ref.textSpan.length,
					sideData => !!sideData.capabilities.definitions,
				)) {
					if (loopChecker.has(ref.fileName + ':' + teleRange.start))
						continue;
					withTeleports(ref.fileName, teleRange.start, tsLs);
				}
			}
		}


function findReferences(fileName: string, position: number): ReturnType<ts.LanguageService['findReferences']> {

		const tsLs = context.getTsLs();
		const loopChecker = new Set<string>();
		let symbols: ts.ReferencedSymbol[] = [];

		if (tsLs)
			withTeleports(fileName, position, tsLs);

		return symbols.map(s => transformReferencedSymbol(s)).filter(notEmpty);

		function withTeleports(fileName: string, position: number, tsLs: ts.LanguageService) {
			if (loopChecker.has(fileName + ':' + position))
				return;
			loopChecker.add(fileName + ':' + position);
			const _symbols = tsLs.findReferences(fileName, position);
			if (!_symbols) return;
			symbols = symbols.concat(_symbols);
			for (const symbol of _symbols) {
				for (const ref of symbol.references) {

					loopChecker.add(ref.fileName + ':' + ref.textSpan.start);

					const teleport = context.vueFiles.getTeleport(ref.fileName);
					if (!teleport)
						continue;

					for (const [teleRange] of teleport.findTeleports(
						ref.textSpan.start,
						ref.textSpan.start + ref.textSpan.length,
						sideData => !!sideData.capabilities.references,
					)) {
						if (loopChecker.has(ref.fileName + ':' + teleRange.start))
							continue;
						withTeleports(ref.fileName, teleRange.start, tsLs);
					}
				}
			}
		}
	}


function withTeleports(fileName: string, position: number, tsLs: ts.LanguageService) {
			if (loopChecker.has(fileName + ':' + position))
				return;
			loopChecker.add(fileName + ':' + position);
			const _symbols = tsLs.findReferences(fileName, position);
			if (!_symbols) return;
			symbols = symbols.concat(_symbols);
			for (const symbol of _symbols) {
				for (const ref of symbol.references) {

					loopChecker.add(ref.fileName + ':' + ref.textSpan.start);

					const teleport = context.vueFiles.getTeleport(ref.fileName);
					if (!teleport)
						continue;

					for (const [teleRange] of teleport.findTeleports(
						ref.textSpan.start,
						ref.textSpan.start + ref.textSpan.length,
						sideData => !!sideData.capabilities.references,
					)) {
						if (loopChecker.has(ref.fileName + ':' + teleRange.start))
							continue;
						withTeleports(ref.fileName, teleRange.start, tsLs);
					}
				}
			}
		}


// transforms
function transformReferencedSymbol(symbol: ts.ReferencedSymbol): ts.ReferencedSymbol | undefined {
		const definition = transformDocumentSpanLike(symbol.definition);
		const references = symbol.references.map(r => transformDocumentSpanLike(r)).filter(notEmpty);
		if (definition) {
			return {
				definition,
				references,
			};
		}
		else if (references.length) { // TODO: remove patching
			return {
				definition: {
					...symbol.definition,
					fileName: references[0].fileName,
					textSpan: references[0].textSpan,
				},
				references,
			};
		}
	}


function transformDocumentSpanLike<T extends ts.DocumentSpan>(documentSpan: T): T | undefined {
		const textSpan = transformSpan(documentSpan.fileName, documentSpan.textSpan);
		if (!textSpan) return;
		const contextSpan = transformSpan(documentSpan.fileName, documentSpan.contextSpan);
		const originalTextSpan = transformSpan(documentSpan.originalFileName, documentSpan.originalTextSpan);
		const originalContextSpan = transformSpan(documentSpan.originalFileName, documentSpan.originalContextSpan);
		return {
			...documentSpan,
			fileName: textSpan.fileName,
			textSpan: textSpan.textSpan,
			contextSpan: contextSpan?.textSpan,
			originalFileName: originalTextSpan?.fileName,
			originalTextSpan: originalTextSpan?.textSpan,
			originalContextSpan: originalContextSpan?.textSpan,
		};
	}


function transformSpan(fileName: string | undefined, textSpan: ts.TextSpan | undefined) {
		if (!fileName) return;
		if (!textSpan) return;
		for (const vueLoc of context.vueFiles.fromEmbeddedLocation(fileName, textSpan.start, textSpan.start + textSpan.length)) {
			return {
				fileName: vueLoc.fileName,
				textSpan: {
					start: vueLoc.range.start,
					length: vueLoc.range.end - vueLoc.range.start,
				},
			};
		}
	}


function notEmpty<T>(value: T | null | undefined): value is T {
	return value !== null && value !== undefined;
}


entry => entry.name.indexOf('__VLS_') === -1


s => transformDocumentSpanLike(s)


sideData => {
						if ((mode === 'definition' || mode === 'typeDefinition' || mode === 'implementation') && !sideData.capabilities.definitions)
							return false;
						if ((mode === 'references') && !sideData.capabilities.references)
							return false;
						if ((mode === 'rename') && !sideData.capabilities.rename)
							return false;
						return true;
					}


sideData => !!sideData.capabilities.definitions


s => transformReferencedSymbol(s)


sideData => !!sideData.capabilities.references


r => transformDocumentSpanLike(r)


export class MruVEconomyServiceClient extends grpc.Client {
  constructor(address: string, credentials: grpc.ChannelCredentials, options?: object);
  registerProduct(argument: economy_economy_pb.RegisterProductRequest, callback: grpc.requestCallback<economy_economy_pb.RegisterProductResponse>): grpc.ClientUnaryCall;
  registerProduct(argument: economy_economy_pb.RegisterProductRequest, metadataOrOptions: grpc.Metadata | grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.RegisterProductResponse>): grpc.ClientUnaryCall;
  registerProduct(argument: economy_economy_pb.RegisterProductRequest, metadata: grpc.Metadata | null, options: grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.RegisterProductResponse>): grpc.ClientUnaryCall;
  getProduct(argument: economy_economy_pb.GetProductRequest, callback: grpc.requestCallback<economy_economy_pb.GetProductResponse>): grpc.ClientUnaryCall;
  getProduct(argument: economy_economy_pb.GetProductRequest, metadataOrOptions: grpc.Metadata | grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.GetProductResponse>): grpc.ClientUnaryCall;
  getProduct(argument: economy_economy_pb.GetProductRequest, metadata: grpc.Metadata | null, options: grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.GetProductResponse>): grpc.ClientUnaryCall;
  updateProduct(argument: economy_economy_pb.UpdateProductRequest, callback: grpc.requestCallback<economy_economy_pb.UpdateProductResponse>): grpc.ClientUnaryCall;
  updateProduct(argument: economy_economy_pb.UpdateProductRequest, metadataOrOptions: grpc.Metadata | grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.UpdateProductResponse>): grpc.ClientUnaryCall;
  updateProduct(argument: economy_economy_pb.UpdateProductRequest, metadata: grpc.Metadata | null, options: grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.UpdateProductResponse>): grpc.ClientUnaryCall;
  deleteProduct(argument: economy_economy_pb.DeleteProductRequest, callback: grpc.requestCallback<economy_economy_pb.DeleteProductResponse>): grpc.ClientUnaryCall;
  deleteProduct(argument: economy_economy_pb.DeleteProductRequest, metadataOrOptions: grpc.Metadata | grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.DeleteProductResponse>): grpc.ClientUnaryCall;
  deleteProduct(argument: economy_economy_pb.DeleteProductRequest, metadata: grpc.Metadata | null, options: grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.DeleteProductResponse>): grpc.ClientUnaryCall;
  updatePrice(argument: economy_economy_pb.UpdatePriceRequest, callback: grpc.requestCallback<economy_economy_pb.UpdatePriceResponse>): grpc.ClientUnaryCall;
  updatePrice(argument: economy_economy_pb.UpdatePriceRequest, metadataOrOptions: grpc.Metadata | grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.UpdatePriceResponse>): grpc.ClientUnaryCall;
  updatePrice(argument: economy_economy_pb.UpdatePriceRequest, metadata: grpc.Metadata | null, options: grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.UpdatePriceResponse>): grpc.ClientUnaryCall;
  getPrice(argument: economy_economy_pb.GetPriceRequest, callback: grpc.requestCallback<economy_economy_pb.GetPriceResponse>): grpc.ClientUnaryCall;
  getPrice(argument: economy_economy_pb.GetPriceRequest, metadataOrOptions: grpc.Metadata | grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.GetPriceResponse>): grpc.ClientUnaryCall;
  getPrice(argument: economy_economy_pb.GetPriceRequest, metadata: grpc.Metadata | null, options: grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.GetPriceResponse>): grpc.ClientUnaryCall;
  buyProduct(argument: economy_economy_pb.BuyProductRequest, callback: grpc.requestCallback<economy_economy_pb.BuyProductResponse>): grpc.ClientUnaryCall;
  buyProduct(argument: economy_economy_pb.BuyProductRequest, metadataOrOptions: grpc.Metadata | grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.BuyProductResponse>): grpc.ClientUnaryCall;
  buyProduct(argument: economy_economy_pb.BuyProductRequest, metadata: grpc.Metadata | null, options: grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.BuyProductResponse>): grpc.ClientUnaryCall;
  watchProduct(argument: economy_economy_pb.WatchProductRequest, metadataOrOptions?: grpc.Metadata | grpc.CallOptions | null): grpc.ClientReadableStream<economy_economy_pb.WatchProductResponse>;
  watchProduct(argument: economy_economy_pb.WatchProductRequest, metadata?: grpc.Metadata | null, options?: grpc.CallOptions | null): grpc.ClientReadableStream<economy_economy_pb.WatchProductResponse>;
  watchPrice(argument: economy_economy_pb.WatchPriceRequest, metadataOrOptions?: grpc.Metadata | grpc.CallOptions | null): grpc.ClientReadableStream<economy_economy_pb.WatchPriceResponse>;
  watchPrice(argument: economy_economy_pb.WatchPriceRequest, metadata?: grpc.Metadata | null, options?: grpc.CallOptions | null): grpc.ClientReadableStream<economy_economy_pb.WatchPriceResponse>;
}


interface IMruVEconomyServiceService extends grpc.ServiceDefinition<grpc.UntypedServiceImplementation> {
  registerProduct: grpc.MethodDefinition<economy_economy_pb.RegisterProductRequest, economy_economy_pb.RegisterProductResponse>;
  getProduct: grpc.MethodDefinition<economy_economy_pb.GetProductRequest, economy_economy_pb.GetProductResponse>;
  updateProduct: grpc.MethodDefinition<economy_economy_pb.UpdateProductRequest, economy_economy_pb.UpdateProductResponse>;
  deleteProduct: grpc.MethodDefinition<economy_economy_pb.DeleteProductRequest, economy_economy_pb.DeleteProductResponse>;
  updatePrice: grpc.MethodDefinition<economy_economy_pb.UpdatePriceRequest, economy_economy_pb.UpdatePriceResponse>;
  getPrice: grpc.MethodDefinition<economy_economy_pb.GetPriceRequest, economy_economy_pb.GetPriceResponse>;
  buyProduct: grpc.MethodDefinition<economy_economy_pb.BuyProductRequest, economy_economy_pb.BuyProductResponse>;
  watchProduct: grpc.MethodDefinition<economy_economy_pb.WatchProductRequest, economy_economy_pb.WatchProductResponse>;
  watchPrice: grpc.MethodDefinition<economy_economy_pb.WatchPriceRequest, economy_economy_pb.WatchPriceResponse>;
}


registerProduct(argument: economy_economy_pb.RegisterProductRequest, callback: grpc.requestCallback<economy_economy_pb.RegisterProductResponse>): grpc.ClientUnaryCall;


registerProduct(argument: economy_economy_pb.RegisterProductRequest, metadataOrOptions: grpc.Metadata | grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.RegisterProductResponse>): grpc.ClientUnaryCall;


registerProduct(argument: economy_economy_pb.RegisterProductRequest, metadata: grpc.Metadata | null, options: grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.RegisterProductResponse>): grpc.ClientUnaryCall;


getProduct(argument: economy_economy_pb.GetProductRequest, callback: grpc.requestCallback<economy_economy_pb.GetProductResponse>): grpc.ClientUnaryCall;


getProduct(argument: economy_economy_pb.GetProductRequest, metadataOrOptions: grpc.Metadata | grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.GetProductResponse>): grpc.ClientUnaryCall;


getProduct(argument: economy_economy_pb.GetProductRequest, metadata: grpc.Metadata | null, options: grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.GetProductResponse>): grpc.ClientUnaryCall;


updateProduct(argument: economy_economy_pb.UpdateProductRequest, callback: grpc.requestCallback<economy_economy_pb.UpdateProductResponse>): grpc.ClientUnaryCall;


updateProduct(argument: economy_economy_pb.UpdateProductRequest, metadataOrOptions: grpc.Metadata | grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.UpdateProductResponse>): grpc.ClientUnaryCall;


updateProduct(argument: economy_economy_pb.UpdateProductRequest, metadata: grpc.Metadata | null, options: grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.UpdateProductResponse>): grpc.ClientUnaryCall;


deleteProduct(argument: economy_economy_pb.DeleteProductRequest, callback: grpc.requestCallback<economy_economy_pb.DeleteProductResponse>): grpc.ClientUnaryCall;


deleteProduct(argument: economy_economy_pb.DeleteProductRequest, metadataOrOptions: grpc.Metadata | grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.DeleteProductResponse>): grpc.ClientUnaryCall;


deleteProduct(argument: economy_economy_pb.DeleteProductRequest, metadata: grpc.Metadata | null, options: grpc.CallOptions | null, callback: grpc.requestCallback<economy_economy_pb.DeleteProductResponse>): grpc.ClientUnaryCall;