import { ApiClient, ApiError } from '../src/lib/api';
import type { StreamEvent, TriggerDefinition } from '../src/types';
import { contentCatalog, contentWorkspace, timestamp, triggerCatalog, triggerWorkspace } from './fixtures';
/** No fetch/EventSource implementation: every supported operation is a browser-memory operation. */
export class MemoryPreviewApi extends ApiClient {
private authenticated: boolean;
private triggers = structuredClone(triggerWorkspace);
private content = structuredClone(contentWorkspace);
constructor(authenticated = true) { super('/api-preview-disabled'); this.authenticated = authenticated; }
override subscribe(_onEvent: (event: StreamEvent) => void, onState: (state: 'open' | 'error') => void): () => void {
queueMicrotask(() => onState('open'));
return () => undefined;
}
override async request<T>(path: string, options: Parameters<ApiClient['request']>[1] = {}): Promise<T> {
const route = path.split('?')[0];
const method = options.method ?? 'GET';
const body = (options.body ?? {}) as Record<string, unknown>;
const copy = (value: unknown): T => structuredClone(value) as T;
const session = () => ({ authenticated: this.authenticated, bootstrapRequired: false, csrfToken: 'memory-preview-only',
actor: { id: 'preview-owner', displayName: '本地预览 · 不连接服务器', roles: ['owner'], permissions: [], totpVerified: true } });
if (route === 'auth/session') return copy(session());
if (route === 'auth/login' || route === 'setup') { this.authenticated = true; return copy(session()); }
if (route === 'auth/logout') { this.authenticated = false; return undefined as T; }
if (route === 'auth/reauthenticate') return undefined as T;
if (route === 'meta') return copy({ setupRequired: false, serverReady: true });
if (route === 'status') return copy({ sampledAt: timestamp, serverName: '方块之境 · 本地内存预览', minecraftVersion: '1.21.11', forgeVersion: '61.0.0', onlineMode: true,
uptimeSeconds: 93784, playersOnline: 12, playersMax: 40, tps: 19.98, mspt: { average: 18.4, p95: 24.8, p99: 31.2, jitter: 2.31 },
cpuLoad: 0.24, heapUsedBytes: 3.2 * 1024 ** 3, heapMaxBytes: 8 * 1024 ** 3, diskFreeBytes: 120 * 1024 ** 3, averagePingMs: 34, gcPauseMs: 3.8,
dimensions: [{ id: 'minecraft:overworld', loadedChunks: 342, entities: 186 }, { id: 'minecraft:the_nether', loadedChunks: 48, entities: 31 }],
samples: Array.from({ length: 40 }, (_, i) => ({ at: new Date(Date.parse(timestamp) - (40 - i) * 1000).toISOString(), tps: 19.98, mspt: 18 + Math.sin(i * 0.7) * 5 + (i % 7) })) });
if (route === 'features') return copy({ consoleEnabled: false, moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false, joinExperienceEnabled: true, metricsEnabled: true });
if (route === 'triggers/catalog') return copy(triggerCatalog);
if (route === 'triggers/validate' || route === 'content/validate') return copy({ valid: true, errors: [], warnings: ['本地预览不执行真实服务端校验。'] });
if (route === 'triggers' && method === 'GET') return copy(this.triggers);
if ((route === 'triggers' && method === 'POST') || (route.startsWith('triggers/') && method === 'PUT')) {
const id = route === 'triggers' ? crypto.randomUUID() : decodeURIComponent(route.slice(9));
const trigger = { ...body, id, revision: Number(body.expectedRevision ?? body.revision ?? 0) + 1, createdBy: 'preview-owner', createdAt: timestamp, updatedAt: timestamp, migrated: false } as unknown as TriggerDefinition;
for (const group of this.triggers.groups) group.triggers = group.triggers.filter((value) => value.id !== id);
this.triggers.groups.find((group) => group.id === trigger.groupId)?.triggers.push(trigger);
this.triggers.totalTriggers = this.triggers.groups.reduce((total, group) => total + group.triggers.length, 0);
return copy({ trigger, workspace: this.triggers });
}
if (route.startsWith('triggers/') && route.endsWith('/variables')) return copy({ triggerId: route.split('/')[1], values: { 'var.welcomeCount': 0 }, observedAt: timestamp });
if (route.startsWith('triggers/') && route.endsWith('/state')) return copy({ triggerId: route.split('/')[1], values: [], observedAt: timestamp });
if (route === 'menus') return copy({ menus: [{ id: 'preview-menu', name: '冒险者大厅', description: '本地预览菜单' }], totalMenus: 1 });
if (route === 'economy') return copy({ currencies: [{ id: 'coins', code: 'coins', name: '金币', symbol: 'G', scale: 2, enabled: true }], overview: { totalAccounts: 12, recentTransactions: [] } });
if (route === 'content/catalog') return copy(contentCatalog);
if (route === 'content' && method === 'GET') return copy(this.content);
if (route === 'content' && method === 'PUT') {
this.content.workspace = { ...this.content.workspace, ...body, revision: this.content.workspace.revision + 1 } as typeof this.content.workspace;
return copy(this.content);
}
if (route === 'content/assets' || route === 'trigger-executions' || route === 'trigger-event-captures' || route === 'trigger-libraries' || route === 'players' || route === 'audit') return copy({ items: [] });
if (route === 'content/publish') { this.content.publishedRevision = this.content.workspace.revision; return copy(this.content); }
throw new ApiError(`本地预览未实现 ${method} ${route};不会访问服务器。`, 501);
}
}
import { ApiClient, ApiError } from '../src/lib/api';
import type { StreamEvent, TriggerDefinition } from '../src/types';
import { contentCatalog, contentWorkspace, timestamp, triggerCatalog, triggerWorkspace } from './fixtures';
/** No fetch/EventSource implementation: every supported operation is a browser-memory operation. */
export class MemoryPreviewApi extends ApiClient {
private authenticated: boolean;
private triggers = structuredClone(triggerWorkspace);
private content = structuredClone(contentWorkspace);
constructor(authenticated = true) { super('/api-preview-disabled'); this.authenticated = authenticated; }
override subscribe(_onEvent: (event: StreamEvent) => void, onState: (state: 'open' | 'error') => void): () => void {
queueMicrotask(() => onState('open'));
return () => undefined;
}
override async request<T>(path: string, options: Parameters<ApiClient['request']>[1] = {}): Promise<T> {
const route = path.split('?')[0];
const method = options.method ?? 'GET';
const body = (options.body ?? {}) as Record<string, unknown>;
const copy = (value: unknown): T => structuredClone(value) as T;
const session = () => ({ authenticated: this.authenticated, bootstrapRequired: false, csrfToken: 'memory-preview-only',
actor: { id: 'preview-owner', displayName: '本地预览 · 不连接服务器', roles: ['owner'], permissions: [], totpVerified: true } });
if (route === 'auth/session') return copy(session());
if (route === 'auth/login' || route === 'setup') { this.authenticated = true; return copy(session()); }
if (route === 'auth/logout') { this.authenticated = false; return undefined as T; }
if (route === 'auth/reauthenticate') return undefined as T;
if (route === 'meta') return copy({ setupRequired: false, serverReady: true });
if (route === 'status') return copy({ sampledAt: timestamp, serverName: '方块之境 · 本地内存预览', minecraftVersion: '1.21.11', forgeVersion: '61.0.0', onlineMode: true,
uptimeSeconds: 93784, playersOnline: 12, playersMax: 40, tps: 19.98, mspt: { average: 18.4, p95: 24.8, p99: 31.2, jitter: 2.31 },
cpuLoad: 0.24, heapUsedBytes: 3.2 * 1024 ** 3, heapMaxBytes: 8 * 1024 ** 3, diskFreeBytes: 120 * 1024 ** 3, averagePingMs: 34, gcPauseMs: 3.8,
dimensions: [{ id: 'minecraft:overworld', loadedChunks: 342, entities: 186 }, { id: 'minecraft:the_nether', loadedChunks: 48, entities: 31 }],
samples: Array.from({ length: 40 }, (_, i) => ({ at: new Date(Date.parse(timestamp) - (40 - i) * 1000).toISOString(), tps: 19.98, mspt: 18 + Math.sin(i * 0.7) * 5 + (i % 7) })) });
if (route === 'features') return copy({ consoleEnabled: false, moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false, joinExperienceEnabled: true, metricsEnabled: true });
if (route === 'triggers/catalog') return copy(triggerCatalog);
if (route === 'triggers/validate' || route === 'content/validate') return copy({ valid: true, errors: [], warnings: ['本地预览不执行真实服务端校验。'] });
if (route === 'triggers' && method === 'GET') return copy(this.triggers);
if ((route === 'triggers' && method === 'POST') || (route.startsWith('triggers/') && method === 'PUT')) {
const id = route === 'triggers' ? crypto.randomUUID() : decodeURIComponent(route.slice(9));
const trigger = { ...body, id, revision: Number(body.expectedRevision ?? body.revision ?? 0) + 1, createdBy: 'preview-owner', createdAt: timestamp, updatedAt: timestamp, migrated: false } as unknown as TriggerDefinition;
for (const group of this.triggers.groups) group.triggers = group.triggers.filter((value) => value.id !== id);
this.triggers.groups.find((group) => group.id === trigger.groupId)?.triggers.push(trigger);
this.triggers.totalTriggers = this.triggers.groups.reduce((total, group) => total + group.triggers.length, 0);
return copy({ trigger, workspace: this.triggers });
}
if (route.startsWith('triggers/') && route.endsWith('/variables')) return copy({ triggerId: route.split('/')[1], values: { 'var.welcomeCount': 0 }, observedAt: timestamp });
if (route.startsWith('triggers/') && route.endsWith('/state')) return copy({ triggerId: route.split('/')[1], values: [], observedAt: timestamp });
if (route === 'menus') return copy({ menus: [{ id: 'preview-menu', name: '冒险者大厅', description: '本地预览菜单' }], totalMenus: 1 });
if (route === 'economy') return copy({ currencies: [{ id: 'coins', code: 'coins', name: '金币', symbol: 'G', scale: 2, enabled: true }], overview: { totalAccounts: 12, recentTransactions: [] } });
if (route === 'content/catalog') return copy(contentCatalog);
if (route === 'content' && method === 'GET') return copy(this.content);
if (route === 'content' && method === 'PUT') {
this.content.workspace = { ...this.content.workspace, ...body, revision: this.content.workspace.revision + 1 } as typeof this.content.workspace;
return copy(this.content);
}
if (route === 'content/assets' || route === 'trigger-executions' || route === 'trigger-event-captures' || route === 'trigger-libraries' || route === 'players' || route === 'audit') return copy({ items: [] });
if (route === 'content/publish') { this.content.publishedRevision = this.content.workspace.revision; return copy(this.content); }
throw new ApiError(`本地预览未实现 ${method} ${route};不会访问服务器。`, 501);
}
}