UNPKG

@chevre/domain

Version:

Chevre Domain Library for Node.js

320 lines (319 loc) 14.8 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StockHolderRepo = void 0; const createDebug = require("debug"); const moment = require("moment"); const factory = require("../factory"); // import { createSchema, IModel, ISetting, modelName } from './mongoose/schemas/setting'; const pendingReservation_1 = require("./pendingReservation"); const debug = createDebug('chevre-domain:repo:stockHolder'); const SEARCH_OFFERS_MAX_LENGTH = 100; const USE_STOCK_HOLDER_CHECK_CONFLICT = process.env.USE_STOCK_HOLDER_CHECK_CONFLICT === '1'; /** * イベントストックホルダーリポジトリ */ class StockHolderRepo { constructor(redisClient, connection) { this.redisClient = redisClient; // this.settingModel = connection.model(modelName, createSchema()); this.pendingReservationRepo = new pendingReservation_1.PendingReservationRepo(connection); } static offer2field(params, hasTicketedSeat) { var _a, _b; if (hasTicketedSeat) { return `${params.seatSection}:${params.seatNumber}`; } else { // 予約IDをfieldにする場合 const serviceOutputId = (_b = (_a = params.itemOffered) === null || _a === void 0 ? void 0 : _a.serviceOutput) === null || _b === void 0 ? void 0 : _b.id; if (typeof serviceOutputId === 'string') { return serviceOutputId; } else { throw new factory.errors.Internal('offer2field requires itemOffered.serviceOutput.id'); } } } static createKey(params) { if (!(params.startDate instanceof Date)) { throw new factory.errors.Argument('startDate', 'must be Date'); } return `${StockHolderRepo.KEY_PREFIX_NEW}:${params.eventId}`; } /** * 座席をロックする(maxキャパシティチェック有) */ lockIfNotLimitExceeded(lockKey, maximum) { return __awaiter(this, void 0, void 0, function* () { const useMongoose = yield this.useMongoose({ project: { id: lockKey.project.id }, eventId: lockKey.eventId, startDate: lockKey.startDate, hasTicketedSeat: lockKey.hasTicketedSeat }); if (useMongoose) { // throw new factory.errors.NotImplemented('new stock holder repository not implemented'); return this.pendingReservationRepo.lockIfNotLimitExceeded(lockKey, maximum); } else { const key = StockHolderRepo.createKey({ eventId: lockKey.eventId, startDate: lockKey.startDate }); yield this.redisClient.watch(key); const hashCount = yield this.redisClient.hLen(key); // Process result // Heavy and time consuming operation here debug('checking hash count...hashCount:', hashCount); if (hashCount + lockKey.offers.length > maximum) { throw new factory.errors.Argument('Event', 'maximumAttendeeCapacity exceeded'); } yield this.lock(lockKey); } }); } /** * 座席をロックする */ // tslint:disable-next-line:max-func-body-length lock(lockKey) { return __awaiter(this, void 0, void 0, function* () { if (!(lockKey.expires instanceof Date)) { throw new factory.errors.Argument('expires', 'must be Date'); } const useMongoose = yield this.useMongoose({ project: { id: lockKey.project.id }, eventId: lockKey.eventId, startDate: lockKey.startDate, hasTicketedSeat: lockKey.hasTicketedSeat }); const key = StockHolderRepo.createKey({ eventId: lockKey.eventId, startDate: lockKey.startDate }); // await this.checkIfConflicted({ key, eventId: lockKey.eventId, useMongoose }); if (useMongoose) { // throw new factory.errors.NotImplemented('new stock holder repository not implemented'); return this.pendingReservationRepo.lock(lockKey); } else { const value = lockKey.holder; const multi = this.redisClient.multi(); const fields = lockKey.offers.map((offer) => StockHolderRepo.offer2field(offer, lockKey.hasTicketedSeat)); // check uniqueness(2025-04-20~) const uniqueFields = [...new Set(fields)]; if (uniqueFields.length !== fields.length) { throw new factory.errors.Argument('offers', 'offers must be unique'); } fields.forEach((field) => { multi.hSetNX(key, field, value); }); const results = yield multi.expireAt(key, moment(lockKey.expires) .unix()) .exec(); const lockedFields = []; if (Array.isArray(results)) { results.slice(0, fields.length) .forEach((r, index) => { if (r === 1 || r === true) { lockedFields.push(fields[index]); } }); } const lockedAll = lockedFields.length === fields.length; debug('lockedAll?', lockedAll); // expireAtReplyの検証も追加する(2023-04-19~) const expiredAll = results.slice(fields.length) .every((r) => (r === 1 || r === true)); debug('expiredAll?', expiredAll); if (!lockedAll || !expiredAll) { if (lockedFields.length > 0) { // 全て仮押さえできなければ仮押さえできたものは解除 yield this.redisClient.multi() .hDel(key, lockedFields) .exec(); } if (!lockedAll) { throw new factory.errors.AlreadyInUse(factory.reservationType.EventReservation, ['ticketedSeat'], 'Already hold'); } else { throw new factory.errors.Internal('timeout cannot be set unexpectedly'); } } } }); } /** * 座席ロックを解除する */ unlock(params) { return __awaiter(this, void 0, void 0, function* () { const useMongoose = yield this.useMongoose({ project: { id: params.project.id }, eventId: params.eventId, startDate: params.startDate, hasTicketedSeat: params.hasTicketedSeat }); const key = StockHolderRepo.createKey({ eventId: params.eventId, startDate: params.startDate }); // await this.checkIfConflicted({ key, eventId: params.eventId, useMongoose }); if (useMongoose) { // throw new factory.errors.NotImplemented('new stock holder repository not implemented'); return this.pendingReservationRepo.unlock(params); } else { const field = StockHolderRepo.offer2field(params.offer, params.hasTicketedSeat); yield this.redisClient.multi() .hDel(key, field) .exec(); } }); } /** * 空席でない座席をカウントする */ countUnavailableOffers(params) { return __awaiter(this, void 0, void 0, function* () { if (yield this.useMongoose({ project: { id: params.project.id }, eventId: params.event.id, startDate: params.event.startDate, hasTicketedSeat: params.event.hasTicketedSeat })) { // throw new factory.errors.NotImplemented('new stock holder repository not implemented'); return this.pendingReservationRepo.countUnavailableOffers(params); } else { const key = StockHolderRepo.createKey({ eventId: params.event.id, startDate: params.event.startDate }); const reply = yield this.redisClient.hLen(key); let fieldCount = 0; if (typeof reply === 'number') { fieldCount = Number(reply); } return fieldCount; } }); } /** * 保持者を取得する */ getHolder(params) { return __awaiter(this, void 0, void 0, function* () { if (yield this.useMongoose({ project: { id: params.project.id }, eventId: params.eventId, startDate: params.startDate, hasTicketedSeat: params.hasTicketedSeat })) { // throw new factory.errors.NotImplemented('new stock holder repository not implemented'); return this.pendingReservationRepo.getHolder(params); } else { const key = StockHolderRepo.createKey({ eventId: params.eventId, startDate: params.startDate }); const field = StockHolderRepo.offer2field(params.offer, params.hasTicketedSeat); return this.redisClient.hGet(key, field); } }); } searchHolders(params) { return __awaiter(this, void 0, void 0, function* () { // validate offers.length(2025-04-18~) if (params.offers.length > SEARCH_OFFERS_MAX_LENGTH) { throw new factory.errors.Argument('offers', `offers.length must be <= ${SEARCH_OFFERS_MAX_LENGTH}`); } if (yield this.useMongoose({ project: { id: params.project.id }, eventId: params.eventId, startDate: params.startDate, hasTicketedSeat: params.hasTicketedSeat })) { // throw new factory.errors.NotImplemented('new stock holder repository not implemented'); return this.pendingReservationRepo.searchHolders(params); } else { const key = StockHolderRepo.createKey({ eventId: params.eventId, startDate: params.startDate }); const fields = params.offers.map((o) => { return StockHolderRepo.offer2field(o, params.hasTicketedSeat); }); // Array reply: list of values associated with the given fields, in the same order as they are requested. const result = yield this.redisClient.hmGet(key, fields); if (!Array.isArray(result)) { throw new factory.errors.Internal(`searchAvailability got non-array: ${typeof result}`); } // そのまま返却(2023-04-17~) return result; } }); } redisKeyExists(params) { return __awaiter(this, void 0, void 0, function* () { const key = StockHolderRepo.createKey(params); const existingRedisKeyCount = yield this.redisClient.exists(key); if (typeof existingRedisKeyCount !== 'number') { throw new factory.errors.Internal(`unexpected existingKeyCount: ${typeof existingRedisKeyCount}`); } return existingRedisKeyCount > 0; }); } checkIfConflicted(params) { return __awaiter(this, void 0, void 0, function* () { const redisKeyExists = yield this.redisKeyExists(params); const mongoDocExists = yield this.pendingReservationRepo.docExists(params); if (redisKeyExists && mongoDocExists) { throw new factory.errors.Internal(`repository conflicted. eventId:${params.eventId}`); } }); } /** * 万が一に備えて、保留予約をredis->mongo移行 */ migrate2mongoJustInCase(params) { return __awaiter(this, void 0, void 0, function* () { const redisKey = StockHolderRepo.createKey({ eventId: params.eventId, startDate: params.startDate }); return { expireTime: yield this.redisClient.expireTime(redisKey), hash: yield this.redisClient.hGetAll(redisKey) }; }); } /** * 新リポジトリを使用するかどうか */ useMongoose(params) { return __awaiter(this, void 0, void 0, function* () { if (!(params.startDate instanceof Date)) { throw new factory.errors.Argument('startDate', 'must be Date'); } let useMongoose = false; // useMongoAsStockHolder設定有の場合のみmongo利用(2025-04-18~) // const useMongoAsStockHolderBySettings = await this.useMongoAsStockHolderBySettings({ project: { id: params.project.id } }); // if (useMongoAsStockHolderBySettings) { // const redisKeyExists = await this.redisKeyExists(params); // if (redisKeyExists) { // useMongoose = false; // } else { // // redis keyが存在しなければmongo利用 // useMongoose = true; // } // } // always use mongo(2025-05-21~) const redisKeyExists = yield this.redisKeyExists(params); if (redisKeyExists) { useMongoose = false; } else { // redis keyが存在しなければmongo利用 useMongoose = true; } // check confliction for test if (USE_STOCK_HOLDER_CHECK_CONFLICT) { yield this.checkIfConflicted(params); } return useMongoose; }); } } exports.StockHolderRepo = StockHolderRepo; StockHolderRepo.KEY_PREFIX_NEW = 'stockHolder';