Перейти к основному содержимому

Команды и события

Взаимодействие в 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, и это единственный надёжный способ не дать их представлениям «разъехаться» со временем. Как команду исполняет обработчик — в разделе «Обработчики команд».