/**
* ServiceWorker Mock Worker
* This file will be compiled to dist/sw-mock-worker.js
*/
// Import types for ServiceWorker context
///
// 多响应配置相关类型定义
interface MultiResponseConfig {
default: MockResponseData;
responses: ConditionalResponse[];
global?: {
delay?: number | [number, number];
errorRate?: number;
headers?: Record;
};
}
interface ConditionalResponse {
name?: string;
conditions: MatchCondition[];
response: MockResponseData;
weight?: number;
}
interface MatchCondition {
type: 'query' | 'body' | 'header' | 'method' | 'path';
rule: MatchRule;
target: string;
value: any;
required?: boolean;
}
interface MatchRule {
mode: 'exact' | 'contains' | 'startsWith' | 'endsWith' | 'regex' | 'range' | 'exists';
ignoreCase?: boolean;
regexFlags?: string;
}
interface MockResponseData {
status?: number;
data: any;
headers?: Record;
delay?: number | [number, number];
errorRate?: number;
errorStatus?: number | number[];
description?: string;
}
interface RequestContext {
method: string;
url: string;
pathname: string;
query: Record;
headers: Record;
body?: any;
timestamp: number;
}
interface MatchResult {
matched: boolean;
response?: MockResponseData;
matchedConditions?: string[];
weight?: number;
responseName?: string;
}
interface SwMockConfig {
enabled: boolean;
debug: boolean;
defaultDelay: number;
interceptPatterns: string[];
excludePatterns: string[];
endpoints: Record;
globalDelay: number | null;
errorRate: number;
mockDataPath: string;
networkFailure: {
enabled: boolean;
scenarios: {
httpErrors: {
enabled: boolean;
statusCodes: number[];
rate: number;
distribution: 'random' | 'sequential' | 'weighted';
};
timeout: {
enabled: boolean;
duration: number;
rate: number;
};
connectionFailure: {
enabled: boolean;
rate: number;
};
responseDelay: {
enabled: boolean;
minDelay: number;
maxDelay: number;
rate: number;
};
intermittentFailure: {
enabled: boolean;
failureDuration: number;
recoveryDuration: number;
rate: number;
};
};
};
}
// Configuration constants
const CONFIG_STORE_NAME = 'sw-mock-config';
const CONFIG_VERSION = 1;
const DEFAULT_CONFIG: SwMockConfig = {
enabled: false,
debug: false,
defaultDelay: 500,
interceptPatterns: [
'/api/',
'microgrids-service',
'digital-portal',
'old-microgrids-service'
],
excludePatterns: [
'/sw-mock-worker.js',
'/mock/',
'.js', '.css',
'.png', '.jpg', '.jpeg',
'.gif', '.svg', '.ico',
'.woff', '.woff2',
'.ttf', '.eot'
],
endpoints: {},
globalDelay: null,
errorRate: 0,
mockDataPath: '/mock',
includeInterceptPrefix: true,
networkFailure: {
enabled: false,
scenarios: {
httpErrors: {
enabled: false,
statusCodes: [400, 401, 403, 404, 500, 502, 503, 504],
rate: 0.1,
distribution: 'random'
},
timeout: {
enabled: false,
duration: 30000,
rate: 0.05
},
connectionFailure: {
enabled: false,
rate: 0.02
},
responseDelay: {
enabled: false,
minDelay: 1000,
maxDelay: 5000,
rate: 0.3
},
intermittentFailure: {
enabled: false,
failureDuration: 10000,
recoveryDuration: 30000,
rate: 0.1
}
}
}
};
// Global variables
let config: SwMockConfig = { ...DEFAULT_CONFIG };
let dbInstance: IDBDatabase | null = null;
let capturedRequests: any[] = [];
let isCapturing = false;
// Network failure state
let networkFailureState = {
httpErrorIndex: 0,
intermittentFailureStartTime: null as number | null,
isInFailureState: false,
lastFailureCheck: 0
};
/**
* Ensure configuration structure integrity
*/
function ensureConfigStructure(configData: Partial): SwMockConfig {
const mergedConfig = { ...DEFAULT_CONFIG, ...configData };
// Ensure networkFailure structure is complete
if (!mergedConfig.networkFailure) {
mergedConfig.networkFailure = { ...DEFAULT_CONFIG.networkFailure };
} else {
mergedConfig.networkFailure = {
...DEFAULT_CONFIG.networkFailure,
...mergedConfig.networkFailure
};
// Ensure scenarios structure is complete
if (!mergedConfig.networkFailure.scenarios) {
mergedConfig.networkFailure.scenarios = { ...DEFAULT_CONFIG.networkFailure.scenarios };
} else {
mergedConfig.networkFailure.scenarios = {
...DEFAULT_CONFIG.networkFailure.scenarios,
...mergedConfig.networkFailure.scenarios
};
}
}
return mergedConfig;
}
/**
* IndexedDB operations
*/
class ConfigDB {
private dbName = 'sw-mock-db';
private version = CONFIG_VERSION;
async init(): Promise {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => {
this.log('IndexedDB open failed', request.error);
reject(request.error);
};
request.onsuccess = () => {
dbInstance = request.result;
this.log('IndexedDB connected successfully');
resolve(dbInstance);
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
this.log(`IndexedDB upgrade: from version ${event.oldVersion} to ${event.newVersion}`);
// Delete old object store if exists
if (db.objectStoreNames.contains(CONFIG_STORE_NAME)) {
db.deleteObjectStore(CONFIG_STORE_NAME);
this.log('Deleted old object store');
}
// Create new object store
db.createObjectStore(CONFIG_STORE_NAME);
this.log('Created new object store');
// Ensure transaction completion
(event.target as IDBOpenDBRequest).transaction!.oncomplete = () => {
this.log('IndexedDB upgrade completed');
};
};
});
}
async getConfig(): Promise {
try {
if (!dbInstance) {
await this.init();
}
return new Promise((resolve, reject) => {
try {
const transaction = dbInstance!.transaction([CONFIG_STORE_NAME], 'readonly');
const store = transaction.objectStore(CONFIG_STORE_NAME);
const request = store.get('config');
request.onerror = () => {
this.log('Read config failed', request.error);
reject(request.error);
};
request.onsuccess = () => {
const rawResult = request.result || { ...DEFAULT_CONFIG };
const result = ensureConfigStructure(rawResult);
this.log('Config read successfully', result);
resolve(result);
};
} catch (error) {
this.log('Create transaction failed', error);
// If transaction creation fails, return default config
resolve(ensureConfigStructure({}));
}
});
} catch (error) {
this.log('Get config failed, using default config', error);
return ensureConfigStructure({});
}
}
async saveConfig(newConfig: SwMockConfig): Promise {
try {
if (!dbInstance) {
await this.init();
}
return new Promise((resolve, reject) => {
try {
const transaction = dbInstance!.transaction([CONFIG_STORE_NAME], 'readwrite');
const store = transaction.objectStore(CONFIG_STORE_NAME);
const request = store.put(newConfig, 'config');
request.onerror = () => {
this.log('Save config failed', request.error);
reject(request.error);
};
request.onsuccess = () => {
this.log('Config saved successfully');
resolve();
};
} catch (error) {
this.log('Create save transaction failed', error);
reject(error);
}
});
} catch (error) {
this.log('Save config failed', error);
throw error;
}
}
async clearConfig(): Promise {
try {
if (!dbInstance) {
await this.init();
}
return new Promise((resolve, reject) => {
try {
const transaction = dbInstance!.transaction([CONFIG_STORE_NAME], 'readwrite');
const store = transaction.objectStore(CONFIG_STORE_NAME);
const request = store.clear();
request.onerror = () => {
this.log('Clear config failed', request.error);
reject(request.error);
};
request.onsuccess = () => {
this.log('Config cleared successfully');
resolve();
};
} catch (error) {
this.log('Create clear transaction failed', error);
reject(error);
}
});
} catch (error) {
this.log('Clear config failed', error);
throw error;
}
}
private log(message: string, data?: any): void {
if (config.debug) {
console.log(`[SW Mock DB] ${message}`, data || '');
}
}
}
const configDB = new ConfigDB();
/**
* Logging utilities
*/
function log(message: string, data?: any): void {
if (config.debug) {
console.log(`[SW Mock] ${message}`, data || '');
}
}
function logError(message: string, error?: any): void {
console.error(`[SW Mock Error] ${message}`, error || '');
}
/**
* URL matching functions
*/
function shouldIntercept(url: string): boolean {
// Check exclude patterns first
for (const pattern of config.excludePatterns) {
if (url.includes(pattern)) {
return false;
}
}
// Check intercept patterns
for (const pattern of config.interceptPatterns) {
if (url.includes(pattern)) {
return true;
}
}
return false;
}
/**
* Generate mock file path from URL
* 支持两种模式:
* 1. 嵌套目录结构:/api/users/list -> /mock/api/users/list.json 或 /mock/users/list.json
* 2. 扁平化结构:/api/users/list -> /mock/api_users_list.json 或 /mock/users_list.json (兼容旧版本)
*/
function generateMockFilePath(url: string, useNestedStructure: boolean = true): string | null {
try {
const urlObj = new URL(url);
let path = urlObj.pathname;
// Remove leading slash
if (path.startsWith('/')) {
path = path.substring(1);
}
// Remove service prefixes
const servicePrefixes = ['microgrids-service', 'digital-portal', 'old-microgrids-service'];
for (const prefix of servicePrefixes) {
if (path.startsWith(prefix + '/')) {
path = path.substring(prefix.length + 1);
}
}
// 根据 includeInterceptPrefix 配置决定是否移除拦截前缀
if (!config.includeInterceptPrefix && config.interceptPatterns) {
for (const pattern of config.interceptPatterns) {
// 移除前导和尾随的斜杠,获取纯前缀
const prefix = pattern.replace(/^\/+|\/+$/g, '');
if (prefix && path.startsWith(prefix + '/')) {
path = path.substring(prefix.length + 1);
log(`Removed intercept prefix '${prefix}' from path: ${urlObj.pathname} -> ${path}`);
break; // 只移除第一个匹配的前缀
}
}
}
// 获取配置的mock数据路径,确保以/开头但不以/结尾
let mockDataPath = config.mockDataPath || '/mock';
if (!mockDataPath.startsWith('/')) {
mockDataPath = '/' + mockDataPath;
}
if (mockDataPath.endsWith('/')) {
mockDataPath = mockDataPath.slice(0, -1);
}
if (useNestedStructure) {
// 嵌套目录结构:保持URL路径结构
return `${mockDataPath}/${path}.json`;
} else {
// 扁平化结构:将斜杠替换为下划线(兼容旧版本)
const flatPath = path.replace(/\//g, '_');
return `${mockDataPath}/${flatPath}.json`;
}
} catch (error) {
logError('Generate mock file path failed', error);
return null;
}
}
/**
* 从请求中提取上下文信息
*/
async function extractRequestContext(request: Request): Promise {
const url = new URL(request.url);
const query: Record = {};
// 提取 query 参数
for (const [key, value] of url.searchParams.entries()) {
if (query[key]) {
// 如果已存在,转换为数组
if (Array.isArray(query[key])) {
(query[key] as string[]).push(value);
} else {
query[key] = [query[key] as string, value];
}
} else {
query[key] = value;
}
}
// 提取请求头
const headers: Record = {};
for (const [key, value] of request.headers.entries()) {
headers[key.toLowerCase()] = value;
}
// 提取请求体
let body: any = null;
if (request.method !== 'GET' && request.method !== 'HEAD') {
try {
const contentType = headers['content-type'] || '';
if (contentType.includes('application/json')) {
const clonedRequest = request.clone();
const text = await clonedRequest.text();
if (text) {
body = JSON.parse(text);
}
} else if (contentType.includes('application/x-www-form-urlencoded')) {
const clonedRequest = request.clone();
const formData = await clonedRequest.formData();
body = {};
for (const [key, value] of formData.entries()) {
body[key] = value;
}
} else if (contentType.includes('multipart/form-data')) {
const clonedRequest = request.clone();
const formData = await clonedRequest.formData();
body = {};
for (const [key, value] of formData.entries()) {
body[key] = value;
}
} else {
const clonedRequest = request.clone();
body = await clonedRequest.text();
}
} catch (error) {
log('Failed to extract request body:', error);
}
}
return {
method: request.method,
url: request.url,
pathname: url.pathname,
query,
headers,
body,
timestamp: Date.now()
};
}
/**
* 参数匹配引擎
*/
function matchValue(actualValue: any, expectedValue: any, rule: MatchRule): boolean {
// 处理 null 和 undefined
if (actualValue == null && expectedValue == null) {
return true;
}
if (actualValue == null || expectedValue == null) {
return rule.mode === 'exists' ? actualValue != null : false;
}
// 转换为字符串进行匹配(除了 range 模式)
let actual = String(actualValue);
let expected = String(expectedValue);
// 处理大小写忽略
if (rule.ignoreCase && typeof actualValue === 'string' && typeof expectedValue === 'string') {
actual = actual.toLowerCase();
expected = expected.toLowerCase();
}
switch (rule.mode) {
case 'exact':
return actual === expected;
case 'contains':
return actual.includes(expected);
case 'startsWith':
return actual.startsWith(expected);
case 'endsWith':
return actual.endsWith(expected);
case 'regex':
try {
const flags = rule.regexFlags || (rule.ignoreCase ? 'i' : '');
const regex = new RegExp(expected, flags);
return regex.test(actual);
} catch (error) {
log(`Invalid regex pattern: ${expected}`, error);
return false;
}
case 'range':
// 数值范围匹配
if (Array.isArray(expectedValue) && expectedValue.length === 2) {
const numValue = Number(actualValue);
const [min, max] = expectedValue.map(Number);
return !isNaN(numValue) && numValue >= min && numValue <= max;
}
return false;
case 'exists':
return actualValue != null;
default:
return false;
}
}
/**
* 从请求上下文中获取目标值
*/
function getTargetValue(context: RequestContext, condition: MatchCondition): any {
switch (condition.type) {
case 'query':
return context.query[condition.target];
case 'header':
return context.headers[condition.target.toLowerCase()];
case 'method':
return context.method;
case 'path':
return condition.target === 'pathname' ? context.pathname : context.url;
case 'body':
if (!context.body) return null;
// 支持嵌套属性访问,如 "user.name"
const keys = condition.target.split('.');
let value = context.body;
for (const key of keys) {
if (value && typeof value === 'object' && key in value) {
value = value[key];
} else {
return null;
}
}
return value;
default:
return null;
}
}
/**
* 检查单个条件是否匹配
*/
function checkCondition(context: RequestContext, condition: MatchCondition): boolean {
const actualValue = getTargetValue(context, condition);
return matchValue(actualValue, condition.value, condition.rule);
}
/**
* 检查条件响应是否匹配
*/
function checkConditionalResponse(context: RequestContext, response: ConditionalResponse): MatchResult {
const matchedConditions: string[] = [];
let allRequired = true;
for (const condition of response.conditions) {
const isMatch = checkCondition(context, condition);
const isRequired = condition.required !== false; // 默认为 true
if (isMatch) {
matchedConditions.push(`${condition.type}:${condition.target}`);
} else if (isRequired) {
allRequired = false;
break;
}
}
// 必须所有必需条件都匹配,且至少有一个条件匹配
const matched = allRequired && (matchedConditions.length > 0 || response.conditions.length === 0);
return {
matched,
response: matched ? response.response : undefined,
matchedConditions,
weight: response.weight || 0,
responseName: response.name
};
}
/**
* 选择最佳匹配的响应
*/
function selectBestResponse(context: RequestContext, multiConfig: MultiResponseConfig): MatchResult {
const matchResults: MatchResult[] = [];
// 检查所有条件响应
for (const response of multiConfig.responses) {
const result = checkConditionalResponse(context, response);
if (result.matched) {
matchResults.push(result);
}
}
if (matchResults.length === 0) {
// 没有匹配的条件响应,使用默认响应
log('No conditional response matched, using default response');
return {
matched: true,
response: multiConfig.default,
matchedConditions: [],
weight: 0,
responseName: 'default'
};
}
// 按权重排序,权重高的优先
matchResults.sort((a, b) => {
const weightA = a.weight || 0;
const weightB = b.weight || 0;
if (weightA !== weightB) {
return weightB - weightA; // 降序
}
// 权重相同时,按匹配条件数量排序
return (b.matchedConditions?.length || 0) - (a.matchedConditions?.length || 0);
});
const bestMatch = matchResults[0];
log(`Selected response: ${bestMatch.responseName}, weight: ${bestMatch.weight}, matched conditions: ${bestMatch.matchedConditions?.join(', ')}`);
return bestMatch;
}
/**
* 检查数据是否为多响应配置
*/
function isMultiResponseConfig(data: any): data is MultiResponseConfig {
return data &&
typeof data === 'object' &&
'default' in data &&
'responses' in data &&
Array.isArray(data.responses);
}
/**
* Load mock data from file
* 支持多种路径格式的回退机制
*/
async function loadMockData(url: string): Promise {
// 尝试多种路径格式
const pathsToTry = [
generateMockFilePath(url, true), // 嵌套目录结构
generateMockFilePath(url, false) // 扁平化结构(兼容旧版本)
].filter(Boolean) as string[];
for (const mockFilePath of pathsToTry) {
try {
const response = await fetch(mockFilePath);
if (response.ok) {
const data = await response.json();
log(`Mock data loaded successfully: ${mockFilePath}`, data);
return data;
} else {
log(`Mock file not found: ${mockFilePath}`);
}
} catch (error) {
log(`Load mock data failed: ${mockFilePath}`, error);
}
}
log(`No mock data found for URL: ${url}`);
return null;
}
/**
* Network failure simulation engine
*/
function shouldTriggerNetworkFailure(url: string): any {
// Check if network failure configuration exists and is enabled
if (!config.networkFailure || !config.networkFailure.enabled) {
return null;
}
const scenarios = config.networkFailure.scenarios;
if (!scenarios) {
return null;
}
// Check scenarios in priority order
// 1. Intermittent failure check
if (scenarios.intermittentFailure && scenarios.intermittentFailure.enabled) {
const intermittentResult = checkIntermittentFailure();
if (intermittentResult) {
return intermittentResult;
}
}
// 2. Connection failure check
if (scenarios.connectionFailure && scenarios.connectionFailure.enabled && Math.random() < scenarios.connectionFailure.rate) {
log(`Trigger connection failure: ${url}`);
return { type: 'connectionFailure' };
}
// 3. Timeout check
if (scenarios.timeout && scenarios.timeout.enabled && Math.random() < scenarios.timeout.rate) {
log(`Trigger timeout: ${url}, duration: ${scenarios.timeout.duration}ms`);
return { type: 'timeout', duration: scenarios.timeout.duration };
}
// 4. HTTP error check
if (scenarios.httpErrors && scenarios.httpErrors.enabled && Math.random() < scenarios.httpErrors.rate) {
const statusCode = selectHttpErrorStatusCode();
log(`Trigger HTTP error: ${url}, status code: ${statusCode}`);
return { type: 'httpError', statusCode };
}
// 5. Response delay check
if (scenarios.responseDelay && scenarios.responseDelay.enabled && Math.random() < scenarios.responseDelay.rate) {
const delay = generateRandomDelay();
log(`Trigger response delay: ${url}, delay: ${delay}ms`);
return { type: 'responseDelay', delay };
}
return null;
}
function checkIntermittentFailure(): any {
if (!config.networkFailure || !config.networkFailure.scenarios || !config.networkFailure.scenarios.intermittentFailure) {
return null;
}
const scenario = config.networkFailure.scenarios.intermittentFailure;
const now = Date.now();
// If not in failure state, check if should enter failure state
if (!networkFailureState.isInFailureState) {
if (Math.random() < scenario.rate) {
networkFailureState.isInFailureState = true;
networkFailureState.intermittentFailureStartTime = now;
log('Enter intermittent failure state');
return { type: 'connectionFailure' };
}
} else {
// Check if should exit failure state
const failureElapsed = now - (networkFailureState.intermittentFailureStartTime || 0);
if (failureElapsed >= scenario.failureDuration) {
networkFailureState.isInFailureState = false;
networkFailureState.intermittentFailureStartTime = now; // Start recovery period
log('Exit intermittent failure state, enter recovery period');
} else {
// Still in failure period
return { type: 'connectionFailure' };
}
}
// Check if in recovery period
if (networkFailureState.intermittentFailureStartTime) {
const recoveryElapsed = now - networkFailureState.intermittentFailureStartTime;
if (recoveryElapsed >= scenario.recoveryDuration) {
networkFailureState.intermittentFailureStartTime = null;
log('Intermittent failure recovery period ended');
}
}
return null;
}
function selectHttpErrorStatusCode(): number {
if (!config.networkFailure || !config.networkFailure.scenarios || !config.networkFailure.scenarios.httpErrors) {
return 500; // Default return 500 error
}
const scenario = config.networkFailure.scenarios.httpErrors;
const statusCodes = scenario.statusCodes || [500];
switch (scenario.distribution) {
case 'sequential':
const statusCode = statusCodes[networkFailureState.httpErrorIndex % statusCodes.length];
networkFailureState.httpErrorIndex++;
return statusCode;
case 'weighted':
// Server errors have higher weight
const weightedCodes = [
...statusCodes.filter(code => code >= 500), // Server error weight 1
...statusCodes.filter(code => code >= 500), // Server error weight 2
...statusCodes.filter(code => code < 500) // Client error weight 1
];
return weightedCodes[Math.floor(Math.random() * weightedCodes.length)];
case 'random':
default:
return statusCodes[Math.floor(Math.random() * statusCodes.length)];
}
}
function generateRandomDelay(): number {
if (!config.networkFailure || !config.networkFailure.scenarios || !config.networkFailure.scenarios.responseDelay) {
return 1000; // Default return 1 second delay
}
const scenario = config.networkFailure.scenarios.responseDelay;
const minDelay = scenario.minDelay || 1000;
const maxDelay = scenario.maxDelay || 5000;
return Math.floor(Math.random() * (maxDelay - minDelay + 1)) + minDelay;
}
/**
* Create network failure response
*/
async function createNetworkFailureResponse(failureInfo: any, url: string): Promise {
switch (failureInfo.type) {
case 'connectionFailure':
// Throw connection failure exception
throw new TypeError('Failed to fetch');
case 'timeout':
// Wait for timeout duration then throw exception
await new Promise(resolve => setTimeout(resolve, failureInfo.duration));
throw new TypeError('Network timeout');
case 'httpError':
// Return HTTP error response
return new Response(JSON.stringify({
error: 'Network Error',
message: `HTTP ${failureInfo.statusCode} Error`,
url: url
}), {
status: failureInfo.statusCode,
statusText: getStatusText(failureInfo.statusCode),
headers: {
'Content-Type': 'application/json',
'X-Mock-Network-Failure': 'true'
}
});
case 'responseDelay':
// Add delay but continue normal processing
await new Promise(resolve => setTimeout(resolve, failureInfo.delay));
return null; // Return null means continue normal processing
default:
return null;
}
}
/**
* Get HTTP status text for status code
*/
function getStatusText(statusCode: number): string {
const statusTexts: Record = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout'
};
return statusTexts[statusCode] || 'Error';
}
/**
* 处理多响应配置的模拟行为
*/
async function applyMultiResponseBehavior(context: RequestContext, mockData: any): Promise {
if (isMultiResponseConfig(mockData)) {
// 多响应配置
const matchResult = selectBestResponse(context, mockData);
if (!matchResult.matched || !matchResult.response) {
throw new Error('No matching response found');
}
const selectedResponse = matchResult.response;
const globalConfig = mockData.global || {};
// 应用延迟
const delay = selectedResponse.delay || globalConfig.delay || config.globalDelay || config.defaultDelay;
if (delay) {
const actualDelay = Array.isArray(delay)
? Math.floor(Math.random() * (delay[1] - delay[0] + 1)) + delay[0]
: delay;
if (actualDelay > 0) {
await new Promise(resolve => setTimeout(resolve, actualDelay));
}
}
// 应用错误率
const errorRate = selectedResponse.errorRate || globalConfig.errorRate || config.errorRate;
if (errorRate > 0 && Math.random() < errorRate) {
const errorStatus = selectedResponse.errorStatus || 500;
const status = Array.isArray(errorStatus)
? errorStatus[Math.floor(Math.random() * errorStatus.length)]
: errorStatus;
throw new Error(`Simulated error - status code: ${status}`);
}
// 合并响应头
const headers = {
...globalConfig.headers,
...selectedResponse.headers
};
return {
...selectedResponse.data,
_mockInfo: {
responseName: matchResult.responseName,
matchedConditions: matchResult.matchedConditions,
weight: matchResult.weight,
status: selectedResponse.status || 200,
headers
}
};
} else {
// 传统单响应配置
return applyMockBehavior(mockData);
}
}
/**
* Apply mock behavior (delay, error simulation) - 传统方式
*/
async function applyMockBehavior(mockData: any): Promise {
const mockConfig = mockData._mock || {};
// Apply delay
const delay = mockConfig.delay || config.globalDelay || config.defaultDelay;
if (delay > 0) {
await new Promise(resolve => setTimeout(resolve, delay));
}
// Apply error rate
const errorRate = mockConfig.errorRate || config.errorRate;
if (errorRate > 0 && Math.random() < errorRate) {
const errorStatus = mockConfig.errorStatus || 500;
throw new Error(`Simulated error - status code: ${errorStatus}`);
}
// Remove _mock field and return actual data
const { _mock, ...responseData } = mockData;
return responseData;
}
/**
* Create mock response
*/
function createMockResponse(data: any, status = 200, customHeaders: Record = {}): Response {
const headers = {
'Content-Type': 'application/json',
'X-Mock-Response': 'true',
...customHeaders
};
return new Response(JSON.stringify(data), {
status,
statusText: 'OK',
headers
});
}
/**
* Handle request interception
*/
async function handleRequest(event: any): Promise {
const { request } = event;
const url = request.url;
log(`Intercept request: ${request.method} ${url}`);
// Record request (if capturing)
if (isCapturing) {
capturedRequests.push({
method: request.method,
url: url,
timestamp: new Date().toISOString(),
headers: Object.fromEntries(request.headers.entries())
});
}
// Check if should intercept
if (!config.enabled || !shouldIntercept(url)) {
log(`Skip interception: ${url}`);
return fetch(request);
}
// Check network failure simulation
const failureInfo = shouldTriggerNetworkFailure(url);
if (failureInfo) {
try {
const failureResponse = await createNetworkFailureResponse(failureInfo, url);
if (failureResponse) {
return failureResponse;
}
// If returns null, it's response delay, continue normal processing
} catch (error) {
// Network exceptions (connection failure, timeout, etc.)
throw error;
}
}
// Load mock data (尝试多种路径格式)
const mockData = await loadMockData(url);
if (!mockData) {
log(`No mock data found, forward to real server: ${url}`);
return fetch(request);
}
try {
// 提取请求上下文
const context = await extractRequestContext(request);
// Apply mock behavior (支持多响应配置)
const responseData = await applyMultiResponseBehavior(context, mockData);
log(`Return mock response: ${url}`, responseData);
// 检查是否有自定义状态码和响应头
const mockInfo = responseData._mockInfo;
const status = mockInfo?.status || 200;
const customHeaders = mockInfo?.headers || {};
// 移除内部信息
const { _mockInfo, ...cleanResponseData } = responseData;
return createMockResponse(cleanResponseData, status, customHeaders);
} catch (error) {
logError(`Mock processing failed: ${url}`, error);
// If it's simulated error, return error response
if (error instanceof Error && error.message.includes('Simulated error')) {
const status = parseInt(error.message.match(/\d+/)?.[0] || '500');
return createMockResponse({ error: 'Simulated server error' }, status);
}
// Other errors, forward to real server
return fetch(request);
}
}
/**
* Message handling
*/
(self as any).addEventListener('message', async (event: any) => {
const { type, payload } = event.data;
log(`[SW Mock] Received message: ${type}`, payload);
try {
// Ensure we have a valid port to respond to
if (!event.ports || !event.ports[0]) {
logError('No message port available for response');
return;
}
switch (type) {
case 'GET_CONFIG':
log(`[SW Mock] Sending config response`, config);
event.ports[0].postMessage({ success: true, data: config });
break;
case 'UPDATE_CONFIG':
config = ensureConfigStructure({ ...config, ...payload });
await configDB.saveConfig(config);
event.ports[0].postMessage({ success: true });
log('Configuration updated', config);
break;
case 'RESET_CONFIG':
config = ensureConfigStructure({});
await configDB.saveConfig(config);
event.ports[0].postMessage({ success: true });
log('Configuration reset');
break;
case 'START_CAPTURE':
isCapturing = true;
capturedRequests = [];
event.ports[0].postMessage({ success: true });
log('Request capture started');
break;
case 'STOP_CAPTURE':
isCapturing = false;
event.ports[0].postMessage({ success: true, data: capturedRequests });
log('Request capture stopped', capturedRequests);
break;
case 'ENABLE_NETWORK_FAILURE':
config.networkFailure.enabled = true;
await configDB.saveConfig(config);
event.ports[0].postMessage({ success: true });
log('Network failure simulation enabled');
break;
case 'DISABLE_NETWORK_FAILURE':
config.networkFailure.enabled = false;
await configDB.saveConfig(config);
event.ports[0].postMessage({ success: true });
log('Network failure simulation disabled');
break;
case 'UPDATE_NETWORK_FAILURE_CONFIG':
config = ensureConfigStructure({
...config,
networkFailure: { ...config.networkFailure, ...payload }
});
await configDB.saveConfig(config);
event.ports[0].postMessage({ success: true });
log('Network failure configuration updated', config.networkFailure);
break;
case 'RESET_NETWORK_FAILURE_STATE':
networkFailureState = {
httpErrorIndex: 0,
intermittentFailureStartTime: null,
isInFailureState: false,
lastFailureCheck: 0
};
event.ports[0].postMessage({ success: true });
log('Network failure state reset');
break;
case 'CLEAR_DATABASE':
try {
await configDB.clearConfig();
config = ensureConfigStructure({});
event.ports[0].postMessage({ success: true });
log('Database cleared, configuration reset');
} catch (clearError) {
logError('Clear database failed', clearError);
event.ports[0].postMessage({ success: false, error: (clearError as Error).message });
}
break;
default:
logError(`Unknown message type: ${type}`);
event.ports[0].postMessage({ success: false, error: `Unknown message type: ${type}` });
}
} catch (error) {
logError('Message processing failed', error);
try {
if (event.ports && event.ports[0]) {
event.ports[0].postMessage({ success: false, error: (error as Error).message });
}
} catch (responseError) {
logError('Failed to send error response', responseError);
}
}
});
/**
* ServiceWorker event listeners
*/
(self as any).addEventListener('install', () => {
log('ServiceWorker installing...');
(self as any).skipWaiting();
});
(self as any).addEventListener('activate', (event: any) => {
log('ServiceWorker activating...');
event.waitUntil(
(async () => {
try {
log('Loading configuration from database...');
const savedConfig = await configDB.getConfig();
config = ensureConfigStructure(savedConfig);
log('Configuration loaded successfully', config);
} catch (error) {
logError('Load configuration failed, using default configuration', error);
config = ensureConfigStructure({});
// Try to clear possibly corrupted database
try {
await configDB.clearConfig();
log('Cleared corrupted configuration data');
} catch (clearError) {
logError('Clear configuration failed', clearError);
}
}
log('ServiceWorker activation complete, ready to handle messages');
})()
);
(self as any).clients.claim();
});
(self as any).addEventListener('fetch', (event: any) => {
event.respondWith(handleRequest(event));
});