refactor: group domain services into folders
All checks were successful
npm release / verify (push) Successful in 14s
npm release / publish to npm (push) Successful in 12s

This commit is contained in:
2026-05-11 19:38:02 +09:00
parent 684b6af5be
commit baea23b8b0
14 changed files with 315 additions and 146 deletions

87
src/timing/index.ts Normal file
View File

@@ -0,0 +1,87 @@
import type { BoxBrainAvailability } from '../core/types';
export type RandomSource = () => number;
export interface TypingDelayOptions {
rng?: RandomSource | undefined;
minSecondsPerCharacter?: number | undefined;
maxSecondsPerCharacter?: number | undefined;
}
export interface ReplyDelayOptions {
isFirstReplyInExchange: boolean;
rng?: RandomSource | undefined;
onlineMinSeconds?: number | undefined;
onlineMaxSeconds?: number | undefined;
dndReplyProbability?: number | undefined;
dndMinSeconds?: number | undefined;
dndMaxSeconds?: number | undefined;
}
export const ONLINE_AVAILABILITY: BoxBrainAvailability = { mode: 'online' };
export const DND_AVAILABILITY: BoxBrainAvailability = { mode: 'do_not_disturb' };
export const OFFLINE_AVAILABILITY: BoxBrainAvailability = { mode: 'offline' };
export class TimingProfile {
createTypingDelay(message: string, options: TypingDelayOptions = {}): number {
if (message.length === 0) {
return 0;
}
const rng = options.rng ?? Math.random;
const min = options.minSecondsPerCharacter ?? 0.05;
const max = options.maxSecondsPerCharacter ?? 0.08;
const secondsPerCharacter = interpolate(min, max, clampUnit(rng()));
return roundSeconds(message.length * secondsPerCharacter);
}
createReplyDelay(availability: BoxBrainAvailability, options: ReplyDelayOptions): number | null {
if (availability.mode === 'offline') {
return null;
}
if (!options.isFirstReplyInExchange) {
return 0;
}
const rng = options.rng ?? Math.random;
if (availability.mode === 'do_not_disturb') {
const probability = options.dndReplyProbability ?? 0.2;
if (clampUnit(rng()) > probability) {
return null;
}
return roundSeconds(interpolate(options.dndMinSeconds ?? 60, options.dndMaxSeconds ?? 600, clampUnit(rng())));
}
return roundSeconds(interpolate(options.onlineMinSeconds ?? 1, options.onlineMaxSeconds ?? 12, clampUnit(rng())));
}
}
const DEFAULT_TIMING_PROFILE = new TimingProfile();
export function createTypingDelay(message: string, options: TypingDelayOptions = {}): number {
return DEFAULT_TIMING_PROFILE.createTypingDelay(message, options);
}
export function createReplyDelay(availability: BoxBrainAvailability, options: ReplyDelayOptions): number | null {
return DEFAULT_TIMING_PROFILE.createReplyDelay(availability, options);
}
function interpolate(min: number, max: number, ratio: number): number {
return min + (max - min) * ratio;
}
function clampUnit(value: number): number {
if (Number.isNaN(value)) {
return 0;
}
return Math.min(1, Math.max(0, value));
}
function roundSeconds(value: number): number {
return Math.round(value * 1_000_000) / 1_000_000;
}