Команды и события
Взаимодействие в hlf-core построено на модели команд, а не на наборе функций контракта — эта
модель приходит из @ts-core/common и остаётся одинаковой, вызываем мы блокчейн или обычный
сервис. Команда упаковывается в конверт и идёт через единственный метод контракта, а дальше
её судьбу определяет не имя функции, а содержимое конверта.
Базовый класс команды — HlfTransportCommandAsync<U, V> из @hlf-core/common, и у него есть флаг
isReadonly, который делит команды на читающие и изменяющие состояние. Команда это небольшой класс
со статическим NAME и парой DTO — запроса и ответа; запрос прогоняется через TransformUtil.toClass,
чтобы попасть внутрь уже типизированным и валидируемым:
import { TransformUtil } from '@ts-core/common';
import { Type } from 'class-transformer';
import { ValidateNested, IsDefined, IsOptional, IsEnum } from 'class-validator';
import { HlfTransportCommandAsync, ICryptoKey, CryptoKey, IInitiatedDto, InitiatedDto } from '@hlf-core/common';
import { CommandName } from './Command';
import { User, UserRole } from '../user';
export class UserAddCommand extends HlfTransportCommandAsync<IUserAddDto, User> {
public static readonly NAME = CommandName.USER_ADD;
constructor(request: IUserAddDto) {
super(UserAddCommand.NAME, TransformUtil.toClass(UserAddDto, request));
}
protected checkResponse(item: User): User {
return TransformUtil.toClass(User, item);
}
}
export interface IUserAddDto extends IInitiatedDto {
roles?: Array<UserRole>;
cryptoKey: ICryptoKey;
}
export class UserAddDto extends InitiatedDto implements IUserAddDto {
@IsDefined()
@Type(() => CryptoKey)
@ValidateNested()
public cryptoKey: CryptoKey;
@IsOptional()
@IsEnum(UserRole, { each: true })
public roles?: Array<UserRole>;
}
Сам запрос едет в TransportFabricRequestPayload<U> — конверте с полями id, name, request,
options, isReadonly и isNeedReply. Ответ возвращается в TransportFabricResponsePayload<V>, и
здесь есть одна важная деталь: результат и ошибка живут в одном поле response. Благодаря этому
доменная ошибка chaincode доходит до клиента не как безликий сбой транзакции, а как ExtendedError
с кодом и деталями — то есть ровно в том виде, в каком её бросил обработчик.
События chaincode отправляются через stub.dispatch(event) и приходят клиенту уже при разборе
блока. Событие устроено так же просто, как команда — класс со статическим NAME, а обработчик шлёт
его после того, как изменил состояние:
import { TransformUtil, TransportEvent } from '@ts-core/common';
import { IInitiatedDto, InitiatedDto } from '@hlf-core/common';
import { IsDefined, ValidateNested } from 'class-validator';
import { Event } from './Event';
import { User } from '../user';
export class UserAddedEvent extends TransportEvent<IUserAddedEventDto> {
public static readonly NAME = Event.USER_ADDED;
constructor(data: IUserAddedEventDto) {
super(UserAddedEvent.NAME, TransformUtil.toClass(UserAddedEventDto, data));
}
}
export interface IUserAddedEventDto extends IInitiatedDto {
user: User;
}
// внутри сервиса, после сохранения пользователя:
await holder.stub.dispatch(new UserAddedEvent({ user: item, initiatorUid: params.initiatorUid }));
Общие классы команд и событий вынесены в *-common-пакеты неспроста: один и тот же код живёт и на
клиенте, и в chaincode, и это единственный надёжный способ не дать их представлениям «разъехаться»
со временем. Как команду исполняет обработчик — в разделе «Обработчики команд».