feat: refactor utility functions for improved consistency and usability; consolidate parsing logic
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
## Planned Monorepo Layout (TypeScript Rewrite)
|
||||
|
||||
- `/packages/common`: shared utilities and type definitions.
|
||||
- Frequently used utility functions should be registered here and utilized.
|
||||
- `/packages/infra`: Prisma/Redis connectors and other runtime infra.
|
||||
- `/packages/logic`: pure game logic with DI/interfaces for external dependencies.
|
||||
- `/app/gateway-frontend`: Gateway UI application.
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { WorldStateRow } from '../context.js';
|
||||
import type { BattleSimJobPayload, BattleSimRequestPayload } from './types.js';
|
||||
import { loadUnitSetDefinitionByName } from './unitSetLoader.js';
|
||||
import type { WarEngineConfig } from '@sammo-ts/logic';
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
const DEFAULT_WAR_CONFIG = {
|
||||
armPerPhase: 500,
|
||||
@@ -11,13 +12,6 @@ const DEFAULT_WAR_CONFIG = {
|
||||
maxAtmosByWar: 150,
|
||||
};
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
};
|
||||
|
||||
const resolveNumber = (record: Record<string, unknown>, keys: string[], fallback: number): number => {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
|
||||
+10
-15
@@ -1,3 +1,5 @@
|
||||
import { parseNumberWithFallback } from '@sammo-ts/common';
|
||||
|
||||
export interface GameApiConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
@@ -13,17 +15,6 @@ export interface GameApiConfig {
|
||||
flushChannel: string;
|
||||
}
|
||||
|
||||
const parseNumber = (value: string | undefined, fallback: number, label: string): number => {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (Number.isNaN(parsed)) {
|
||||
throw new Error(`${label} must be a number.`);
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
export const resolveGameApiConfigFromEnv = (env: NodeJS.ProcessEnv = process.env): GameApiConfig => {
|
||||
const profile = env.PROFILE ?? env.SERVER_PROFILE ?? 'hwe';
|
||||
const scenario = env.SCENARIO ?? 'default';
|
||||
@@ -36,19 +27,23 @@ export const resolveGameApiConfigFromEnv = (env: NodeJS.ProcessEnv = process.env
|
||||
|
||||
return {
|
||||
host: env.GAME_API_HOST ?? '0.0.0.0',
|
||||
port: parseNumber(env.GAME_API_PORT, 14000, 'GAME_API_PORT'),
|
||||
port: parseNumberWithFallback(env.GAME_API_PORT, 14000, 'GAME_API_PORT'),
|
||||
trpcPath: env.TRPC_PATH ?? '/trpc',
|
||||
eventsPath: env.GAME_API_EVENTS_PATH ?? '/events',
|
||||
profile,
|
||||
scenario,
|
||||
profileName,
|
||||
daemonRequestTimeoutMs: parseNumber(env.DAEMON_REQUEST_TIMEOUT_MS, 5000, 'DAEMON_REQUEST_TIMEOUT_MS'),
|
||||
battleSimRequestTimeoutMs: parseNumber(
|
||||
daemonRequestTimeoutMs: parseNumberWithFallback(env.DAEMON_REQUEST_TIMEOUT_MS, 5000, 'DAEMON_REQUEST_TIMEOUT_MS'),
|
||||
battleSimRequestTimeoutMs: parseNumberWithFallback(
|
||||
env.BATTLE_SIM_REQUEST_TIMEOUT_MS,
|
||||
8000,
|
||||
'BATTLE_SIM_REQUEST_TIMEOUT_MS'
|
||||
),
|
||||
battleSimResultTtlSeconds: parseNumber(env.BATTLE_SIM_RESULT_TTL_SECONDS, 60, 'BATTLE_SIM_RESULT_TTL_SECONDS'),
|
||||
battleSimResultTtlSeconds: parseNumberWithFallback(
|
||||
env.BATTLE_SIM_RESULT_TTL_SECONDS,
|
||||
60,
|
||||
'BATTLE_SIM_RESULT_TTL_SECONDS'
|
||||
),
|
||||
gameTokenSecret: secret,
|
||||
flushChannel: `${gatewayPrefix}:flush`,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { GameApiContext, WorldStateRow } from '../context.js';
|
||||
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||
|
||||
export type MapCityCompact = [number, number, number, number, number, number];
|
||||
export type MapNationCompact = [number, string, string, number];
|
||||
@@ -45,11 +46,6 @@ const MAP_VERSION = 0 as const;
|
||||
const BASE_MAP_TTL_SECONDS = 30;
|
||||
const PUBLIC_MAP_TTL_SECONDS = 600;
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
||||
|
||||
const resolveStartYear = (worldState: WorldStateRow): number => {
|
||||
const meta = asRecord(worldState.meta);
|
||||
const scenarioMeta = asRecord(meta.scenarioMeta);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { randomBytes } from 'node:crypto';
|
||||
|
||||
import type { WorldStateRow } from '../../context.js';
|
||||
import { authedProcedure, router } from '../../trpc.js';
|
||||
import { asNumber, asRecord, asStringArray } from '@sammo-ts/common';
|
||||
import {
|
||||
isPersonalityTraitKey,
|
||||
isWarTraitKey,
|
||||
@@ -23,15 +24,6 @@ const DEFAULT_JOIN_STAT = {
|
||||
bonusMax: 5,
|
||||
};
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> =>
|
||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
||||
|
||||
const asNumber = (value: unknown, fallback: number): number =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
||||
|
||||
const asStringArray = (value: unknown): string[] =>
|
||||
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
||||
|
||||
const resolveJoinStat = (worldState: WorldStateRow) => {
|
||||
const config = asRecord(worldState.config);
|
||||
const stat = asRecord(config.stat);
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
TriggerValue,
|
||||
} from '@sammo-ts/logic';
|
||||
import { evaluateConstraints, loadGeneralTurnCommandSpecs, loadNationTurnCommandSpecs } from '@sammo-ts/logic';
|
||||
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||
|
||||
import type { CityRow, GeneralRow, NationRow, WorldStateRow } from '../context.js';
|
||||
import { loadTurnCommandProfile } from './turnCommandProfile.js';
|
||||
@@ -73,11 +74,6 @@ const DEFAULT_GENERAL_GOLD = 1000;
|
||||
const DEFAULT_GENERAL_RICE = 1000;
|
||||
const DEFAULT_CREW_TYPE_ID = 1100;
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
||||
|
||||
const asTriggerRecord = (value: unknown): Record<string, TriggerValue> =>
|
||||
isRecord(value) ? (value as Record<string, TriggerValue>) : {};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DatabaseClient, GeneralTurnRow, NationTurnRow, InputJsonValue } from '../context.js';
|
||||
import { isRecord } from '@sammo-ts/common';
|
||||
|
||||
export const DEFAULT_TURN_ACTION = '휴식';
|
||||
export const MAX_GENERAL_TURNS = 30;
|
||||
@@ -15,9 +16,6 @@ export interface ReservedTurnView {
|
||||
args: InputJsonValue;
|
||||
}
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const normalizeAction = (action: string | null | undefined): string =>
|
||||
action && action.length > 0 ? action : DEFAULT_TURN_ACTION;
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { City, Nation } from '@sammo-ts/logic';
|
||||
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||
|
||||
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
export const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
||||
export { asRecord, isRecord };
|
||||
|
||||
export const readNumber = (value: unknown, fallback = 0): number => {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TurnSchedule } from '@sammo-ts/logic';
|
||||
import { parseOptionalBoolean, parseOptionalNumber } from '@sammo-ts/common';
|
||||
|
||||
import type { TurnRunBudget } from '../lifecycle/types.js';
|
||||
import { resolveDatabaseUrl } from '../scenario/databaseUrl.js';
|
||||
@@ -24,36 +25,11 @@ const DEFAULT_BUDGET: TurnRunBudget = {
|
||||
catchUpCap: 1,
|
||||
};
|
||||
|
||||
const parseNumber = (value: string | undefined): number | undefined => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return undefined;
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const parseBoolean = (value: string | undefined): boolean | undefined => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const buildBudgetOverride = (env: NodeJS.ProcessEnv, override?: Partial<TurnRunBudget>): TurnRunBudget | undefined => {
|
||||
const budgetOverride: Partial<TurnRunBudget> = {
|
||||
budgetMs: parseNumber(env.TURN_BUDGET_MS),
|
||||
maxGenerals: parseNumber(env.TURN_MAX_GENERALS),
|
||||
catchUpCap: parseNumber(env.TURN_CATCH_UP_CAP),
|
||||
budgetMs: parseOptionalNumber(env.TURN_BUDGET_MS),
|
||||
maxGenerals: parseOptionalNumber(env.TURN_MAX_GENERALS),
|
||||
catchUpCap: parseOptionalNumber(env.TURN_CATCH_UP_CAP),
|
||||
...override,
|
||||
};
|
||||
|
||||
@@ -78,10 +54,10 @@ export const runTurnDaemonCli = async (options: TurnDaemonCliOptions = {}): Prom
|
||||
schema: env.GATEWAY_DB_SCHEMA ?? 'public',
|
||||
}));
|
||||
const budget = buildBudgetOverride(env, options.budget);
|
||||
const tickMinutes = options.tickMinutes ?? parseNumber(env.TURN_TICK_MINUTES);
|
||||
const enableDatabaseFlush = options.enableDatabaseFlush ?? parseBoolean(env.TURN_FLUSH_DB) ?? true;
|
||||
const pauseGateIntervalMs = parseNumber(env.TURN_PAUSE_GATE_MS);
|
||||
const adminActionIntervalMs = options.adminActionIntervalMs ?? parseNumber(env.TURN_ADMIN_ACTION_MS);
|
||||
const tickMinutes = options.tickMinutes ?? parseOptionalNumber(env.TURN_TICK_MINUTES);
|
||||
const enableDatabaseFlush = options.enableDatabaseFlush ?? parseOptionalBoolean(env.TURN_FLUSH_DB) ?? true;
|
||||
const pauseGateIntervalMs = parseOptionalNumber(env.TURN_PAUSE_GATE_MS);
|
||||
const adminActionIntervalMs = options.adminActionIntervalMs ?? parseOptionalNumber(env.TURN_ADMIN_ACTION_MS);
|
||||
|
||||
const runtime = await createTurnDaemonRuntime({
|
||||
profile,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createGatewayPostgresConnector } from '@sammo-ts/infra';
|
||||
import { isRecord } from '@sammo-ts/common';
|
||||
|
||||
export type GatewayAdminActionStatus = 'REQUESTED' | 'APPLIED' | 'FAILED' | 'IGNORED';
|
||||
|
||||
@@ -53,9 +54,6 @@ export interface GatewayAdminActionConsumer {
|
||||
|
||||
const DEFAULT_POLL_MS = 5000;
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const normalizeMeta = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
||||
|
||||
const normalizeStatus = (value: unknown): GatewayAdminActionStatus | null => {
|
||||
|
||||
@@ -15,16 +15,12 @@ import {
|
||||
ITEM_KEYS,
|
||||
loadItemModules,
|
||||
} from '@sammo-ts/logic';
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
const DEFAULT_GENERAL_GOLD = 1000;
|
||||
const DEFAULT_GENERAL_RICE = 1000;
|
||||
const DEFAULT_CREW_TYPE_ID = 1100;
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
||||
|
||||
const normalizeCode = (value: string | null | undefined): string | null => {
|
||||
if (!value || value === 'None') {
|
||||
return null;
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
resolveGeneralAction,
|
||||
} from '@sammo-ts/logic';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
|
||||
import { LiteHashDRBG } from '@sammo-ts/common';
|
||||
import { asRecord, LiteHashDRBG } from '@sammo-ts/common';
|
||||
|
||||
import type { ConstraintContext, StateView } from '@sammo-ts/logic';
|
||||
|
||||
@@ -43,11 +43,6 @@ import type { AiReservedTurnProvider } from './ai/types.js';
|
||||
|
||||
const DEFAULT_ACTION = '휴식';
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
||||
|
||||
const resolveConstraintEnv = (
|
||||
world: TurnWorldState,
|
||||
scenarioMeta: ScenarioMeta | undefined,
|
||||
@@ -325,7 +320,7 @@ class WorldStateView implements StateView {
|
||||
}
|
||||
}
|
||||
|
||||
const extractArgsRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
||||
const extractArgsRecord = (value: unknown): Record<string, unknown> => asRecord(value);
|
||||
|
||||
const buildConstraintContext = (
|
||||
general: TurnGeneral,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createGamePostgresConnector, type InputJsonValue, type TurnEngineDatabaseClient } from '@sammo-ts/infra';
|
||||
import { isRecord } from '@sammo-ts/common';
|
||||
|
||||
export interface ReservedTurnEntry {
|
||||
action: string;
|
||||
@@ -22,9 +23,6 @@ const DEFAULT_NATION_TURNS = 12;
|
||||
|
||||
const asJson = (value: unknown): InputJsonValue => value as InputJsonValue;
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const normalizeAction = (action: string | null | undefined): string =>
|
||||
action && action.length > 0 ? action : DEFAULT_TURN_ACTION;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@sammo-ts/infra';
|
||||
import type { City, Nation, ScenarioConfig, ScenarioMeta, Troop, TriggerValue } from '@sammo-ts/logic';
|
||||
import { z } from 'zod';
|
||||
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||
|
||||
import { getNextTickTime } from '../lifecycle/getNextTickTime.js';
|
||||
import type { MapLoaderOptions } from '../scenario/mapLoader.js';
|
||||
@@ -27,11 +28,6 @@ interface TurnWorldLoaderOptions {
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
const isRecord = (value: unknown): value is JsonRecord =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const asRecord = (value: unknown): JsonRecord => (isRecord(value) ? value : {});
|
||||
|
||||
const asTriggerRecord = (value: unknown): Record<string, TriggerValue> =>
|
||||
isRecord(value) ? (value as Record<string, TriggerValue>) : {};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { parseJson } from '@sammo-ts/common';
|
||||
|
||||
export type OAuthMode = 'login' | 'change_pw';
|
||||
|
||||
@@ -41,17 +42,6 @@ interface RedisClientLike {
|
||||
multi(): RedisPipeline;
|
||||
}
|
||||
|
||||
const parseJson = <T>(raw: string | null): T | null => {
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export class RedisOAuthSessionStore implements OAuthSessionStore {
|
||||
private readonly client: RedisClientLike;
|
||||
private readonly prefix: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { parseJson } from '@sammo-ts/common';
|
||||
|
||||
import { createGatewayRedisKeyBuilder } from './redisKeys.js';
|
||||
import type {
|
||||
@@ -30,17 +31,6 @@ interface RedisClientLike {
|
||||
del(key: string): Promise<number>;
|
||||
}
|
||||
|
||||
const parseJson = <T>(raw: string | null): T | null => {
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Redis 세션 저장소는 게이트웨이와 게임 서버 간 SSO 토큰을 관리한다.
|
||||
export class RedisGatewaySessionService implements GatewaySessionService {
|
||||
private readonly client: RedisClientLike;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from 'node:path';
|
||||
import { parseBooleanWithFallback, parseNumberWithFallback } from '@sammo-ts/common';
|
||||
|
||||
export interface GatewayApiConfig {
|
||||
host: string;
|
||||
@@ -36,31 +37,6 @@ export interface GatewayOrchestratorConfig {
|
||||
worktreeRoot: string;
|
||||
}
|
||||
|
||||
const parseNumber = (value: string | undefined, fallback: number, label: string): number => {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (Number.isNaN(parsed)) {
|
||||
throw new Error(`${label} must be a number.`);
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const parseBoolean = (value: string | undefined, fallback: boolean): boolean => {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const resolveSchemaName = (value: string | undefined): string => {
|
||||
if (!value) {
|
||||
return 'public';
|
||||
@@ -83,36 +59,44 @@ export const resolveGatewayApiConfigFromEnv = (env: NodeJS.ProcessEnv = process.
|
||||
const redisKeyPrefix = env.GATEWAY_REDIS_PREFIX ?? 'sammo:gateway';
|
||||
return {
|
||||
host: env.GATEWAY_API_HOST ?? '0.0.0.0',
|
||||
port: parseNumber(env.GATEWAY_API_PORT, 13000, 'GATEWAY_API_PORT'),
|
||||
port: parseNumberWithFallback(env.GATEWAY_API_PORT, 13000, 'GATEWAY_API_PORT'),
|
||||
trpcPath: env.TRPC_PATH ?? '/trpc',
|
||||
dbSchema: resolveSchemaName(env.GATEWAY_DB_SCHEMA),
|
||||
redisKeyPrefix,
|
||||
flushChannel: `${redisKeyPrefix}:flush`,
|
||||
sessionTtlSeconds: parseNumber(env.SESSION_TTL_SECONDS, 60 * 60 * 24 * 7, 'SESSION_TTL_SECONDS'),
|
||||
gameSessionTtlSeconds: parseNumber(env.GAME_SESSION_TTL_SECONDS, 60 * 60 * 6, 'GAME_SESSION_TTL_SECONDS'),
|
||||
sessionTtlSeconds: parseNumberWithFallback(env.SESSION_TTL_SECONDS, 60 * 60 * 24 * 7, 'SESSION_TTL_SECONDS'),
|
||||
gameSessionTtlSeconds: parseNumberWithFallback(
|
||||
env.GAME_SESSION_TTL_SECONDS,
|
||||
60 * 60 * 6,
|
||||
'GAME_SESSION_TTL_SECONDS'
|
||||
),
|
||||
gameTokenSecret: secret,
|
||||
oauthSessionTtlSeconds: parseNumber(env.OAUTH_SESSION_TTL_SECONDS, 10 * 60, 'OAUTH_SESSION_TTL_SECONDS'),
|
||||
oauthSessionTtlSeconds: parseNumberWithFallback(
|
||||
env.OAUTH_SESSION_TTL_SECONDS,
|
||||
10 * 60,
|
||||
'OAUTH_SESSION_TTL_SECONDS'
|
||||
),
|
||||
kakaoRestKey,
|
||||
kakaoAdminKey: env.KAKAO_ADMIN_KEY,
|
||||
kakaoRedirectUri,
|
||||
publicBaseUrl,
|
||||
orchestratorEnabled: parseBoolean(env.GATEWAY_ORCHESTRATOR_ENABLED, false),
|
||||
orchestratorReconcileIntervalMs: parseNumber(
|
||||
orchestratorEnabled: parseBooleanWithFallback(env.GATEWAY_ORCHESTRATOR_ENABLED, false),
|
||||
orchestratorReconcileIntervalMs: parseNumberWithFallback(
|
||||
env.GATEWAY_ORCHESTRATOR_RECONCILE_MS,
|
||||
15000,
|
||||
'GATEWAY_ORCHESTRATOR_RECONCILE_MS'
|
||||
),
|
||||
orchestratorScheduleIntervalMs: parseNumber(
|
||||
orchestratorScheduleIntervalMs: parseNumberWithFallback(
|
||||
env.GATEWAY_ORCHESTRATOR_SCHEDULE_MS,
|
||||
5000,
|
||||
'GATEWAY_ORCHESTRATOR_SCHEDULE_MS'
|
||||
),
|
||||
orchestratorBuildIntervalMs: parseNumber(
|
||||
orchestratorBuildIntervalMs: parseNumberWithFallback(
|
||||
env.GATEWAY_ORCHESTRATOR_BUILD_MS,
|
||||
10000,
|
||||
'GATEWAY_ORCHESTRATOR_BUILD_MS'
|
||||
),
|
||||
orchestratorAdminIntervalMs: parseNumber(
|
||||
orchestratorAdminIntervalMs: parseNumberWithFallback(
|
||||
env.GATEWAY_ORCHESTRATOR_ADMIN_MS,
|
||||
5000,
|
||||
'GATEWAY_ORCHESTRATOR_ADMIN_MS'
|
||||
@@ -135,22 +119,22 @@ export const resolveGatewayOrchestratorConfigFromEnv = (
|
||||
dbSchema: resolveSchemaName(env.GATEWAY_DB_SCHEMA),
|
||||
redisKeyPrefix,
|
||||
gameTokenSecret: secret,
|
||||
orchestratorReconcileIntervalMs: parseNumber(
|
||||
orchestratorReconcileIntervalMs: parseNumberWithFallback(
|
||||
env.GATEWAY_ORCHESTRATOR_RECONCILE_MS,
|
||||
15000,
|
||||
'GATEWAY_ORCHESTRATOR_RECONCILE_MS'
|
||||
),
|
||||
orchestratorScheduleIntervalMs: parseNumber(
|
||||
orchestratorScheduleIntervalMs: parseNumberWithFallback(
|
||||
env.GATEWAY_ORCHESTRATOR_SCHEDULE_MS,
|
||||
5000,
|
||||
'GATEWAY_ORCHESTRATOR_SCHEDULE_MS'
|
||||
),
|
||||
orchestratorBuildIntervalMs: parseNumber(
|
||||
orchestratorBuildIntervalMs: parseNumberWithFallback(
|
||||
env.GATEWAY_ORCHESTRATOR_BUILD_MS,
|
||||
10000,
|
||||
'GATEWAY_ORCHESTRATOR_BUILD_MS'
|
||||
),
|
||||
orchestratorAdminIntervalMs: parseNumber(
|
||||
orchestratorAdminIntervalMs: parseNumberWithFallback(
|
||||
env.GATEWAY_ORCHESTRATOR_ADMIN_MS,
|
||||
5000,
|
||||
'GATEWAY_ORCHESTRATOR_ADMIN_MS'
|
||||
|
||||
@@ -2,6 +2,7 @@ import path from 'node:path';
|
||||
|
||||
import { seedScenarioToDatabase, type ScenarioInstallOptions } from '@sammo-ts/game-engine';
|
||||
import { createGamePostgresConnector, resolvePostgresConfigFromEnv } from '@sammo-ts/infra';
|
||||
import { isRecord } from '@sammo-ts/common';
|
||||
|
||||
import type { BuildRunner } from './buildRunner.js';
|
||||
import type { ProcessManager } from './processManager.js';
|
||||
@@ -103,9 +104,6 @@ interface GatewayAdminActionResult {
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const normalizeMeta = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
||||
|
||||
const normalizeStatus = (value: unknown): GatewayAdminActionStatus | null => {
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from './util/RNG.js';
|
||||
export * from './util/RandUtil.js';
|
||||
export * from './util/TestRNG.js';
|
||||
export * from './util/sha512.js';
|
||||
export * from './util/parse.js';
|
||||
export * from './turnDaemon/types.js';
|
||||
export * from './realtime/keys.js';
|
||||
export * from './realtime/types.js';
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
export const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
||||
|
||||
export const asNumber = (value: unknown, fallback: number): number =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
||||
|
||||
export const asString = (value: unknown, fallback: string): string => (typeof value === 'string' ? value : fallback);
|
||||
|
||||
export const asNullableNumber = (value: unknown): number | null =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||
|
||||
export const asNullableString = (value: unknown): string | null => (typeof value === 'string' ? value : null);
|
||||
|
||||
export const asStringArray = (value: unknown): string[] =>
|
||||
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
||||
|
||||
export const asNullableStringArray = (value: unknown): string[] | null => {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
return asStringArray(value);
|
||||
};
|
||||
|
||||
export const parseNumberWithFallback = (value: string | undefined, fallback: number, label?: string): number => {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (Number.isNaN(parsed)) {
|
||||
if (label) {
|
||||
throw new Error(`${label} must be a number.`);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
export const parseOptionalNumber = (value: string | undefined): number | undefined => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return undefined;
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
export const parseBooleanWithFallback = (value: string | undefined, fallback: boolean): boolean => {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
export const parseOptionalBoolean = (value: string | undefined): boolean | undefined => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const parseJson = <T>(raw: string | null): T | null => {
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import type { ScenarioMeta } from '@sammo-ts/logic/world/types.js';
|
||||
import type { WarAftermathConfig, WarEngineConfig, WarTimeContext } from '@sammo-ts/logic/war/types.js';
|
||||
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
import type { ActionContextWorldRef, ActionContextWorldState } from './actionContext.js';
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
export interface WorldSummary {
|
||||
totalGeneralCount: number;
|
||||
@@ -112,9 +113,6 @@ const DEFAULT_AFTER_CONFIG = {
|
||||
defaultCityWall: 1000,
|
||||
};
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> =>
|
||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
||||
|
||||
const resolveNumber = (record: Record<string, unknown>, keys: string[], fallback: number): number => {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { GENERAL_TURN_COMMAND_KEYS, isGeneralTurnCommandKey, type GeneralTurnCommandKey } from './general/index.js';
|
||||
import { NATION_TURN_COMMAND_KEYS, isNationTurnCommandKey, type NationTurnCommandKey } from './nation/index.js';
|
||||
import { asStringArray, isRecord } from '@sammo-ts/common';
|
||||
|
||||
export interface TurnCommandProfile {
|
||||
general: GeneralTurnCommandKey[];
|
||||
nation: NationTurnCommandKey[];
|
||||
}
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const asStringArray = (value: unknown): string[] | null => {
|
||||
const asStringArrayOrNull = (value: unknown): string[] | null => {
|
||||
if (!Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const list = value.filter((entry): entry is string => typeof entry === 'string');
|
||||
const list = asStringArray(value);
|
||||
return list.length > 0 ? list : null;
|
||||
};
|
||||
|
||||
@@ -23,7 +21,7 @@ const parseKeyList = <T extends string>(options: {
|
||||
isKey: (value: string) => value is T;
|
||||
label: string;
|
||||
}): T[] => {
|
||||
const rawList = asStringArray(options.raw);
|
||||
const rawList = asStringArrayOrNull(options.raw);
|
||||
if (!rawList) {
|
||||
return options.defaults;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { asNullableNumber, asNullableString, asNumber, asString, asStringArray, isRecord } from '@sammo-ts/common';
|
||||
|
||||
import type {
|
||||
ScenarioConfig,
|
||||
@@ -23,24 +24,10 @@ const FALLBACK_STAT: ScenarioStatBlock = {
|
||||
chiefMin: 0,
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is UnknownRecord =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
const toRecordOrUndefined = (value: unknown): UnknownRecord | undefined => (isRecord(value) ? value : undefined);
|
||||
|
||||
const toArrayOrUndefined = (value: unknown): unknown[] | undefined => (Array.isArray(value) ? value : undefined);
|
||||
|
||||
const asNumber = (value: unknown, fallback: number): number => (typeof value === 'number' ? value : fallback);
|
||||
|
||||
const asString = (value: unknown, fallback: string): string => (typeof value === 'string' ? value : fallback);
|
||||
|
||||
const asNullableNumber = (value: unknown): number | null => (typeof value === 'number' ? value : null);
|
||||
|
||||
const asNullableString = (value: unknown): string | null => (typeof value === 'string' ? value : null);
|
||||
|
||||
const asStringArray = (value: unknown): string[] =>
|
||||
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
||||
|
||||
const zRecord = z.record(z.string(), z.unknown());
|
||||
const zUnknownArray = z.array(z.unknown());
|
||||
const zOptionalRecord = z.preprocess(toRecordOrUndefined, zRecord.optional());
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { isRecord } from '@sammo-ts/common';
|
||||
|
||||
export interface WarDexAux {
|
||||
isAttacker?: boolean;
|
||||
opposeType?: { armType: number };
|
||||
}
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === 'object';
|
||||
|
||||
export const parseWarDexAux = (aux: unknown): WarDexAux => {
|
||||
if (!isRecord(aux)) {
|
||||
return {};
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import type { City, General, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { CrewTypeDefinition, CrewTypeRequirement, MapDefinition, UnitSetDefinition } from './types.js';
|
||||
import {
|
||||
asNullableStringArray,
|
||||
asNumber,
|
||||
asRecord,
|
||||
asString,
|
||||
asStringArray,
|
||||
isRecord,
|
||||
} from '@sammo-ts/common';
|
||||
|
||||
const DEFAULT_REGION_MAP: Record<string, number> = {
|
||||
하북: 1,
|
||||
@@ -14,26 +22,6 @@ const DEFAULT_REGION_MAP: Record<string, number> = {
|
||||
|
||||
const DEFAULT_MAX_TECH_LEVEL = 12;
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
||||
|
||||
const asNumber = (value: unknown, fallback: number): number =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
||||
|
||||
const asString = (value: unknown, fallback: string): string => (typeof value === 'string' ? value : fallback);
|
||||
|
||||
const asStringArray = (value: unknown): string[] =>
|
||||
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
||||
|
||||
const asNullableStringArray = (value: unknown): string[] | null => {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
return asStringArray(value);
|
||||
};
|
||||
|
||||
const normalizeCoef = (value: unknown): Record<string, number> => {
|
||||
if (!isRecord(value)) {
|
||||
return {};
|
||||
|
||||
Reference in New Issue
Block a user