@a-cube-io/ereceipts-js-sdk 2.0.8 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -7
- package/dist/index.cjs.js +218 -2654
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +24 -250
- package/dist/index.esm.js +219 -2650
- package/dist/index.esm.js.map +1 -1
- package/dist/index.native.js +219 -2650
- package/dist/index.native.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs.js
CHANGED
|
@@ -1415,6 +1415,15 @@ function from(input, scheduler) {
|
|
|
1415
1415
|
return scheduler ? scheduled(input, scheduler) : innerFrom(input);
|
|
1416
1416
|
}
|
|
1417
1417
|
|
|
1418
|
+
function of() {
|
|
1419
|
+
var args = [];
|
|
1420
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
|
1421
|
+
args[_i] = arguments[_i];
|
|
1422
|
+
}
|
|
1423
|
+
var scheduler = popScheduler(args);
|
|
1424
|
+
return from(args, scheduler);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1418
1427
|
function isValidDate(value) {
|
|
1419
1428
|
return value instanceof Date && !isNaN(value);
|
|
1420
1429
|
}
|
|
@@ -2126,7 +2135,7 @@ function formatDecimal(value, decimals = 2) {
|
|
|
2126
2135
|
return num.toFixed(decimals);
|
|
2127
2136
|
}
|
|
2128
2137
|
|
|
2129
|
-
const log$
|
|
2138
|
+
const log$b = createPrefixedLogger('AUTH-SERVICE');
|
|
2130
2139
|
class AuthenticationService {
|
|
2131
2140
|
get user$() {
|
|
2132
2141
|
return this.userSubject.asObservable();
|
|
@@ -2148,7 +2157,7 @@ class AuthenticationService {
|
|
|
2148
2157
|
}
|
|
2149
2158
|
async login(credentials) {
|
|
2150
2159
|
this.authStateSubject.next('authenticating');
|
|
2151
|
-
log$
|
|
2160
|
+
log$b.info('Login attempt', {
|
|
2152
2161
|
authUrl: this.config.authUrl,
|
|
2153
2162
|
email: credentials.email,
|
|
2154
2163
|
});
|
|
@@ -2159,7 +2168,7 @@ class AuthenticationService {
|
|
|
2159
2168
|
});
|
|
2160
2169
|
const jwtPayload = parseJwt(response.data.token);
|
|
2161
2170
|
const expiresAt = jwtPayload.exp * 1000;
|
|
2162
|
-
log$
|
|
2171
|
+
log$b.info('Login successful', {
|
|
2163
2172
|
authUrl: this.config.authUrl,
|
|
2164
2173
|
tokenPrefix: response.data.token.substring(0, 30) + '...',
|
|
2165
2174
|
expiresAt: new Date(expiresAt).toISOString(),
|
|
@@ -2188,21 +2197,21 @@ class AuthenticationService {
|
|
|
2188
2197
|
const token = await this.tokenStorage.getAccessToken();
|
|
2189
2198
|
if (!token) {
|
|
2190
2199
|
// No token - clear any stale user state
|
|
2191
|
-
log$
|
|
2200
|
+
log$b.debug('getCurrentUser: No token in storage');
|
|
2192
2201
|
if (this.userSubject.value) {
|
|
2193
2202
|
this.userSubject.next(null);
|
|
2194
2203
|
this.authStateSubject.next('idle');
|
|
2195
2204
|
}
|
|
2196
2205
|
return null;
|
|
2197
2206
|
}
|
|
2198
|
-
log$
|
|
2207
|
+
log$b.debug('getCurrentUser: Token found', {
|
|
2199
2208
|
tokenPrefix: token.substring(0, 30) + '...',
|
|
2200
2209
|
tokenLength: token.length,
|
|
2201
2210
|
});
|
|
2202
2211
|
const jwtPayload = parseJwt(token);
|
|
2203
2212
|
if (isTokenExpired(jwtPayload)) {
|
|
2204
2213
|
// Token expired - clear everything
|
|
2205
|
-
log$
|
|
2214
|
+
log$b.warn('getCurrentUser: Token expired');
|
|
2206
2215
|
await this.tokenStorage.clearTokens();
|
|
2207
2216
|
this.userSubject.next(null);
|
|
2208
2217
|
this.authStateSubject.next('idle');
|
|
@@ -2212,7 +2221,7 @@ class AuthenticationService {
|
|
|
2212
2221
|
// Token is valid - return cached user if available
|
|
2213
2222
|
const currentUser = this.userSubject.value;
|
|
2214
2223
|
if (currentUser) {
|
|
2215
|
-
log$
|
|
2224
|
+
log$b.debug('getCurrentUser: Returning cached user', {
|
|
2216
2225
|
email: currentUser.email,
|
|
2217
2226
|
roles: currentUser.roles,
|
|
2218
2227
|
});
|
|
@@ -2235,12 +2244,12 @@ class AuthenticationService {
|
|
|
2235
2244
|
async isAuthenticated() {
|
|
2236
2245
|
const token = await this.tokenStorage.getAccessToken();
|
|
2237
2246
|
if (!token) {
|
|
2238
|
-
log$
|
|
2247
|
+
log$b.debug('isAuthenticated: No token in storage');
|
|
2239
2248
|
return false;
|
|
2240
2249
|
}
|
|
2241
2250
|
const jwtPayload = parseJwt(token);
|
|
2242
2251
|
const expired = isTokenExpired(jwtPayload);
|
|
2243
|
-
log$
|
|
2252
|
+
log$b.debug('isAuthenticated: Token check', {
|
|
2244
2253
|
hasToken: true,
|
|
2245
2254
|
expired,
|
|
2246
2255
|
expiresAt: new Date(jwtPayload.exp * 1000).toISOString(),
|
|
@@ -2912,7 +2921,7 @@ class ACubeSDKError extends Error {
|
|
|
2912
2921
|
}
|
|
2913
2922
|
}
|
|
2914
2923
|
|
|
2915
|
-
const log$
|
|
2924
|
+
const log$a = createPrefixedLogger('AUTH-STRATEGY');
|
|
2916
2925
|
class AuthStrategy {
|
|
2917
2926
|
constructor(jwtHandler, mtlsHandler, userProvider, mtlsAdapter) {
|
|
2918
2927
|
this.jwtHandler = jwtHandler;
|
|
@@ -2927,7 +2936,7 @@ class AuthStrategy {
|
|
|
2927
2936
|
const platform = this.detectPlatform();
|
|
2928
2937
|
const userRole = await this.getUserRole();
|
|
2929
2938
|
const isReceiptEndpoint = this.isReceiptEndpoint(url);
|
|
2930
|
-
log$
|
|
2939
|
+
log$a.debug('Determining auth config', {
|
|
2931
2940
|
url,
|
|
2932
2941
|
method,
|
|
2933
2942
|
platform,
|
|
@@ -3058,7 +3067,7 @@ class JwtAuthHandler {
|
|
|
3058
3067
|
}
|
|
3059
3068
|
}
|
|
3060
3069
|
|
|
3061
|
-
const log$
|
|
3070
|
+
const log$9 = createPrefixedLogger('MTLS-HANDLER');
|
|
3062
3071
|
class MtlsAuthHandler {
|
|
3063
3072
|
constructor(mtlsAdapter, certificatePort) {
|
|
3064
3073
|
this.mtlsAdapter = mtlsAdapter;
|
|
@@ -3100,7 +3109,7 @@ class MtlsAuthHandler {
|
|
|
3100
3109
|
async makeRequest(url, config, jwtToken) {
|
|
3101
3110
|
const requestKey = this.generateRequestKey(url, config, jwtToken);
|
|
3102
3111
|
if (this.pendingRequests.has(requestKey)) {
|
|
3103
|
-
log$
|
|
3112
|
+
log$9.debug('Deduplicating concurrent request:', url);
|
|
3104
3113
|
return this.pendingRequests.get(requestKey);
|
|
3105
3114
|
}
|
|
3106
3115
|
const requestPromise = this.executeRequest(url, config, jwtToken, false);
|
|
@@ -3124,10 +3133,10 @@ class MtlsAuthHandler {
|
|
|
3124
3133
|
};
|
|
3125
3134
|
if (jwtToken) {
|
|
3126
3135
|
headers['Authorization'] = `Bearer ${jwtToken}`;
|
|
3127
|
-
log$
|
|
3136
|
+
log$9.debug('JWT token present:', jwtToken.substring(0, 20) + '...');
|
|
3128
3137
|
}
|
|
3129
3138
|
else {
|
|
3130
|
-
log$
|
|
3139
|
+
log$9.warn('No JWT token provided for mTLS request');
|
|
3131
3140
|
}
|
|
3132
3141
|
const fullUrl = this.constructMtlsUrl(url);
|
|
3133
3142
|
const mtlsConfig = {
|
|
@@ -3138,25 +3147,25 @@ class MtlsAuthHandler {
|
|
|
3138
3147
|
timeout: config.timeout,
|
|
3139
3148
|
responseType: config.responseType,
|
|
3140
3149
|
};
|
|
3141
|
-
log$
|
|
3142
|
-
log$
|
|
3150
|
+
log$9.debug('header-mtls', headers);
|
|
3151
|
+
log$9.debug(`${config.method} ${fullUrl}`);
|
|
3143
3152
|
if (config.data) {
|
|
3144
|
-
log$
|
|
3153
|
+
log$9.debug('Request body:', config.data);
|
|
3145
3154
|
}
|
|
3146
3155
|
try {
|
|
3147
3156
|
const response = await this.mtlsAdapter.request(mtlsConfig);
|
|
3148
|
-
log$
|
|
3157
|
+
log$9.debug(`Response ${response.status} from ${fullUrl}`);
|
|
3149
3158
|
if (response.data) {
|
|
3150
|
-
log$
|
|
3159
|
+
log$9.debug('Response body:', response.data);
|
|
3151
3160
|
}
|
|
3152
3161
|
return response.data;
|
|
3153
3162
|
}
|
|
3154
3163
|
catch (error) {
|
|
3155
|
-
log$
|
|
3164
|
+
log$9.error(`Response error from ${fullUrl}:`, error);
|
|
3156
3165
|
if (error && typeof error === 'object' && 'response' in error) {
|
|
3157
3166
|
const axiosError = error;
|
|
3158
3167
|
if (axiosError.response?.data) {
|
|
3159
|
-
log$
|
|
3168
|
+
log$9.error('Response body:', axiosError.response.data);
|
|
3160
3169
|
}
|
|
3161
3170
|
}
|
|
3162
3171
|
if (isRetryAttempt) {
|
|
@@ -3166,7 +3175,7 @@ class MtlsAuthHandler {
|
|
|
3166
3175
|
if (!shouldRetry) {
|
|
3167
3176
|
throw error;
|
|
3168
3177
|
}
|
|
3169
|
-
log$
|
|
3178
|
+
log$9.debug('Request failed, reconfiguring certificate and retrying...');
|
|
3170
3179
|
try {
|
|
3171
3180
|
await this.configureCertificate(certificate);
|
|
3172
3181
|
return await this.executeRequest(url, config, jwtToken, true);
|
|
@@ -3250,1923 +3259,27 @@ class MtlsAuthHandler {
|
|
|
3250
3259
|
adapterAvailable: !!this.mtlsAdapter,
|
|
3251
3260
|
certificatePortAvailable: !!this.certificatePort,
|
|
3252
3261
|
isReady: false,
|
|
3253
|
-
hasCertificate: false,
|
|
3254
|
-
certificateInfo: null,
|
|
3255
|
-
platformInfo: this.mtlsAdapter?.getPlatformInfo() || null,
|
|
3256
|
-
pendingRequestsCount: this.pendingRequests.size,
|
|
3257
|
-
};
|
|
3258
|
-
if (this.certificatePort) {
|
|
3259
|
-
try {
|
|
3260
|
-
status.hasCertificate = await this.certificatePort.hasCertificate();
|
|
3261
|
-
if (status.hasCertificate) {
|
|
3262
|
-
status.certificateInfo = await this.certificatePort.getCertificateInfo();
|
|
3263
|
-
}
|
|
3264
|
-
}
|
|
3265
|
-
catch {
|
|
3266
|
-
// Ignore errors
|
|
3267
|
-
}
|
|
3268
|
-
}
|
|
3269
|
-
status.isReady = await this.isMtlsReady();
|
|
3270
|
-
return status;
|
|
3271
|
-
}
|
|
3272
|
-
clearPendingRequests() {
|
|
3273
|
-
this.pendingRequests.clear();
|
|
3274
|
-
}
|
|
3275
|
-
}
|
|
3276
|
-
|
|
3277
|
-
const DEFAULT_QUEUE_CONFIG = {
|
|
3278
|
-
maxRetries: 3,
|
|
3279
|
-
retryDelay: 1000,
|
|
3280
|
-
maxRetryDelay: 30000,
|
|
3281
|
-
backoffMultiplier: 2,
|
|
3282
|
-
maxQueueSize: 1000,
|
|
3283
|
-
batchSize: 10,
|
|
3284
|
-
syncInterval: 30000,
|
|
3285
|
-
};
|
|
3286
|
-
|
|
3287
|
-
class OperationQueue {
|
|
3288
|
-
constructor(storage, config = DEFAULT_QUEUE_CONFIG, events = {}) {
|
|
3289
|
-
this.storage = storage;
|
|
3290
|
-
this.config = config;
|
|
3291
|
-
this.events = events;
|
|
3292
|
-
this.queue = [];
|
|
3293
|
-
this.processing = false;
|
|
3294
|
-
this.config = { ...DEFAULT_QUEUE_CONFIG, ...config };
|
|
3295
|
-
this.loadQueue();
|
|
3296
|
-
if (this.config.syncInterval > 0) {
|
|
3297
|
-
this.startAutoSync();
|
|
3298
|
-
}
|
|
3299
|
-
}
|
|
3300
|
-
async addOperation(type, resource, endpoint, method, data, priority = 1) {
|
|
3301
|
-
if (this.queue.length >= this.config.maxQueueSize) {
|
|
3302
|
-
const lowPriorityIndex = this.queue.findIndex((op) => op.priority === 1);
|
|
3303
|
-
if (lowPriorityIndex !== -1) {
|
|
3304
|
-
this.queue.splice(lowPriorityIndex, 1);
|
|
3305
|
-
}
|
|
3306
|
-
else {
|
|
3307
|
-
throw new Error('Queue is full');
|
|
3308
|
-
}
|
|
3309
|
-
}
|
|
3310
|
-
const operation = {
|
|
3311
|
-
id: this.generateId(),
|
|
3312
|
-
type,
|
|
3313
|
-
resource,
|
|
3314
|
-
endpoint,
|
|
3315
|
-
method,
|
|
3316
|
-
data,
|
|
3317
|
-
status: 'pending',
|
|
3318
|
-
createdAt: Date.now(),
|
|
3319
|
-
updatedAt: Date.now(),
|
|
3320
|
-
retryCount: 0,
|
|
3321
|
-
maxRetries: this.config.maxRetries,
|
|
3322
|
-
priority,
|
|
3323
|
-
};
|
|
3324
|
-
const insertIndex = this.queue.findIndex((op) => op.priority < priority);
|
|
3325
|
-
if (insertIndex === -1) {
|
|
3326
|
-
this.queue.push(operation);
|
|
3327
|
-
}
|
|
3328
|
-
else {
|
|
3329
|
-
this.queue.splice(insertIndex, 0, operation);
|
|
3330
|
-
}
|
|
3331
|
-
await this.saveQueue();
|
|
3332
|
-
this.events.onOperationAdded?.(operation);
|
|
3333
|
-
return operation.id;
|
|
3334
|
-
}
|
|
3335
|
-
getPendingOperations() {
|
|
3336
|
-
return this.queue.filter((op) => op.status === 'pending' || op.status === 'failed');
|
|
3337
|
-
}
|
|
3338
|
-
getOperation(id) {
|
|
3339
|
-
return this.queue.find((op) => op.id === id);
|
|
3340
|
-
}
|
|
3341
|
-
async removeOperation(id) {
|
|
3342
|
-
const index = this.queue.findIndex((op) => op.id === id);
|
|
3343
|
-
if (index === -1)
|
|
3344
|
-
return false;
|
|
3345
|
-
this.queue.splice(index, 1);
|
|
3346
|
-
await this.saveQueue();
|
|
3347
|
-
return true;
|
|
3348
|
-
}
|
|
3349
|
-
async updateOperation(id, updates) {
|
|
3350
|
-
const operation = this.queue.find((op) => op.id === id);
|
|
3351
|
-
if (!operation)
|
|
3352
|
-
return false;
|
|
3353
|
-
Object.assign(operation, { ...updates, updatedAt: Date.now() });
|
|
3354
|
-
await this.saveQueue();
|
|
3355
|
-
return true;
|
|
3356
|
-
}
|
|
3357
|
-
getStats() {
|
|
3358
|
-
return {
|
|
3359
|
-
total: this.queue.length,
|
|
3360
|
-
pending: this.queue.filter((op) => op.status === 'pending').length,
|
|
3361
|
-
processing: this.queue.filter((op) => op.status === 'processing').length,
|
|
3362
|
-
completed: this.queue.filter((op) => op.status === 'completed').length,
|
|
3363
|
-
failed: this.queue.filter((op) => op.status === 'failed').length,
|
|
3364
|
-
};
|
|
3365
|
-
}
|
|
3366
|
-
async clearQueue() {
|
|
3367
|
-
this.queue = [];
|
|
3368
|
-
await this.saveQueue();
|
|
3369
|
-
}
|
|
3370
|
-
async clearCompleted() {
|
|
3371
|
-
this.queue = this.queue.filter((op) => op.status !== 'completed');
|
|
3372
|
-
await this.saveQueue();
|
|
3373
|
-
}
|
|
3374
|
-
async clearFailed() {
|
|
3375
|
-
this.queue = this.queue.filter((op) => op.status !== 'failed');
|
|
3376
|
-
await this.saveQueue();
|
|
3377
|
-
}
|
|
3378
|
-
async retryFailed() {
|
|
3379
|
-
for (const operation of this.queue.filter((op) => op.status === 'failed')) {
|
|
3380
|
-
if (operation.retryCount < operation.maxRetries) {
|
|
3381
|
-
operation.status = 'pending';
|
|
3382
|
-
operation.retryCount++;
|
|
3383
|
-
operation.updatedAt = Date.now();
|
|
3384
|
-
delete operation.error;
|
|
3385
|
-
}
|
|
3386
|
-
}
|
|
3387
|
-
await this.saveQueue();
|
|
3388
|
-
}
|
|
3389
|
-
getNextBatch() {
|
|
3390
|
-
return this.queue
|
|
3391
|
-
.filter((op) => op.status === 'pending')
|
|
3392
|
-
.sort((a, b) => b.priority - a.priority || a.createdAt - b.createdAt)
|
|
3393
|
-
.slice(0, this.config.batchSize);
|
|
3394
|
-
}
|
|
3395
|
-
isEmpty() {
|
|
3396
|
-
return this.getPendingOperations().length === 0;
|
|
3397
|
-
}
|
|
3398
|
-
startAutoSync() {
|
|
3399
|
-
if (this.syncIntervalId)
|
|
3400
|
-
return;
|
|
3401
|
-
this.syncIntervalId = setInterval(() => {
|
|
3402
|
-
if (!this.isEmpty() && !this.processing) {
|
|
3403
|
-
this.events.onQueueEmpty?.();
|
|
3404
|
-
}
|
|
3405
|
-
}, this.config.syncInterval);
|
|
3406
|
-
}
|
|
3407
|
-
stopAutoSync() {
|
|
3408
|
-
if (this.syncIntervalId) {
|
|
3409
|
-
clearInterval(this.syncIntervalId);
|
|
3410
|
-
this.syncIntervalId = undefined;
|
|
3411
|
-
}
|
|
3412
|
-
}
|
|
3413
|
-
setProcessing(value) {
|
|
3414
|
-
this.processing = value;
|
|
3415
|
-
}
|
|
3416
|
-
isCurrentlyProcessing() {
|
|
3417
|
-
return this.processing;
|
|
3418
|
-
}
|
|
3419
|
-
async loadQueue() {
|
|
3420
|
-
try {
|
|
3421
|
-
const queueData = await this.storage.get(OperationQueue.QUEUE_KEY);
|
|
3422
|
-
if (queueData) {
|
|
3423
|
-
this.queue = JSON.parse(queueData);
|
|
3424
|
-
this.queue.forEach((op) => {
|
|
3425
|
-
if (op.status === 'processing') {
|
|
3426
|
-
op.status = 'pending';
|
|
3427
|
-
}
|
|
3428
|
-
});
|
|
3429
|
-
}
|
|
3430
|
-
}
|
|
3431
|
-
catch {
|
|
3432
|
-
this.queue = [];
|
|
3433
|
-
}
|
|
3434
|
-
}
|
|
3435
|
-
async saveQueue() {
|
|
3436
|
-
try {
|
|
3437
|
-
await this.storage.set(OperationQueue.QUEUE_KEY, JSON.stringify(this.queue));
|
|
3438
|
-
}
|
|
3439
|
-
catch (error) {
|
|
3440
|
-
this.events.onError?.(new Error(`Failed to save queue: ${error}`));
|
|
3441
|
-
}
|
|
3442
|
-
}
|
|
3443
|
-
generateId() {
|
|
3444
|
-
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
3445
|
-
}
|
|
3446
|
-
destroy() {
|
|
3447
|
-
this.stopAutoSync();
|
|
3448
|
-
}
|
|
3449
|
-
}
|
|
3450
|
-
OperationQueue.QUEUE_KEY = 'acube_operation_queue';
|
|
3451
|
-
|
|
3452
|
-
class SyncManager {
|
|
3453
|
-
constructor(queue, httpPort, networkMonitor, config, events = {}) {
|
|
3454
|
-
this.queue = queue;
|
|
3455
|
-
this.httpPort = httpPort;
|
|
3456
|
-
this.networkMonitor = networkMonitor;
|
|
3457
|
-
this.config = config;
|
|
3458
|
-
this.events = events;
|
|
3459
|
-
this.isOnline = true;
|
|
3460
|
-
this.destroy$ = new Subject();
|
|
3461
|
-
this.setupNetworkMonitoring();
|
|
3462
|
-
}
|
|
3463
|
-
setupNetworkMonitoring() {
|
|
3464
|
-
// Subscribe to online$ to track current state
|
|
3465
|
-
this.networkSubscription = this.networkMonitor.online$
|
|
3466
|
-
.pipe(startWith(true), // Assume online initially
|
|
3467
|
-
pairwise(), filter(([wasOnline, isNowOnline]) => !wasOnline && isNowOnline), takeUntil(this.destroy$))
|
|
3468
|
-
.subscribe(() => {
|
|
3469
|
-
// Offline → Online transition detected
|
|
3470
|
-
this.syncPendingOperations();
|
|
3471
|
-
});
|
|
3472
|
-
// Track current online state
|
|
3473
|
-
this.networkMonitor.online$.pipe(takeUntil(this.destroy$)).subscribe((online) => {
|
|
3474
|
-
this.isOnline = online;
|
|
3475
|
-
});
|
|
3476
|
-
}
|
|
3477
|
-
async syncPendingOperations() {
|
|
3478
|
-
if (!this.isOnline) {
|
|
3479
|
-
throw new Error('Cannot sync while offline');
|
|
3480
|
-
}
|
|
3481
|
-
if (this.queue.isCurrentlyProcessing()) {
|
|
3482
|
-
throw new Error('Sync already in progress');
|
|
3483
|
-
}
|
|
3484
|
-
this.queue.setProcessing(true);
|
|
3485
|
-
try {
|
|
3486
|
-
const results = [];
|
|
3487
|
-
let successCount = 0;
|
|
3488
|
-
let failureCount = 0;
|
|
3489
|
-
while (!this.queue.isEmpty()) {
|
|
3490
|
-
const batch = this.queue.getNextBatch();
|
|
3491
|
-
if (batch.length === 0)
|
|
3492
|
-
break;
|
|
3493
|
-
const batchPromises = batch.map((operation) => this.processOperation(operation));
|
|
3494
|
-
const batchResults = await Promise.allSettled(batchPromises);
|
|
3495
|
-
batchResults.forEach((result, index) => {
|
|
3496
|
-
const operation = batch[index];
|
|
3497
|
-
if (!operation)
|
|
3498
|
-
return;
|
|
3499
|
-
if (result.status === 'fulfilled') {
|
|
3500
|
-
const syncResult = result.value;
|
|
3501
|
-
results.push(syncResult);
|
|
3502
|
-
if (syncResult.success) {
|
|
3503
|
-
successCount++;
|
|
3504
|
-
this.events.onOperationCompleted?.(syncResult);
|
|
3505
|
-
}
|
|
3506
|
-
else {
|
|
3507
|
-
failureCount++;
|
|
3508
|
-
this.events.onOperationFailed?.(syncResult);
|
|
3509
|
-
}
|
|
3510
|
-
}
|
|
3511
|
-
else {
|
|
3512
|
-
const syncResult = {
|
|
3513
|
-
operation,
|
|
3514
|
-
success: false,
|
|
3515
|
-
error: result.reason?.message || 'Unknown error',
|
|
3516
|
-
};
|
|
3517
|
-
results.push(syncResult);
|
|
3518
|
-
failureCount++;
|
|
3519
|
-
this.events.onOperationFailed?.(syncResult);
|
|
3520
|
-
this.queue.updateOperation(operation.id, {
|
|
3521
|
-
status: 'failed',
|
|
3522
|
-
error: syncResult.error,
|
|
3523
|
-
});
|
|
3524
|
-
}
|
|
3525
|
-
});
|
|
3526
|
-
if (!this.queue.isEmpty()) {
|
|
3527
|
-
await this.delay(500);
|
|
3528
|
-
}
|
|
3529
|
-
}
|
|
3530
|
-
const batchResult = {
|
|
3531
|
-
totalOperations: results.length,
|
|
3532
|
-
successCount,
|
|
3533
|
-
failureCount,
|
|
3534
|
-
results,
|
|
3535
|
-
};
|
|
3536
|
-
this.events.onBatchSyncCompleted?.(batchResult);
|
|
3537
|
-
if (this.queue.isEmpty()) {
|
|
3538
|
-
this.events.onQueueEmpty?.();
|
|
3539
|
-
}
|
|
3540
|
-
return batchResult;
|
|
3541
|
-
}
|
|
3542
|
-
finally {
|
|
3543
|
-
this.queue.setProcessing(false);
|
|
3544
|
-
}
|
|
3545
|
-
}
|
|
3546
|
-
async processOperation(operation) {
|
|
3547
|
-
await this.queue.updateOperation(operation.id, { status: 'processing' });
|
|
3548
|
-
try {
|
|
3549
|
-
const response = await this.executeOperation(operation);
|
|
3550
|
-
await this.queue.updateOperation(operation.id, { status: 'completed' });
|
|
3551
|
-
return { operation, success: true, response };
|
|
3552
|
-
}
|
|
3553
|
-
catch (error) {
|
|
3554
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
3555
|
-
if (operation.retryCount < operation.maxRetries && this.isRetryableError(error)) {
|
|
3556
|
-
const delay = this.calculateRetryDelay(operation.retryCount);
|
|
3557
|
-
await this.queue.updateOperation(operation.id, {
|
|
3558
|
-
status: 'pending',
|
|
3559
|
-
retryCount: operation.retryCount + 1,
|
|
3560
|
-
error: errorMessage,
|
|
3561
|
-
});
|
|
3562
|
-
setTimeout(() => {
|
|
3563
|
-
if (this.isOnline && !this.queue.isCurrentlyProcessing()) {
|
|
3564
|
-
this.syncPendingOperations();
|
|
3565
|
-
}
|
|
3566
|
-
}, delay);
|
|
3567
|
-
return { operation, success: false, error: `Retrying: ${errorMessage}` };
|
|
3568
|
-
}
|
|
3569
|
-
else {
|
|
3570
|
-
await this.queue.updateOperation(operation.id, {
|
|
3571
|
-
status: 'failed',
|
|
3572
|
-
error: errorMessage,
|
|
3573
|
-
});
|
|
3574
|
-
return { operation, success: false, error: errorMessage };
|
|
3575
|
-
}
|
|
3576
|
-
}
|
|
3577
|
-
}
|
|
3578
|
-
async executeOperation(operation) {
|
|
3579
|
-
const { method, endpoint, data, headers } = operation;
|
|
3580
|
-
const config = headers ? { headers } : undefined;
|
|
3581
|
-
switch (method) {
|
|
3582
|
-
case 'GET':
|
|
3583
|
-
return (await this.httpPort.get(endpoint, config)).data;
|
|
3584
|
-
case 'POST':
|
|
3585
|
-
return (await this.httpPort.post(endpoint, data, config)).data;
|
|
3586
|
-
case 'PUT':
|
|
3587
|
-
return (await this.httpPort.put(endpoint, data, config)).data;
|
|
3588
|
-
case 'PATCH':
|
|
3589
|
-
return (await this.httpPort.patch(endpoint, data, config)).data;
|
|
3590
|
-
case 'DELETE':
|
|
3591
|
-
return (await this.httpPort.delete(endpoint, config)).data;
|
|
3592
|
-
default:
|
|
3593
|
-
throw new Error(`Unsupported HTTP method: ${method}`);
|
|
3594
|
-
}
|
|
3595
|
-
}
|
|
3596
|
-
isRetryableError(error) {
|
|
3597
|
-
const errorObj = error;
|
|
3598
|
-
if (errorObj.code === 'NETWORK_ERROR')
|
|
3599
|
-
return true;
|
|
3600
|
-
if (errorObj.statusCode && errorObj.statusCode >= 500)
|
|
3601
|
-
return true;
|
|
3602
|
-
if (errorObj.statusCode === 429)
|
|
3603
|
-
return true;
|
|
3604
|
-
const errorMessage = error?.message;
|
|
3605
|
-
if (errorObj.code === 'ECONNABORTED' || errorMessage?.includes('timeout'))
|
|
3606
|
-
return true;
|
|
3607
|
-
return false;
|
|
3608
|
-
}
|
|
3609
|
-
calculateRetryDelay(retryCount) {
|
|
3610
|
-
const delay = this.config.retryDelay * Math.pow(this.config.backoffMultiplier, retryCount);
|
|
3611
|
-
return Math.min(delay, this.config.maxRetryDelay);
|
|
3612
|
-
}
|
|
3613
|
-
delay(ms) {
|
|
3614
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3615
|
-
}
|
|
3616
|
-
isCurrentlyOnline() {
|
|
3617
|
-
return this.isOnline;
|
|
3618
|
-
}
|
|
3619
|
-
async triggerSync() {
|
|
3620
|
-
if (!this.isOnline)
|
|
3621
|
-
return null;
|
|
3622
|
-
if (this.queue.isEmpty()) {
|
|
3623
|
-
return { totalOperations: 0, successCount: 0, failureCount: 0, results: [] };
|
|
3624
|
-
}
|
|
3625
|
-
return await this.syncPendingOperations();
|
|
3626
|
-
}
|
|
3627
|
-
getSyncStatus() {
|
|
3628
|
-
return {
|
|
3629
|
-
isOnline: this.isOnline,
|
|
3630
|
-
isProcessing: this.queue.isCurrentlyProcessing(),
|
|
3631
|
-
queueStats: this.queue.getStats(),
|
|
3632
|
-
};
|
|
3633
|
-
}
|
|
3634
|
-
destroy() {
|
|
3635
|
-
this.destroy$.next();
|
|
3636
|
-
this.destroy$.complete();
|
|
3637
|
-
this.networkSubscription?.unsubscribe();
|
|
3638
|
-
}
|
|
3639
|
-
}
|
|
3640
|
-
|
|
3641
|
-
class OfflineManager {
|
|
3642
|
-
get queue$() {
|
|
3643
|
-
return this.queueSubject.asObservable();
|
|
3644
|
-
}
|
|
3645
|
-
get syncStatus$() {
|
|
3646
|
-
return this.syncStatusSubject.asObservable();
|
|
3647
|
-
}
|
|
3648
|
-
constructor(storage, httpPort, networkMonitor, config = {}, events = {}, _cache) {
|
|
3649
|
-
this.queueSubject = new BehaviorSubject([]);
|
|
3650
|
-
this.syncStatusSubject = new BehaviorSubject({
|
|
3651
|
-
isOnline: true,
|
|
3652
|
-
isProcessing: false,
|
|
3653
|
-
queueStats: { total: 0, pending: 0, processing: 0, completed: 0, failed: 0 },
|
|
3654
|
-
});
|
|
3655
|
-
this.destroy$ = new Subject();
|
|
3656
|
-
const finalConfig = { ...DEFAULT_QUEUE_CONFIG, ...config };
|
|
3657
|
-
const wrappedEvents = {
|
|
3658
|
-
...events,
|
|
3659
|
-
onOperationAdded: (op) => {
|
|
3660
|
-
this.updateQueueState();
|
|
3661
|
-
events.onOperationAdded?.(op);
|
|
3662
|
-
},
|
|
3663
|
-
onOperationCompleted: (result) => {
|
|
3664
|
-
this.updateQueueState();
|
|
3665
|
-
events.onOperationCompleted?.(result);
|
|
3666
|
-
},
|
|
3667
|
-
onOperationFailed: (result) => {
|
|
3668
|
-
this.updateQueueState();
|
|
3669
|
-
events.onOperationFailed?.(result);
|
|
3670
|
-
},
|
|
3671
|
-
onBatchSyncCompleted: (result) => {
|
|
3672
|
-
this.updateQueueState();
|
|
3673
|
-
events.onBatchSyncCompleted?.(result);
|
|
3674
|
-
},
|
|
3675
|
-
};
|
|
3676
|
-
this.queue = new OperationQueue(storage, finalConfig, wrappedEvents);
|
|
3677
|
-
this.syncManager = new SyncManager(this.queue, httpPort, networkMonitor, finalConfig, wrappedEvents);
|
|
3678
|
-
this.updateQueueState();
|
|
3679
|
-
}
|
|
3680
|
-
updateQueueState() {
|
|
3681
|
-
this.queueSubject.next(this.queue.getPendingOperations());
|
|
3682
|
-
this.syncStatusSubject.next(this.syncManager.getSyncStatus());
|
|
3683
|
-
}
|
|
3684
|
-
async queueOperation(type, resource, endpoint, method, data, priority = 1) {
|
|
3685
|
-
const id = await this.queue.addOperation(type, resource, endpoint, method, data, priority);
|
|
3686
|
-
this.updateQueueState();
|
|
3687
|
-
return id;
|
|
3688
|
-
}
|
|
3689
|
-
async queueReceiptCreation(receiptData, priority = 2) {
|
|
3690
|
-
return await this.queueOperation('CREATE', 'receipt', '/mf1/receipts', 'POST', receiptData, priority);
|
|
3691
|
-
}
|
|
3692
|
-
async queueReceiptVoid(voidData, priority = 3) {
|
|
3693
|
-
return await this.queueOperation('DELETE', 'receipt', '/mf1/receipts', 'DELETE', voidData, priority);
|
|
3694
|
-
}
|
|
3695
|
-
async queueReceiptReturn(returnData, priority = 3) {
|
|
3696
|
-
return await this.queueOperation('CREATE', 'receipt', '/mf1/receipts/return', 'POST', returnData, priority);
|
|
3697
|
-
}
|
|
3698
|
-
async queueCashierCreation(cashierData, priority = 1) {
|
|
3699
|
-
return await this.queueOperation('CREATE', 'cashier', '/mf1/cashiers', 'POST', cashierData, priority);
|
|
3700
|
-
}
|
|
3701
|
-
isOnline() {
|
|
3702
|
-
return this.syncManager.isCurrentlyOnline();
|
|
3703
|
-
}
|
|
3704
|
-
getStatus() {
|
|
3705
|
-
return this.syncManager.getSyncStatus();
|
|
3706
|
-
}
|
|
3707
|
-
getPendingCount() {
|
|
3708
|
-
return this.queue.getPendingOperations().length;
|
|
3709
|
-
}
|
|
3710
|
-
isEmpty() {
|
|
3711
|
-
return this.queue.isEmpty();
|
|
3712
|
-
}
|
|
3713
|
-
async sync() {
|
|
3714
|
-
const result = await this.syncManager.triggerSync();
|
|
3715
|
-
this.updateQueueState();
|
|
3716
|
-
return result;
|
|
3717
|
-
}
|
|
3718
|
-
async retryFailed() {
|
|
3719
|
-
await this.queue.retryFailed();
|
|
3720
|
-
this.updateQueueState();
|
|
3721
|
-
if (this.isOnline()) {
|
|
3722
|
-
await this.sync();
|
|
3723
|
-
}
|
|
3724
|
-
}
|
|
3725
|
-
async clearCompleted() {
|
|
3726
|
-
await this.queue.clearCompleted();
|
|
3727
|
-
this.updateQueueState();
|
|
3728
|
-
}
|
|
3729
|
-
async clearFailed() {
|
|
3730
|
-
await this.queue.clearFailed();
|
|
3731
|
-
this.updateQueueState();
|
|
3732
|
-
}
|
|
3733
|
-
async clearAll() {
|
|
3734
|
-
await this.queue.clearQueue();
|
|
3735
|
-
this.updateQueueState();
|
|
3736
|
-
}
|
|
3737
|
-
getOperation(id) {
|
|
3738
|
-
return this.queue.getOperation(id);
|
|
3739
|
-
}
|
|
3740
|
-
async removeOperation(id) {
|
|
3741
|
-
const result = await this.queue.removeOperation(id);
|
|
3742
|
-
this.updateQueueState();
|
|
3743
|
-
return result;
|
|
3744
|
-
}
|
|
3745
|
-
getQueueStats() {
|
|
3746
|
-
return this.queue.getStats();
|
|
3747
|
-
}
|
|
3748
|
-
startAutoSync() {
|
|
3749
|
-
this.queue.startAutoSync();
|
|
3750
|
-
}
|
|
3751
|
-
stopAutoSync() {
|
|
3752
|
-
this.queue.stopAutoSync();
|
|
3753
|
-
}
|
|
3754
|
-
destroy() {
|
|
3755
|
-
this.destroy$.next();
|
|
3756
|
-
this.destroy$.complete();
|
|
3757
|
-
this.queue.destroy();
|
|
3758
|
-
this.syncManager.destroy();
|
|
3759
|
-
}
|
|
3760
|
-
}
|
|
3761
|
-
|
|
3762
|
-
class CompressionAdapter {
|
|
3763
|
-
compress(data, threshold = 1024) {
|
|
3764
|
-
const originalSize = data.length * 2;
|
|
3765
|
-
if (originalSize < threshold) {
|
|
3766
|
-
return {
|
|
3767
|
-
data,
|
|
3768
|
-
compressed: false,
|
|
3769
|
-
originalSize,
|
|
3770
|
-
compressedSize: originalSize,
|
|
3771
|
-
};
|
|
3772
|
-
}
|
|
3773
|
-
try {
|
|
3774
|
-
const compressed = this.compressString(data);
|
|
3775
|
-
const compressedSize = compressed.length * 2;
|
|
3776
|
-
if (compressedSize < originalSize) {
|
|
3777
|
-
return {
|
|
3778
|
-
data: compressed,
|
|
3779
|
-
compressed: true,
|
|
3780
|
-
originalSize,
|
|
3781
|
-
compressedSize,
|
|
3782
|
-
};
|
|
3783
|
-
}
|
|
3784
|
-
return {
|
|
3785
|
-
data,
|
|
3786
|
-
compressed: false,
|
|
3787
|
-
originalSize,
|
|
3788
|
-
compressedSize: originalSize,
|
|
3789
|
-
};
|
|
3790
|
-
}
|
|
3791
|
-
catch {
|
|
3792
|
-
return {
|
|
3793
|
-
data,
|
|
3794
|
-
compressed: false,
|
|
3795
|
-
originalSize,
|
|
3796
|
-
compressedSize: originalSize,
|
|
3797
|
-
};
|
|
3798
|
-
}
|
|
3799
|
-
}
|
|
3800
|
-
decompress(data, compressed) {
|
|
3801
|
-
if (!compressed) {
|
|
3802
|
-
return { data, wasCompressed: false };
|
|
3803
|
-
}
|
|
3804
|
-
try {
|
|
3805
|
-
const decompressed = this.decompressString(data);
|
|
3806
|
-
return { data: decompressed, wasCompressed: true };
|
|
3807
|
-
}
|
|
3808
|
-
catch {
|
|
3809
|
-
return { data, wasCompressed: false };
|
|
3810
|
-
}
|
|
3811
|
-
}
|
|
3812
|
-
estimateSavings(data) {
|
|
3813
|
-
const repeated = data.match(/(.)\1{3,}/g);
|
|
3814
|
-
if (!repeated)
|
|
3815
|
-
return 0;
|
|
3816
|
-
let savings = 0;
|
|
3817
|
-
for (const match of repeated) {
|
|
3818
|
-
const originalBytes = match.length * 2;
|
|
3819
|
-
const compressedBytes = 6;
|
|
3820
|
-
if (originalBytes > compressedBytes) {
|
|
3821
|
-
savings += originalBytes - compressedBytes;
|
|
3822
|
-
}
|
|
3823
|
-
}
|
|
3824
|
-
return Math.min(savings, data.length * 2 * 0.5);
|
|
3825
|
-
}
|
|
3826
|
-
compressString(input) {
|
|
3827
|
-
let compressed = '';
|
|
3828
|
-
let i = 0;
|
|
3829
|
-
while (i < input.length) {
|
|
3830
|
-
let count = 1;
|
|
3831
|
-
const char = input[i];
|
|
3832
|
-
while (i + count < input.length && input[i + count] === char && count < 255) {
|
|
3833
|
-
count++;
|
|
3834
|
-
}
|
|
3835
|
-
if (count > 3) {
|
|
3836
|
-
compressed += `~${count}${char}`;
|
|
3837
|
-
}
|
|
3838
|
-
else {
|
|
3839
|
-
for (let j = 0; j < count; j++) {
|
|
3840
|
-
compressed += char;
|
|
3841
|
-
}
|
|
3842
|
-
}
|
|
3843
|
-
i += count;
|
|
3844
|
-
}
|
|
3845
|
-
return `COMP:${btoa(compressed)}`;
|
|
3846
|
-
}
|
|
3847
|
-
decompressString(input) {
|
|
3848
|
-
if (!input.startsWith('COMP:')) {
|
|
3849
|
-
return input;
|
|
3850
|
-
}
|
|
3851
|
-
const encodedData = input.substring(5);
|
|
3852
|
-
if (!encodedData) {
|
|
3853
|
-
return input;
|
|
3854
|
-
}
|
|
3855
|
-
const compressed = atob(encodedData);
|
|
3856
|
-
let decompressed = '';
|
|
3857
|
-
let i = 0;
|
|
3858
|
-
while (i < compressed.length) {
|
|
3859
|
-
if (compressed[i] === '~' && i + 2 < compressed.length) {
|
|
3860
|
-
let countStr = '';
|
|
3861
|
-
i++;
|
|
3862
|
-
while (i < compressed.length) {
|
|
3863
|
-
const char = compressed[i];
|
|
3864
|
-
if (char && /\d/.test(char)) {
|
|
3865
|
-
countStr += char;
|
|
3866
|
-
i++;
|
|
3867
|
-
}
|
|
3868
|
-
else {
|
|
3869
|
-
break;
|
|
3870
|
-
}
|
|
3871
|
-
}
|
|
3872
|
-
if (countStr && i < compressed.length) {
|
|
3873
|
-
const count = parseInt(countStr, 10);
|
|
3874
|
-
const char = compressed[i];
|
|
3875
|
-
for (let j = 0; j < count; j++) {
|
|
3876
|
-
decompressed += char;
|
|
3877
|
-
}
|
|
3878
|
-
i++;
|
|
3879
|
-
}
|
|
3880
|
-
}
|
|
3881
|
-
else {
|
|
3882
|
-
decompressed += compressed[i];
|
|
3883
|
-
i++;
|
|
3884
|
-
}
|
|
3885
|
-
}
|
|
3886
|
-
return decompressed;
|
|
3887
|
-
}
|
|
3888
|
-
}
|
|
3889
|
-
function compressData(data, threshold = 1024) {
|
|
3890
|
-
return new CompressionAdapter().compress(data, threshold);
|
|
3891
|
-
}
|
|
3892
|
-
function decompressData(data, compressed) {
|
|
3893
|
-
return new CompressionAdapter().decompress(data, compressed);
|
|
3894
|
-
}
|
|
3895
|
-
|
|
3896
|
-
const log$d = createPrefixedLogger('CACHE-RN');
|
|
3897
|
-
/**
|
|
3898
|
-
* React Native cache adapter using SQLite (Expo or react-native-sqlite-storage)
|
|
3899
|
-
* Cache never expires - data persists until explicitly invalidated
|
|
3900
|
-
*/
|
|
3901
|
-
class ReactNativeCacheAdapter {
|
|
3902
|
-
constructor(options = {}) {
|
|
3903
|
-
this.db = null;
|
|
3904
|
-
this.initPromise = null;
|
|
3905
|
-
this.isExpo = false;
|
|
3906
|
-
this.hasCompressedColumn = false;
|
|
3907
|
-
this.options = {
|
|
3908
|
-
maxSize: 50 * 1024 * 1024, // 50MB
|
|
3909
|
-
maxEntries: 10000,
|
|
3910
|
-
compression: false,
|
|
3911
|
-
compressionThreshold: 1024,
|
|
3912
|
-
...options,
|
|
3913
|
-
};
|
|
3914
|
-
this.initPromise = this.initialize();
|
|
3915
|
-
}
|
|
3916
|
-
normalizeResults(results) {
|
|
3917
|
-
if (this.isExpo) {
|
|
3918
|
-
const expoResults = results;
|
|
3919
|
-
if (Array.isArray(expoResults)) {
|
|
3920
|
-
return expoResults;
|
|
3921
|
-
}
|
|
3922
|
-
return expoResults.results || [];
|
|
3923
|
-
}
|
|
3924
|
-
else {
|
|
3925
|
-
const rnResults = results;
|
|
3926
|
-
const rows = rnResults.rows;
|
|
3927
|
-
if (!rows || rows.length === 0)
|
|
3928
|
-
return [];
|
|
3929
|
-
const normalizedRows = [];
|
|
3930
|
-
for (let i = 0; i < rows.length; i++) {
|
|
3931
|
-
normalizedRows.push(rows.item(i));
|
|
3932
|
-
}
|
|
3933
|
-
return normalizedRows;
|
|
3934
|
-
}
|
|
3935
|
-
}
|
|
3936
|
-
async initialize() {
|
|
3937
|
-
if (this.db)
|
|
3938
|
-
return;
|
|
3939
|
-
try {
|
|
3940
|
-
// Try Expo SQLite first
|
|
3941
|
-
const ExpoSQLite = require('expo-sqlite');
|
|
3942
|
-
this.db = await ExpoSQLite.openDatabaseAsync(ReactNativeCacheAdapter.DB_NAME);
|
|
3943
|
-
this.isExpo = true;
|
|
3944
|
-
await this.createTables();
|
|
3945
|
-
}
|
|
3946
|
-
catch (expoError) {
|
|
3947
|
-
try {
|
|
3948
|
-
// Fallback to react-native-sqlite-storage
|
|
3949
|
-
const SQLite = require('react-native-sqlite-storage');
|
|
3950
|
-
this.db = await new Promise((resolve, reject) => {
|
|
3951
|
-
SQLite.openDatabase({
|
|
3952
|
-
name: ReactNativeCacheAdapter.DB_NAME,
|
|
3953
|
-
location: 'default',
|
|
3954
|
-
}, resolve, reject);
|
|
3955
|
-
});
|
|
3956
|
-
this.isExpo = false;
|
|
3957
|
-
await this.createTables();
|
|
3958
|
-
}
|
|
3959
|
-
catch (rnError) {
|
|
3960
|
-
throw new Error(`Failed to initialize SQLite: Expo error: ${expoError}, RN error: ${rnError}`);
|
|
3961
|
-
}
|
|
3962
|
-
}
|
|
3963
|
-
}
|
|
3964
|
-
async createTables() {
|
|
3965
|
-
// Create table with simplified schema (no TTL)
|
|
3966
|
-
const createTableSQL = `
|
|
3967
|
-
CREATE TABLE IF NOT EXISTS ${ReactNativeCacheAdapter.TABLE_NAME} (
|
|
3968
|
-
cache_key TEXT PRIMARY KEY,
|
|
3969
|
-
data TEXT NOT NULL,
|
|
3970
|
-
timestamp INTEGER NOT NULL
|
|
3971
|
-
);
|
|
3972
|
-
|
|
3973
|
-
CREATE INDEX IF NOT EXISTS idx_timestamp ON ${ReactNativeCacheAdapter.TABLE_NAME}(timestamp);
|
|
3974
|
-
`;
|
|
3975
|
-
await this.executeSql(createTableSQL);
|
|
3976
|
-
// Then, run migrations to add new columns if they don't exist
|
|
3977
|
-
await this.runMigrations();
|
|
3978
|
-
}
|
|
3979
|
-
async runMigrations() {
|
|
3980
|
-
log$d.debug('Running database migrations...');
|
|
3981
|
-
try {
|
|
3982
|
-
// Check if compressed column exists
|
|
3983
|
-
this.hasCompressedColumn = await this.checkColumnExists('compressed');
|
|
3984
|
-
if (!this.hasCompressedColumn) {
|
|
3985
|
-
log$d.debug('Adding compressed column to cache table');
|
|
3986
|
-
const addColumnSQL = `ALTER TABLE ${ReactNativeCacheAdapter.TABLE_NAME} ADD COLUMN compressed INTEGER DEFAULT 0`;
|
|
3987
|
-
await this.executeSql(addColumnSQL);
|
|
3988
|
-
this.hasCompressedColumn = true;
|
|
3989
|
-
log$d.debug('Successfully added compressed column');
|
|
3990
|
-
}
|
|
3991
|
-
else {
|
|
3992
|
-
log$d.debug('Compressed column already exists');
|
|
3993
|
-
}
|
|
3994
|
-
log$d.debug('Database migrations completed', {
|
|
3995
|
-
hasCompressedColumn: this.hasCompressedColumn,
|
|
3996
|
-
});
|
|
3997
|
-
}
|
|
3998
|
-
catch (error) {
|
|
3999
|
-
log$d.debug('Migration failed, disabling compression features', error);
|
|
4000
|
-
this.hasCompressedColumn = false;
|
|
4001
|
-
// Don't throw - allow the app to continue even if migration fails
|
|
4002
|
-
// The compressed feature will just be disabled
|
|
4003
|
-
}
|
|
4004
|
-
}
|
|
4005
|
-
async checkColumnExists(columnName) {
|
|
4006
|
-
try {
|
|
4007
|
-
const pragmaSQL = `PRAGMA table_info(${ReactNativeCacheAdapter.TABLE_NAME})`;
|
|
4008
|
-
const results = await this.executeSql(pragmaSQL);
|
|
4009
|
-
const columns = this.normalizeResults(results);
|
|
4010
|
-
log$d.debug('Table columns found', { columns: columns.map((c) => c.name) });
|
|
4011
|
-
return columns.some((column) => column.name === columnName);
|
|
4012
|
-
}
|
|
4013
|
-
catch (error) {
|
|
4014
|
-
log$d.debug('Error checking column existence', error);
|
|
4015
|
-
return false;
|
|
4016
|
-
}
|
|
4017
|
-
}
|
|
4018
|
-
async get(key) {
|
|
4019
|
-
await this.ensureInitialized();
|
|
4020
|
-
const sql = `SELECT * FROM ${ReactNativeCacheAdapter.TABLE_NAME} WHERE cache_key = ?`;
|
|
4021
|
-
log$d.debug('Executing get query', { sql, key });
|
|
4022
|
-
const results = await this.executeSql(sql, [key]);
|
|
4023
|
-
log$d.debug('Get query results', { key, hasResults: !!results });
|
|
4024
|
-
// Normalize results from different SQLite implementations
|
|
4025
|
-
const rows = this.normalizeResults(results);
|
|
4026
|
-
if (!rows || rows.length === 0) {
|
|
4027
|
-
return null;
|
|
4028
|
-
}
|
|
4029
|
-
const row = rows[0];
|
|
4030
|
-
if (!row) {
|
|
4031
|
-
return null;
|
|
4032
|
-
}
|
|
4033
|
-
const isCompressed = this.hasCompressedColumn ? !!row.compressed : false;
|
|
4034
|
-
const rawData = isCompressed ? decompressData(row.data, true).data : row.data;
|
|
4035
|
-
return {
|
|
4036
|
-
data: JSON.parse(rawData),
|
|
4037
|
-
timestamp: row.timestamp,
|
|
4038
|
-
compressed: isCompressed,
|
|
4039
|
-
};
|
|
4040
|
-
}
|
|
4041
|
-
async set(key, data) {
|
|
4042
|
-
const item = {
|
|
4043
|
-
data,
|
|
4044
|
-
timestamp: Date.now(),
|
|
4045
|
-
};
|
|
4046
|
-
log$d.debug('Setting cache item', { key });
|
|
4047
|
-
return this.setItem(key, item);
|
|
4048
|
-
}
|
|
4049
|
-
async setItem(key, item) {
|
|
4050
|
-
await this.ensureInitialized();
|
|
4051
|
-
// Handle compression if enabled and compressed column is available
|
|
4052
|
-
const serializedData = JSON.stringify(item.data);
|
|
4053
|
-
let finalData = serializedData;
|
|
4054
|
-
let isCompressed = false;
|
|
4055
|
-
if (this.options.compression && this.options.compressionThreshold && this.hasCompressedColumn) {
|
|
4056
|
-
const compressionResult = compressData(serializedData, this.options.compressionThreshold);
|
|
4057
|
-
finalData = compressionResult.data;
|
|
4058
|
-
isCompressed = compressionResult.compressed;
|
|
4059
|
-
log$d.debug('Compression result', {
|
|
4060
|
-
key,
|
|
4061
|
-
originalSize: compressionResult.originalSize,
|
|
4062
|
-
compressedSize: compressionResult.compressedSize,
|
|
4063
|
-
compressed: isCompressed,
|
|
4064
|
-
savings: compressionResult.originalSize - compressionResult.compressedSize,
|
|
4065
|
-
});
|
|
4066
|
-
}
|
|
4067
|
-
log$d.debug('Setting item with metadata', {
|
|
4068
|
-
key,
|
|
4069
|
-
timestamp: item.timestamp,
|
|
4070
|
-
compressed: isCompressed,
|
|
4071
|
-
hasCompressedColumn: this.hasCompressedColumn,
|
|
4072
|
-
});
|
|
4073
|
-
// Build SQL and parameters based on available columns
|
|
4074
|
-
let sql;
|
|
4075
|
-
let params;
|
|
4076
|
-
if (this.hasCompressedColumn) {
|
|
4077
|
-
sql = `
|
|
4078
|
-
INSERT OR REPLACE INTO ${ReactNativeCacheAdapter.TABLE_NAME}
|
|
4079
|
-
(cache_key, data, timestamp, compressed)
|
|
4080
|
-
VALUES (?, ?, ?, ?)
|
|
4081
|
-
`;
|
|
4082
|
-
params = [key, finalData, item.timestamp, isCompressed ? 1 : 0];
|
|
4083
|
-
}
|
|
4084
|
-
else {
|
|
4085
|
-
// Fallback for databases without compressed column
|
|
4086
|
-
sql = `
|
|
4087
|
-
INSERT OR REPLACE INTO ${ReactNativeCacheAdapter.TABLE_NAME}
|
|
4088
|
-
(cache_key, data, timestamp)
|
|
4089
|
-
VALUES (?, ?, ?)
|
|
4090
|
-
`;
|
|
4091
|
-
params = [key, finalData, item.timestamp];
|
|
4092
|
-
}
|
|
4093
|
-
log$d.debug('Executing setItem SQL', { key, paramsCount: params.length });
|
|
4094
|
-
await this.executeSql(sql, params);
|
|
4095
|
-
}
|
|
4096
|
-
async setBatch(items) {
|
|
4097
|
-
if (items.length === 0)
|
|
4098
|
-
return;
|
|
4099
|
-
await this.ensureInitialized();
|
|
4100
|
-
log$d.debug('Batch setting items', { count: items.length });
|
|
4101
|
-
if (this.isExpo) {
|
|
4102
|
-
await this.db.withTransactionAsync(async () => {
|
|
4103
|
-
for (const [key, item] of items) {
|
|
4104
|
-
await this.setBatchItem(key, item);
|
|
4105
|
-
}
|
|
4106
|
-
});
|
|
4107
|
-
}
|
|
4108
|
-
else {
|
|
4109
|
-
return new Promise((resolve, reject) => {
|
|
4110
|
-
this.db.transaction((tx) => {
|
|
4111
|
-
const promises = items.map(([key, item]) => this.setBatchItemRN(tx, key, item));
|
|
4112
|
-
Promise.all(promises)
|
|
4113
|
-
.then(() => resolve())
|
|
4114
|
-
.catch(reject);
|
|
4115
|
-
}, reject, () => resolve());
|
|
4116
|
-
});
|
|
4117
|
-
}
|
|
4118
|
-
log$d.debug('Batch operation completed', { count: items.length });
|
|
4119
|
-
}
|
|
4120
|
-
async setBatchItem(key, item) {
|
|
4121
|
-
// Handle compression if enabled and compressed column is available
|
|
4122
|
-
const serializedData = JSON.stringify(item.data);
|
|
4123
|
-
let finalData = serializedData;
|
|
4124
|
-
let isCompressed = false;
|
|
4125
|
-
if (this.options.compression && this.options.compressionThreshold && this.hasCompressedColumn) {
|
|
4126
|
-
const compressionResult = compressData(serializedData, this.options.compressionThreshold);
|
|
4127
|
-
finalData = compressionResult.data;
|
|
4128
|
-
isCompressed = compressionResult.compressed;
|
|
4129
|
-
}
|
|
4130
|
-
// Build SQL and parameters based on available columns
|
|
4131
|
-
let sql;
|
|
4132
|
-
let params;
|
|
4133
|
-
if (this.hasCompressedColumn) {
|
|
4134
|
-
sql = `
|
|
4135
|
-
INSERT OR REPLACE INTO ${ReactNativeCacheAdapter.TABLE_NAME}
|
|
4136
|
-
(cache_key, data, timestamp, compressed)
|
|
4137
|
-
VALUES (?, ?, ?, ?)
|
|
4138
|
-
`;
|
|
4139
|
-
params = [key, finalData, item.timestamp, isCompressed ? 1 : 0];
|
|
4140
|
-
}
|
|
4141
|
-
else {
|
|
4142
|
-
sql = `
|
|
4143
|
-
INSERT OR REPLACE INTO ${ReactNativeCacheAdapter.TABLE_NAME}
|
|
4144
|
-
(cache_key, data, timestamp)
|
|
4145
|
-
VALUES (?, ?, ?)
|
|
4146
|
-
`;
|
|
4147
|
-
params = [key, finalData, item.timestamp];
|
|
4148
|
-
}
|
|
4149
|
-
await this.db.runAsync(sql, params);
|
|
4150
|
-
}
|
|
4151
|
-
async setBatchItemRN(tx, key, item) {
|
|
4152
|
-
// Handle compression if enabled and compressed column is available
|
|
4153
|
-
const serializedData = JSON.stringify(item.data);
|
|
4154
|
-
let finalData = serializedData;
|
|
4155
|
-
let isCompressed = false;
|
|
4156
|
-
if (this.options.compression && this.options.compressionThreshold && this.hasCompressedColumn) {
|
|
4157
|
-
const compressionResult = compressData(serializedData, this.options.compressionThreshold);
|
|
4158
|
-
finalData = compressionResult.data;
|
|
4159
|
-
isCompressed = compressionResult.compressed;
|
|
4160
|
-
}
|
|
4161
|
-
// Build SQL and parameters based on available columns
|
|
4162
|
-
let sql;
|
|
4163
|
-
let params;
|
|
4164
|
-
if (this.hasCompressedColumn) {
|
|
4165
|
-
sql = `
|
|
4166
|
-
INSERT OR REPLACE INTO ${ReactNativeCacheAdapter.TABLE_NAME}
|
|
4167
|
-
(cache_key, data, timestamp, compressed)
|
|
4168
|
-
VALUES (?, ?, ?, ?)
|
|
4169
|
-
`;
|
|
4170
|
-
params = [key, finalData, item.timestamp, isCompressed ? 1 : 0];
|
|
4171
|
-
}
|
|
4172
|
-
else {
|
|
4173
|
-
sql = `
|
|
4174
|
-
INSERT OR REPLACE INTO ${ReactNativeCacheAdapter.TABLE_NAME}
|
|
4175
|
-
(cache_key, data, timestamp)
|
|
4176
|
-
VALUES (?, ?, ?)
|
|
4177
|
-
`;
|
|
4178
|
-
params = [key, finalData, item.timestamp];
|
|
4179
|
-
}
|
|
4180
|
-
return new Promise((resolve, reject) => {
|
|
4181
|
-
tx.executeSql(sql, params, () => resolve(), (_, error) => {
|
|
4182
|
-
reject(error);
|
|
4183
|
-
return false;
|
|
4184
|
-
});
|
|
4185
|
-
});
|
|
4186
|
-
}
|
|
4187
|
-
async invalidate(pattern) {
|
|
4188
|
-
await this.ensureInitialized();
|
|
4189
|
-
const keys = await this.getKeys(pattern);
|
|
4190
|
-
if (keys.length === 0)
|
|
4191
|
-
return;
|
|
4192
|
-
const placeholders = keys.map(() => '?').join(',');
|
|
4193
|
-
const sql = `DELETE FROM ${ReactNativeCacheAdapter.TABLE_NAME} WHERE cache_key IN (${placeholders})`;
|
|
4194
|
-
await this.executeSql(sql, keys);
|
|
4195
|
-
}
|
|
4196
|
-
async clear() {
|
|
4197
|
-
await this.ensureInitialized();
|
|
4198
|
-
const sql = `DELETE FROM ${ReactNativeCacheAdapter.TABLE_NAME}`;
|
|
4199
|
-
await this.executeSql(sql);
|
|
4200
|
-
}
|
|
4201
|
-
async getSize() {
|
|
4202
|
-
await this.ensureInitialized();
|
|
4203
|
-
const sql = `
|
|
4204
|
-
SELECT
|
|
4205
|
-
COUNT(*) as entries,
|
|
4206
|
-
SUM(LENGTH(data)) as bytes
|
|
4207
|
-
FROM ${ReactNativeCacheAdapter.TABLE_NAME}
|
|
4208
|
-
`;
|
|
4209
|
-
const results = await this.executeSql(sql);
|
|
4210
|
-
const rows = this.normalizeResults(results);
|
|
4211
|
-
const row = rows[0] || { entries: 0, bytes: 0 };
|
|
4212
|
-
return {
|
|
4213
|
-
entries: row.entries || 0,
|
|
4214
|
-
bytes: (row.bytes || 0) * 2,
|
|
4215
|
-
lastCleanup: Date.now(),
|
|
4216
|
-
};
|
|
4217
|
-
}
|
|
4218
|
-
async cleanup() {
|
|
4219
|
-
// No cleanup needed - cache never expires
|
|
4220
|
-
return 0;
|
|
4221
|
-
}
|
|
4222
|
-
async getKeys(pattern) {
|
|
4223
|
-
await this.ensureInitialized();
|
|
4224
|
-
let sql = `SELECT cache_key FROM ${ReactNativeCacheAdapter.TABLE_NAME}`;
|
|
4225
|
-
const params = [];
|
|
4226
|
-
if (pattern) {
|
|
4227
|
-
// Simple pattern matching with LIKE
|
|
4228
|
-
const likePattern = pattern.replace(/\*/g, '%').replace(/\?/g, '_');
|
|
4229
|
-
sql += ' WHERE cache_key LIKE ?';
|
|
4230
|
-
params.push(likePattern);
|
|
4231
|
-
}
|
|
4232
|
-
const results = await this.executeSql(sql, params);
|
|
4233
|
-
const keys = [];
|
|
4234
|
-
const rows = this.normalizeResults(results);
|
|
4235
|
-
for (const row of rows) {
|
|
4236
|
-
keys.push(row.cache_key);
|
|
4237
|
-
}
|
|
4238
|
-
return keys;
|
|
4239
|
-
}
|
|
4240
|
-
async executeSql(sql, params = []) {
|
|
4241
|
-
if (this.isExpo) {
|
|
4242
|
-
const expoDB = this.db;
|
|
4243
|
-
if (sql.toLowerCase().includes('select') || sql.toLowerCase().includes('pragma')) {
|
|
4244
|
-
const result = await expoDB.getAllAsync(sql, params);
|
|
4245
|
-
return Array.isArray(result) ? { results: result } : result;
|
|
4246
|
-
}
|
|
4247
|
-
else {
|
|
4248
|
-
return await expoDB.runAsync(sql, params);
|
|
4249
|
-
}
|
|
4250
|
-
}
|
|
4251
|
-
else {
|
|
4252
|
-
// react-native-sqlite-storage
|
|
4253
|
-
return new Promise((resolve, reject) => {
|
|
4254
|
-
this.db.transaction((tx) => {
|
|
4255
|
-
tx.executeSql(sql, params, (_, results) => resolve(results), (_, error) => {
|
|
4256
|
-
reject(error);
|
|
4257
|
-
return false;
|
|
4258
|
-
});
|
|
4259
|
-
});
|
|
4260
|
-
});
|
|
4261
|
-
}
|
|
4262
|
-
}
|
|
4263
|
-
async ensureInitialized() {
|
|
4264
|
-
if (!this.initPromise) {
|
|
4265
|
-
this.initPromise = this.initialize();
|
|
4266
|
-
}
|
|
4267
|
-
await this.initPromise;
|
|
4268
|
-
}
|
|
4269
|
-
}
|
|
4270
|
-
ReactNativeCacheAdapter.DB_NAME = 'acube_cache.db';
|
|
4271
|
-
ReactNativeCacheAdapter.TABLE_NAME = 'cache_entries';
|
|
4272
|
-
/**
|
|
4273
|
-
* Memory-based fallback cache adapter for environments without SQLite
|
|
4274
|
-
* Cache never expires - data persists until explicitly invalidated
|
|
4275
|
-
*/
|
|
4276
|
-
class MemoryCacheAdapter {
|
|
4277
|
-
constructor(options = {}) {
|
|
4278
|
-
this.cache = new Map();
|
|
4279
|
-
this.totalBytes = 0;
|
|
4280
|
-
this.options = {
|
|
4281
|
-
maxEntries: 1000,
|
|
4282
|
-
...options,
|
|
4283
|
-
};
|
|
4284
|
-
}
|
|
4285
|
-
calculateItemSize(key, item) {
|
|
4286
|
-
// Calculate rough size estimation for memory usage
|
|
4287
|
-
const keySize = key.length * 2; // UTF-16 estimation
|
|
4288
|
-
const itemSize = JSON.stringify(item).length * 2; // UTF-16 estimation
|
|
4289
|
-
return keySize + itemSize;
|
|
4290
|
-
}
|
|
4291
|
-
async get(key) {
|
|
4292
|
-
log$d.debug('Getting cache item', { key });
|
|
4293
|
-
const item = this.cache.get(key);
|
|
4294
|
-
if (!item) {
|
|
4295
|
-
log$d.debug('Cache miss', { key });
|
|
4296
|
-
return null;
|
|
4297
|
-
}
|
|
4298
|
-
// Handle decompression if needed
|
|
4299
|
-
const isCompressed = !!item.compressed;
|
|
4300
|
-
let finalData = item.data;
|
|
4301
|
-
if (isCompressed) {
|
|
4302
|
-
const decompressed = decompressData(item.data, true);
|
|
4303
|
-
finalData = JSON.parse(decompressed.data);
|
|
4304
|
-
}
|
|
4305
|
-
log$d.debug('Cache hit', { key, compressed: isCompressed });
|
|
4306
|
-
return {
|
|
4307
|
-
...item,
|
|
4308
|
-
data: finalData,
|
|
4309
|
-
compressed: isCompressed,
|
|
4310
|
-
};
|
|
4311
|
-
}
|
|
4312
|
-
async set(key, data) {
|
|
4313
|
-
log$d.debug('Setting cache item', { key });
|
|
4314
|
-
// Handle compression if enabled
|
|
4315
|
-
let finalData = data;
|
|
4316
|
-
let isCompressed = false;
|
|
4317
|
-
if (this.options.compression && this.options.compressionThreshold) {
|
|
4318
|
-
const serializedData = JSON.stringify(data);
|
|
4319
|
-
const compressionResult = compressData(serializedData, this.options.compressionThreshold);
|
|
4320
|
-
if (compressionResult.compressed) {
|
|
4321
|
-
finalData = compressionResult.data;
|
|
4322
|
-
isCompressed = true;
|
|
4323
|
-
log$d.debug('Compression result', {
|
|
4324
|
-
key,
|
|
4325
|
-
originalSize: compressionResult.originalSize,
|
|
4326
|
-
compressedSize: compressionResult.compressedSize,
|
|
4327
|
-
savings: compressionResult.originalSize - compressionResult.compressedSize,
|
|
4328
|
-
});
|
|
4329
|
-
}
|
|
4330
|
-
}
|
|
4331
|
-
const item = {
|
|
4332
|
-
data: finalData,
|
|
4333
|
-
timestamp: Date.now(),
|
|
4334
|
-
compressed: isCompressed,
|
|
4335
|
-
};
|
|
4336
|
-
return this.setItem(key, item);
|
|
4337
|
-
}
|
|
4338
|
-
async setItem(key, item) {
|
|
4339
|
-
// Calculate size of new item
|
|
4340
|
-
const newItemSize = this.calculateItemSize(key, item);
|
|
4341
|
-
// If item already exists, subtract old size
|
|
4342
|
-
if (this.cache.has(key)) {
|
|
4343
|
-
const oldItem = this.cache.get(key);
|
|
4344
|
-
const oldItemSize = this.calculateItemSize(key, oldItem);
|
|
4345
|
-
this.totalBytes -= oldItemSize;
|
|
4346
|
-
}
|
|
4347
|
-
// Enforce max entries limit
|
|
4348
|
-
if (this.cache.size >= (this.options.maxEntries || 1000) && !this.cache.has(key)) {
|
|
4349
|
-
const oldestKey = this.cache.keys().next().value;
|
|
4350
|
-
if (oldestKey) {
|
|
4351
|
-
const oldestItem = this.cache.get(oldestKey);
|
|
4352
|
-
const oldestItemSize = this.calculateItemSize(oldestKey, oldestItem);
|
|
4353
|
-
this.totalBytes -= oldestItemSize;
|
|
4354
|
-
this.cache.delete(oldestKey);
|
|
4355
|
-
log$d.debug('Removed oldest item for capacity', { oldestKey, freedBytes: oldestItemSize });
|
|
4356
|
-
}
|
|
4357
|
-
}
|
|
4358
|
-
// Set new item and update total size
|
|
4359
|
-
this.cache.set(key, item);
|
|
4360
|
-
this.totalBytes += newItemSize;
|
|
4361
|
-
log$d.debug('Updated cache size', {
|
|
4362
|
-
entries: this.cache.size,
|
|
4363
|
-
totalBytes: this.totalBytes,
|
|
4364
|
-
newItemSize,
|
|
4365
|
-
});
|
|
4366
|
-
}
|
|
4367
|
-
async setBatch(items) {
|
|
4368
|
-
if (items.length === 0)
|
|
4369
|
-
return;
|
|
4370
|
-
log$d.debug('Batch setting items', { count: items.length });
|
|
4371
|
-
let totalNewBytes = 0;
|
|
4372
|
-
let totalOldBytes = 0;
|
|
4373
|
-
const itemsToRemove = [];
|
|
4374
|
-
// First pass: calculate size changes and identify capacity issues
|
|
4375
|
-
for (const [key, item] of items) {
|
|
4376
|
-
const newItemSize = this.calculateItemSize(key, item);
|
|
4377
|
-
totalNewBytes += newItemSize;
|
|
4378
|
-
// If item already exists, track old size for removal
|
|
4379
|
-
if (this.cache.has(key)) {
|
|
4380
|
-
const oldItem = this.cache.get(key);
|
|
4381
|
-
const oldItemSize = this.calculateItemSize(key, oldItem);
|
|
4382
|
-
totalOldBytes += oldItemSize;
|
|
4383
|
-
}
|
|
4384
|
-
}
|
|
4385
|
-
// Handle capacity limits - remove oldest items if needed
|
|
4386
|
-
const projectedEntries = this.cache.size + items.filter(([key]) => !this.cache.has(key)).length;
|
|
4387
|
-
const maxEntries = this.options.maxEntries || 1000;
|
|
4388
|
-
if (projectedEntries > maxEntries) {
|
|
4389
|
-
const entriesToRemove = projectedEntries - maxEntries;
|
|
4390
|
-
const oldestKeys = Array.from(this.cache.keys()).slice(0, entriesToRemove);
|
|
4391
|
-
for (const oldKey of oldestKeys) {
|
|
4392
|
-
const oldItem = this.cache.get(oldKey);
|
|
4393
|
-
const oldItemSize = this.calculateItemSize(oldKey, oldItem);
|
|
4394
|
-
this.totalBytes -= oldItemSize;
|
|
4395
|
-
this.cache.delete(oldKey);
|
|
4396
|
-
itemsToRemove.push(oldKey);
|
|
4397
|
-
}
|
|
4398
|
-
if (itemsToRemove.length > 0) {
|
|
4399
|
-
log$d.debug('Removed items for batch capacity', {
|
|
4400
|
-
removedCount: itemsToRemove.length,
|
|
4401
|
-
removedKeys: itemsToRemove,
|
|
4402
|
-
});
|
|
4403
|
-
}
|
|
4404
|
-
}
|
|
4405
|
-
// Update total bytes accounting
|
|
4406
|
-
this.totalBytes = this.totalBytes - totalOldBytes + totalNewBytes;
|
|
4407
|
-
// Second pass: set all items
|
|
4408
|
-
for (const [key, item] of items) {
|
|
4409
|
-
this.cache.set(key, item);
|
|
4410
|
-
}
|
|
4411
|
-
log$d.debug('Batch operation completed', {
|
|
4412
|
-
count: items.length,
|
|
4413
|
-
totalBytes: this.totalBytes,
|
|
4414
|
-
entries: this.cache.size,
|
|
4415
|
-
bytesAdded: totalNewBytes - totalOldBytes,
|
|
4416
|
-
});
|
|
4417
|
-
}
|
|
4418
|
-
async invalidate(pattern) {
|
|
4419
|
-
const regex = this.patternToRegex(pattern);
|
|
4420
|
-
let removed = 0;
|
|
4421
|
-
let bytesFreed = 0;
|
|
4422
|
-
for (const key of this.cache.keys()) {
|
|
4423
|
-
if (regex.test(key)) {
|
|
4424
|
-
const item = this.cache.get(key);
|
|
4425
|
-
const itemSize = this.calculateItemSize(key, item);
|
|
4426
|
-
this.cache.delete(key);
|
|
4427
|
-
this.totalBytes -= itemSize;
|
|
4428
|
-
bytesFreed += itemSize;
|
|
4429
|
-
removed++;
|
|
4430
|
-
}
|
|
4431
|
-
}
|
|
4432
|
-
if (removed > 0) {
|
|
4433
|
-
log$d.debug('Invalidation completed', {
|
|
4434
|
-
pattern,
|
|
4435
|
-
entriesRemoved: removed,
|
|
4436
|
-
bytesFreed,
|
|
4437
|
-
remainingEntries: this.cache.size,
|
|
4438
|
-
remainingBytes: this.totalBytes,
|
|
4439
|
-
});
|
|
4440
|
-
}
|
|
4441
|
-
}
|
|
4442
|
-
async clear() {
|
|
4443
|
-
this.cache.clear();
|
|
4444
|
-
this.totalBytes = 0;
|
|
4445
|
-
log$d.debug('Cache cleared', { entries: 0, bytes: 0 });
|
|
4446
|
-
}
|
|
4447
|
-
async getSize() {
|
|
4448
|
-
return {
|
|
4449
|
-
entries: this.cache.size,
|
|
4450
|
-
bytes: this.totalBytes,
|
|
4451
|
-
lastCleanup: Date.now(),
|
|
4452
|
-
};
|
|
4453
|
-
}
|
|
4454
|
-
async cleanup() {
|
|
4455
|
-
// No cleanup needed - cache never expires
|
|
4456
|
-
return 0;
|
|
4457
|
-
}
|
|
4458
|
-
async getKeys(pattern) {
|
|
4459
|
-
const keys = Array.from(this.cache.keys());
|
|
4460
|
-
if (!pattern)
|
|
4461
|
-
return keys;
|
|
4462
|
-
const regex = this.patternToRegex(pattern);
|
|
4463
|
-
return keys.filter((key) => regex.test(key));
|
|
4464
|
-
}
|
|
4465
|
-
patternToRegex(pattern) {
|
|
4466
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
4467
|
-
const regexPattern = escaped.replace(/\\\*/g, '.*').replace(/\\\?/g, '.');
|
|
4468
|
-
return new RegExp(`^${regexPattern}$`);
|
|
4469
|
-
}
|
|
4470
|
-
}
|
|
4471
|
-
|
|
4472
|
-
const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
|
|
4473
|
-
|
|
4474
|
-
let idbProxyableTypes;
|
|
4475
|
-
let cursorAdvanceMethods;
|
|
4476
|
-
// This is a function to prevent it throwing up in node environments.
|
|
4477
|
-
function getIdbProxyableTypes() {
|
|
4478
|
-
return (idbProxyableTypes ||
|
|
4479
|
-
(idbProxyableTypes = [
|
|
4480
|
-
IDBDatabase,
|
|
4481
|
-
IDBObjectStore,
|
|
4482
|
-
IDBIndex,
|
|
4483
|
-
IDBCursor,
|
|
4484
|
-
IDBTransaction,
|
|
4485
|
-
]));
|
|
4486
|
-
}
|
|
4487
|
-
// This is a function to prevent it throwing up in node environments.
|
|
4488
|
-
function getCursorAdvanceMethods() {
|
|
4489
|
-
return (cursorAdvanceMethods ||
|
|
4490
|
-
(cursorAdvanceMethods = [
|
|
4491
|
-
IDBCursor.prototype.advance,
|
|
4492
|
-
IDBCursor.prototype.continue,
|
|
4493
|
-
IDBCursor.prototype.continuePrimaryKey,
|
|
4494
|
-
]));
|
|
4495
|
-
}
|
|
4496
|
-
const transactionDoneMap = new WeakMap();
|
|
4497
|
-
const transformCache = new WeakMap();
|
|
4498
|
-
const reverseTransformCache = new WeakMap();
|
|
4499
|
-
function promisifyRequest(request) {
|
|
4500
|
-
const promise = new Promise((resolve, reject) => {
|
|
4501
|
-
const unlisten = () => {
|
|
4502
|
-
request.removeEventListener('success', success);
|
|
4503
|
-
request.removeEventListener('error', error);
|
|
4504
|
-
};
|
|
4505
|
-
const success = () => {
|
|
4506
|
-
resolve(wrap(request.result));
|
|
4507
|
-
unlisten();
|
|
4508
|
-
};
|
|
4509
|
-
const error = () => {
|
|
4510
|
-
reject(request.error);
|
|
4511
|
-
unlisten();
|
|
4512
|
-
};
|
|
4513
|
-
request.addEventListener('success', success);
|
|
4514
|
-
request.addEventListener('error', error);
|
|
4515
|
-
});
|
|
4516
|
-
// This mapping exists in reverseTransformCache but doesn't exist in transformCache. This
|
|
4517
|
-
// is because we create many promises from a single IDBRequest.
|
|
4518
|
-
reverseTransformCache.set(promise, request);
|
|
4519
|
-
return promise;
|
|
4520
|
-
}
|
|
4521
|
-
function cacheDonePromiseForTransaction(tx) {
|
|
4522
|
-
// Early bail if we've already created a done promise for this transaction.
|
|
4523
|
-
if (transactionDoneMap.has(tx))
|
|
4524
|
-
return;
|
|
4525
|
-
const done = new Promise((resolve, reject) => {
|
|
4526
|
-
const unlisten = () => {
|
|
4527
|
-
tx.removeEventListener('complete', complete);
|
|
4528
|
-
tx.removeEventListener('error', error);
|
|
4529
|
-
tx.removeEventListener('abort', error);
|
|
4530
|
-
};
|
|
4531
|
-
const complete = () => {
|
|
4532
|
-
resolve();
|
|
4533
|
-
unlisten();
|
|
4534
|
-
};
|
|
4535
|
-
const error = () => {
|
|
4536
|
-
reject(tx.error || new DOMException('AbortError', 'AbortError'));
|
|
4537
|
-
unlisten();
|
|
4538
|
-
};
|
|
4539
|
-
tx.addEventListener('complete', complete);
|
|
4540
|
-
tx.addEventListener('error', error);
|
|
4541
|
-
tx.addEventListener('abort', error);
|
|
4542
|
-
});
|
|
4543
|
-
// Cache it for later retrieval.
|
|
4544
|
-
transactionDoneMap.set(tx, done);
|
|
4545
|
-
}
|
|
4546
|
-
let idbProxyTraps = {
|
|
4547
|
-
get(target, prop, receiver) {
|
|
4548
|
-
if (target instanceof IDBTransaction) {
|
|
4549
|
-
// Special handling for transaction.done.
|
|
4550
|
-
if (prop === 'done')
|
|
4551
|
-
return transactionDoneMap.get(target);
|
|
4552
|
-
// Make tx.store return the only store in the transaction, or undefined if there are many.
|
|
4553
|
-
if (prop === 'store') {
|
|
4554
|
-
return receiver.objectStoreNames[1]
|
|
4555
|
-
? undefined
|
|
4556
|
-
: receiver.objectStore(receiver.objectStoreNames[0]);
|
|
4557
|
-
}
|
|
4558
|
-
}
|
|
4559
|
-
// Else transform whatever we get back.
|
|
4560
|
-
return wrap(target[prop]);
|
|
4561
|
-
},
|
|
4562
|
-
set(target, prop, value) {
|
|
4563
|
-
target[prop] = value;
|
|
4564
|
-
return true;
|
|
4565
|
-
},
|
|
4566
|
-
has(target, prop) {
|
|
4567
|
-
if (target instanceof IDBTransaction &&
|
|
4568
|
-
(prop === 'done' || prop === 'store')) {
|
|
4569
|
-
return true;
|
|
4570
|
-
}
|
|
4571
|
-
return prop in target;
|
|
4572
|
-
},
|
|
4573
|
-
};
|
|
4574
|
-
function replaceTraps(callback) {
|
|
4575
|
-
idbProxyTraps = callback(idbProxyTraps);
|
|
4576
|
-
}
|
|
4577
|
-
function wrapFunction(func) {
|
|
4578
|
-
// Due to expected object equality (which is enforced by the caching in `wrap`), we
|
|
4579
|
-
// only create one new func per func.
|
|
4580
|
-
// Cursor methods are special, as the behaviour is a little more different to standard IDB. In
|
|
4581
|
-
// IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the
|
|
4582
|
-
// cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense
|
|
4583
|
-
// with real promises, so each advance methods returns a new promise for the cursor object, or
|
|
4584
|
-
// undefined if the end of the cursor has been reached.
|
|
4585
|
-
if (getCursorAdvanceMethods().includes(func)) {
|
|
4586
|
-
return function (...args) {
|
|
4587
|
-
// Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
|
|
4588
|
-
// the original object.
|
|
4589
|
-
func.apply(unwrap(this), args);
|
|
4590
|
-
return wrap(this.request);
|
|
4591
|
-
};
|
|
4592
|
-
}
|
|
4593
|
-
return function (...args) {
|
|
4594
|
-
// Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use
|
|
4595
|
-
// the original object.
|
|
4596
|
-
return wrap(func.apply(unwrap(this), args));
|
|
4597
|
-
};
|
|
4598
|
-
}
|
|
4599
|
-
function transformCachableValue(value) {
|
|
4600
|
-
if (typeof value === 'function')
|
|
4601
|
-
return wrapFunction(value);
|
|
4602
|
-
// This doesn't return, it just creates a 'done' promise for the transaction,
|
|
4603
|
-
// which is later returned for transaction.done (see idbObjectHandler).
|
|
4604
|
-
if (value instanceof IDBTransaction)
|
|
4605
|
-
cacheDonePromiseForTransaction(value);
|
|
4606
|
-
if (instanceOfAny(value, getIdbProxyableTypes()))
|
|
4607
|
-
return new Proxy(value, idbProxyTraps);
|
|
4608
|
-
// Return the same value back if we're not going to transform it.
|
|
4609
|
-
return value;
|
|
4610
|
-
}
|
|
4611
|
-
function wrap(value) {
|
|
4612
|
-
// We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because
|
|
4613
|
-
// IDB is weird and a single IDBRequest can yield many responses, so these can't be cached.
|
|
4614
|
-
if (value instanceof IDBRequest)
|
|
4615
|
-
return promisifyRequest(value);
|
|
4616
|
-
// If we've already transformed this value before, reuse the transformed value.
|
|
4617
|
-
// This is faster, but it also provides object equality.
|
|
4618
|
-
if (transformCache.has(value))
|
|
4619
|
-
return transformCache.get(value);
|
|
4620
|
-
const newValue = transformCachableValue(value);
|
|
4621
|
-
// Not all types are transformed.
|
|
4622
|
-
// These may be primitive types, so they can't be WeakMap keys.
|
|
4623
|
-
if (newValue !== value) {
|
|
4624
|
-
transformCache.set(value, newValue);
|
|
4625
|
-
reverseTransformCache.set(newValue, value);
|
|
4626
|
-
}
|
|
4627
|
-
return newValue;
|
|
4628
|
-
}
|
|
4629
|
-
const unwrap = (value) => reverseTransformCache.get(value);
|
|
4630
|
-
|
|
4631
|
-
/**
|
|
4632
|
-
* Open a database.
|
|
4633
|
-
*
|
|
4634
|
-
* @param name Name of the database.
|
|
4635
|
-
* @param version Schema version.
|
|
4636
|
-
* @param callbacks Additional callbacks.
|
|
4637
|
-
*/
|
|
4638
|
-
function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) {
|
|
4639
|
-
const request = indexedDB.open(name, version);
|
|
4640
|
-
const openPromise = wrap(request);
|
|
4641
|
-
if (upgrade) {
|
|
4642
|
-
request.addEventListener('upgradeneeded', (event) => {
|
|
4643
|
-
upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event);
|
|
4644
|
-
});
|
|
4645
|
-
}
|
|
4646
|
-
if (blocked) {
|
|
4647
|
-
request.addEventListener('blocked', (event) => blocked(
|
|
4648
|
-
// Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
|
|
4649
|
-
event.oldVersion, event.newVersion, event));
|
|
4650
|
-
}
|
|
4651
|
-
openPromise
|
|
4652
|
-
.then((db) => {
|
|
4653
|
-
if (terminated)
|
|
4654
|
-
db.addEventListener('close', () => terminated());
|
|
4655
|
-
if (blocking) {
|
|
4656
|
-
db.addEventListener('versionchange', (event) => blocking(event.oldVersion, event.newVersion, event));
|
|
4657
|
-
}
|
|
4658
|
-
})
|
|
4659
|
-
.catch(() => { });
|
|
4660
|
-
return openPromise;
|
|
4661
|
-
}
|
|
4662
|
-
/**
|
|
4663
|
-
* Delete a database.
|
|
4664
|
-
*
|
|
4665
|
-
* @param name Name of the database.
|
|
4666
|
-
*/
|
|
4667
|
-
function deleteDB(name, { blocked } = {}) {
|
|
4668
|
-
const request = indexedDB.deleteDatabase(name);
|
|
4669
|
-
if (blocked) {
|
|
4670
|
-
request.addEventListener('blocked', (event) => blocked(
|
|
4671
|
-
// Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405
|
|
4672
|
-
event.oldVersion, event));
|
|
4673
|
-
}
|
|
4674
|
-
return wrap(request).then(() => undefined);
|
|
4675
|
-
}
|
|
4676
|
-
|
|
4677
|
-
const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count'];
|
|
4678
|
-
const writeMethods = ['put', 'add', 'delete', 'clear'];
|
|
4679
|
-
const cachedMethods = new Map();
|
|
4680
|
-
function getMethod(target, prop) {
|
|
4681
|
-
if (!(target instanceof IDBDatabase &&
|
|
4682
|
-
!(prop in target) &&
|
|
4683
|
-
typeof prop === 'string')) {
|
|
4684
|
-
return;
|
|
4685
|
-
}
|
|
4686
|
-
if (cachedMethods.get(prop))
|
|
4687
|
-
return cachedMethods.get(prop);
|
|
4688
|
-
const targetFuncName = prop.replace(/FromIndex$/, '');
|
|
4689
|
-
const useIndex = prop !== targetFuncName;
|
|
4690
|
-
const isWrite = writeMethods.includes(targetFuncName);
|
|
4691
|
-
if (
|
|
4692
|
-
// Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge.
|
|
4693
|
-
!(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) ||
|
|
4694
|
-
!(isWrite || readMethods.includes(targetFuncName))) {
|
|
4695
|
-
return;
|
|
4696
|
-
}
|
|
4697
|
-
const method = async function (storeName, ...args) {
|
|
4698
|
-
// isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :(
|
|
4699
|
-
const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly');
|
|
4700
|
-
let target = tx.store;
|
|
4701
|
-
if (useIndex)
|
|
4702
|
-
target = target.index(args.shift());
|
|
4703
|
-
// Must reject if op rejects.
|
|
4704
|
-
// If it's a write operation, must reject if tx.done rejects.
|
|
4705
|
-
// Must reject with op rejection first.
|
|
4706
|
-
// Must resolve with op value.
|
|
4707
|
-
// Must handle both promises (no unhandled rejections)
|
|
4708
|
-
return (await Promise.all([
|
|
4709
|
-
target[targetFuncName](...args),
|
|
4710
|
-
isWrite && tx.done,
|
|
4711
|
-
]))[0];
|
|
4712
|
-
};
|
|
4713
|
-
cachedMethods.set(prop, method);
|
|
4714
|
-
return method;
|
|
4715
|
-
}
|
|
4716
|
-
replaceTraps((oldTraps) => ({
|
|
4717
|
-
...oldTraps,
|
|
4718
|
-
get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver),
|
|
4719
|
-
has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop),
|
|
4720
|
-
}));
|
|
4721
|
-
|
|
4722
|
-
const advanceMethodProps = ['continue', 'continuePrimaryKey', 'advance'];
|
|
4723
|
-
const methodMap = {};
|
|
4724
|
-
const advanceResults = new WeakMap();
|
|
4725
|
-
const ittrProxiedCursorToOriginalProxy = new WeakMap();
|
|
4726
|
-
const cursorIteratorTraps = {
|
|
4727
|
-
get(target, prop) {
|
|
4728
|
-
if (!advanceMethodProps.includes(prop))
|
|
4729
|
-
return target[prop];
|
|
4730
|
-
let cachedFunc = methodMap[prop];
|
|
4731
|
-
if (!cachedFunc) {
|
|
4732
|
-
cachedFunc = methodMap[prop] = function (...args) {
|
|
4733
|
-
advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args));
|
|
4734
|
-
};
|
|
4735
|
-
}
|
|
4736
|
-
return cachedFunc;
|
|
4737
|
-
},
|
|
4738
|
-
};
|
|
4739
|
-
async function* iterate(...args) {
|
|
4740
|
-
// tslint:disable-next-line:no-this-assignment
|
|
4741
|
-
let cursor = this;
|
|
4742
|
-
if (!(cursor instanceof IDBCursor)) {
|
|
4743
|
-
cursor = await cursor.openCursor(...args);
|
|
4744
|
-
}
|
|
4745
|
-
if (!cursor)
|
|
4746
|
-
return;
|
|
4747
|
-
cursor = cursor;
|
|
4748
|
-
const proxiedCursor = new Proxy(cursor, cursorIteratorTraps);
|
|
4749
|
-
ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor);
|
|
4750
|
-
// Map this double-proxy back to the original, so other cursor methods work.
|
|
4751
|
-
reverseTransformCache.set(proxiedCursor, unwrap(cursor));
|
|
4752
|
-
while (cursor) {
|
|
4753
|
-
yield proxiedCursor;
|
|
4754
|
-
// If one of the advancing methods was not called, call continue().
|
|
4755
|
-
cursor = await (advanceResults.get(proxiedCursor) || cursor.continue());
|
|
4756
|
-
advanceResults.delete(proxiedCursor);
|
|
4757
|
-
}
|
|
4758
|
-
}
|
|
4759
|
-
function isIteratorProp(target, prop) {
|
|
4760
|
-
return ((prop === Symbol.asyncIterator &&
|
|
4761
|
-
instanceOfAny(target, [IDBIndex, IDBObjectStore, IDBCursor])) ||
|
|
4762
|
-
(prop === 'iterate' && instanceOfAny(target, [IDBIndex, IDBObjectStore])));
|
|
4763
|
-
}
|
|
4764
|
-
replaceTraps((oldTraps) => ({
|
|
4765
|
-
...oldTraps,
|
|
4766
|
-
get(target, prop, receiver) {
|
|
4767
|
-
if (isIteratorProp(target, prop))
|
|
4768
|
-
return iterate;
|
|
4769
|
-
return oldTraps.get(target, prop, receiver);
|
|
4770
|
-
},
|
|
4771
|
-
has(target, prop) {
|
|
4772
|
-
return isIteratorProp(target, prop) || oldTraps.has(target, prop);
|
|
4773
|
-
},
|
|
4774
|
-
}));
|
|
4775
|
-
|
|
4776
|
-
const log$c = createPrefixedLogger('CACHE-WEB');
|
|
4777
|
-
/**
|
|
4778
|
-
* Web cache adapter using IndexedDB with automatic error recovery
|
|
4779
|
-
* Cache never expires - data persists until explicitly invalidated
|
|
4780
|
-
*/
|
|
4781
|
-
class WebCacheAdapter {
|
|
4782
|
-
constructor(options = {}) {
|
|
4783
|
-
this.db = null;
|
|
4784
|
-
this.initPromise = null;
|
|
4785
|
-
this.retryCount = 0;
|
|
4786
|
-
this.maxRetries = 3;
|
|
4787
|
-
this.options = {
|
|
4788
|
-
maxSize: 50 * 1024 * 1024, // 50MB
|
|
4789
|
-
maxEntries: 10000,
|
|
4790
|
-
compression: false,
|
|
4791
|
-
compressionThreshold: 1024,
|
|
4792
|
-
...options,
|
|
4793
|
-
};
|
|
4794
|
-
this.initPromise = this.initialize();
|
|
4795
|
-
}
|
|
4796
|
-
async initialize() {
|
|
4797
|
-
if (this.db)
|
|
4798
|
-
return;
|
|
4799
|
-
log$c.debug('Initializing IndexedDB cache', {
|
|
4800
|
-
dbName: WebCacheAdapter.DB_NAME,
|
|
4801
|
-
version: WebCacheAdapter.DB_VERSION,
|
|
4802
|
-
});
|
|
4803
|
-
try {
|
|
4804
|
-
this.db = await this.openDatabase();
|
|
4805
|
-
log$c.debug('IndexedDB cache initialized successfully');
|
|
4806
|
-
this.retryCount = 0; // Reset retry count on success
|
|
4807
|
-
}
|
|
4808
|
-
catch (error) {
|
|
4809
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
4810
|
-
log$c.debug('Failed to initialize IndexedDB', { error: errorMessage });
|
|
4811
|
-
// Check if this is a version conflict error
|
|
4812
|
-
if (this.isVersionConflictError(errorMessage)) {
|
|
4813
|
-
await this.handleVersionConflict();
|
|
4814
|
-
}
|
|
4815
|
-
else {
|
|
4816
|
-
throw new Error(`Failed to initialize IndexedDB: ${errorMessage}`);
|
|
4817
|
-
}
|
|
4818
|
-
}
|
|
4819
|
-
}
|
|
4820
|
-
async openDatabase() {
|
|
4821
|
-
return await openDB(WebCacheAdapter.DB_NAME, WebCacheAdapter.DB_VERSION, {
|
|
4822
|
-
upgrade: (db, oldVersion, newVersion, transaction) => {
|
|
4823
|
-
log$c.debug('Database upgrade needed', { oldVersion, newVersion });
|
|
4824
|
-
this.handleUpgrade(db, oldVersion, newVersion, transaction);
|
|
4825
|
-
},
|
|
4826
|
-
blocked: () => {
|
|
4827
|
-
log$c.debug('Database blocked - another tab may be open');
|
|
4828
|
-
},
|
|
4829
|
-
blocking: () => {
|
|
4830
|
-
log$c.debug('Database blocking - will close connection');
|
|
4831
|
-
if (this.db) {
|
|
4832
|
-
this.db.close();
|
|
4833
|
-
this.db = null;
|
|
4834
|
-
}
|
|
4835
|
-
},
|
|
4836
|
-
terminated: () => {
|
|
4837
|
-
log$c.debug('Database connection terminated unexpectedly');
|
|
4838
|
-
this.db = null;
|
|
4839
|
-
},
|
|
4840
|
-
});
|
|
4841
|
-
}
|
|
4842
|
-
handleUpgrade(db, oldVersion, newVersion, transaction) {
|
|
4843
|
-
log$c.debug('Handling database upgrade', { oldVersion, newVersion });
|
|
4844
|
-
// Create cache store if it doesn't exist (initial setup)
|
|
4845
|
-
if (!db.objectStoreNames.contains(WebCacheAdapter.STORE_NAME)) {
|
|
4846
|
-
const store = db.createObjectStore(WebCacheAdapter.STORE_NAME, { keyPath: 'key' });
|
|
4847
|
-
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
4848
|
-
log$c.debug('Created cache store and timestamp index');
|
|
4849
|
-
}
|
|
4850
|
-
// Handle migration from version 1 to 2
|
|
4851
|
-
if (oldVersion < 2) {
|
|
4852
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
4853
|
-
// Remove unused indexes from simplified cache structure
|
|
4854
|
-
const indexesToRemove = ['tags', 'source', 'syncStatus'];
|
|
4855
|
-
indexesToRemove.forEach((indexName) => {
|
|
4856
|
-
try {
|
|
4857
|
-
if (store.indexNames.contains(indexName)) {
|
|
4858
|
-
store.deleteIndex(indexName);
|
|
4859
|
-
log$c.debug(`Removed unused index: ${indexName}`);
|
|
4860
|
-
}
|
|
4861
|
-
}
|
|
4862
|
-
catch (error) {
|
|
4863
|
-
// Ignore errors if indexes don't exist
|
|
4864
|
-
log$c.debug(`Warning: Could not remove index ${indexName}`, error);
|
|
4865
|
-
}
|
|
4866
|
-
});
|
|
4867
|
-
}
|
|
4868
|
-
log$c.debug('Database upgrade completed');
|
|
4869
|
-
}
|
|
4870
|
-
isVersionConflictError(errorMessage) {
|
|
4871
|
-
return (errorMessage.includes('less than the existing version') ||
|
|
4872
|
-
errorMessage.includes('version conflict') ||
|
|
4873
|
-
errorMessage.includes('VersionError'));
|
|
4874
|
-
}
|
|
4875
|
-
async handleVersionConflict() {
|
|
4876
|
-
log$c.debug('Handling version conflict, attempting recovery...');
|
|
4877
|
-
if (this.retryCount >= this.maxRetries) {
|
|
4878
|
-
throw new Error('Failed to resolve IndexedDB version conflict after multiple attempts');
|
|
4879
|
-
}
|
|
4880
|
-
this.retryCount++;
|
|
4881
|
-
try {
|
|
4882
|
-
// Close any existing connection
|
|
4883
|
-
if (this.db) {
|
|
4884
|
-
this.db.close();
|
|
4885
|
-
this.db = null;
|
|
4886
|
-
}
|
|
4887
|
-
// Delete the problematic database
|
|
4888
|
-
log$c.debug('Deleting problematic database to resolve version conflict');
|
|
4889
|
-
await deleteDB(WebCacheAdapter.DB_NAME);
|
|
4890
|
-
// Wait a bit for the deletion to complete
|
|
4891
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
4892
|
-
// Try to open the database again
|
|
4893
|
-
log$c.debug(`Retrying database initialization (attempt ${this.retryCount}/${this.maxRetries})`);
|
|
4894
|
-
this.db = await this.openDatabase();
|
|
4895
|
-
log$c.debug('Successfully recovered from version conflict');
|
|
4896
|
-
this.retryCount = 0; // Reset retry count on success
|
|
4897
|
-
}
|
|
4898
|
-
catch (retryError) {
|
|
4899
|
-
const retryErrorMessage = retryError instanceof Error ? retryError.message : 'Unknown error';
|
|
4900
|
-
log$c.debug('Recovery attempt failed', { attempt: this.retryCount, error: retryErrorMessage });
|
|
4901
|
-
if (this.retryCount < this.maxRetries) {
|
|
4902
|
-
// Try again
|
|
4903
|
-
await this.handleVersionConflict();
|
|
4904
|
-
}
|
|
4905
|
-
else {
|
|
4906
|
-
throw new Error(`Failed to recover from IndexedDB version conflict: ${retryErrorMessage}`);
|
|
4907
|
-
}
|
|
4908
|
-
}
|
|
4909
|
-
}
|
|
4910
|
-
async get(key) {
|
|
4911
|
-
await this.ensureInitialized();
|
|
4912
|
-
log$c.debug('Getting cache item', { key });
|
|
4913
|
-
try {
|
|
4914
|
-
const transaction = this.db.transaction([WebCacheAdapter.STORE_NAME], 'readonly');
|
|
4915
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
4916
|
-
const result = await store.get(key);
|
|
4917
|
-
if (!result) {
|
|
4918
|
-
return null;
|
|
4919
|
-
}
|
|
4920
|
-
const item = result;
|
|
4921
|
-
// Handle decompression if needed
|
|
4922
|
-
const isCompressed = !!item.compressed;
|
|
4923
|
-
let finalData;
|
|
4924
|
-
if (isCompressed) {
|
|
4925
|
-
const decompressed = decompressData(item.data, true);
|
|
4926
|
-
finalData = JSON.parse(decompressed.data);
|
|
4927
|
-
}
|
|
4928
|
-
else {
|
|
4929
|
-
finalData = item.data;
|
|
4930
|
-
}
|
|
4931
|
-
return {
|
|
4932
|
-
data: finalData,
|
|
4933
|
-
timestamp: item.timestamp,
|
|
4934
|
-
compressed: isCompressed,
|
|
4935
|
-
};
|
|
4936
|
-
}
|
|
4937
|
-
catch (error) {
|
|
4938
|
-
log$c.debug('Error getting cache item', { key, error });
|
|
4939
|
-
return null; // Return null on error instead of throwing
|
|
4940
|
-
}
|
|
4941
|
-
}
|
|
4942
|
-
async set(key, data) {
|
|
4943
|
-
const item = {
|
|
4944
|
-
data,
|
|
4945
|
-
timestamp: Date.now(),
|
|
4946
|
-
};
|
|
4947
|
-
return this.setItem(key, item);
|
|
4948
|
-
}
|
|
4949
|
-
async setItem(key, item) {
|
|
4950
|
-
await this.ensureInitialized();
|
|
4951
|
-
// Handle compression if enabled
|
|
4952
|
-
let finalData = item.data;
|
|
4953
|
-
let isCompressed = false;
|
|
4954
|
-
if (this.options.compression && this.options.compressionThreshold) {
|
|
4955
|
-
const serializedData = JSON.stringify(item.data);
|
|
4956
|
-
const compressionResult = compressData(serializedData, this.options.compressionThreshold);
|
|
4957
|
-
if (compressionResult.compressed) {
|
|
4958
|
-
finalData = compressionResult.data;
|
|
4959
|
-
isCompressed = true;
|
|
4960
|
-
log$c.debug('Compression result', {
|
|
4961
|
-
key,
|
|
4962
|
-
originalSize: compressionResult.originalSize,
|
|
4963
|
-
compressedSize: compressionResult.compressedSize,
|
|
4964
|
-
compressed: isCompressed,
|
|
4965
|
-
savings: compressionResult.originalSize - compressionResult.compressedSize,
|
|
4966
|
-
});
|
|
4967
|
-
}
|
|
4968
|
-
}
|
|
4969
|
-
log$c.debug('Setting cache item', { key, timestamp: item.timestamp, compressed: isCompressed });
|
|
4970
|
-
const storedItem = {
|
|
4971
|
-
key,
|
|
4972
|
-
data: finalData,
|
|
4973
|
-
timestamp: item.timestamp,
|
|
4974
|
-
compressed: isCompressed,
|
|
4975
|
-
};
|
|
4976
|
-
try {
|
|
4977
|
-
const transaction = this.db.transaction([WebCacheAdapter.STORE_NAME], 'readwrite');
|
|
4978
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
4979
|
-
await store.put(storedItem);
|
|
4980
|
-
}
|
|
4981
|
-
catch (error) {
|
|
4982
|
-
log$c.debug('Error setting cache item', { key, error });
|
|
4983
|
-
// Silently fail for cache writes
|
|
4984
|
-
}
|
|
4985
|
-
}
|
|
4986
|
-
async setBatch(items) {
|
|
4987
|
-
if (items.length === 0)
|
|
4988
|
-
return;
|
|
4989
|
-
await this.ensureInitialized();
|
|
4990
|
-
log$c.debug('Batch setting items', { count: items.length });
|
|
4991
|
-
try {
|
|
4992
|
-
const transaction = this.db.transaction([WebCacheAdapter.STORE_NAME], 'readwrite');
|
|
4993
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
4994
|
-
// Process all items in the transaction
|
|
4995
|
-
const promises = items.map(([key, item]) => {
|
|
4996
|
-
const storedItem = this.prepareBatchItem(key, item);
|
|
4997
|
-
return store.put(storedItem);
|
|
4998
|
-
});
|
|
4999
|
-
await Promise.all(promises);
|
|
5000
|
-
log$c.debug('Batch operation completed', { count: items.length });
|
|
5001
|
-
}
|
|
5002
|
-
catch (error) {
|
|
5003
|
-
log$c.debug('Error in batch operation', { count: items.length, error });
|
|
5004
|
-
// Silently fail for batch writes
|
|
5005
|
-
}
|
|
5006
|
-
}
|
|
5007
|
-
prepareBatchItem(key, item) {
|
|
5008
|
-
// Handle compression if enabled (same logic as setItem)
|
|
5009
|
-
let finalData = item.data;
|
|
5010
|
-
let isCompressed = false;
|
|
5011
|
-
if (this.options.compression && this.options.compressionThreshold) {
|
|
5012
|
-
const serializedData = JSON.stringify(item.data);
|
|
5013
|
-
const compressionResult = compressData(serializedData, this.options.compressionThreshold);
|
|
5014
|
-
if (compressionResult.compressed) {
|
|
5015
|
-
finalData = compressionResult.data;
|
|
5016
|
-
isCompressed = true;
|
|
5017
|
-
}
|
|
5018
|
-
}
|
|
5019
|
-
return {
|
|
5020
|
-
key,
|
|
5021
|
-
data: finalData,
|
|
5022
|
-
timestamp: item.timestamp,
|
|
5023
|
-
compressed: isCompressed,
|
|
5024
|
-
};
|
|
5025
|
-
}
|
|
5026
|
-
async invalidate(pattern) {
|
|
5027
|
-
await this.ensureInitialized();
|
|
5028
|
-
const keys = await this.getKeys(pattern);
|
|
5029
|
-
const deletePromises = keys.map((key) => this.delete(key));
|
|
5030
|
-
await Promise.all(deletePromises);
|
|
5031
|
-
}
|
|
5032
|
-
async clear() {
|
|
5033
|
-
await this.ensureInitialized();
|
|
5034
|
-
try {
|
|
5035
|
-
const transaction = this.db.transaction([WebCacheAdapter.STORE_NAME], 'readwrite');
|
|
5036
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
5037
|
-
await store.clear();
|
|
5038
|
-
log$c.debug('Cache cleared successfully');
|
|
5039
|
-
}
|
|
5040
|
-
catch (error) {
|
|
5041
|
-
log$c.debug('Error clearing cache', error);
|
|
5042
|
-
// Silently fail for cache clear
|
|
5043
|
-
}
|
|
5044
|
-
}
|
|
5045
|
-
async getSize() {
|
|
5046
|
-
await this.ensureInitialized();
|
|
5047
|
-
try {
|
|
5048
|
-
const transaction = this.db.transaction([WebCacheAdapter.STORE_NAME], 'readonly');
|
|
5049
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
5050
|
-
let entries = 0;
|
|
5051
|
-
let bytes = 0;
|
|
5052
|
-
// Use cursor for efficient iteration
|
|
5053
|
-
let cursor = await store.openCursor();
|
|
5054
|
-
while (cursor) {
|
|
5055
|
-
entries++;
|
|
5056
|
-
// Rough estimation of size
|
|
5057
|
-
bytes += JSON.stringify(cursor.value).length * 2; // UTF-16 encoding
|
|
5058
|
-
cursor = await cursor.continue();
|
|
5059
|
-
}
|
|
5060
|
-
return {
|
|
5061
|
-
entries,
|
|
5062
|
-
bytes,
|
|
5063
|
-
lastCleanup: Date.now(),
|
|
5064
|
-
};
|
|
5065
|
-
}
|
|
5066
|
-
catch (error) {
|
|
5067
|
-
log$c.debug('Error getting cache size', error);
|
|
5068
|
-
return {
|
|
5069
|
-
entries: 0,
|
|
5070
|
-
bytes: 0,
|
|
5071
|
-
lastCleanup: Date.now(),
|
|
5072
|
-
};
|
|
5073
|
-
}
|
|
5074
|
-
}
|
|
5075
|
-
async cleanup() {
|
|
5076
|
-
// No cleanup needed - cache never expires
|
|
5077
|
-
return 0;
|
|
5078
|
-
}
|
|
5079
|
-
async getKeys(pattern) {
|
|
5080
|
-
await this.ensureInitialized();
|
|
5081
|
-
try {
|
|
5082
|
-
const transaction = this.db.transaction([WebCacheAdapter.STORE_NAME], 'readonly');
|
|
5083
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
5084
|
-
const allKeys = (await store.getAllKeys());
|
|
5085
|
-
if (!pattern) {
|
|
5086
|
-
return allKeys;
|
|
5087
|
-
}
|
|
5088
|
-
const regex = this.patternToRegex(pattern);
|
|
5089
|
-
return allKeys.filter((key) => regex.test(key));
|
|
5090
|
-
}
|
|
5091
|
-
catch (error) {
|
|
5092
|
-
log$c.debug('Error getting cache keys', error);
|
|
5093
|
-
return [];
|
|
5094
|
-
}
|
|
5095
|
-
}
|
|
5096
|
-
async delete(key) {
|
|
5097
|
-
await this.ensureInitialized();
|
|
5098
|
-
try {
|
|
5099
|
-
const transaction = this.db.transaction([WebCacheAdapter.STORE_NAME], 'readwrite');
|
|
5100
|
-
const store = transaction.objectStore(WebCacheAdapter.STORE_NAME);
|
|
5101
|
-
await store.delete(key);
|
|
5102
|
-
return true;
|
|
5103
|
-
}
|
|
5104
|
-
catch (error) {
|
|
5105
|
-
log$c.debug('Error deleting cache item', { key, error });
|
|
5106
|
-
return false;
|
|
5107
|
-
}
|
|
5108
|
-
}
|
|
5109
|
-
async ensureInitialized() {
|
|
5110
|
-
if (!this.initPromise) {
|
|
5111
|
-
this.initPromise = this.initialize();
|
|
5112
|
-
}
|
|
5113
|
-
try {
|
|
5114
|
-
await this.initPromise;
|
|
5115
|
-
}
|
|
5116
|
-
catch (error) {
|
|
5117
|
-
log$c.debug('Failed to ensure initialization', error);
|
|
5118
|
-
// Reset and try once more
|
|
5119
|
-
this.initPromise = null;
|
|
5120
|
-
this.db = null;
|
|
5121
|
-
this.initPromise = this.initialize();
|
|
5122
|
-
await this.initPromise;
|
|
5123
|
-
}
|
|
5124
|
-
}
|
|
5125
|
-
patternToRegex(pattern) {
|
|
5126
|
-
// Convert simple glob patterns to regex
|
|
5127
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
5128
|
-
const regexPattern = escaped.replace(/\\\*/g, '.*').replace(/\\\?/g, '.');
|
|
5129
|
-
return new RegExp(`^${regexPattern}$`);
|
|
5130
|
-
}
|
|
5131
|
-
}
|
|
5132
|
-
WebCacheAdapter.DB_NAME = 'acube_cache';
|
|
5133
|
-
WebCacheAdapter.DB_VERSION = 2;
|
|
5134
|
-
WebCacheAdapter.STORE_NAME = 'cache_entries';
|
|
5135
|
-
|
|
5136
|
-
const log$b = createPrefixedLogger('CACHE-LOADER');
|
|
5137
|
-
function loadCacheAdapter(platform) {
|
|
5138
|
-
try {
|
|
5139
|
-
switch (platform) {
|
|
5140
|
-
case 'web':
|
|
5141
|
-
return new WebCacheAdapter({
|
|
5142
|
-
maxSize: 50 * 1024 * 1024,
|
|
5143
|
-
maxEntries: 10000,
|
|
5144
|
-
compression: false,
|
|
5145
|
-
});
|
|
5146
|
-
case 'react-native':
|
|
5147
|
-
try {
|
|
5148
|
-
return new ReactNativeCacheAdapter({
|
|
5149
|
-
maxSize: 100 * 1024 * 1024,
|
|
5150
|
-
maxEntries: 15000,
|
|
5151
|
-
});
|
|
5152
|
-
}
|
|
5153
|
-
catch {
|
|
5154
|
-
return new MemoryCacheAdapter({
|
|
5155
|
-
maxSize: 10 * 1024 * 1024,
|
|
5156
|
-
maxEntries: 5000,
|
|
5157
|
-
});
|
|
3262
|
+
hasCertificate: false,
|
|
3263
|
+
certificateInfo: null,
|
|
3264
|
+
platformInfo: this.mtlsAdapter?.getPlatformInfo() || null,
|
|
3265
|
+
pendingRequestsCount: this.pendingRequests.size,
|
|
3266
|
+
};
|
|
3267
|
+
if (this.certificatePort) {
|
|
3268
|
+
try {
|
|
3269
|
+
status.hasCertificate = await this.certificatePort.hasCertificate();
|
|
3270
|
+
if (status.hasCertificate) {
|
|
3271
|
+
status.certificateInfo = await this.certificatePort.getCertificateInfo();
|
|
5158
3272
|
}
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
maxEntries: 5000,
|
|
5164
|
-
});
|
|
3273
|
+
}
|
|
3274
|
+
catch {
|
|
3275
|
+
// Ignore errors
|
|
3276
|
+
}
|
|
5165
3277
|
}
|
|
3278
|
+
status.isReady = await this.isMtlsReady();
|
|
3279
|
+
return status;
|
|
5166
3280
|
}
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
return undefined;
|
|
3281
|
+
clearPendingRequests() {
|
|
3282
|
+
this.pendingRequests.clear();
|
|
5170
3283
|
}
|
|
5171
3284
|
}
|
|
5172
3285
|
|
|
@@ -5222,7 +3335,7 @@ class BaseSecureStorageAdapter {
|
|
|
5222
3335
|
}
|
|
5223
3336
|
}
|
|
5224
3337
|
|
|
5225
|
-
const log$
|
|
3338
|
+
const log$8 = createPrefixedLogger('NETWORK-BASE');
|
|
5226
3339
|
class NetworkBase {
|
|
5227
3340
|
constructor(initialOnline = true, debounceMs = 300) {
|
|
5228
3341
|
this.destroy$ = new Subject();
|
|
@@ -5242,14 +3355,14 @@ class NetworkBase {
|
|
|
5242
3355
|
const current = this.statusSubject.getValue();
|
|
5243
3356
|
if (current.online !== online) {
|
|
5244
3357
|
this.statusSubject.next({ online, timestamp: Date.now() });
|
|
5245
|
-
log$
|
|
3358
|
+
log$8.debug(`Network status changed: ${online ? 'online' : 'offline'}`);
|
|
5246
3359
|
}
|
|
5247
3360
|
}
|
|
5248
3361
|
destroy() {
|
|
5249
3362
|
this.destroy$.next();
|
|
5250
3363
|
this.destroy$.complete();
|
|
5251
3364
|
this.statusSubject.complete();
|
|
5252
|
-
log$
|
|
3365
|
+
log$8.debug('Network monitor destroyed');
|
|
5253
3366
|
}
|
|
5254
3367
|
}
|
|
5255
3368
|
|
|
@@ -5309,7 +3422,7 @@ class NodeSecureStorageAdapter extends BaseSecureStorageAdapter {
|
|
|
5309
3422
|
}
|
|
5310
3423
|
}
|
|
5311
3424
|
|
|
5312
|
-
const log$
|
|
3425
|
+
const log$7 = createPrefixedLogger('RN-STORAGE');
|
|
5313
3426
|
/**
|
|
5314
3427
|
* React Native storage adapter using AsyncStorage
|
|
5315
3428
|
* Note: Uses native batch operations for better performance (not base class)
|
|
@@ -5341,7 +3454,7 @@ class ReactNativeStorageAdapter {
|
|
|
5341
3454
|
return await this.AsyncStorage.getItem(key);
|
|
5342
3455
|
}
|
|
5343
3456
|
catch (error) {
|
|
5344
|
-
log$
|
|
3457
|
+
log$7.error('Failed to get item from AsyncStorage:', error);
|
|
5345
3458
|
return null;
|
|
5346
3459
|
}
|
|
5347
3460
|
}
|
|
@@ -5382,7 +3495,7 @@ class ReactNativeStorageAdapter {
|
|
|
5382
3495
|
return await this.AsyncStorage.getAllKeys();
|
|
5383
3496
|
}
|
|
5384
3497
|
catch (error) {
|
|
5385
|
-
log$
|
|
3498
|
+
log$7.error('Failed to get all keys:', error);
|
|
5386
3499
|
return [];
|
|
5387
3500
|
}
|
|
5388
3501
|
}
|
|
@@ -5399,7 +3512,7 @@ class ReactNativeStorageAdapter {
|
|
|
5399
3512
|
return result;
|
|
5400
3513
|
}
|
|
5401
3514
|
catch (error) {
|
|
5402
|
-
log$
|
|
3515
|
+
log$7.error('Failed to get multiple items:', error);
|
|
5403
3516
|
const result = {};
|
|
5404
3517
|
keys.forEach((key) => {
|
|
5405
3518
|
result[key] = null;
|
|
@@ -5449,7 +3562,7 @@ class ReactNativeSecureStorageAdapter extends BaseSecureStorageAdapter {
|
|
|
5449
3562
|
return;
|
|
5450
3563
|
}
|
|
5451
3564
|
catch {
|
|
5452
|
-
log$
|
|
3565
|
+
log$7.debug('expo-secure-store not available, trying react-native-keychain');
|
|
5453
3566
|
}
|
|
5454
3567
|
try {
|
|
5455
3568
|
const Keychain = require('react-native-keychain');
|
|
@@ -5458,7 +3571,7 @@ class ReactNativeSecureStorageAdapter extends BaseSecureStorageAdapter {
|
|
|
5458
3571
|
return;
|
|
5459
3572
|
}
|
|
5460
3573
|
catch {
|
|
5461
|
-
log$
|
|
3574
|
+
log$7.error('react-native-keychain not available');
|
|
5462
3575
|
}
|
|
5463
3576
|
throw new Error('No secure storage available. Please install expo-secure-store or react-native-keychain');
|
|
5464
3577
|
}
|
|
@@ -5476,7 +3589,7 @@ class ReactNativeSecureStorageAdapter extends BaseSecureStorageAdapter {
|
|
|
5476
3589
|
}
|
|
5477
3590
|
}
|
|
5478
3591
|
catch (error) {
|
|
5479
|
-
log$
|
|
3592
|
+
log$7.error('Failed to get secure item:', error);
|
|
5480
3593
|
}
|
|
5481
3594
|
return null;
|
|
5482
3595
|
}
|
|
@@ -5513,10 +3626,10 @@ class ReactNativeSecureStorageAdapter extends BaseSecureStorageAdapter {
|
|
|
5513
3626
|
}
|
|
5514
3627
|
}
|
|
5515
3628
|
async clear() {
|
|
5516
|
-
log$
|
|
3629
|
+
log$7.warn('Clear all secure items not fully implemented for React Native');
|
|
5517
3630
|
}
|
|
5518
3631
|
async getAllKeys() {
|
|
5519
|
-
log$
|
|
3632
|
+
log$7.warn('Get all secure keys not implemented for React Native');
|
|
5520
3633
|
return [];
|
|
5521
3634
|
}
|
|
5522
3635
|
async isAvailable() {
|
|
@@ -5734,7 +3847,7 @@ class NodeNetworkMonitor extends NetworkBase {
|
|
|
5734
3847
|
}
|
|
5735
3848
|
}
|
|
5736
3849
|
|
|
5737
|
-
const log$
|
|
3850
|
+
const log$6 = createPrefixedLogger('RN-NETWORK');
|
|
5738
3851
|
/**
|
|
5739
3852
|
* React Native network monitor using RxJS
|
|
5740
3853
|
* Supports both @react-native-community/netinfo and expo-network
|
|
@@ -5747,7 +3860,7 @@ class ReactNativeNetworkMonitor extends NetworkBase {
|
|
|
5747
3860
|
this.moduleReady$ = new Subject();
|
|
5748
3861
|
this.isExpo = detectPlatform().isExpo;
|
|
5749
3862
|
this.init().catch((error) => {
|
|
5750
|
-
log$
|
|
3863
|
+
log$6.error('Network monitor initialization failed:', error);
|
|
5751
3864
|
});
|
|
5752
3865
|
}
|
|
5753
3866
|
async init() {
|
|
@@ -5766,10 +3879,10 @@ class ReactNativeNetworkMonitor extends NetworkBase {
|
|
|
5766
3879
|
try {
|
|
5767
3880
|
const module = require('@react-native-community/netinfo');
|
|
5768
3881
|
this.netInfoModule = module.default || module;
|
|
5769
|
-
log$
|
|
3882
|
+
log$6.debug('Loaded @react-native-community/netinfo module');
|
|
5770
3883
|
}
|
|
5771
3884
|
catch (error) {
|
|
5772
|
-
log$
|
|
3885
|
+
log$6.error('Failed to load React Native NetInfo module:', error);
|
|
5773
3886
|
this.netInfoModule = null;
|
|
5774
3887
|
}
|
|
5775
3888
|
}
|
|
@@ -5777,10 +3890,10 @@ class ReactNativeNetworkMonitor extends NetworkBase {
|
|
|
5777
3890
|
try {
|
|
5778
3891
|
const module = require('expo-network');
|
|
5779
3892
|
this.netInfoModule = module.default || module;
|
|
5780
|
-
log$
|
|
3893
|
+
log$6.debug('Loaded expo-network module');
|
|
5781
3894
|
}
|
|
5782
3895
|
catch (error) {
|
|
5783
|
-
log$
|
|
3896
|
+
log$6.error('Failed to load Expo Network module:', error);
|
|
5784
3897
|
this.netInfoModule = null;
|
|
5785
3898
|
}
|
|
5786
3899
|
}
|
|
@@ -5797,16 +3910,16 @@ class ReactNativeNetworkMonitor extends NetworkBase {
|
|
|
5797
3910
|
}
|
|
5798
3911
|
const online = !!(state.isConnected && state.isInternetReachable !== false);
|
|
5799
3912
|
this.updateStatus(online);
|
|
5800
|
-
log$
|
|
3913
|
+
log$6.debug('Initial network state:', online ? 'online' : 'offline');
|
|
5801
3914
|
}
|
|
5802
3915
|
catch (error) {
|
|
5803
|
-
log$
|
|
3916
|
+
log$6.warn('Could not fetch initial network state:', error);
|
|
5804
3917
|
}
|
|
5805
3918
|
}
|
|
5806
3919
|
subscribeToStateChanges() {
|
|
5807
3920
|
if (!this.netInfoModule)
|
|
5808
3921
|
return;
|
|
5809
|
-
log$
|
|
3922
|
+
log$6.debug('Subscribing to network state changes');
|
|
5810
3923
|
const handleState = (state) => {
|
|
5811
3924
|
const online = !!(state.isConnected && (state.isInternetReachable ?? true));
|
|
5812
3925
|
this.updateStatus(online);
|
|
@@ -5848,7 +3961,7 @@ class ReactNativeNetworkMonitor extends NetworkBase {
|
|
|
5848
3961
|
};
|
|
5849
3962
|
}
|
|
5850
3963
|
catch (error) {
|
|
5851
|
-
log$
|
|
3964
|
+
log$6.error('Failed to fetch detailed network info:', error);
|
|
5852
3965
|
return null;
|
|
5853
3966
|
}
|
|
5854
3967
|
}
|
|
@@ -5984,7 +4097,7 @@ class CertificateValidator {
|
|
|
5984
4097
|
}
|
|
5985
4098
|
}
|
|
5986
4099
|
|
|
5987
|
-
const log$
|
|
4100
|
+
const log$5 = createPrefixedLogger('RN-MTLS');
|
|
5988
4101
|
/**
|
|
5989
4102
|
* React Native mTLS Adapter using @a-cube-io/expo-mutual-tls
|
|
5990
4103
|
*/
|
|
@@ -6006,7 +4119,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6006
4119
|
this.expoMTLS = ExpoMutualTls;
|
|
6007
4120
|
// Set up debug logging with the correct event signature
|
|
6008
4121
|
const debugListener = ExpoMutualTls.onDebugLog((event) => {
|
|
6009
|
-
log$
|
|
4122
|
+
log$5.debug(`${event.type}: ${event.message}`, {
|
|
6010
4123
|
method: event.method,
|
|
6011
4124
|
url: event.url,
|
|
6012
4125
|
statusCode: event.statusCode,
|
|
@@ -6016,28 +4129,28 @@ class ReactNativeMTLSAdapter {
|
|
|
6016
4129
|
this.eventListeners.push(debugListener);
|
|
6017
4130
|
// Set up error logging with the correct event signature
|
|
6018
4131
|
const errorListener = ExpoMutualTls.onError((event) => {
|
|
6019
|
-
log$
|
|
4132
|
+
log$5.error(event.message, {
|
|
6020
4133
|
code: event.code,
|
|
6021
4134
|
});
|
|
6022
4135
|
});
|
|
6023
4136
|
this.eventListeners.push(errorListener);
|
|
6024
4137
|
// Set up certificate expiry monitoring with the correct event signature
|
|
6025
4138
|
const expiryListener = ExpoMutualTls.onCertificateExpiry((event) => {
|
|
6026
|
-
log$
|
|
4139
|
+
log$5.warn(`Certificate ${event.subject} expires at ${new Date(event.expiry)}`, {
|
|
6027
4140
|
alias: event.alias,
|
|
6028
4141
|
warning: event.warning,
|
|
6029
4142
|
});
|
|
6030
4143
|
});
|
|
6031
4144
|
this.eventListeners.push(expiryListener);
|
|
6032
|
-
log$
|
|
4145
|
+
log$5.debug('Expo mTLS module loaded successfully');
|
|
6033
4146
|
}
|
|
6034
4147
|
catch (error) {
|
|
6035
|
-
log$
|
|
4148
|
+
log$5.warn('@a-cube-io/expo-mutual-tls not available:', error);
|
|
6036
4149
|
}
|
|
6037
4150
|
}
|
|
6038
4151
|
async isMTLSSupported() {
|
|
6039
4152
|
const supported = this.expoMTLS !== null;
|
|
6040
|
-
log$
|
|
4153
|
+
log$5.debug('mTLS support check:', {
|
|
6041
4154
|
supported,
|
|
6042
4155
|
platform: this.getPlatformInfo().platform,
|
|
6043
4156
|
moduleAvailable: !!this.expoMTLS,
|
|
@@ -6049,7 +4162,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6049
4162
|
throw new MTLSError(exports.MTLSErrorType.NOT_SUPPORTED, 'Expo mTLS module not available');
|
|
6050
4163
|
}
|
|
6051
4164
|
this.config = config;
|
|
6052
|
-
log$
|
|
4165
|
+
log$5.debug('Initialized with config:', {
|
|
6053
4166
|
baseUrl: config.baseUrl,
|
|
6054
4167
|
port: config.port,
|
|
6055
4168
|
timeout: config.timeout,
|
|
@@ -6063,7 +4176,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6063
4176
|
if (!this.config) {
|
|
6064
4177
|
throw new MTLSError(exports.MTLSErrorType.CONFIGURATION_ERROR, 'Adapter not initialized. Call initialize() first.');
|
|
6065
4178
|
}
|
|
6066
|
-
log$
|
|
4179
|
+
log$5.debug('Configuring certificate:', {
|
|
6067
4180
|
format: certificateData.format,
|
|
6068
4181
|
hasPassword: !!certificateData.password,
|
|
6069
4182
|
certificateLength: certificateData.certificate.length,
|
|
@@ -6080,14 +4193,14 @@ class ReactNativeMTLSAdapter {
|
|
|
6080
4193
|
'client-key-service', // keyService
|
|
6081
4194
|
true // enableLogging - let the native module handle its own debug logging
|
|
6082
4195
|
);
|
|
6083
|
-
log$
|
|
4196
|
+
log$5.debug('PEM services configured:', configResult);
|
|
6084
4197
|
if (!configResult.success) {
|
|
6085
4198
|
throw new MTLSError(exports.MTLSErrorType.CONFIGURATION_ERROR, `PEM configuration failed: ${configResult.state}`);
|
|
6086
4199
|
}
|
|
6087
4200
|
// Step 2: Store the actual PEM certificate and private key
|
|
6088
4201
|
const storeResult = await this.expoMTLS.storePEM(certificateData.certificate, certificateData.privateKey, certificateData.password // passphrase (optional)
|
|
6089
4202
|
);
|
|
6090
|
-
log$
|
|
4203
|
+
log$5.debug('PEM certificate store result:', storeResult);
|
|
6091
4204
|
if (!storeResult) {
|
|
6092
4205
|
throw new MTLSError(exports.MTLSErrorType.CERTIFICATE_INVALID, 'Failed to store PEM certificate');
|
|
6093
4206
|
}
|
|
@@ -6100,14 +4213,14 @@ class ReactNativeMTLSAdapter {
|
|
|
6100
4213
|
const configResult = await this.expoMTLS.configureP12('client-p12-service', // keychainService
|
|
6101
4214
|
true // enableLogging - let the native module handle its own debug logging
|
|
6102
4215
|
);
|
|
6103
|
-
log$
|
|
4216
|
+
log$5.debug('P12 service configured:', configResult);
|
|
6104
4217
|
if (!configResult.success) {
|
|
6105
4218
|
throw new MTLSError(exports.MTLSErrorType.CONFIGURATION_ERROR, `P12 configuration failed: ${configResult.state}`);
|
|
6106
4219
|
}
|
|
6107
4220
|
// Step 2: Store the P12 certificate data
|
|
6108
4221
|
const storeResult = await this.expoMTLS.storeP12(certificateData.certificate, // P12 data in certificate field
|
|
6109
4222
|
certificateData.password);
|
|
6110
|
-
log$
|
|
4223
|
+
log$5.debug('P12 certificate store result:', storeResult);
|
|
6111
4224
|
if (!storeResult) {
|
|
6112
4225
|
throw new MTLSError(exports.MTLSErrorType.CERTIFICATE_INVALID, 'Failed to store P12 certificate');
|
|
6113
4226
|
}
|
|
@@ -6121,7 +4234,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6121
4234
|
if (error instanceof MTLSError) {
|
|
6122
4235
|
throw error;
|
|
6123
4236
|
}
|
|
6124
|
-
log$
|
|
4237
|
+
log$5.error('Certificate configuration failed:', error);
|
|
6125
4238
|
throw new MTLSError(exports.MTLSErrorType.CONFIGURATION_ERROR, 'Failed to configure certificate', error);
|
|
6126
4239
|
}
|
|
6127
4240
|
}
|
|
@@ -6132,38 +4245,38 @@ class ReactNativeMTLSAdapter {
|
|
|
6132
4245
|
try {
|
|
6133
4246
|
// Use static method call
|
|
6134
4247
|
const hasCert = await this.expoMTLS.hasCertificate();
|
|
6135
|
-
log$
|
|
4248
|
+
log$5.debug('Certificate availability check:', hasCert);
|
|
6136
4249
|
return hasCert;
|
|
6137
4250
|
}
|
|
6138
4251
|
catch (error) {
|
|
6139
|
-
log$
|
|
4252
|
+
log$5.error('Certificate check failed:', error);
|
|
6140
4253
|
return false;
|
|
6141
4254
|
}
|
|
6142
4255
|
}
|
|
6143
4256
|
async getCertificateInfo() {
|
|
6144
4257
|
if (!this.expoMTLS) {
|
|
6145
|
-
log$
|
|
4258
|
+
log$5.debug('Certificate info requested but module not available');
|
|
6146
4259
|
return null;
|
|
6147
4260
|
}
|
|
6148
4261
|
try {
|
|
6149
4262
|
const hasCert = await this.hasCertificate();
|
|
6150
4263
|
if (!hasCert) {
|
|
6151
|
-
log$
|
|
4264
|
+
log$5.debug('No certificate stored');
|
|
6152
4265
|
return null;
|
|
6153
4266
|
}
|
|
6154
4267
|
// Use getCertificatesInfo to retrieve information about stored certificates
|
|
6155
4268
|
const result = await this.expoMTLS.getCertificatesInfo();
|
|
6156
4269
|
if (!result || !result.certificates || result.certificates.length === 0) {
|
|
6157
|
-
log$
|
|
4270
|
+
log$5.debug('No certificate information available');
|
|
6158
4271
|
return null;
|
|
6159
4272
|
}
|
|
6160
4273
|
// Get the first certificate (primary client certificate)
|
|
6161
4274
|
const cert = result.certificates[0];
|
|
6162
4275
|
if (!cert) {
|
|
6163
|
-
log$
|
|
4276
|
+
log$5.debug('Certificate data is empty');
|
|
6164
4277
|
return null;
|
|
6165
4278
|
}
|
|
6166
|
-
log$
|
|
4279
|
+
log$5.debug('Retrieved certificate info:', {
|
|
6167
4280
|
subject: cert.subject.commonName,
|
|
6168
4281
|
issuer: cert.issuer.commonName,
|
|
6169
4282
|
validFrom: new Date(cert.validFrom),
|
|
@@ -6182,7 +4295,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6182
4295
|
};
|
|
6183
4296
|
}
|
|
6184
4297
|
catch (error) {
|
|
6185
|
-
log$
|
|
4298
|
+
log$5.error('Failed to get certificate info:', error);
|
|
6186
4299
|
return null;
|
|
6187
4300
|
}
|
|
6188
4301
|
}
|
|
@@ -6193,7 +4306,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6193
4306
|
*/
|
|
6194
4307
|
async parseCertificateData(certificateData) {
|
|
6195
4308
|
if (!this.expoMTLS) {
|
|
6196
|
-
log$
|
|
4309
|
+
log$5.debug('Parse certificate: Module not available');
|
|
6197
4310
|
return null;
|
|
6198
4311
|
}
|
|
6199
4312
|
try {
|
|
@@ -6210,14 +4323,14 @@ class ReactNativeMTLSAdapter {
|
|
|
6210
4323
|
else {
|
|
6211
4324
|
throw new MTLSError(exports.MTLSErrorType.CERTIFICATE_INVALID, `Unsupported certificate format: ${certificateData.format}`);
|
|
6212
4325
|
}
|
|
6213
|
-
log$
|
|
4326
|
+
log$5.debug('Certificate parsed successfully:', {
|
|
6214
4327
|
certificateCount: result.certificates.length,
|
|
6215
4328
|
subjects: result.certificates.map((cert) => cert.subject.commonName),
|
|
6216
4329
|
});
|
|
6217
4330
|
return result;
|
|
6218
4331
|
}
|
|
6219
4332
|
catch (error) {
|
|
6220
|
-
log$
|
|
4333
|
+
log$5.error('Failed to parse certificate:', error);
|
|
6221
4334
|
if (error instanceof MTLSError) {
|
|
6222
4335
|
throw error;
|
|
6223
4336
|
}
|
|
@@ -6232,7 +4345,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6232
4345
|
if (!hasCert) {
|
|
6233
4346
|
throw new MTLSError(exports.MTLSErrorType.CERTIFICATE_NOT_FOUND, 'No certificate configured');
|
|
6234
4347
|
}
|
|
6235
|
-
log$
|
|
4348
|
+
log$5.debug('Making mTLS request:', {
|
|
6236
4349
|
method: requestConfig.method || 'GET',
|
|
6237
4350
|
url: requestConfig.url,
|
|
6238
4351
|
headers: requestConfig.headers,
|
|
@@ -6240,7 +4353,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6240
4353
|
responseType: requestConfig.responseType,
|
|
6241
4354
|
});
|
|
6242
4355
|
if (requestConfig.data) {
|
|
6243
|
-
log$
|
|
4356
|
+
log$5.debug('mTLS request body:', requestConfig.data);
|
|
6244
4357
|
}
|
|
6245
4358
|
try {
|
|
6246
4359
|
const response = await this.expoMTLS.request(requestConfig.url, {
|
|
@@ -6249,7 +4362,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6249
4362
|
body: requestConfig.data ? JSON.stringify(requestConfig.data) : undefined,
|
|
6250
4363
|
responseType: requestConfig.responseType,
|
|
6251
4364
|
});
|
|
6252
|
-
log$
|
|
4365
|
+
log$5.debug('mTLS request successful:', response);
|
|
6253
4366
|
if (!response.success) {
|
|
6254
4367
|
throw new MTLSError(exports.MTLSErrorType.CONNECTION_FAILED, `mTLS request failed: ${response.statusMessage} (${response.statusCode})`, undefined, response.statusCode);
|
|
6255
4368
|
}
|
|
@@ -6261,7 +4374,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6261
4374
|
data = JSON.parse(response.body);
|
|
6262
4375
|
}
|
|
6263
4376
|
catch (parseError) {
|
|
6264
|
-
log$
|
|
4377
|
+
log$5.warn('Failed to parse JSON response:', parseError);
|
|
6265
4378
|
// If parsing fails, keep raw body
|
|
6266
4379
|
}
|
|
6267
4380
|
}
|
|
@@ -6278,7 +4391,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6278
4391
|
};
|
|
6279
4392
|
}
|
|
6280
4393
|
catch (error) {
|
|
6281
|
-
log$
|
|
4394
|
+
log$5.error('mTLS request failed:', error);
|
|
6282
4395
|
throw new MTLSError(exports.MTLSErrorType.CONNECTION_FAILED, 'mTLS request failed', error);
|
|
6283
4396
|
}
|
|
6284
4397
|
}
|
|
@@ -6291,18 +4404,18 @@ class ReactNativeMTLSAdapter {
|
|
|
6291
4404
|
*/
|
|
6292
4405
|
async testConnection() {
|
|
6293
4406
|
if (!this.expoMTLS || !this.config) {
|
|
6294
|
-
log$
|
|
4407
|
+
log$5.debug('Diagnostic test: No mTLS module or config available');
|
|
6295
4408
|
return false;
|
|
6296
4409
|
}
|
|
6297
4410
|
try {
|
|
6298
4411
|
const hasCert = await this.hasCertificate();
|
|
6299
4412
|
if (!hasCert) {
|
|
6300
|
-
log$
|
|
4413
|
+
log$5.debug('Diagnostic test: No certificate configured');
|
|
6301
4414
|
return false;
|
|
6302
4415
|
}
|
|
6303
|
-
log$
|
|
4416
|
+
log$5.debug('Running diagnostic test (may fail even if mTLS works):', this.config.baseUrl);
|
|
6304
4417
|
const result = await this.expoMTLS.testConnection(this.config.baseUrl);
|
|
6305
|
-
log$
|
|
4418
|
+
log$5.debug('Diagnostic test result (NOT validation):', {
|
|
6306
4419
|
success: result.success,
|
|
6307
4420
|
statusCode: result.statusCode,
|
|
6308
4421
|
statusMessage: result.statusMessage,
|
|
@@ -6313,13 +4426,13 @@ class ReactNativeMTLSAdapter {
|
|
|
6313
4426
|
return result.success;
|
|
6314
4427
|
}
|
|
6315
4428
|
catch (error) {
|
|
6316
|
-
log$
|
|
4429
|
+
log$5.warn('Diagnostic test failed (this is expected):', error);
|
|
6317
4430
|
return false;
|
|
6318
4431
|
}
|
|
6319
4432
|
}
|
|
6320
4433
|
async removeCertificate() {
|
|
6321
4434
|
if (!this.expoMTLS) {
|
|
6322
|
-
log$
|
|
4435
|
+
log$5.debug('Remove certificate: Module not available');
|
|
6323
4436
|
return;
|
|
6324
4437
|
}
|
|
6325
4438
|
try {
|
|
@@ -6328,10 +4441,10 @@ class ReactNativeMTLSAdapter {
|
|
|
6328
4441
|
this.isConfigured = false;
|
|
6329
4442
|
// Cleanup event listeners
|
|
6330
4443
|
this.cleanupEventListeners();
|
|
6331
|
-
log$
|
|
4444
|
+
log$5.debug('Certificate removed successfully');
|
|
6332
4445
|
}
|
|
6333
4446
|
catch (error) {
|
|
6334
|
-
log$
|
|
4447
|
+
log$5.error('Failed to remove certificate:', error);
|
|
6335
4448
|
throw new MTLSError(exports.MTLSErrorType.CONFIGURATION_ERROR, 'Failed to remove certificate', error);
|
|
6336
4449
|
}
|
|
6337
4450
|
}
|
|
@@ -6340,7 +4453,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6340
4453
|
*/
|
|
6341
4454
|
cleanupEventListeners() {
|
|
6342
4455
|
if (this.eventListeners.length > 0) {
|
|
6343
|
-
log$
|
|
4456
|
+
log$5.debug(`Cleaning up ${this.eventListeners.length} event listeners`);
|
|
6344
4457
|
}
|
|
6345
4458
|
// Remove individual listeners if they have remove methods
|
|
6346
4459
|
this.eventListeners.forEach((listener) => {
|
|
@@ -6377,7 +4490,7 @@ class ReactNativeMTLSAdapter {
|
|
|
6377
4490
|
}
|
|
6378
4491
|
}
|
|
6379
4492
|
|
|
6380
|
-
const log$
|
|
4493
|
+
const log$4 = createPrefixedLogger('WEB-MTLS');
|
|
6381
4494
|
/**
|
|
6382
4495
|
* Web mTLS Adapter - Graceful fallback for web browsers
|
|
6383
4496
|
*
|
|
@@ -6391,13 +4504,13 @@ const log$6 = createPrefixedLogger('WEB-MTLS');
|
|
|
6391
4504
|
*/
|
|
6392
4505
|
class WebMTLSAdapter {
|
|
6393
4506
|
constructor() {
|
|
6394
|
-
log$
|
|
6395
|
-
log$
|
|
4507
|
+
log$4.warn('Web browsers do not support programmatic mTLS configuration');
|
|
4508
|
+
log$4.info('Use JWT authentication or configure client certificates in browser settings');
|
|
6396
4509
|
}
|
|
6397
4510
|
async isMTLSSupported() {
|
|
6398
4511
|
// mTLS is not supported programmatically in web browsers
|
|
6399
4512
|
const supported = false;
|
|
6400
|
-
log$
|
|
4513
|
+
log$4.debug('mTLS support check:', {
|
|
6401
4514
|
supported,
|
|
6402
4515
|
platform: this.getPlatformInfo().platform,
|
|
6403
4516
|
reason: 'Browser security model prevents programmatic certificate configuration',
|
|
@@ -6406,14 +4519,14 @@ class WebMTLSAdapter {
|
|
|
6406
4519
|
return supported;
|
|
6407
4520
|
}
|
|
6408
4521
|
async initialize(config) {
|
|
6409
|
-
log$
|
|
4522
|
+
log$4.warn('Initialized but mTLS not available in web browsers:', {
|
|
6410
4523
|
baseUrl: config.baseUrl,
|
|
6411
4524
|
port: config.port,
|
|
6412
4525
|
recommendation: 'Use standard HTTPS with JWT authentication',
|
|
6413
4526
|
});
|
|
6414
4527
|
}
|
|
6415
4528
|
async configureCertificate(certificateData) {
|
|
6416
|
-
log$
|
|
4529
|
+
log$4.error('Certificate configuration attempted:', {
|
|
6417
4530
|
format: certificateData.format,
|
|
6418
4531
|
reason: 'Not supported in web browsers',
|
|
6419
4532
|
alternatives: [
|
|
@@ -6428,15 +4541,15 @@ class WebMTLSAdapter {
|
|
|
6428
4541
|
}
|
|
6429
4542
|
async hasCertificate() {
|
|
6430
4543
|
// We cannot detect if the browser has certificates configured
|
|
6431
|
-
log$
|
|
4544
|
+
log$4.debug('Certificate availability check: Cannot detect browser certificates programmatically');
|
|
6432
4545
|
return false;
|
|
6433
4546
|
}
|
|
6434
4547
|
async getCertificateInfo() {
|
|
6435
|
-
log$
|
|
4548
|
+
log$4.debug('Certificate info requested: Not accessible in web browsers');
|
|
6436
4549
|
return null;
|
|
6437
4550
|
}
|
|
6438
4551
|
async request(requestConfig) {
|
|
6439
|
-
log$
|
|
4552
|
+
log$4.error('mTLS request attempted:', {
|
|
6440
4553
|
method: requestConfig.method,
|
|
6441
4554
|
url: requestConfig.url,
|
|
6442
4555
|
reason: 'Not supported in web browsers',
|
|
@@ -6451,11 +4564,11 @@ class WebMTLSAdapter {
|
|
|
6451
4564
|
'are properly configured in the browser certificate store.');
|
|
6452
4565
|
}
|
|
6453
4566
|
async testConnection() {
|
|
6454
|
-
log$
|
|
4567
|
+
log$4.debug('Connection test: mTLS not available in web browsers');
|
|
6455
4568
|
return false;
|
|
6456
4569
|
}
|
|
6457
4570
|
async removeCertificate() {
|
|
6458
|
-
log$
|
|
4571
|
+
log$4.debug('Remove certificate: No certificates to remove (not supported in web browsers)');
|
|
6459
4572
|
// No-op - cannot remove certificates programmatically in browsers
|
|
6460
4573
|
}
|
|
6461
4574
|
/**
|
|
@@ -6463,7 +4576,7 @@ class WebMTLSAdapter {
|
|
|
6463
4576
|
* Always returns null for web browsers as mTLS is not supported
|
|
6464
4577
|
*/
|
|
6465
4578
|
getBaseUrl() {
|
|
6466
|
-
log$
|
|
4579
|
+
log$4.debug('Base URL requested: Not supported in web browsers');
|
|
6467
4580
|
return null;
|
|
6468
4581
|
}
|
|
6469
4582
|
getPlatformInfo() {
|
|
@@ -6496,7 +4609,7 @@ class WebMTLSAdapter {
|
|
|
6496
4609
|
}
|
|
6497
4610
|
}
|
|
6498
4611
|
|
|
6499
|
-
const log$
|
|
4612
|
+
const log$3 = createPrefixedLogger('MTLS-LOADER');
|
|
6500
4613
|
function loadMTLSAdapter(platform, config) {
|
|
6501
4614
|
try {
|
|
6502
4615
|
let adapter;
|
|
@@ -6530,7 +4643,7 @@ function loadMTLSAdapter(platform, config) {
|
|
|
6530
4643
|
return adapter;
|
|
6531
4644
|
}
|
|
6532
4645
|
catch (error) {
|
|
6533
|
-
log$
|
|
4646
|
+
log$3.warn(`mTLS adapter not available for platform ${platform}:`, error);
|
|
6534
4647
|
return null;
|
|
6535
4648
|
}
|
|
6536
4649
|
}
|
|
@@ -6540,7 +4653,7 @@ async function initializeAdapterAsync(adapter, config) {
|
|
|
6540
4653
|
if (isSupported) {
|
|
6541
4654
|
await adapter.initialize(config);
|
|
6542
4655
|
const platformInfo = adapter.getPlatformInfo();
|
|
6543
|
-
log$
|
|
4656
|
+
log$3.debug('mTLS adapter initialized:', {
|
|
6544
4657
|
platform: platformInfo.platform,
|
|
6545
4658
|
mtlsSupported: platformInfo.mtlsSupported,
|
|
6546
4659
|
certificateStorage: platformInfo.certificateStorage,
|
|
@@ -6549,31 +4662,28 @@ async function initializeAdapterAsync(adapter, config) {
|
|
|
6549
4662
|
}
|
|
6550
4663
|
}
|
|
6551
4664
|
catch (error) {
|
|
6552
|
-
log$
|
|
4665
|
+
log$3.warn('Failed to initialize mTLS adapter:', error);
|
|
6553
4666
|
}
|
|
6554
4667
|
}
|
|
6555
4668
|
|
|
6556
|
-
const log$
|
|
4669
|
+
const log$2 = createPrefixedLogger('ADAPTER-LOADER');
|
|
6557
4670
|
function loadPlatformAdapters(options = {}) {
|
|
6558
4671
|
const { mtlsConfig } = options;
|
|
6559
4672
|
const { platform } = detectPlatform();
|
|
6560
|
-
log$
|
|
4673
|
+
log$2.debug('Loading adapters for platform:', platform);
|
|
6561
4674
|
const storageAdapters = loadStorageAdapters(platform);
|
|
6562
4675
|
const networkMonitor = loadNetworkMonitor(platform);
|
|
6563
|
-
const cache = loadCacheAdapter(platform);
|
|
6564
4676
|
const mtls = loadMTLSAdapter(platform, mtlsConfig);
|
|
6565
|
-
log$
|
|
4677
|
+
log$2.debug('Adapters loaded:', {
|
|
6566
4678
|
platform,
|
|
6567
4679
|
hasStorage: !!storageAdapters.storage,
|
|
6568
4680
|
hasSecureStorage: !!storageAdapters.secureStorage,
|
|
6569
4681
|
hasNetworkMonitor: !!networkMonitor,
|
|
6570
|
-
hasCache: !!cache,
|
|
6571
4682
|
hasMTLS: !!mtls,
|
|
6572
4683
|
});
|
|
6573
4684
|
return {
|
|
6574
4685
|
...storageAdapters,
|
|
6575
4686
|
networkMonitor,
|
|
6576
|
-
cache,
|
|
6577
4687
|
mtls: mtls || undefined,
|
|
6578
4688
|
};
|
|
6579
4689
|
}
|
|
@@ -6590,12 +4700,9 @@ function createACubeMTLSConfig(baseUrl, timeout, autoInitialize = true, forcePor
|
|
|
6590
4700
|
|
|
6591
4701
|
const DI_TOKENS = {
|
|
6592
4702
|
HTTP_PORT: Symbol('HTTP_PORT'),
|
|
6593
|
-
BASE_HTTP_PORT: Symbol('BASE_HTTP_PORT'),
|
|
6594
4703
|
STORAGE_PORT: Symbol('STORAGE_PORT'),
|
|
6595
4704
|
SECURE_STORAGE_PORT: Symbol('SECURE_STORAGE_PORT'),
|
|
6596
4705
|
NETWORK_PORT: Symbol('NETWORK_PORT'),
|
|
6597
|
-
CACHE_PORT: Symbol('CACHE_PORT'),
|
|
6598
|
-
CACHE_KEY_GENERATOR: Symbol('CACHE_KEY_GENERATOR'),
|
|
6599
4706
|
MTLS_PORT: Symbol('MTLS_PORT'),
|
|
6600
4707
|
TOKEN_STORAGE_PORT: Symbol('TOKEN_STORAGE_PORT'),
|
|
6601
4708
|
RECEIPT_REPOSITORY: Symbol('RECEIPT_REPOSITORY'),
|
|
@@ -6613,7 +4720,6 @@ const DI_TOKENS = {
|
|
|
6613
4720
|
AUTH_SERVICE: Symbol('AUTH_SERVICE'),
|
|
6614
4721
|
AUTHENTICATION_SERVICE: Symbol('AUTHENTICATION_SERVICE'),
|
|
6615
4722
|
CERTIFICATE_SERVICE: Symbol('CERTIFICATE_SERVICE'),
|
|
6616
|
-
OFFLINE_SERVICE: Symbol('OFFLINE_SERVICE'),
|
|
6617
4723
|
NOTIFICATION_SERVICE: Symbol('NOTIFICATION_SERVICE'),
|
|
6618
4724
|
TELEMETRY_SERVICE: Symbol('TELEMETRY_SERVICE'),
|
|
6619
4725
|
};
|
|
@@ -7588,468 +5694,6 @@ class TelemetryRepositoryImpl {
|
|
|
7588
5694
|
}
|
|
7589
5695
|
}
|
|
7590
5696
|
|
|
7591
|
-
const log$3 = createPrefixedLogger('CACHE-KEY');
|
|
7592
|
-
const URL_PATTERNS = [
|
|
7593
|
-
// Receipt (mf1) - specific patterns first
|
|
7594
|
-
{
|
|
7595
|
-
pattern: /^\/mf1\/receipts\/([^/]+)\/returnable-items$/,
|
|
7596
|
-
resource: 'receipt',
|
|
7597
|
-
action: 'returnable',
|
|
7598
|
-
},
|
|
7599
|
-
{
|
|
7600
|
-
pattern: /^\/mf1\/receipts\/([^/]+)\/details$/,
|
|
7601
|
-
resource: 'receipt',
|
|
7602
|
-
action: 'details',
|
|
7603
|
-
},
|
|
7604
|
-
{ pattern: /^\/mf1\/receipts\/([^/]+)$/, resource: 'receipt' },
|
|
7605
|
-
{
|
|
7606
|
-
pattern: /^\/mf1\/pems\/([^/]+)\/receipts$/,
|
|
7607
|
-
resource: 'receipt',
|
|
7608
|
-
parent: 'point-of-sale',
|
|
7609
|
-
isList: true,
|
|
7610
|
-
},
|
|
7611
|
-
{ pattern: /^\/mf1\/receipts$/, resource: 'receipt', isList: true },
|
|
7612
|
-
// Merchant (mf2)
|
|
7613
|
-
{ pattern: /^\/mf2\/merchants\/([^/]+)$/, resource: 'merchant' },
|
|
7614
|
-
{ pattern: /^\/mf2\/merchants$/, resource: 'merchant', isList: true },
|
|
7615
|
-
// Cashier (mf1)
|
|
7616
|
-
{ pattern: /^\/mf1\/cashiers\/me$/, resource: 'cashier', action: 'me' },
|
|
7617
|
-
{ pattern: /^\/mf1\/cashiers\/([^/]+)$/, resource: 'cashier' },
|
|
7618
|
-
{ pattern: /^\/mf1\/cashiers$/, resource: 'cashier', isList: true },
|
|
7619
|
-
// Cash Register (mf1)
|
|
7620
|
-
{ pattern: /^\/mf1\/cash-registers\/([^/]+)$/, resource: 'cash-register' },
|
|
7621
|
-
{ pattern: /^\/mf1\/cash-registers$/, resource: 'cash-register', isList: true },
|
|
7622
|
-
// Point of Sale (mf1)
|
|
7623
|
-
{ pattern: /^\/mf1\/pems\/([^/]+)$/, resource: 'point-of-sale' },
|
|
7624
|
-
{ pattern: /^\/mf1\/pems$/, resource: 'point-of-sale', isList: true },
|
|
7625
|
-
// Nested resources under merchant (mf2)
|
|
7626
|
-
{
|
|
7627
|
-
pattern: /^\/mf2\/merchants\/([^/]+)\/suppliers\/([^/]+)$/,
|
|
7628
|
-
resource: 'supplier',
|
|
7629
|
-
parent: 'merchant',
|
|
7630
|
-
},
|
|
7631
|
-
{
|
|
7632
|
-
pattern: /^\/mf2\/merchants\/([^/]+)\/suppliers$/,
|
|
7633
|
-
resource: 'supplier',
|
|
7634
|
-
parent: 'merchant',
|
|
7635
|
-
isList: true,
|
|
7636
|
-
},
|
|
7637
|
-
{
|
|
7638
|
-
pattern: /^\/mf2\/merchants\/([^/]+)\/daily-reports/,
|
|
7639
|
-
resource: 'daily-report',
|
|
7640
|
-
parent: 'merchant',
|
|
7641
|
-
isList: true,
|
|
7642
|
-
},
|
|
7643
|
-
{
|
|
7644
|
-
pattern: /^\/mf2\/merchants\/([^/]+)\/journals/,
|
|
7645
|
-
resource: 'journal',
|
|
7646
|
-
parent: 'merchant',
|
|
7647
|
-
isList: true,
|
|
7648
|
-
},
|
|
7649
|
-
// PEM (mf2)
|
|
7650
|
-
{
|
|
7651
|
-
pattern: /^\/mf2\/pems\/([^/]+)\/certificates$/,
|
|
7652
|
-
resource: 'pem',
|
|
7653
|
-
action: 'certificates',
|
|
7654
|
-
},
|
|
7655
|
-
{ pattern: /^\/mf2\/pems\/([^/]+)$/, resource: 'pem' },
|
|
7656
|
-
// Others
|
|
7657
|
-
{ pattern: /^\/mf1\/notifications/, resource: 'notification', isList: true },
|
|
7658
|
-
{
|
|
7659
|
-
pattern: /^\/mf1\/pems\/([^/]+)\/telemetry$/,
|
|
7660
|
-
resource: 'telemetry',
|
|
7661
|
-
},
|
|
7662
|
-
];
|
|
7663
|
-
const DEFAULT_TTL_CONFIG = {
|
|
7664
|
-
// Data that rarely changes - 30 min TTL for items only
|
|
7665
|
-
merchant: { ttlMs: 30 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7666
|
-
'point-of-sale': { ttlMs: 30 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7667
|
-
'cash-register': { ttlMs: 30 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7668
|
-
pem: { ttlMs: 30 * 60 * 1000, cacheList: false, cacheItem: false },
|
|
7669
|
-
// Data that changes moderately - 10 min TTL for items only
|
|
7670
|
-
cashier: { ttlMs: 10 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7671
|
-
supplier: { ttlMs: 10 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7672
|
-
// Data that can change - 5 min TTL for items only
|
|
7673
|
-
receipt: { ttlMs: 5 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7674
|
-
'daily-report': { ttlMs: 5 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7675
|
-
journal: { ttlMs: 5 * 60 * 1000, cacheList: false, cacheItem: true },
|
|
7676
|
-
// Real-time data - 1 min TTL
|
|
7677
|
-
notification: { ttlMs: 1 * 60 * 1000, cacheList: false, cacheItem: false },
|
|
7678
|
-
telemetry: { ttlMs: 1 * 60 * 1000, cacheList: false, cacheItem: false },
|
|
7679
|
-
};
|
|
7680
|
-
const DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes
|
|
7681
|
-
class CacheKeyGenerator {
|
|
7682
|
-
constructor(customConfig) {
|
|
7683
|
-
this.config = { ...DEFAULT_TTL_CONFIG, ...customConfig };
|
|
7684
|
-
log$3.info('CacheKeyGenerator initialized with config:', {
|
|
7685
|
-
resources: Object.keys(this.config),
|
|
7686
|
-
});
|
|
7687
|
-
}
|
|
7688
|
-
generate(url, params) {
|
|
7689
|
-
const parsed = this.parseUrl(url);
|
|
7690
|
-
if (!parsed) {
|
|
7691
|
-
// Fallback: use URL as key
|
|
7692
|
-
const paramStr = params ? this.serializeParams(params) : '';
|
|
7693
|
-
const key = paramStr ? `${url}?${paramStr}` : url;
|
|
7694
|
-
log$3.debug('URL not matched, using fallback key:', { url, key });
|
|
7695
|
-
return key;
|
|
7696
|
-
}
|
|
7697
|
-
const { resource, ids, action, isList, parent } = parsed;
|
|
7698
|
-
if (isList) {
|
|
7699
|
-
const paramStr = params ? this.serializeParams(params) : '';
|
|
7700
|
-
const parentPart = parent && ids.length > 0 ? `${parent}=${ids[0]}&` : '';
|
|
7701
|
-
const key = `${resource}:list:${parentPart}${paramStr}`;
|
|
7702
|
-
log$3.debug('Generated list cache key:', { url, key, resource });
|
|
7703
|
-
return key;
|
|
7704
|
-
}
|
|
7705
|
-
// Single item
|
|
7706
|
-
if (ids.length === 0 && action) {
|
|
7707
|
-
// Special case for endpoints like /cashiers/me
|
|
7708
|
-
const key = `${resource}:${action}`;
|
|
7709
|
-
log$3.debug('Generated special action cache key:', { url, key, resource, action });
|
|
7710
|
-
return key;
|
|
7711
|
-
}
|
|
7712
|
-
let key = `${resource}:${ids.join(':')}`;
|
|
7713
|
-
if (action) {
|
|
7714
|
-
key += `:${action}`;
|
|
7715
|
-
}
|
|
7716
|
-
log$3.debug('Generated item cache key:', { url, key, resource, ids, action });
|
|
7717
|
-
return key;
|
|
7718
|
-
}
|
|
7719
|
-
parseResource(url) {
|
|
7720
|
-
const parsed = this.parseUrl(url);
|
|
7721
|
-
return parsed?.resource;
|
|
7722
|
-
}
|
|
7723
|
-
getTTL(url) {
|
|
7724
|
-
const resource = this.parseResource(url);
|
|
7725
|
-
if (!resource) {
|
|
7726
|
-
log$3.debug('No resource found for URL, using default TTL:', { url, ttl: DEFAULT_TTL });
|
|
7727
|
-
return DEFAULT_TTL;
|
|
7728
|
-
}
|
|
7729
|
-
const ttl = this.config[resource].ttlMs;
|
|
7730
|
-
log$3.debug('TTL for resource:', { url, resource, ttlMs: ttl, ttlMin: ttl / 60000 });
|
|
7731
|
-
return ttl;
|
|
7732
|
-
}
|
|
7733
|
-
shouldCache(url) {
|
|
7734
|
-
const parsed = this.parseUrl(url);
|
|
7735
|
-
if (!parsed) {
|
|
7736
|
-
log$3.debug('URL not recognized, should not cache:', { url });
|
|
7737
|
-
return false;
|
|
7738
|
-
}
|
|
7739
|
-
const { resource, isList } = parsed;
|
|
7740
|
-
const config = this.config[resource];
|
|
7741
|
-
if (isList) {
|
|
7742
|
-
log$3.debug('List endpoint cache decision:', {
|
|
7743
|
-
url,
|
|
7744
|
-
resource,
|
|
7745
|
-
isList: true,
|
|
7746
|
-
shouldCache: config.cacheList,
|
|
7747
|
-
});
|
|
7748
|
-
return config.cacheList;
|
|
7749
|
-
}
|
|
7750
|
-
log$3.debug('Item endpoint cache decision:', {
|
|
7751
|
-
url,
|
|
7752
|
-
resource,
|
|
7753
|
-
isList: false,
|
|
7754
|
-
shouldCache: config.cacheItem,
|
|
7755
|
-
});
|
|
7756
|
-
return config.cacheItem;
|
|
7757
|
-
}
|
|
7758
|
-
getInvalidationPatterns(url, method) {
|
|
7759
|
-
const parsed = this.parseUrl(url);
|
|
7760
|
-
if (!parsed) {
|
|
7761
|
-
log$3.debug('No patterns to invalidate for URL:', { url, method });
|
|
7762
|
-
return [];
|
|
7763
|
-
}
|
|
7764
|
-
const { resource, ids, parent } = parsed;
|
|
7765
|
-
const patterns = [];
|
|
7766
|
-
// Always invalidate list on mutations
|
|
7767
|
-
if (method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE') {
|
|
7768
|
-
if (parent && ids.length > 0) {
|
|
7769
|
-
patterns.push(`${resource}:list:${parent}=${ids[0]}*`);
|
|
7770
|
-
}
|
|
7771
|
-
patterns.push(`${resource}:list:*`);
|
|
7772
|
-
}
|
|
7773
|
-
// Invalidate specific item on PUT/PATCH/DELETE
|
|
7774
|
-
if (method === 'PUT' || method === 'PATCH' || method === 'DELETE') {
|
|
7775
|
-
if (ids.length > 0) {
|
|
7776
|
-
patterns.push(`${resource}:${ids.join(':')}*`);
|
|
7777
|
-
}
|
|
7778
|
-
}
|
|
7779
|
-
// Special cases
|
|
7780
|
-
if (resource === 'cashier' && (method === 'PUT' || method === 'DELETE')) {
|
|
7781
|
-
patterns.push('cashier:me');
|
|
7782
|
-
}
|
|
7783
|
-
log$3.debug('Invalidation patterns:', { url, method, patterns });
|
|
7784
|
-
return patterns;
|
|
7785
|
-
}
|
|
7786
|
-
parseUrl(url) {
|
|
7787
|
-
// Remove query string for pattern matching
|
|
7788
|
-
const urlPath = url.split('?')[0];
|
|
7789
|
-
for (const pattern of URL_PATTERNS) {
|
|
7790
|
-
const match = urlPath?.match(pattern.pattern);
|
|
7791
|
-
if (match) {
|
|
7792
|
-
// Extract IDs from capture groups
|
|
7793
|
-
const ids = match.slice(1).filter(Boolean);
|
|
7794
|
-
return {
|
|
7795
|
-
resource: pattern.resource,
|
|
7796
|
-
ids,
|
|
7797
|
-
action: pattern.action,
|
|
7798
|
-
isList: pattern.isList,
|
|
7799
|
-
parent: pattern.parent,
|
|
7800
|
-
};
|
|
7801
|
-
}
|
|
7802
|
-
}
|
|
7803
|
-
return null;
|
|
7804
|
-
}
|
|
7805
|
-
serializeParams(params) {
|
|
7806
|
-
const sortedKeys = Object.keys(params).sort();
|
|
7807
|
-
const parts = [];
|
|
7808
|
-
for (const key of sortedKeys) {
|
|
7809
|
-
const value = params[key];
|
|
7810
|
-
if (value !== undefined && value !== null) {
|
|
7811
|
-
parts.push(`${key}=${String(value)}`);
|
|
7812
|
-
}
|
|
7813
|
-
}
|
|
7814
|
-
return parts.join('&');
|
|
7815
|
-
}
|
|
7816
|
-
}
|
|
7817
|
-
|
|
7818
|
-
const log$2 = createPrefixedLogger('CACHE');
|
|
7819
|
-
class CachingHttpDecorator {
|
|
7820
|
-
constructor(http, cache, keyGenerator, networkMonitor, config = {}) {
|
|
7821
|
-
this.http = http;
|
|
7822
|
-
this.cache = cache;
|
|
7823
|
-
this.keyGenerator = keyGenerator;
|
|
7824
|
-
this.networkMonitor = networkMonitor;
|
|
7825
|
-
this.config = config;
|
|
7826
|
-
this.currentOnlineState = true;
|
|
7827
|
-
this.authToken = null;
|
|
7828
|
-
log$2.info('CachingHttpDecorator initialized', {
|
|
7829
|
-
enabled: config.enabled !== false,
|
|
7830
|
-
hasNetworkMonitor: !!networkMonitor,
|
|
7831
|
-
});
|
|
7832
|
-
this.setupNetworkMonitoring();
|
|
7833
|
-
}
|
|
7834
|
-
setupNetworkMonitoring() {
|
|
7835
|
-
if (this.networkMonitor) {
|
|
7836
|
-
this.networkSubscription = this.networkMonitor.online$.subscribe((online) => {
|
|
7837
|
-
if (this.currentOnlineState !== online) {
|
|
7838
|
-
log$2.info('Network state changed:', { online });
|
|
7839
|
-
}
|
|
7840
|
-
this.currentOnlineState = online;
|
|
7841
|
-
});
|
|
7842
|
-
}
|
|
7843
|
-
}
|
|
7844
|
-
isOnline() {
|
|
7845
|
-
if (this.networkMonitor) {
|
|
7846
|
-
return this.currentOnlineState;
|
|
7847
|
-
}
|
|
7848
|
-
if (typeof navigator !== 'undefined' && 'onLine' in navigator) {
|
|
7849
|
-
return navigator.onLine;
|
|
7850
|
-
}
|
|
7851
|
-
return true;
|
|
7852
|
-
}
|
|
7853
|
-
async get(url, config) {
|
|
7854
|
-
const startTime = Date.now();
|
|
7855
|
-
// Check if caching is disabled globally
|
|
7856
|
-
if (this.config.enabled === false) {
|
|
7857
|
-
log$2.debug('GET (cache disabled globally):', { url });
|
|
7858
|
-
return this.http.get(url, config);
|
|
7859
|
-
}
|
|
7860
|
-
// Check if this URL should be cached
|
|
7861
|
-
const shouldCache = this.keyGenerator.shouldCache(url);
|
|
7862
|
-
if (!shouldCache) {
|
|
7863
|
-
log$2.debug('GET (not cacheable - likely a list endpoint):', { url });
|
|
7864
|
-
return this.http.get(url, config);
|
|
7865
|
-
}
|
|
7866
|
-
const cacheKey = this.keyGenerator.generate(url, config?.params);
|
|
7867
|
-
const ttl = this.keyGenerator.getTTL(url);
|
|
7868
|
-
const resource = this.keyGenerator.parseResource(url);
|
|
7869
|
-
log$2.info('GET request starting:', {
|
|
7870
|
-
url,
|
|
7871
|
-
resource,
|
|
7872
|
-
cacheKey,
|
|
7873
|
-
ttlMs: ttl,
|
|
7874
|
-
ttlMin: Math.round(ttl / 60000),
|
|
7875
|
-
params: config?.params,
|
|
7876
|
-
});
|
|
7877
|
-
// Check cache
|
|
7878
|
-
let cached = null;
|
|
7879
|
-
try {
|
|
7880
|
-
cached = await this.cache.get(cacheKey);
|
|
7881
|
-
if (cached) {
|
|
7882
|
-
log$2.debug('Cache entry found:', {
|
|
7883
|
-
cacheKey,
|
|
7884
|
-
timestamp: new Date(cached.timestamp).toISOString(),
|
|
7885
|
-
ageMs: Date.now() - cached.timestamp,
|
|
7886
|
-
});
|
|
7887
|
-
}
|
|
7888
|
-
else {
|
|
7889
|
-
log$2.debug('Cache entry not found:', { cacheKey });
|
|
7890
|
-
}
|
|
7891
|
-
}
|
|
7892
|
-
catch (error) {
|
|
7893
|
-
log$2.warn('Cache lookup failed:', {
|
|
7894
|
-
cacheKey,
|
|
7895
|
-
error: error instanceof Error ? error.message : error,
|
|
7896
|
-
});
|
|
7897
|
-
}
|
|
7898
|
-
if (cached) {
|
|
7899
|
-
const age = Date.now() - cached.timestamp;
|
|
7900
|
-
const isExpired = age >= ttl;
|
|
7901
|
-
log$2.debug('Cache analysis:', {
|
|
7902
|
-
cacheKey,
|
|
7903
|
-
ageMs: age,
|
|
7904
|
-
ageSec: Math.round(age / 1000),
|
|
7905
|
-
ttlMs: ttl,
|
|
7906
|
-
isExpired,
|
|
7907
|
-
isOnline: this.isOnline(),
|
|
7908
|
-
});
|
|
7909
|
-
// If within TTL, return cached data
|
|
7910
|
-
if (!isExpired) {
|
|
7911
|
-
const duration = Date.now() - startTime;
|
|
7912
|
-
log$2.info('CACHE HIT:', {
|
|
7913
|
-
url,
|
|
7914
|
-
cacheKey,
|
|
7915
|
-
ageMs: age,
|
|
7916
|
-
durationMs: duration,
|
|
7917
|
-
});
|
|
7918
|
-
return {
|
|
7919
|
-
data: cached.data,
|
|
7920
|
-
status: 200,
|
|
7921
|
-
headers: { 'x-cache': 'HIT' },
|
|
7922
|
-
};
|
|
7923
|
-
}
|
|
7924
|
-
// If offline and cache is stale, return stale data
|
|
7925
|
-
if (!this.isOnline()) {
|
|
7926
|
-
const duration = Date.now() - startTime;
|
|
7927
|
-
log$2.info('CACHE STALE (offline):', {
|
|
7928
|
-
url,
|
|
7929
|
-
cacheKey,
|
|
7930
|
-
ageMs: age,
|
|
7931
|
-
durationMs: duration,
|
|
7932
|
-
});
|
|
7933
|
-
return {
|
|
7934
|
-
data: cached.data,
|
|
7935
|
-
status: 200,
|
|
7936
|
-
headers: { 'x-cache': 'STALE' },
|
|
7937
|
-
};
|
|
7938
|
-
}
|
|
7939
|
-
log$2.debug('Cache expired, fetching fresh data:', { cacheKey, ageMs: age, ttlMs: ttl });
|
|
7940
|
-
}
|
|
7941
|
-
// Fetch fresh data
|
|
7942
|
-
try {
|
|
7943
|
-
log$2.debug('Fetching from network:', { url });
|
|
7944
|
-
const response = await this.http.get(url, config);
|
|
7945
|
-
// Cache the response
|
|
7946
|
-
try {
|
|
7947
|
-
await this.cache.set(cacheKey, response.data);
|
|
7948
|
-
log$2.debug('Response cached successfully:', { cacheKey });
|
|
7949
|
-
}
|
|
7950
|
-
catch (error) {
|
|
7951
|
-
log$2.error('Failed to cache response:', {
|
|
7952
|
-
cacheKey,
|
|
7953
|
-
error: error instanceof Error ? error.message : error,
|
|
7954
|
-
});
|
|
7955
|
-
}
|
|
7956
|
-
const duration = Date.now() - startTime;
|
|
7957
|
-
log$2.info('CACHE MISS (fetched fresh):', {
|
|
7958
|
-
url,
|
|
7959
|
-
cacheKey,
|
|
7960
|
-
status: response.status,
|
|
7961
|
-
durationMs: duration,
|
|
7962
|
-
});
|
|
7963
|
-
return {
|
|
7964
|
-
...response,
|
|
7965
|
-
headers: { ...response.headers, 'x-cache': 'MISS' },
|
|
7966
|
-
};
|
|
7967
|
-
}
|
|
7968
|
-
catch (error) {
|
|
7969
|
-
// On error, return stale cache if available
|
|
7970
|
-
if (cached) {
|
|
7971
|
-
const duration = Date.now() - startTime;
|
|
7972
|
-
log$2.warn('CACHE STALE (network error):', {
|
|
7973
|
-
url,
|
|
7974
|
-
cacheKey,
|
|
7975
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
7976
|
-
durationMs: duration,
|
|
7977
|
-
});
|
|
7978
|
-
return {
|
|
7979
|
-
data: cached.data,
|
|
7980
|
-
status: 200,
|
|
7981
|
-
headers: { 'x-cache': 'STALE' },
|
|
7982
|
-
};
|
|
7983
|
-
}
|
|
7984
|
-
log$2.error('Network error with no cache fallback:', {
|
|
7985
|
-
url,
|
|
7986
|
-
error: error instanceof Error ? error.message : error,
|
|
7987
|
-
});
|
|
7988
|
-
throw error;
|
|
7989
|
-
}
|
|
7990
|
-
}
|
|
7991
|
-
async post(url, data, config) {
|
|
7992
|
-
log$2.info('POST request:', { url });
|
|
7993
|
-
const response = await this.http.post(url, data, config);
|
|
7994
|
-
await this.invalidateRelated(url, 'POST');
|
|
7995
|
-
return response;
|
|
7996
|
-
}
|
|
7997
|
-
async put(url, data, config) {
|
|
7998
|
-
log$2.info('PUT request:', { url });
|
|
7999
|
-
const response = await this.http.put(url, data, config);
|
|
8000
|
-
await this.invalidateRelated(url, 'PUT');
|
|
8001
|
-
return response;
|
|
8002
|
-
}
|
|
8003
|
-
async patch(url, data, config) {
|
|
8004
|
-
log$2.info('PATCH request:', { url });
|
|
8005
|
-
const response = await this.http.patch(url, data, config);
|
|
8006
|
-
await this.invalidateRelated(url, 'PATCH');
|
|
8007
|
-
return response;
|
|
8008
|
-
}
|
|
8009
|
-
async delete(url, config) {
|
|
8010
|
-
log$2.info('DELETE request:', { url });
|
|
8011
|
-
const response = await this.http.delete(url, config);
|
|
8012
|
-
await this.invalidateRelated(url, 'DELETE');
|
|
8013
|
-
return response;
|
|
8014
|
-
}
|
|
8015
|
-
setAuthToken(token) {
|
|
8016
|
-
log$2.debug('CachingHttpDecorator.setAuthToken called:', {
|
|
8017
|
-
hasToken: !!token,
|
|
8018
|
-
tokenPrefix: token?.substring(0, 20),
|
|
8019
|
-
underlyingHttpType: this.http.constructor.name,
|
|
8020
|
-
});
|
|
8021
|
-
this.authToken = token;
|
|
8022
|
-
this.http.setAuthToken(token);
|
|
8023
|
-
}
|
|
8024
|
-
getAuthToken() {
|
|
8025
|
-
return this.authToken;
|
|
8026
|
-
}
|
|
8027
|
-
async invalidateRelated(url, method) {
|
|
8028
|
-
const patterns = this.keyGenerator.getInvalidationPatterns(url, method);
|
|
8029
|
-
if (patterns.length === 0) {
|
|
8030
|
-
log$2.debug('No cache patterns to invalidate:', { url, method });
|
|
8031
|
-
return;
|
|
8032
|
-
}
|
|
8033
|
-
log$2.info('Invalidating cache patterns:', { url, method, patterns });
|
|
8034
|
-
for (const pattern of patterns) {
|
|
8035
|
-
try {
|
|
8036
|
-
await this.cache.invalidate(pattern);
|
|
8037
|
-
log$2.debug('Cache pattern invalidated:', { pattern });
|
|
8038
|
-
}
|
|
8039
|
-
catch (error) {
|
|
8040
|
-
log$2.error('Failed to invalidate pattern:', {
|
|
8041
|
-
pattern,
|
|
8042
|
-
error: error instanceof Error ? error.message : error,
|
|
8043
|
-
});
|
|
8044
|
-
}
|
|
8045
|
-
}
|
|
8046
|
-
}
|
|
8047
|
-
destroy() {
|
|
8048
|
-
log$2.debug('CachingHttpDecorator destroyed');
|
|
8049
|
-
this.networkSubscription?.unsubscribe();
|
|
8050
|
-
}
|
|
8051
|
-
}
|
|
8052
|
-
|
|
8053
5697
|
const logJwt = createPrefixedLogger('HTTP-JWT');
|
|
8054
5698
|
const logMtls = createPrefixedLogger('HTTP-MTLS');
|
|
8055
5699
|
class AxiosHttpAdapter {
|
|
@@ -8319,7 +5963,6 @@ class SDKFactory {
|
|
|
8319
5963
|
baseUrl: config.baseUrl,
|
|
8320
5964
|
timeout: config.timeout,
|
|
8321
5965
|
});
|
|
8322
|
-
container.register(DI_TOKENS.BASE_HTTP_PORT, httpAdapter);
|
|
8323
5966
|
container.register(DI_TOKENS.HTTP_PORT, httpAdapter);
|
|
8324
5967
|
container.registerFactory(DI_TOKENS.RECEIPT_REPOSITORY, () => {
|
|
8325
5968
|
const http = container.get(DI_TOKENS.HTTP_PORT);
|
|
@@ -8367,23 +6010,6 @@ class SDKFactory {
|
|
|
8367
6010
|
});
|
|
8368
6011
|
return container;
|
|
8369
6012
|
}
|
|
8370
|
-
static registerCacheServices(container, cache, network) {
|
|
8371
|
-
container.register(DI_TOKENS.CACHE_PORT, cache);
|
|
8372
|
-
if (network) {
|
|
8373
|
-
container.register(DI_TOKENS.NETWORK_PORT, network);
|
|
8374
|
-
}
|
|
8375
|
-
const keyGenerator = new CacheKeyGenerator();
|
|
8376
|
-
container.register(DI_TOKENS.CACHE_KEY_GENERATOR, keyGenerator);
|
|
8377
|
-
const baseHttp = container.get(DI_TOKENS.BASE_HTTP_PORT);
|
|
8378
|
-
const cachingHttp = new CachingHttpDecorator(baseHttp, cache, keyGenerator, network);
|
|
8379
|
-
container.register(DI_TOKENS.HTTP_PORT, cachingHttp);
|
|
8380
|
-
}
|
|
8381
|
-
static getCacheKeyGenerator(container) {
|
|
8382
|
-
if (container.has(DI_TOKENS.CACHE_KEY_GENERATOR)) {
|
|
8383
|
-
return container.get(DI_TOKENS.CACHE_KEY_GENERATOR);
|
|
8384
|
-
}
|
|
8385
|
-
return undefined;
|
|
8386
|
-
}
|
|
8387
6013
|
static registerAuthServices(container, secureStorage, config) {
|
|
8388
6014
|
const tokenStorage = new TokenStorageAdapter(secureStorage);
|
|
8389
6015
|
container.register(DI_TOKENS.TOKEN_STORAGE_PORT, tokenStorage);
|
|
@@ -8451,7 +6077,6 @@ class ACubeSDK {
|
|
|
8451
6077
|
mtlsConfig,
|
|
8452
6078
|
});
|
|
8453
6079
|
log$1.info('Platform adapters loaded', {
|
|
8454
|
-
hasCache: !!this.adapters.cache,
|
|
8455
6080
|
hasNetworkMonitor: !!this.adapters.networkMonitor,
|
|
8456
6081
|
hasMtls: !!this.adapters.mtls,
|
|
8457
6082
|
hasSecureStorage: !!this.adapters.secureStorage,
|
|
@@ -8467,25 +6092,10 @@ class ACubeSDK {
|
|
|
8467
6092
|
this.container = SDKFactory.createContainer(factoryConfig);
|
|
8468
6093
|
log$1.debug('Registering auth services');
|
|
8469
6094
|
SDKFactory.registerAuthServices(this.container, this.adapters.secureStorage, factoryConfig);
|
|
8470
|
-
if (this.adapters.cache) {
|
|
8471
|
-
log$1.info('Registering cache services', {
|
|
8472
|
-
hasNetworkMonitor: !!this.adapters.networkMonitor,
|
|
8473
|
-
});
|
|
8474
|
-
SDKFactory.registerCacheServices(this.container, this.adapters.cache, this.adapters.networkMonitor);
|
|
8475
|
-
}
|
|
8476
|
-
else {
|
|
8477
|
-
log$1.debug('No cache adapter available, caching disabled');
|
|
8478
|
-
}
|
|
8479
6095
|
log$1.debug('Initializing certificate service');
|
|
8480
6096
|
this.certificateService = new CertificateService(this.adapters.secureStorage);
|
|
8481
6097
|
const tokenStorage = this.container.get(DI_TOKENS.TOKEN_STORAGE_PORT);
|
|
8482
6098
|
const httpPort = this.container.get(DI_TOKENS.HTTP_PORT);
|
|
8483
|
-
const baseHttpPort = this.container.get(DI_TOKENS.BASE_HTTP_PORT);
|
|
8484
|
-
log$1.debug('HTTP ports initialized', {
|
|
8485
|
-
httpPortType: httpPort.constructor.name,
|
|
8486
|
-
baseHttpPortType: baseHttpPort.constructor.name,
|
|
8487
|
-
areSameInstance: httpPort === baseHttpPort,
|
|
8488
|
-
});
|
|
8489
6099
|
log$1.debug('Initializing authentication service');
|
|
8490
6100
|
this.authService = new AuthenticationService(httpPort, tokenStorage, {
|
|
8491
6101
|
authUrl: this.config.getAuthUrl(),
|
|
@@ -8496,27 +6106,9 @@ class ACubeSDK {
|
|
|
8496
6106
|
this.events.onAuthError?.(new ACubeSDKError('AUTH_ERROR', error.message, error));
|
|
8497
6107
|
},
|
|
8498
6108
|
});
|
|
8499
|
-
log$1.debug('Initializing offline manager');
|
|
8500
|
-
const queueEvents = {
|
|
8501
|
-
onOperationAdded: (operation) => {
|
|
8502
|
-
this.events.onOfflineOperationAdded?.(operation.id);
|
|
8503
|
-
},
|
|
8504
|
-
onOperationCompleted: (result) => {
|
|
8505
|
-
this.events.onOfflineOperationCompleted?.(result.operation.id, result.success);
|
|
8506
|
-
},
|
|
8507
|
-
onOperationFailed: (result) => {
|
|
8508
|
-
this.events.onOfflineOperationCompleted?.(result.operation.id, false);
|
|
8509
|
-
},
|
|
8510
|
-
};
|
|
8511
|
-
this.offlineManager = new OfflineManager(this.adapters.storage, httpPort, this.adapters.networkMonitor, {
|
|
8512
|
-
syncInterval: 30000,
|
|
8513
|
-
}, queueEvents);
|
|
8514
6109
|
this.networkSubscription = this.adapters.networkMonitor.online$.subscribe((online) => {
|
|
8515
6110
|
this.currentOnlineState = online;
|
|
8516
6111
|
this.events.onNetworkStatusChanged?.(online);
|
|
8517
|
-
if (online && this.offlineManager) {
|
|
8518
|
-
this.offlineManager.sync().catch(() => { });
|
|
8519
|
-
}
|
|
8520
6112
|
});
|
|
8521
6113
|
const isAuth = await this.authService.isAuthenticated();
|
|
8522
6114
|
log$1.debug('Checking authentication status during init', { isAuthenticated: isAuth });
|
|
@@ -8534,12 +6126,12 @@ class ACubeSDK {
|
|
|
8534
6126
|
else {
|
|
8535
6127
|
log$1.warn('User not authenticated during SDK init - token will be set after login');
|
|
8536
6128
|
}
|
|
8537
|
-
if (this.adapters?.mtls && 'setMTLSAdapter' in
|
|
6129
|
+
if (this.adapters?.mtls && 'setMTLSAdapter' in httpPort) {
|
|
8538
6130
|
log$1.debug('Connecting mTLS adapter to HTTP port');
|
|
8539
|
-
const httpWithMtls =
|
|
6131
|
+
const httpWithMtls = httpPort;
|
|
8540
6132
|
httpWithMtls.setMTLSAdapter(this.adapters.mtls);
|
|
8541
6133
|
}
|
|
8542
|
-
if ('setAuthStrategy' in
|
|
6134
|
+
if ('setAuthStrategy' in httpPort) {
|
|
8543
6135
|
log$1.debug('Configuring auth strategy');
|
|
8544
6136
|
const jwtHandler = new JwtAuthHandler(tokenStorage);
|
|
8545
6137
|
const certificatePort = this.certificateService
|
|
@@ -8571,7 +6163,7 @@ class ACubeSDK {
|
|
|
8571
6163
|
},
|
|
8572
6164
|
};
|
|
8573
6165
|
const authStrategy = new AuthStrategy(jwtHandler, mtlsHandler, userProvider, this.adapters?.mtls || null);
|
|
8574
|
-
const httpWithStrategy =
|
|
6166
|
+
const httpWithStrategy = httpPort;
|
|
8575
6167
|
httpWithStrategy.setAuthStrategy(authStrategy);
|
|
8576
6168
|
}
|
|
8577
6169
|
if (this.adapters?.mtls && this.certificateService) {
|
|
@@ -8596,7 +6188,6 @@ class ACubeSDK {
|
|
|
8596
6188
|
}
|
|
8597
6189
|
this.isInitialized = true;
|
|
8598
6190
|
log$1.info('SDK initialized successfully', {
|
|
8599
|
-
hasCache: !!this.adapters.cache,
|
|
8600
6191
|
hasMtls: !!this.adapters.mtls,
|
|
8601
6192
|
});
|
|
8602
6193
|
}
|
|
@@ -8687,10 +6278,6 @@ class ACubeSDK {
|
|
|
8687
6278
|
this.ensureInitialized();
|
|
8688
6279
|
return await this.authService.isAuthenticated();
|
|
8689
6280
|
}
|
|
8690
|
-
getOfflineManager() {
|
|
8691
|
-
this.ensureInitialized();
|
|
8692
|
-
return this.offlineManager;
|
|
8693
|
-
}
|
|
8694
6281
|
isOnline() {
|
|
8695
6282
|
this.ensureInitialized();
|
|
8696
6283
|
return this.currentOnlineState;
|
|
@@ -8818,7 +6405,6 @@ class ACubeSDK {
|
|
|
8818
6405
|
}
|
|
8819
6406
|
destroy() {
|
|
8820
6407
|
this.networkSubscription?.unsubscribe();
|
|
8821
|
-
this.offlineManager?.destroy();
|
|
8822
6408
|
this.container?.clear();
|
|
8823
6409
|
this.isInitialized = false;
|
|
8824
6410
|
}
|
|
@@ -8843,6 +6429,7 @@ const INITIAL_STATE = {
|
|
|
8843
6429
|
remainingMs: 0,
|
|
8844
6430
|
},
|
|
8845
6431
|
lastNotification: null,
|
|
6432
|
+
certificateMissing: false,
|
|
8846
6433
|
};
|
|
8847
6434
|
class AppStateService {
|
|
8848
6435
|
get state$() {
|
|
@@ -8857,28 +6444,33 @@ class AppStateService {
|
|
|
8857
6444
|
get warning$() {
|
|
8858
6445
|
return this.state$.pipe(map((s) => s.warning), distinctUntilChanged((a, b) => a.active === b.active && a.remainingMs === b.remainingMs));
|
|
8859
6446
|
}
|
|
8860
|
-
|
|
6447
|
+
get certificateMissing$() {
|
|
6448
|
+
return this.state$.pipe(map((s) => s.certificateMissing), distinctUntilChanged());
|
|
6449
|
+
}
|
|
6450
|
+
constructor(notifications$, networkPort, certificateMissingInput$ = of(false)) {
|
|
8861
6451
|
this.notifications$ = notifications$;
|
|
8862
6452
|
this.networkPort = networkPort;
|
|
6453
|
+
this.certificateMissingInput$ = certificateMissingInput$;
|
|
8863
6454
|
this.stateSubject = new BehaviorSubject(INITIAL_STATE);
|
|
8864
6455
|
this.destroy$ = new Subject();
|
|
8865
6456
|
this.warningTimerId = null;
|
|
8866
6457
|
this.setupSubscriptions();
|
|
8867
6458
|
}
|
|
8868
6459
|
setupSubscriptions() {
|
|
8869
|
-
combineLatest([this.notifications$, this.networkPort.online$])
|
|
6460
|
+
combineLatest([this.notifications$, this.networkPort.online$, this.certificateMissingInput$])
|
|
8870
6461
|
.pipe(takeUntil(this.destroy$))
|
|
8871
|
-
.subscribe(([notifications, isOnline]) => {
|
|
8872
|
-
this.processState(notifications, isOnline);
|
|
6462
|
+
.subscribe(([notifications, isOnline, certificateMissing]) => {
|
|
6463
|
+
this.processState(notifications, isOnline, certificateMissing);
|
|
8873
6464
|
});
|
|
8874
6465
|
}
|
|
8875
|
-
processState(notifications, isOnline) {
|
|
6466
|
+
processState(notifications, isOnline, certificateMissing) {
|
|
8876
6467
|
if (!isOnline) {
|
|
8877
6468
|
this.updateState({
|
|
8878
6469
|
mode: 'OFFLINE',
|
|
8879
6470
|
isOnline: false,
|
|
8880
6471
|
warning: { active: false, blockAt: null, remainingMs: 0 },
|
|
8881
6472
|
lastNotification: null,
|
|
6473
|
+
certificateMissing,
|
|
8882
6474
|
});
|
|
8883
6475
|
this.stopWarningTimer();
|
|
8884
6476
|
return;
|
|
@@ -8940,6 +6532,7 @@ class AppStateService {
|
|
|
8940
6532
|
isOnline: true,
|
|
8941
6533
|
warning: warningState,
|
|
8942
6534
|
lastNotification,
|
|
6535
|
+
certificateMissing,
|
|
8943
6536
|
});
|
|
8944
6537
|
}
|
|
8945
6538
|
getLatestNotificationByCode(notifications) {
|
|
@@ -9097,7 +6690,6 @@ class TelemetryService {
|
|
|
9097
6690
|
this.events = events;
|
|
9098
6691
|
this.stateSubject = new BehaviorSubject({
|
|
9099
6692
|
data: null,
|
|
9100
|
-
isCached: false,
|
|
9101
6693
|
isLoading: false,
|
|
9102
6694
|
lastFetchedAt: null,
|
|
9103
6695
|
});
|
|
@@ -9167,7 +6759,6 @@ class TelemetryService {
|
|
|
9167
6759
|
const data = await this.repository.getTelemetry(this.currentPemId);
|
|
9168
6760
|
const newState = {
|
|
9169
6761
|
data,
|
|
9170
|
-
isCached: false,
|
|
9171
6762
|
isLoading: false,
|
|
9172
6763
|
lastFetchedAt: Date.now(),
|
|
9173
6764
|
};
|
|
@@ -9192,7 +6783,6 @@ class TelemetryService {
|
|
|
9192
6783
|
clearTelemetry() {
|
|
9193
6784
|
this.stateSubject.next({
|
|
9194
6785
|
data: null,
|
|
9195
|
-
isCached: false,
|
|
9196
6786
|
isLoading: false,
|
|
9197
6787
|
lastFetchedAt: null,
|
|
9198
6788
|
});
|
|
@@ -9205,6 +6795,7 @@ class TelemetryService {
|
|
|
9205
6795
|
}
|
|
9206
6796
|
}
|
|
9207
6797
|
|
|
6798
|
+
const log = createPrefixedLogger('SDK-MANAGER');
|
|
9208
6799
|
/**
|
|
9209
6800
|
* SDKManager - Singleton wrapper for ACubeSDK with simplified API
|
|
9210
6801
|
*
|
|
@@ -9255,6 +6846,7 @@ class SDKManager {
|
|
|
9255
6846
|
this.appStateService = null;
|
|
9256
6847
|
this.isInitialized = false;
|
|
9257
6848
|
this.isPollingActive = false;
|
|
6849
|
+
this.certificateMissingSubject = new BehaviorSubject(false);
|
|
9258
6850
|
/**
|
|
9259
6851
|
* Handle user state changes (login/logout/token expiration)
|
|
9260
6852
|
* Manages polling lifecycle based on user role
|
|
@@ -9265,9 +6857,14 @@ class SDKManager {
|
|
|
9265
6857
|
if (!this.isInitialized)
|
|
9266
6858
|
return;
|
|
9267
6859
|
if (user) {
|
|
9268
|
-
// User logged in - check role and
|
|
6860
|
+
// User logged in - check role and certificate before starting polling
|
|
9269
6861
|
const canPoll = hasAnyRole(user.roles, ['ROLE_MERCHANT', 'ROLE_CASHIER']);
|
|
9270
6862
|
if (canPoll && !this.isPollingActive) {
|
|
6863
|
+
const hasCert = await this.checkCertificate();
|
|
6864
|
+
if (!hasCert) {
|
|
6865
|
+
log.warn('Certificate missing — polling blocked until certificate is installed');
|
|
6866
|
+
return;
|
|
6867
|
+
}
|
|
9271
6868
|
this.notificationService?.startPolling();
|
|
9272
6869
|
await this.startTelemetryPollingAuto();
|
|
9273
6870
|
this.isPollingActive = true;
|
|
@@ -9281,6 +6878,7 @@ class SDKManager {
|
|
|
9281
6878
|
this.telemetryService?.clearTelemetry();
|
|
9282
6879
|
this.isPollingActive = false;
|
|
9283
6880
|
}
|
|
6881
|
+
this.certificateMissingSubject.next(false);
|
|
9284
6882
|
}
|
|
9285
6883
|
};
|
|
9286
6884
|
}
|
|
@@ -9347,7 +6945,7 @@ class SDKManager {
|
|
|
9347
6945
|
this.telemetryService = new TelemetryService(telemetryRepo, networkPort, {
|
|
9348
6946
|
pollIntervalMs: this.config.telemetryPollIntervalMs ?? 60000,
|
|
9349
6947
|
});
|
|
9350
|
-
this.appStateService = new AppStateService(this.notificationService.notifications$, networkPort);
|
|
6948
|
+
this.appStateService = new AppStateService(this.notificationService.notifications$, networkPort, this.certificateMissingSubject.asObservable());
|
|
9351
6949
|
if (this.events?.onAppStateChanged) {
|
|
9352
6950
|
this.appStateService.state$.subscribe(this.events.onAppStateChanged);
|
|
9353
6951
|
}
|
|
@@ -9359,9 +6957,15 @@ class SDKManager {
|
|
|
9359
6957
|
const user = await this.sdk.getCurrentUser();
|
|
9360
6958
|
const canPoll = user && hasAnyRole(user.roles, ['ROLE_MERCHANT', 'ROLE_CASHIER']);
|
|
9361
6959
|
if (canPoll) {
|
|
9362
|
-
this.
|
|
9363
|
-
|
|
9364
|
-
|
|
6960
|
+
const hasCert = await this.checkCertificate();
|
|
6961
|
+
if (hasCert) {
|
|
6962
|
+
this.notificationService.startPolling();
|
|
6963
|
+
await this.startTelemetryPollingAuto();
|
|
6964
|
+
this.isPollingActive = true;
|
|
6965
|
+
}
|
|
6966
|
+
else {
|
|
6967
|
+
log.warn('Certificate missing at init — polling blocked until certificate is installed');
|
|
6968
|
+
}
|
|
9365
6969
|
}
|
|
9366
6970
|
// AppStateService remains active for all users (handles OFFLINE network state)
|
|
9367
6971
|
}
|
|
@@ -9396,7 +7000,14 @@ class SDKManager {
|
|
|
9396
7000
|
return this.appStateService.warning$;
|
|
9397
7001
|
}
|
|
9398
7002
|
/**
|
|
9399
|
-
* Observable stream
|
|
7003
|
+
* Observable stream indicating if certificate is missing
|
|
7004
|
+
* When true, polling is blocked and the user should install a certificate
|
|
7005
|
+
*/
|
|
7006
|
+
get certificateMissing$() {
|
|
7007
|
+
return this.certificateMissingSubject.asObservable();
|
|
7008
|
+
}
|
|
7009
|
+
/**
|
|
7010
|
+
* Observable stream of telemetry state (data, isLoading, error)
|
|
9400
7011
|
*/
|
|
9401
7012
|
get telemetryState$() {
|
|
9402
7013
|
this.ensureInitialized();
|
|
@@ -9472,9 +7083,37 @@ class SDKManager {
|
|
|
9472
7083
|
logout: () => sdk.logout(),
|
|
9473
7084
|
getCurrentUser: () => sdk.getCurrentUser(),
|
|
9474
7085
|
isAuthenticated: () => sdk.isAuthenticated(),
|
|
9475
|
-
storeCertificate: (certificate, privateKey, options) =>
|
|
7086
|
+
storeCertificate: async (certificate, privateKey, options) => {
|
|
7087
|
+
await sdk.storeCertificate(certificate, privateKey, options);
|
|
7088
|
+
this.certificateMissingSubject.next(false);
|
|
7089
|
+
// Start polling if user can poll and polling is not active
|
|
7090
|
+
if (!this.isPollingActive) {
|
|
7091
|
+
const user = await sdk.getCurrentUser();
|
|
7092
|
+
const canPoll = user && hasAnyRole(user.roles, ['ROLE_MERCHANT', 'ROLE_CASHIER']);
|
|
7093
|
+
if (canPoll) {
|
|
7094
|
+
log.info('Certificate installed — starting polling');
|
|
7095
|
+
this.notificationService?.startPolling();
|
|
7096
|
+
await this.startTelemetryPollingAuto();
|
|
7097
|
+
this.isPollingActive = true;
|
|
7098
|
+
}
|
|
7099
|
+
}
|
|
7100
|
+
},
|
|
9476
7101
|
hasCertificate: () => sdk.hasCertificate(),
|
|
9477
|
-
clearCertificate: () =>
|
|
7102
|
+
clearCertificate: async () => {
|
|
7103
|
+
await sdk.clearCertificate();
|
|
7104
|
+
this.certificateMissingSubject.next(true);
|
|
7105
|
+
// Stop polling since certificate is required
|
|
7106
|
+
if (this.isPollingActive) {
|
|
7107
|
+
log.info('Certificate removed — stopping polling');
|
|
7108
|
+
this.notificationService?.stopPolling();
|
|
7109
|
+
this.telemetryService?.stopPolling();
|
|
7110
|
+
this.telemetryService?.clearTelemetry();
|
|
7111
|
+
this.isPollingActive = false;
|
|
7112
|
+
}
|
|
7113
|
+
},
|
|
7114
|
+
getCertificate: () => sdk.getCertificate(),
|
|
7115
|
+
getCertificatesInfo: () => sdk.getCertificatesInfo(),
|
|
7116
|
+
notifications: sdk.notifications,
|
|
9478
7117
|
isOnline: () => sdk.isOnline(),
|
|
9479
7118
|
};
|
|
9480
7119
|
}
|
|
@@ -9505,6 +7144,21 @@ class SDKManager {
|
|
|
9505
7144
|
this.ensureInitialized();
|
|
9506
7145
|
return this.sdk;
|
|
9507
7146
|
}
|
|
7147
|
+
/**
|
|
7148
|
+
* Check certificate availability and update certificateMissing state.
|
|
7149
|
+
* Returns true if certificate is available, false otherwise.
|
|
7150
|
+
*/
|
|
7151
|
+
async checkCertificate() {
|
|
7152
|
+
try {
|
|
7153
|
+
const hasCert = await this.sdk.hasCertificate();
|
|
7154
|
+
this.certificateMissingSubject.next(!hasCert);
|
|
7155
|
+
return hasCert;
|
|
7156
|
+
}
|
|
7157
|
+
catch {
|
|
7158
|
+
this.certificateMissingSubject.next(true);
|
|
7159
|
+
return false;
|
|
7160
|
+
}
|
|
7161
|
+
}
|
|
9508
7162
|
cleanup() {
|
|
9509
7163
|
this.notificationService?.destroy();
|
|
9510
7164
|
this.telemetryService?.destroy();
|
|
@@ -9530,91 +7184,6 @@ const RECEIPT_SENT = 'sent';
|
|
|
9530
7184
|
const RECEIPT_SORT_DESCENDING = 'descending';
|
|
9531
7185
|
const RECEIPT_SORT_ASCENDING = 'ascending';
|
|
9532
7186
|
|
|
9533
|
-
const log = createPrefixedLogger('CACHE-HANDLER');
|
|
9534
|
-
class CacheHandler {
|
|
9535
|
-
constructor(cache, networkMonitor) {
|
|
9536
|
-
this.cache = cache;
|
|
9537
|
-
this.networkMonitor = networkMonitor;
|
|
9538
|
-
this.currentOnlineState = true;
|
|
9539
|
-
this.setupNetworkMonitoring();
|
|
9540
|
-
}
|
|
9541
|
-
setupNetworkMonitoring() {
|
|
9542
|
-
if (this.networkMonitor) {
|
|
9543
|
-
this.networkSubscription = this.networkMonitor.online$.subscribe((online) => {
|
|
9544
|
-
this.currentOnlineState = online;
|
|
9545
|
-
});
|
|
9546
|
-
}
|
|
9547
|
-
}
|
|
9548
|
-
isOnline() {
|
|
9549
|
-
if (this.networkMonitor) {
|
|
9550
|
-
return this.currentOnlineState;
|
|
9551
|
-
}
|
|
9552
|
-
if (typeof navigator !== 'undefined' && 'onLine' in navigator) {
|
|
9553
|
-
return navigator.onLine;
|
|
9554
|
-
}
|
|
9555
|
-
return false;
|
|
9556
|
-
}
|
|
9557
|
-
async handleCachedRequest(url, requestFn, config) {
|
|
9558
|
-
if (!this.cache || config?.useCache === false) {
|
|
9559
|
-
const response = await requestFn();
|
|
9560
|
-
return response.data;
|
|
9561
|
-
}
|
|
9562
|
-
const cacheKey = this.generateCacheKey(url);
|
|
9563
|
-
const online = this.isOnline();
|
|
9564
|
-
log.debug('Request:', { url, cacheKey, online });
|
|
9565
|
-
if (online) {
|
|
9566
|
-
try {
|
|
9567
|
-
const response = await requestFn();
|
|
9568
|
-
if (this.cache) {
|
|
9569
|
-
await this.cache.set(cacheKey, response.data).catch((error) => {
|
|
9570
|
-
log.error('Failed to cache:', error instanceof Error ? error.message : error);
|
|
9571
|
-
});
|
|
9572
|
-
}
|
|
9573
|
-
return response.data;
|
|
9574
|
-
}
|
|
9575
|
-
catch (error) {
|
|
9576
|
-
const cached = await this.cache.get(cacheKey).catch(() => null);
|
|
9577
|
-
if (cached) {
|
|
9578
|
-
log.debug('Network failed, using cache fallback');
|
|
9579
|
-
return cached.data;
|
|
9580
|
-
}
|
|
9581
|
-
throw error;
|
|
9582
|
-
}
|
|
9583
|
-
}
|
|
9584
|
-
else {
|
|
9585
|
-
const cached = await this.cache.get(cacheKey).catch(() => null);
|
|
9586
|
-
if (cached) {
|
|
9587
|
-
log.debug('Offline, returning cached data');
|
|
9588
|
-
return cached.data;
|
|
9589
|
-
}
|
|
9590
|
-
throw new Error('Offline: No cached data available');
|
|
9591
|
-
}
|
|
9592
|
-
}
|
|
9593
|
-
generateCacheKey(url) {
|
|
9594
|
-
return url;
|
|
9595
|
-
}
|
|
9596
|
-
async invalidateCache(pattern) {
|
|
9597
|
-
if (!this.cache)
|
|
9598
|
-
return;
|
|
9599
|
-
try {
|
|
9600
|
-
await this.cache.invalidate(pattern);
|
|
9601
|
-
}
|
|
9602
|
-
catch (error) {
|
|
9603
|
-
log.error('Invalidation failed:', error instanceof Error ? error.message : error);
|
|
9604
|
-
}
|
|
9605
|
-
}
|
|
9606
|
-
getCacheStatus() {
|
|
9607
|
-
return {
|
|
9608
|
-
available: !!this.cache,
|
|
9609
|
-
networkMonitorAvailable: !!this.networkMonitor,
|
|
9610
|
-
isOnline: this.isOnline(),
|
|
9611
|
-
};
|
|
9612
|
-
}
|
|
9613
|
-
destroy() {
|
|
9614
|
-
this.networkSubscription?.unsubscribe();
|
|
9615
|
-
}
|
|
9616
|
-
}
|
|
9617
|
-
|
|
9618
7187
|
function transformError(error) {
|
|
9619
7188
|
if (axios.isAxiosError(error)) {
|
|
9620
7189
|
const response = error.response;
|
|
@@ -9802,7 +7371,6 @@ exports.AppStateService = AppStateService;
|
|
|
9802
7371
|
exports.AuthStrategy = AuthStrategy;
|
|
9803
7372
|
exports.AuthenticationService = AuthenticationService;
|
|
9804
7373
|
exports.AxiosHttpAdapter = AxiosHttpAdapter;
|
|
9805
|
-
exports.CacheHandler = CacheHandler;
|
|
9806
7374
|
exports.CashRegisterCreateSchema = CashRegisterCreateSchema;
|
|
9807
7375
|
exports.CashRegisterMapper = CashRegisterMapper;
|
|
9808
7376
|
exports.CashRegisterRepositoryImpl = CashRegisterRepositoryImpl;
|
|
@@ -9813,7 +7381,6 @@ exports.CertificateService = CertificateService;
|
|
|
9813
7381
|
exports.CertificateValidator = CertificateValidator;
|
|
9814
7382
|
exports.ConfigManager = ConfigManager;
|
|
9815
7383
|
exports.DAILY_REPORT_STATUS_OPTIONS = DAILY_REPORT_STATUS_OPTIONS;
|
|
9816
|
-
exports.DEFAULT_QUEUE_CONFIG = DEFAULT_QUEUE_CONFIG;
|
|
9817
7384
|
exports.DIContainer = DIContainer;
|
|
9818
7385
|
exports.DI_TOKENS = DI_TOKENS;
|
|
9819
7386
|
exports.DailyReportMapper = DailyReportMapper;
|
|
@@ -9852,8 +7419,6 @@ exports.NotificationRepositoryImpl = NotificationRepositoryImpl;
|
|
|
9852
7419
|
exports.NotificationSchema = NotificationSchema;
|
|
9853
7420
|
exports.NotificationScopeSchema = NotificationScopeSchema;
|
|
9854
7421
|
exports.NotificationService = NotificationService;
|
|
9855
|
-
exports.OfflineManager = OfflineManager;
|
|
9856
|
-
exports.OperationQueue = OperationQueue;
|
|
9857
7422
|
exports.PEMStatusOfflineRequestSchema = PEMStatusOfflineRequestSchema;
|
|
9858
7423
|
exports.PEMStatusSchema = PEMStatusSchema;
|
|
9859
7424
|
exports.PEM_STATUS_OPTIONS = PEM_STATUS_OPTIONS;
|
|
@@ -9888,7 +7453,6 @@ exports.SupplierCreateInputSchema = SupplierCreateInputSchema;
|
|
|
9888
7453
|
exports.SupplierMapper = SupplierMapper;
|
|
9889
7454
|
exports.SupplierRepositoryImpl = SupplierRepositoryImpl;
|
|
9890
7455
|
exports.SupplierUpdateInputSchema = SupplierUpdateInputSchema;
|
|
9891
|
-
exports.SyncManager = SyncManager;
|
|
9892
7456
|
exports.TelemetryMapper = TelemetryMapper;
|
|
9893
7457
|
exports.TelemetryMerchantSchema = TelemetryMerchantSchema;
|
|
9894
7458
|
exports.TelemetryRepositoryImpl = TelemetryRepositoryImpl;
|